feat(gateway): add two-phase response and waitDecision handler for exec approvals

Send immediate 'accepted' response after registration so callers can confirm
the approval ID is valid. Add exec.approval.waitDecision endpoint to wait for
decision on already-registered approvals.
This commit is contained in:
rshirali 2026-01-28 14:53:52 +01:00
parent fa7eeca7ce
commit 20c2a99476

View File

@ -62,7 +62,9 @@ export function createExecApprovalHandlers(
sessionKey: p.sessionKey ?? null,
};
const record = manager.create(request, timeoutMs, explicitId);
const decisionPromise = manager.waitForDecision(record, timeoutMs);
// Use register() to synchronously add to pending map before sending any response.
// This ensures the approval ID is valid immediately after the "accepted" response.
const decisionPromise = manager.register(record, timeoutMs);
context.broadcast(
"exec.approval.requested",
{
@ -83,7 +85,24 @@ export function createExecApprovalHandlers(
.catch((err) => {
context.logGateway?.error?.(`exec approvals: forward request failed: ${String(err)}`);
});
// Send immediate "accepted" response so callers know the approval ID is registered.
// Callers using expectFinal:false will receive this and can return immediately.
// Callers using expectFinal:true will continue waiting for the decision.
// Note: "accepted" status is recognized by the gateway client for dual-response pattern.
respond(
true,
{
status: "accepted",
id: record.id,
createdAtMs: record.createdAtMs,
expiresAtMs: record.expiresAtMs,
},
undefined,
);
const decision = await decisionPromise;
// Send final response with decision for callers using expectFinal:true.
respond(
true,
{
@ -95,6 +114,31 @@ export function createExecApprovalHandlers(
undefined,
);
},
"exec.approval.waitDecision": async ({ params, respond }) => {
const p = params as { id?: string; timeoutMs?: number };
const id = typeof p.id === "string" ? p.id.trim() : "";
if (!id) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "id is required"));
return;
}
const decisionPromise = manager.awaitDecision(id);
if (!decisionPromise) {
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unknown approval id"));
return;
}
const decision = await decisionPromise;
const snapshot = manager.getSnapshot(id);
respond(
true,
{
id,
decision,
createdAtMs: snapshot?.createdAtMs,
expiresAtMs: snapshot?.expiresAtMs,
},
undefined,
);
},
"exec.approval.resolve": async ({ params, respond, client, context }) => {
if (!validateExecApprovalResolveParams(params)) {
respond(