fix(exec): await approval registration before returning approval-pending
Ensures the approval ID is registered in the gateway before the tool returns. Uses exec.approval.request with expectFinal:false for registration, then fire-and-forget exec.approval.waitDecision for the decision phase. Fixes #2402
This commit is contained in:
parent
20c2a99476
commit
b1d409738b
@ -1008,29 +1008,47 @@ export function createExecTool(
|
||||
if (requiresAsk) {
|
||||
const approvalId = crypto.randomUUID();
|
||||
const approvalSlug = createApprovalSlug(approvalId);
|
||||
const expiresAtMs = Date.now() + DEFAULT_APPROVAL_TIMEOUT_MS;
|
||||
const contextKey = `exec:${approvalId}`;
|
||||
const noticeSeconds = Math.max(1, Math.round(approvalRunningNoticeMs / 1000));
|
||||
const warningText = warnings.length ? `${warnings.join("\n")}\n\n` : "";
|
||||
|
||||
// Register the approval with expectFinal:false to get immediate confirmation.
|
||||
// This ensures the approval ID is valid before we return.
|
||||
let expiresAtMs = Date.now() + DEFAULT_APPROVAL_TIMEOUT_MS;
|
||||
try {
|
||||
const registrationResult = (await callGatewayTool(
|
||||
"exec.approval.request",
|
||||
{ timeoutMs: 10_000 },
|
||||
{
|
||||
id: approvalId,
|
||||
command: commandText,
|
||||
cwd: workdir,
|
||||
host: "node",
|
||||
security: hostSecurity,
|
||||
ask: hostAsk,
|
||||
agentId,
|
||||
resolvedPath: undefined,
|
||||
sessionKey: defaults?.sessionKey,
|
||||
timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
|
||||
},
|
||||
{ expectFinal: false },
|
||||
)) as { status?: string; expiresAtMs?: number } | null;
|
||||
if (registrationResult?.expiresAtMs) {
|
||||
expiresAtMs = registrationResult.expiresAtMs;
|
||||
}
|
||||
} catch (err) {
|
||||
// Registration failed - throw to caller
|
||||
throw new Error(`Exec approval registration failed: ${String(err)}`);
|
||||
}
|
||||
|
||||
// Fire-and-forget: wait for decision via waitDecision endpoint, then execute.
|
||||
void (async () => {
|
||||
let decision: string | null = null;
|
||||
try {
|
||||
const decisionResult = (await callGatewayTool(
|
||||
"exec.approval.request",
|
||||
"exec.approval.waitDecision",
|
||||
{ timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS },
|
||||
{
|
||||
id: approvalId,
|
||||
command: commandText,
|
||||
cwd: workdir,
|
||||
host: "node",
|
||||
security: hostSecurity,
|
||||
ask: hostAsk,
|
||||
agentId,
|
||||
resolvedPath: undefined,
|
||||
sessionKey: defaults?.sessionKey,
|
||||
timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
|
||||
},
|
||||
{ id: approvalId },
|
||||
)) as { decision?: string } | null;
|
||||
decision =
|
||||
decisionResult && typeof decisionResult === "object"
|
||||
@ -1185,7 +1203,6 @@ export function createExecTool(
|
||||
if (requiresAsk) {
|
||||
const approvalId = crypto.randomUUID();
|
||||
const approvalSlug = createApprovalSlug(approvalId);
|
||||
const expiresAtMs = Date.now() + DEFAULT_APPROVAL_TIMEOUT_MS;
|
||||
const contextKey = `exec:${approvalId}`;
|
||||
const resolvedPath = allowlistEval.segments[0]?.resolution?.resolvedPath;
|
||||
const noticeSeconds = Math.max(1, Math.round(approvalRunningNoticeMs / 1000));
|
||||
@ -1194,24 +1211,43 @@ export function createExecTool(
|
||||
typeof params.timeout === "number" ? params.timeout : defaultTimeoutSec;
|
||||
const warningText = warnings.length ? `${warnings.join("\n")}\n\n` : "";
|
||||
|
||||
// Register the approval with expectFinal:false to get immediate confirmation.
|
||||
// This ensures the approval ID is valid before we return.
|
||||
let expiresAtMs = Date.now() + DEFAULT_APPROVAL_TIMEOUT_MS;
|
||||
try {
|
||||
const registrationResult = (await callGatewayTool(
|
||||
"exec.approval.request",
|
||||
{ timeoutMs: 10_000 },
|
||||
{
|
||||
id: approvalId,
|
||||
command: commandText,
|
||||
cwd: workdir,
|
||||
host: "gateway",
|
||||
security: hostSecurity,
|
||||
ask: hostAsk,
|
||||
agentId,
|
||||
resolvedPath,
|
||||
sessionKey: defaults?.sessionKey,
|
||||
timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
|
||||
},
|
||||
{ expectFinal: false },
|
||||
)) as { status?: string; expiresAtMs?: number } | null;
|
||||
if (registrationResult?.expiresAtMs) {
|
||||
expiresAtMs = registrationResult.expiresAtMs;
|
||||
}
|
||||
} catch (err) {
|
||||
// Registration failed - throw to caller
|
||||
throw new Error(`Exec approval registration failed: ${String(err)}`);
|
||||
}
|
||||
|
||||
// Fire-and-forget: wait for decision via waitDecision endpoint, then execute.
|
||||
void (async () => {
|
||||
let decision: string | null = null;
|
||||
try {
|
||||
const decisionResult = (await callGatewayTool(
|
||||
"exec.approval.request",
|
||||
"exec.approval.waitDecision",
|
||||
{ timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS },
|
||||
{
|
||||
id: approvalId,
|
||||
command: commandText,
|
||||
cwd: workdir,
|
||||
host: "gateway",
|
||||
security: hostSecurity,
|
||||
ask: hostAsk,
|
||||
agentId,
|
||||
resolvedPath,
|
||||
sessionKey: defaults?.sessionKey,
|
||||
timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
|
||||
},
|
||||
{ id: approvalId },
|
||||
)) as { decision?: string } | null;
|
||||
decision =
|
||||
decisionResult && typeof decisionResult === "object"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user