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) {
|
if (requiresAsk) {
|
||||||
const approvalId = crypto.randomUUID();
|
const approvalId = crypto.randomUUID();
|
||||||
const approvalSlug = createApprovalSlug(approvalId);
|
const approvalSlug = createApprovalSlug(approvalId);
|
||||||
const expiresAtMs = Date.now() + DEFAULT_APPROVAL_TIMEOUT_MS;
|
|
||||||
const contextKey = `exec:${approvalId}`;
|
const contextKey = `exec:${approvalId}`;
|
||||||
const noticeSeconds = Math.max(1, Math.round(approvalRunningNoticeMs / 1000));
|
const noticeSeconds = Math.max(1, Math.round(approvalRunningNoticeMs / 1000));
|
||||||
const warningText = warnings.length ? `${warnings.join("\n")}\n\n` : "";
|
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 () => {
|
void (async () => {
|
||||||
let decision: string | null = null;
|
let decision: string | null = null;
|
||||||
try {
|
try {
|
||||||
const decisionResult = (await callGatewayTool(
|
const decisionResult = (await callGatewayTool(
|
||||||
"exec.approval.request",
|
"exec.approval.waitDecision",
|
||||||
{ timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS },
|
{ timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS },
|
||||||
{
|
{ id: approvalId },
|
||||||
id: approvalId,
|
|
||||||
command: commandText,
|
|
||||||
cwd: workdir,
|
|
||||||
host: "node",
|
|
||||||
security: hostSecurity,
|
|
||||||
ask: hostAsk,
|
|
||||||
agentId,
|
|
||||||
resolvedPath: undefined,
|
|
||||||
sessionKey: defaults?.sessionKey,
|
|
||||||
timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
|
|
||||||
},
|
|
||||||
)) as { decision?: string } | null;
|
)) as { decision?: string } | null;
|
||||||
decision =
|
decision =
|
||||||
decisionResult && typeof decisionResult === "object"
|
decisionResult && typeof decisionResult === "object"
|
||||||
@ -1185,7 +1203,6 @@ export function createExecTool(
|
|||||||
if (requiresAsk) {
|
if (requiresAsk) {
|
||||||
const approvalId = crypto.randomUUID();
|
const approvalId = crypto.randomUUID();
|
||||||
const approvalSlug = createApprovalSlug(approvalId);
|
const approvalSlug = createApprovalSlug(approvalId);
|
||||||
const expiresAtMs = Date.now() + DEFAULT_APPROVAL_TIMEOUT_MS;
|
|
||||||
const contextKey = `exec:${approvalId}`;
|
const contextKey = `exec:${approvalId}`;
|
||||||
const resolvedPath = allowlistEval.segments[0]?.resolution?.resolvedPath;
|
const resolvedPath = allowlistEval.segments[0]?.resolution?.resolvedPath;
|
||||||
const noticeSeconds = Math.max(1, Math.round(approvalRunningNoticeMs / 1000));
|
const noticeSeconds = Math.max(1, Math.round(approvalRunningNoticeMs / 1000));
|
||||||
@ -1194,24 +1211,43 @@ export function createExecTool(
|
|||||||
typeof params.timeout === "number" ? params.timeout : defaultTimeoutSec;
|
typeof params.timeout === "number" ? params.timeout : defaultTimeoutSec;
|
||||||
const warningText = warnings.length ? `${warnings.join("\n")}\n\n` : "";
|
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 () => {
|
void (async () => {
|
||||||
let decision: string | null = null;
|
let decision: string | null = null;
|
||||||
try {
|
try {
|
||||||
const decisionResult = (await callGatewayTool(
|
const decisionResult = (await callGatewayTool(
|
||||||
"exec.approval.request",
|
"exec.approval.waitDecision",
|
||||||
{ timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS },
|
{ timeoutMs: DEFAULT_APPROVAL_REQUEST_TIMEOUT_MS },
|
||||||
{
|
{ id: approvalId },
|
||||||
id: approvalId,
|
|
||||||
command: commandText,
|
|
||||||
cwd: workdir,
|
|
||||||
host: "gateway",
|
|
||||||
security: hostSecurity,
|
|
||||||
ask: hostAsk,
|
|
||||||
agentId,
|
|
||||||
resolvedPath,
|
|
||||||
sessionKey: defaults?.sessionKey,
|
|
||||||
timeoutMs: DEFAULT_APPROVAL_TIMEOUT_MS,
|
|
||||||
},
|
|
||||||
)) as { decision?: string } | null;
|
)) as { decision?: string } | null;
|
||||||
decision =
|
decision =
|
||||||
decisionResult && typeof decisionResult === "object"
|
decisionResult && typeof decisionResult === "object"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user