fix(exec): resolve remote approval regressions (#58792)

* fix(exec): restore remote approval policy defaults

* fix(exec): handle headless cron approval conflicts

* fix(exec): make allow-always durable

* fix(exec): persist exact-command shell trust

* fix(doctor): match host exec fallback

* fix(exec): preserve blocked and inline approval state

* Doctor: surface allow-always ask bypass

* Doctor: match effective exec policy

* Exec: match node durable command text

* Exec: tighten durable approval security

* Exec: restore owner approver fallback

* Config: refresh Slack approval metadata

---------

Co-authored-by: scoootscooob <zhentongfan@gmail.com>
This commit is contained in:
Vincent Koc
2026-04-01 18:07:20 +09:00
committed by GitHub
parent 4ceb01f9ed
commit 2d53ffdec1
34 changed files with 1609 additions and 226 deletions

View File

@@ -5,6 +5,7 @@ import {
type ExecAsk,
type ExecSecurity,
evaluateShellAllowlist,
hasDurableExecApproval,
requiresExecApproval,
resolveExecApprovalsFromFile,
} from "../infra/exec-approvals.js";
@@ -43,6 +44,7 @@ export type ExecuteNodeHostCommandParams = {
turnSourceTo?: string;
turnSourceAccountId?: string;
turnSourceThreadId?: string | number;
trigger?: string;
agentId?: string;
security: ExecSecurity;
ask: ExecAsk;
@@ -134,6 +136,7 @@ export async function executeNodeHostCommand(
});
let analysisOk = baseAllowlistEval.analysisOk;
let allowlistSatisfied = false;
let durableApprovalSatisfied = false;
const inlineEvalHit =
params.strictInlineEval === true
? (baseAllowlistEval.segments
@@ -149,7 +152,7 @@ export async function executeNodeHostCommand(
)}.`,
);
}
if (hostAsk === "on-miss" && hostSecurity === "allowlist" && analysisOk) {
if ((hostAsk === "always" || hostSecurity === "allowlist") && analysisOk) {
try {
const approvalsSnapshot = await callGatewayTool<{ file: string }>(
"exec.approvals.node.get",
@@ -176,6 +179,12 @@ export async function executeNodeHostCommand(
platform: nodeInfo?.platform,
trustedSafeBinDirs: params.trustedSafeBinDirs,
});
durableApprovalSatisfied = hasDurableExecApproval({
analysisOk: allowlistEval.analysisOk,
segmentAllowlistEntries: allowlistEval.segmentAllowlistEntries,
allowlist: resolved.allowlist,
commandText: runRawCommand,
});
allowlistSatisfied = allowlistEval.allowlistSatisfied;
analysisOk = allowlistEval.analysisOk;
}
@@ -196,6 +205,7 @@ export async function executeNodeHostCommand(
security: hostSecurity,
analysisOk,
allowlistSatisfied,
durableApprovalSatisfied,
}) ||
inlineEvalHit !== null ||
obfuscation.detected;
@@ -232,6 +242,9 @@ export async function executeNodeHostCommand(
idempotencyKey: crypto.randomUUID(),
}) satisfies Record<string, unknown>;
let inlineApprovedByAsk = false;
let inlineApprovalDecision: "allow-once" | "allow-always" | null = null;
let inlineApprovalId: string | undefined;
if (requiresAsk) {
const requestArgs = execHostShared.buildDefaultExecApprovalRequestArgs({
warnings: params.warnings,
@@ -269,119 +282,149 @@ export async function executeNodeHostCommand(
...requestArgs,
register: registerNodeApproval,
});
const followupTarget = execHostShared.buildExecApprovalFollowupTarget({
approvalId,
sessionKey: params.notifySessionKey,
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
});
void (async () => {
const decision = await execHostShared.resolveApprovalDecisionOrUndefined({
approvalId,
if (
execHostShared.shouldResolveExecApprovalUnavailableInline({
trigger: params.trigger,
unavailableReason,
preResolvedDecision,
onFailure: () =>
void execHostShared.sendExecApprovalFollowupResult(
followupTarget,
`Exec denied (node=${nodeId} id=${approvalId}, approval-request-failed): ${params.command}`,
),
});
if (decision === undefined) {
return;
}
const {
baseDecision,
approvedByAsk: initialApprovedByAsk,
deniedReason: initialDeniedReason,
} = execHostShared.createExecApprovalDecisionState({
decision,
})
) {
const { approvedByAsk, deniedReason } = execHostShared.createExecApprovalDecisionState({
decision: preResolvedDecision,
askFallback,
obfuscationDetected: obfuscation.detected,
});
let approvedByAsk = initialApprovedByAsk;
let approvalDecision: "allow-once" | "allow-always" | null = null;
let deniedReason = initialDeniedReason;
if (baseDecision.timedOut && askFallback === "full" && approvedByAsk) {
approvalDecision = "allow-once";
} else if (decision === "allow-once") {
approvedByAsk = true;
approvalDecision = "allow-once";
} else if (decision === "allow-always") {
approvedByAsk = true;
approvalDecision = "allow-always";
}
if (deniedReason) {
await execHostShared.sendExecApprovalFollowupResult(
followupTarget,
`Exec denied (node=${nodeId} id=${approvalId}, ${deniedReason}): ${params.command}`,
);
return;
}
try {
const raw = await callGatewayTool<{
payload?: {
stdout?: string;
stderr?: string;
error?: string | null;
exitCode?: number | null;
timedOut?: boolean;
};
}>(
"node.invoke",
{ timeoutMs: invokeTimeoutMs },
buildInvokeParams(approvedByAsk, approvalDecision, approvalId, true),
);
const payload =
raw?.payload && typeof raw.payload === "object"
? (raw.payload as {
stdout?: string;
stderr?: string;
error?: string | null;
exitCode?: number | null;
timedOut?: boolean;
})
: {};
const combined = [payload.stdout, payload.stderr, payload.error].filter(Boolean).join("\n");
const output = normalizeNotifyOutput(combined.slice(-DEFAULT_NOTIFY_TAIL_CHARS));
const exitLabel = payload.timedOut ? "timeout" : `code ${payload.exitCode ?? "?"}`;
const summary = output
? `Exec finished (node=${nodeId} id=${approvalId}, ${exitLabel})\n${output}`
: `Exec finished (node=${nodeId} id=${approvalId}, ${exitLabel})`;
await execHostShared.sendExecApprovalFollowupResult(followupTarget, summary);
} catch {
await execHostShared.sendExecApprovalFollowupResult(
followupTarget,
`Exec denied (node=${nodeId} id=${approvalId}, invoke-failed): ${params.command}`,
if (deniedReason || !approvedByAsk) {
throw new Error(
execHostShared.buildHeadlessExecApprovalDeniedMessage({
trigger: params.trigger,
host: "node",
security: hostSecurity,
ask: hostAsk,
askFallback,
}),
);
}
})();
inlineApprovedByAsk = approvedByAsk;
inlineApprovalDecision = approvedByAsk ? "allow-once" : null;
inlineApprovalId = approvalId;
} else {
const followupTarget = execHostShared.buildExecApprovalFollowupTarget({
approvalId,
sessionKey: params.notifySessionKey,
turnSourceChannel: params.turnSourceChannel,
turnSourceTo: params.turnSourceTo,
turnSourceAccountId: params.turnSourceAccountId,
turnSourceThreadId: params.turnSourceThreadId,
});
return execHostShared.buildExecApprovalPendingToolResult({
host: "node",
command: params.command,
cwd: params.workdir,
warningText,
approvalId,
approvalSlug,
expiresAtMs,
initiatingSurface,
sentApproverDms,
unavailableReason,
nodeId,
});
void (async () => {
const decision = await execHostShared.resolveApprovalDecisionOrUndefined({
approvalId,
preResolvedDecision,
onFailure: () =>
void execHostShared.sendExecApprovalFollowupResult(
followupTarget,
`Exec denied (node=${nodeId} id=${approvalId}, approval-request-failed): ${params.command}`,
),
});
if (decision === undefined) {
return;
}
const {
baseDecision,
approvedByAsk: initialApprovedByAsk,
deniedReason: initialDeniedReason,
} = execHostShared.createExecApprovalDecisionState({
decision,
askFallback,
obfuscationDetected: obfuscation.detected,
});
let approvedByAsk = initialApprovedByAsk;
let approvalDecision: "allow-once" | "allow-always" | null = null;
let deniedReason = initialDeniedReason;
if (baseDecision.timedOut && askFallback === "full" && approvedByAsk) {
approvalDecision = "allow-once";
} else if (decision === "allow-once") {
approvedByAsk = true;
approvalDecision = "allow-once";
} else if (decision === "allow-always") {
approvedByAsk = true;
approvalDecision = "allow-always";
}
if (deniedReason) {
await execHostShared.sendExecApprovalFollowupResult(
followupTarget,
`Exec denied (node=${nodeId} id=${approvalId}, ${deniedReason}): ${params.command}`,
);
return;
}
try {
const raw = await callGatewayTool<{
payload?: {
stdout?: string;
stderr?: string;
error?: string | null;
exitCode?: number | null;
timedOut?: boolean;
};
}>(
"node.invoke",
{ timeoutMs: invokeTimeoutMs },
buildInvokeParams(approvedByAsk, approvalDecision, approvalId, true),
);
const payload =
raw?.payload && typeof raw.payload === "object"
? (raw.payload as {
stdout?: string;
stderr?: string;
error?: string | null;
exitCode?: number | null;
timedOut?: boolean;
})
: {};
const combined = [payload.stdout, payload.stderr, payload.error]
.filter(Boolean)
.join("\n");
const output = normalizeNotifyOutput(combined.slice(-DEFAULT_NOTIFY_TAIL_CHARS));
const exitLabel = payload.timedOut ? "timeout" : `code ${payload.exitCode ?? "?"}`;
const summary = output
? `Exec finished (node=${nodeId} id=${approvalId}, ${exitLabel})\n${output}`
: `Exec finished (node=${nodeId} id=${approvalId}, ${exitLabel})`;
await execHostShared.sendExecApprovalFollowupResult(followupTarget, summary);
} catch {
await execHostShared.sendExecApprovalFollowupResult(
followupTarget,
`Exec denied (node=${nodeId} id=${approvalId}, invoke-failed): ${params.command}`,
);
}
})();
return execHostShared.buildExecApprovalPendingToolResult({
host: "node",
command: params.command,
cwd: params.workdir,
warningText,
approvalId,
approvalSlug,
expiresAtMs,
initiatingSurface,
sentApproverDms,
unavailableReason,
nodeId,
});
}
}
const startedAt = Date.now();
const raw = await callGatewayTool(
"node.invoke",
{ timeoutMs: invokeTimeoutMs },
buildInvokeParams(false, null),
buildInvokeParams(inlineApprovedByAsk, inlineApprovalDecision, inlineApprovalId),
);
const payload =
raw && typeof raw === "object" ? (raw as { payload?: unknown }).payload : undefined;