fix(gateway): bind system.run approvals to exec approvals

This commit is contained in:
Peter Steinberger
2026-02-14 13:02:48 +01:00
parent 233483d2b9
commit 318379cdba
12 changed files with 437 additions and 3 deletions

View File

@@ -15,7 +15,7 @@ export function createExecApprovalHandlers(
opts?: { forwarder?: ExecApprovalForwarder },
): GatewayRequestHandlers {
return {
"exec.approval.request": async ({ params, respond, context }) => {
"exec.approval.request": async ({ params, respond, context, client }) => {
if (!validateExecApprovalRequestParams(params)) {
respond(
false,
@@ -64,6 +64,9 @@ export function createExecApprovalHandlers(
sessionKey: p.sessionKey ?? null,
};
const record = manager.create(request, timeoutMs, explicitId);
record.requestedByConnId = client?.connId ?? null;
record.requestedByDeviceId = client?.connect?.device?.id ?? null;
record.requestedByClientId = client?.connect?.client?.id ?? null;
// Use register() to synchronously add to pending map before sending any response.
// This ensures the approval ID is valid immediately after the "accepted" response.
let decisionPromise: Promise<

View File

@@ -10,6 +10,7 @@ import {
verifyNodeToken,
} from "../../infra/node-pairing.js";
import { isNodeCommandAllowed, resolveNodeCommandAllowlist } from "../node-command-policy.js";
import { sanitizeSystemRunParamsForForwarding } from "../node-invoke-system-run-approval.js";
import {
ErrorCodes,
errorShape,
@@ -361,7 +362,7 @@ export const nodeHandlers: GatewayRequestHandlers = {
);
});
},
"node.invoke": async ({ params, respond, context }) => {
"node.invoke": async ({ params, respond, context, client }) => {
if (!validateNodeInvokeParams(params)) {
respondInvalidParams({
respond,
@@ -417,10 +418,28 @@ export const nodeHandlers: GatewayRequestHandlers = {
);
return;
}
const forwardedParams =
command === "system.run"
? sanitizeSystemRunParamsForForwarding({
rawParams: p.params,
client,
execApprovalManager: context.execApprovalManager,
})
: ({ ok: true, params: p.params } as const);
if (!forwardedParams.ok) {
respond(
false,
undefined,
errorShape(ErrorCodes.INVALID_REQUEST, forwardedParams.message, {
details: forwardedParams.details ?? null,
}),
);
return;
}
const res = await context.nodeRegistry.invoke({
nodeId,
command,
params: p.params,
params: forwardedParams.params,
timeoutMs: p.timeoutMs,
idempotencyKey: p.idempotencyKey,
});

View File

@@ -5,6 +5,7 @@ import type { CronService } from "../../cron/service.js";
import type { createSubsystemLogger } from "../../logging/subsystem.js";
import type { WizardSession } from "../../wizard/session.js";
import type { ChatAbortControllerEntry } from "../chat-abort.js";
import type { ExecApprovalManager } from "../exec-approval-manager.js";
import type { NodeRegistry } from "../node-registry.js";
import type { ConnectParams, ErrorShape, RequestFrame } from "../protocol/index.js";
import type { ChannelRuntimeSnapshot } from "../server-channels.js";
@@ -28,6 +29,7 @@ export type GatewayRequestContext = {
deps: ReturnType<typeof createDefaultDeps>;
cron: CronService;
cronStorePath: string;
execApprovalManager?: ExecApprovalManager;
loadGatewayModelCatalog: () => Promise<ModelCatalogEntry[]>;
getHealthCache: () => HealthSummary | null;
refreshHealthSnapshot: (opts?: { probe?: boolean }) => Promise<HealthSummary>;