feat: route exec approvals via gateway

This commit is contained in:
Peter Steinberger
2026-01-20 12:03:18 +00:00
parent 67be9aed28
commit 87343c374e
12 changed files with 359 additions and 5 deletions

View File

@@ -477,6 +477,7 @@ export function createExecTool(
(hostAsk === "on-miss" && hostSecurity === "allowlist" && !allowlistMatch);
let approvedByAsk = false;
let approvalDecision: "allow-once" | "allow-always" | null = null;
if (requiresAsk) {
const decisionResult = (await callGatewayTool(
"exec.approval.request",
@@ -504,20 +505,24 @@ export function createExecTool(
if (!decision) {
if (askFallback === "full") {
approvedByAsk = true;
approvalDecision = "allow-once";
} else if (askFallback === "allowlist") {
if (!allowlistMatch) {
throw new Error("exec denied: approval required (approval UI not available)");
}
approvedByAsk = true;
approvalDecision = "allow-once";
} else {
throw new Error("exec denied: approval required (approval UI not available)");
}
}
if (decision === "allow-once") {
approvedByAsk = true;
approvalDecision = "allow-once";
}
if (decision === "allow-always") {
approvedByAsk = true;
approvalDecision = "allow-always";
if (hostSecurity === "allowlist") {
const pattern =
resolution?.resolvedPath ??
@@ -556,6 +561,7 @@ export function createExecTool(
agentId: defaults?.agentId,
sessionKey: defaults?.sessionKey,
approved: approvedByAsk,
approvalDecision: approvalDecision ?? undefined,
},
idempotencyKey: crypto.randomUUID(),
};

View File

@@ -10,6 +10,7 @@ export type ExecHostRequest = {
needsScreenRecording?: boolean | null;
agentId?: string | null;
sessionKey?: string | null;
approvalDecision?: "allow-once" | "allow-always" | null;
};
export type ExecHostRunResult = {

View File

@@ -50,6 +50,7 @@ type SystemRunParams = {
agentId?: string | null;
sessionKey?: string | null;
approved?: boolean | null;
approvalDecision?: string | null;
};
type SystemWhichParams = {
@@ -561,6 +562,10 @@ async function handleInvoke(
const useMacAppExec =
process.platform === "darwin" && (execHostEnforced || !execHostFallbackAllowed);
if (useMacAppExec) {
const approvalDecision =
params.approvalDecision === "allow-once" || params.approvalDecision === "allow-always"
? params.approvalDecision
: null;
const execRequest: ExecHostRequest = {
command: argv,
rawCommand: rawCommand || null,
@@ -570,6 +575,7 @@ async function handleInvoke(
needsScreenRecording: params.needsScreenRecording ?? null,
agentId: agentId ?? null,
sessionKey: sessionKey ?? null,
approvalDecision,
};
const response = await runViaMacAppExecHost({ approvals, request: execRequest });
if (!response) {
@@ -660,7 +666,11 @@ async function handleInvoke(
ask === "always" ||
(ask === "on-miss" && security === "allowlist" && !allowlistMatch && !skillAllow);
const approvedByAsk = params.approved === true;
const approvalDecision =
params.approvalDecision === "allow-once" || params.approvalDecision === "allow-always"
? params.approvalDecision
: null;
const approvedByAsk = approvalDecision !== null || params.approved === true;
if (requiresAsk && !approvedByAsk) {
await sendNodeEvent(
client,
@@ -679,7 +689,7 @@ async function handleInvoke(
});
return;
}
if (approvedByAsk && security === "allowlist") {
if (approvalDecision === "allow-always" && security === "allowlist") {
const pattern = resolution?.resolvedPath ?? resolution?.rawExecutable ?? argv[0] ?? "";
if (pattern) addAllowlistEntry(approvals.file, agentId, pattern);
}