From 21e486b731dd112ed4908eafdd7a6fbfad9477c7 Mon Sep 17 00:00:00 2001 From: benpankow Date: Wed, 29 Jul 2026 13:11:22 -0700 Subject: [PATCH 1/2] refactor(eve): split approval authorization runner Signed-off-by: benpankow --- .../harness/authorize-approval-response.ts | 339 ++++++++++++++++ .../eve/src/harness/input-requests.test.ts | 2 +- packages/eve/src/harness/input-requests.ts | 367 +----------------- packages/eve/src/harness/tool-loop.ts | 2 +- 4 files changed, 347 insertions(+), 363 deletions(-) create mode 100644 packages/eve/src/harness/authorize-approval-response.ts diff --git a/packages/eve/src/harness/authorize-approval-response.ts b/packages/eve/src/harness/authorize-approval-response.ts new file mode 100644 index 000000000..6e915f384 --- /dev/null +++ b/packages/eve/src/harness/authorize-approval-response.ts @@ -0,0 +1,339 @@ +import type { SessionAuthContext } from "#channel/types.js"; +import { buildCallbackContext } from "#context/build-callback-context.js"; +import { + buildApprovalResponseAuth, + handleApprovalResponseAuthorizationError, +} from "#execution/tool-auth.js"; +import { + cancelApprovalRequest, + createApprovalCandidate, + finishApprovalCandidate, + getApprovalAuditState, + markApprovalCandidateAuthorizationRequired, + settleAllowedCandidate, +} from "#harness/approval-candidates.js"; +import { + getAuthorizationResult, + isAuthorizationSignal, + type AuthorizationSignal, +} from "#harness/authorization.js"; +import { + getPendingInputBatch, + getResponseAuthRequiredRequestIds, + isApprovalRequest, +} from "#harness/input-requests.js"; +import type { HarnessSession, HarnessToolMap, StepInput } from "#harness/types.js"; +import type { InputResponse } from "#runtime/input/types.js"; + +const APPROVAL_AUTHORIZER_TIMEOUT_MS = 10_000; +const APPROVAL_CANDIDATE_TTL_MS = 10 * 60_000; + +export type PendingApprovalAuthorizationResult = + | { readonly kind: "continue"; readonly session: HarnessSession; readonly stepInput?: StepInput } + | { + readonly authorization: AuthorizationSignal; + readonly candidateId: string; + readonly kind: "authorization-required"; + readonly requestId: string; + readonly session: HarnessSession; + } + | { + readonly candidateId?: string; + readonly kind: "rejected" | "duplicate" | "stale" | "failed"; + readonly requestId: string; + readonly safeReason?: string; + readonly session: HarnessSession; + readonly stepInput?: StepInput; + }; + +/** Runs response authorization for the first approval response in this delivery. */ +export async function authorizePendingApprovalResponse(input: { + readonly now?: number; + readonly runtimeRevision?: string; + readonly session: HarnessSession; + readonly stepInput?: StepInput; + readonly tools: HarnessToolMap; +}): Promise { + const batch = getPendingInputBatch(input.session.state); + const activeCandidate = getApprovalAuditState(input.session.state).activeCandidates.find( + (candidate) => + candidate.status === "authorization-required" && + getAuthorizationResult(candidate.candidateId) !== undefined, + ); + const response = + input.stepInput?.inputResponses?.find((entry) => + batch?.requests.some( + (request) => request.requestId === entry.requestId && isApprovalRequest(request), + ), + ) ?? + (activeCandidate === undefined + ? undefined + : { optionId: "approve", requestId: activeCandidate.requestId }); + if (batch === undefined || response === undefined) { + return { kind: "continue", session: input.session, stepInput: input.stepInput }; + } + if (response.optionId === "cancel") { + if (!getResponseAuthRequiredRequestIds(input.session.state).has(response.requestId)) { + return { kind: "continue", session: input.session, stepInput: input.stepInput }; + } + const responder = buildCallbackContext().session.auth.current; + if (responder === null) { + return rejectWithoutCandidate( + input, + response.requestId, + "An authenticated responder is required.", + ); + } + const settled = cancelApprovalRequest({ + actor: responder, + requestId: response.requestId, + settledAt: input.now ?? Date.now(), + state: input.session.state, + }); + return { + kind: settled.result.kind === "settled" ? "continue" : "stale", + requestId: response.requestId, + session: { ...input.session, state: settled.state }, + stepInput: + settled.result.kind === "settled" + ? onlyInputResponse(input.stepInput, response) + : removeInputResponse(input.stepInput, response.requestId), + }; + } + if (response.optionId !== "approve") { + return { kind: "continue", session: input.session, stepInput: input.stepInput }; + } + + const request = batch.requests.find((entry) => entry.requestId === response.requestId)!; + if (!getResponseAuthRequiredRequestIds(input.session.state).has(request.requestId)) { + return { kind: "continue", session: input.session, stepInput: input.stepInput }; + } + + const responder = buildCallbackContext().session.auth.current; + if (responder === null) { + return rejectWithoutCandidate( + input, + response.requestId, + "An authenticated responder is required.", + ); + } + const now = input.now ?? Date.now(); + const candidateId = + activeCandidate?.candidateId ?? approvalCandidateId(request.requestId, responder); + const created = createApprovalCandidate({ + candidateId, + createdAt: now, + expiresAt: now + APPROVAL_CANDIDATE_TTL_MS, + requestId: request.requestId, + responder, + runtimeRevision: input.runtimeRevision, + state: input.session.state, + }); + let session = { ...input.session, state: created.state }; + if ( + activeCandidate === undefined && + (created.result.kind === "duplicate" || created.result.kind === "stale") + ) { + return { + candidateId: created.result.kind === "duplicate" ? candidateId : undefined, + kind: created.result.kind, + requestId: response.requestId, + session, + stepInput: removeInputResponse(input.stepInput, response.requestId), + }; + } + + const approval = input.tools.get(request.action.toolName)?.approval; + const authorizer = + approval !== undefined && typeof approval !== "function" + ? approval.authorizeResponse + : undefined; + if (authorizer === undefined) { + return failCandidate({ + candidateId, + now, + requestId: response.requestId, + safeReason: "Approval authorization is temporarily unavailable. Please try again.", + session, + stepInput: input.stepInput, + }); + } + + try { + const context = buildCallbackContext(); + const outcome = await withAuthorizerTimeout( + authorizer({ + auth: buildApprovalResponseAuth({ scope: candidateId }), + request: { + callId: request.action.callId, + requestId: request.requestId, + toolInput: request.action.input, + toolName: request.action.toolName, + }, + responder, + session: { + id: context.session.id, + initiator: context.session.auth.initiator, + parent: context.session.parent, + turn: context.session.turn, + }, + }), + ); + if (outcome !== "allowed") { + session = { + ...session, + state: finishApprovalCandidate({ + candidateId, + completedAt: now, + safeReason: outcome.safeReason, + state: session.state, + status: "rejected", + }), + }; + return { + candidateId, + kind: "rejected", + requestId: response.requestId, + safeReason: outcome.safeReason, + session, + stepInput: removeInputResponse(input.stepInput, response.requestId), + }; + } + const settled = settleAllowedCandidate({ candidateId, settledAt: now, state: session.state }); + return { + kind: settled.result.kind === "settled" ? "continue" : "stale", + requestId: response.requestId, + session: { ...session, state: settled.state }, + stepInput: + settled.result.kind === "settled" + ? onlyInputResponse(input.stepInput, response) + : removeInputResponse(input.stepInput, response.requestId), + }; + } catch (error) { + const authorization = await handleApprovalResponseAuthorizationError(error).catch( + () => undefined, + ); + if (isAuthorizationSignal(authorization)) { + const providerExpiresAt = authorization.challenges + .map((entry) => Date.parse(entry.challenge.expiresAt ?? "")) + .filter(Number.isFinite) + .sort((a, b) => a - b)[0]; + session = { + ...session, + state: markApprovalCandidateAuthorizationRequired({ + candidateId, + expiresAt: providerExpiresAt, + provider: authorization.challenges[0]?.challenge.displayName, + state: session.state, + }), + }; + return { + authorization: { + ...authorization, + challenges: authorization.challenges.map((challenge) => ({ + ...challenge, + candidateId, + })), + }, + candidateId, + kind: "authorization-required", + requestId: response.requestId, + session, + }; + } + return failCandidate({ + candidateId, + now, + requestId: response.requestId, + safeReason: "We couldn’t verify your approval. Please try again.", + session, + stepInput: input.stepInput, + }); + } +} + +function rejectWithoutCandidate( + input: { readonly session: HarnessSession; readonly stepInput?: StepInput }, + requestId: string, + safeReason: string, +): PendingApprovalAuthorizationResult { + return { + kind: "rejected", + requestId, + safeReason, + session: input.session, + stepInput: removeInputResponse(input.stepInput, requestId), + }; +} + +function failCandidate(input: { + readonly candidateId: string; + readonly now: number; + readonly requestId: string; + readonly safeReason: string; + readonly session: HarnessSession; + readonly stepInput?: StepInput; +}): PendingApprovalAuthorizationResult { + return { + candidateId: input.candidateId, + kind: "failed", + requestId: input.requestId, + safeReason: input.safeReason, + session: { + ...input.session, + state: finishApprovalCandidate({ + candidateId: input.candidateId, + completedAt: input.now, + state: input.session.state, + status: "failed", + }), + }, + stepInput: removeInputResponse(input.stepInput, input.requestId), + }; +} + +function onlyInputResponse(stepInput: StepInput | undefined, response: InputResponse): StepInput { + return { ...stepInput, inputResponses: [response] }; +} + +function removeInputResponse( + stepInput: StepInput | undefined, + requestId: string, +): StepInput | undefined { + if (stepInput?.inputResponses === undefined) return stepInput; + return { + ...stepInput, + inputResponses: stepInput.inputResponses.filter((response) => response.requestId !== requestId), + }; +} + +function approvalCandidateId(requestId: string, responder: SessionAuthContext): string { + const principal = [ + responder.authenticator, + responder.issuer ?? "", + responder.principalType, + responder.principalId, + ].join(":"); + return `${encodeCandidateIdPart(requestId)}.${encodeCandidateIdPart(principal)}`; +} + +function encodeCandidateIdPart(value: string): string { + return Array.from(value, (character) => character.codePointAt(0)!.toString(36)).join("-"); +} + +async function withAuthorizerTimeout(promise: Promise | T): Promise { + let timer: ReturnType | undefined; + try { + return await Promise.race([ + Promise.resolve(promise), + new Promise((_resolve, reject) => { + timer = setTimeout( + () => reject(new Error("Approval response authorizer timed out.")), + APPROVAL_AUTHORIZER_TIMEOUT_MS, + ); + }), + ]); + } finally { + if (timer !== undefined) clearTimeout(timer); + } +} diff --git a/packages/eve/src/harness/input-requests.test.ts b/packages/eve/src/harness/input-requests.test.ts index 815080613..3181766d7 100644 --- a/packages/eve/src/harness/input-requests.test.ts +++ b/packages/eve/src/harness/input-requests.test.ts @@ -6,8 +6,8 @@ import { SessionKey } from "#context/keys.js"; import { once } from "#public/tools/approval/approval-helpers.js"; import type { InputRequest } from "#runtime/input/types.js"; import type { HarnessToolDefinition } from "#harness/execute-tool.js"; +import { authorizePendingApprovalResponse } from "#harness/authorize-approval-response.js"; import { - authorizePendingApprovalResponse, consumeDeferredStepInput, createRuntimeToolCallActionFromToolCall, getApprovedTools, diff --git a/packages/eve/src/harness/input-requests.ts b/packages/eve/src/harness/input-requests.ts index 5dbd1f0fc..9cee18213 100644 --- a/packages/eve/src/harness/input-requests.ts +++ b/packages/eve/src/harness/input-requests.ts @@ -1,26 +1,6 @@ import type { ModelMessage } from "ai"; -import type { SessionAuthContext } from "#channel/types.js"; -import { buildCallbackContext } from "#context/build-callback-context.js"; -import { - buildApprovalResponseAuth, - handleApprovalResponseAuthorizationError, -} from "#execution/tool-auth.js"; -import { - cancelApprovalRequest, - createApprovalCandidate, - finishApprovalCandidate, - getApprovalAuditState, - markApprovalCandidateAuthorizationRequired, - settleAllowedCandidate, -} from "#harness/approval-candidates.js"; -import { - getAuthorizationResult, - isAuthorizationSignal, - type AuthorizationSignal, -} from "#harness/authorization.js"; -import type { HarnessToolMap } from "#harness/types.js"; - +import { getApprovalAuditState } from "#harness/approval-candidates.js"; import type { RuntimeToolCallActionRequest, RuntimeToolResultActionResult, @@ -38,8 +18,6 @@ import type { HarnessSession, SessionStateMap, StepInput } from "#harness/types. const PENDING_INPUT_BATCH_KEY = "eve.runtime.pendingInputBatch"; const APPROVED_TOOLS_KEY = "eve.runtime.hitl.approvedTools"; const DEFERRED_STEP_INPUT_KEY = "eve.runtime.deferredStepInput"; -const APPROVAL_AUTHORIZER_TIMEOUT_MS = 10_000; -const APPROVAL_CANDIDATE_TTL_MS = 10 * 60_000; const IGNORED_INPUT_REASON = "Ignored because the user continued without responding."; @@ -80,329 +58,7 @@ export interface RejectedActionBatch { type ApprovalTerminalStatus = "approved" | "denied" | "ignored" | "invalid"; -export type PendingApprovalAuthorizationResult = - | { readonly kind: "continue"; readonly session: HarnessSession; readonly stepInput?: StepInput } - | { - readonly authorization: AuthorizationSignal; - readonly candidateId: string; - readonly kind: "authorization-required"; - readonly requestId: string; - readonly session: HarnessSession; - } - | { - readonly candidateId?: string; - readonly kind: "rejected" | "duplicate" | "stale" | "failed"; - readonly requestId: string; - readonly safeReason?: string; - readonly session: HarnessSession; - readonly stepInput?: StepInput; - }; - -/** Runs response authorization for the first approval response in this delivery. */ -export async function authorizePendingApprovalResponse(input: { - readonly now?: number; - readonly runtimeRevision?: string; - readonly session: HarnessSession; - readonly stepInput?: StepInput; - readonly tools: HarnessToolMap; -}): Promise { - return await authorizePendingApprovalResponseInternal(input); -} - -async function authorizePendingApprovalResponseInternal(input: { - readonly now?: number; - readonly runtimeRevision?: string; - readonly session: HarnessSession; - readonly stepInput?: StepInput; - readonly tools: HarnessToolMap; -}): Promise { - const batch = getPendingInputBatch(input.session.state); - const activeCandidate = getApprovalAuditState(input.session.state).activeCandidates.find( - (candidate) => - candidate.status === "authorization-required" && - getAuthorizationResult(candidate.candidateId) !== undefined, - ); - const response = - input.stepInput?.inputResponses?.find((entry) => - batch?.requests.some( - (request) => request.requestId === entry.requestId && isApprovalRequest(request), - ), - ) ?? - (activeCandidate === undefined - ? undefined - : { optionId: "approve", requestId: activeCandidate.requestId }); - if (batch === undefined || response === undefined) { - return { kind: "continue", session: input.session, stepInput: input.stepInput }; - } - if (response.optionId === "cancel") { - if (!getResponseAuthRequiredRequestIds(input.session.state).has(response.requestId)) { - return { kind: "continue", session: input.session, stepInput: input.stepInput }; - } - const responder = buildCallbackContext().session.auth.current; - if (responder === null) { - return rejectWithoutCandidate( - input, - response.requestId, - "An authenticated responder is required.", - ); - } - const settled = cancelApprovalRequest({ - actor: responder, - requestId: response.requestId, - settledAt: input.now ?? Date.now(), - state: input.session.state, - }); - return { - kind: settled.result.kind === "settled" ? "continue" : "stale", - requestId: response.requestId, - session: { ...input.session, state: settled.state }, - stepInput: - settled.result.kind === "settled" - ? onlyInputResponse(input.stepInput, response) - : removeInputResponse(input.stepInput, response.requestId), - }; - } - if (response.optionId !== "approve") { - return { kind: "continue", session: input.session, stepInput: input.stepInput }; - } - - const request = batch.requests.find((entry) => entry.requestId === response.requestId)!; - if (!getResponseAuthRequiredRequestIds(input.session.state).has(request.requestId)) { - return { kind: "continue", session: input.session, stepInput: input.stepInput }; - } - - const responder = buildCallbackContext().session.auth.current; - if (responder === null) { - return rejectWithoutCandidate( - input, - response.requestId, - "An authenticated responder is required.", - ); - } - const now = input.now ?? Date.now(); - const candidateId = - activeCandidate?.candidateId ?? approvalCandidateId(request.requestId, responder); - const created = createApprovalCandidate({ - candidateId, - createdAt: now, - expiresAt: now + APPROVAL_CANDIDATE_TTL_MS, - requestId: request.requestId, - responder, - runtimeRevision: input.runtimeRevision, - state: input.session.state, - }); - let session = { ...input.session, state: created.state }; - if ( - activeCandidate === undefined && - (created.result.kind === "duplicate" || created.result.kind === "stale") - ) { - return { - candidateId: created.result.kind === "duplicate" ? candidateId : undefined, - kind: created.result.kind, - requestId: response.requestId, - session, - stepInput: removeInputResponse(input.stepInput, response.requestId), - }; - } - - const approval = input.tools.get(request.action.toolName)?.approval; - const authorizer = - approval !== undefined && typeof approval !== "function" - ? approval.authorizeResponse - : undefined; - if (authorizer === undefined) { - return failCandidate({ - candidateId, - now, - safeReason: "Approval authorization is temporarily unavailable. Please try again.", - session, - requestId: response.requestId, - stepInput: input.stepInput, - }); - } - - try { - const context = buildCallbackContext(); - const outcome = await withAuthorizerTimeout( - authorizer({ - auth: buildApprovalResponseAuth({ scope: candidateId }), - request: { - callId: request.action.callId, - requestId: request.requestId, - toolInput: request.action.input, - toolName: request.action.toolName, - }, - responder, - session: { - id: context.session.id, - initiator: context.session.auth.initiator, - parent: context.session.parent, - turn: context.session.turn, - }, - }), - ); - if (outcome !== "allowed") { - session = { - ...session, - state: finishApprovalCandidate({ - candidateId, - completedAt: now, - safeReason: outcome.safeReason, - state: session.state, - status: "rejected", - }), - }; - return { - candidateId, - kind: "rejected", - requestId: response.requestId, - safeReason: outcome.safeReason, - session, - stepInput: removeInputResponse(input.stepInput, response.requestId), - }; - } - const settled = settleAllowedCandidate({ candidateId, settledAt: now, state: session.state }); - return { - kind: settled.result.kind === "settled" ? "continue" : "stale", - requestId: response.requestId, - session: { ...session, state: settled.state }, - stepInput: - settled.result.kind === "settled" - ? onlyInputResponse(input.stepInput, response) - : removeInputResponse(input.stepInput, response.requestId), - }; - } catch (error) { - const authorization = await handleApprovalResponseAuthorizationError(error).catch( - () => undefined, - ); - if (isAuthorizationSignal(authorization)) { - const providerExpiresAt = authorization.challenges - .map((entry) => Date.parse(entry.challenge.expiresAt ?? "")) - .filter(Number.isFinite) - .sort((a, b) => a - b)[0]; - session = { - ...session, - state: markApprovalCandidateAuthorizationRequired({ - candidateId, - expiresAt: providerExpiresAt, - provider: authorization.challenges[0]?.challenge.displayName, - state: session.state, - }), - }; - return { - authorization: { - ...authorization, - challenges: authorization.challenges.map((challenge) => ({ - ...challenge, - candidateId, - })), - }, - candidateId, - kind: "authorization-required", - requestId: response.requestId, - session, - }; - } - return failCandidate({ - candidateId, - now, - safeReason: "We couldn’t verify your approval. Please try again.", - session, - requestId: response.requestId, - stepInput: input.stepInput, - }); - } -} - -function rejectWithoutCandidate( - input: { readonly session: HarnessSession; readonly stepInput?: StepInput }, - requestId: string, - safeReason: string, -): PendingApprovalAuthorizationResult { - return { - kind: "rejected", - requestId, - safeReason, - session: input.session, - stepInput: removeInputResponse(input.stepInput, requestId), - }; -} - -function failCandidate(input: { - readonly candidateId: string; - readonly now: number; - readonly safeReason: string; - readonly session: HarnessSession; - readonly requestId: string; - readonly stepInput?: StepInput; -}): PendingApprovalAuthorizationResult { - return { - candidateId: input.candidateId, - kind: "failed", - requestId: input.requestId, - safeReason: input.safeReason, - session: { - ...input.session, - state: finishApprovalCandidate({ - candidateId: input.candidateId, - completedAt: input.now, - state: input.session.state, - status: "failed", - }), - }, - stepInput: removeInputResponse(input.stepInput, input.requestId), - }; -} - -function onlyInputResponse(stepInput: StepInput | undefined, response: InputResponse): StepInput { - return { ...stepInput, inputResponses: [response] }; -} - -function removeInputResponse( - stepInput: StepInput | undefined, - requestId: string, -): StepInput | undefined { - if (stepInput?.inputResponses === undefined) return stepInput; - return { - ...stepInput, - inputResponses: stepInput.inputResponses.filter((response) => response.requestId !== requestId), - }; -} - -function approvalCandidateId(requestId: string, responder: SessionAuthContext): string { - const principal = [ - responder.authenticator, - responder.issuer ?? "", - responder.principalType, - responder.principalId, - ].join(":"); - return `${encodeCandidateIdPart(requestId)}.${encodeCandidateIdPart(principal)}`; -} - -function encodeCandidateIdPart(value: string): string { - return Array.from(value, (character) => character.codePointAt(0)!.toString(36)).join("-"); -} - -async function withAuthorizerTimeout(promise: Promise | T): Promise { - let timer: ReturnType | undefined; - try { - return await Promise.race([ - Promise.resolve(promise), - new Promise((_resolve, reject) => { - timer = setTimeout( - () => reject(new Error("Approval response authorizer timed out.")), - APPROVAL_AUTHORIZER_TIMEOUT_MS, - ); - }), - ]); - } finally { - if (timer !== undefined) clearTimeout(timer); - } -} - -/** - * Returns true when the step input carries user-facing turn input. - */ +/** Returns true when the step input carries user-facing turn input. */ export function hasStepInput(input?: StepInput): boolean { if (input === undefined) { return false; @@ -411,10 +67,6 @@ export function hasStepInput(input?: StepInput): boolean { return input.message !== undefined || (input.inputResponses?.length ?? 0) > 0; } -// --------------------------------------------------------------------------- -// Deferred step input -// --------------------------------------------------------------------------- - /** * Merges any queued follow-up input into the current step input and clears it * from session state. @@ -712,13 +364,15 @@ export function getPendingInputRequestIds(state: SessionStateMap | undefined): R return new Set(getPendingInputBatch(state)?.requests.map((request) => request.requestId)); } -function getResponseAuthRequiredRequestIds( +export function getResponseAuthRequiredRequestIds( state: SessionStateMap | undefined, ): ReadonlySet { return new Set(getPendingInputBatch(state)?.responseAuthRequiredRequestIds ?? []); } -function getPendingInputBatch(state: SessionStateMap | undefined): PendingInputBatch | undefined { +export function getPendingInputBatch( + state: SessionStateMap | undefined, +): PendingInputBatch | undefined { const value = state?.[PENDING_INPUT_BATCH_KEY]; if (typeof value !== "object" || value === null) { @@ -766,10 +420,6 @@ function clearPendingInputBatch(session: HarnessSession): HarnessSession { return { ...session, state: Object.keys(state).length > 0 ? state : undefined }; } -// --------------------------------------------------------------------------- -// Deferred step input state -// --------------------------------------------------------------------------- - function getDeferredStepInput(session: HarnessSession): StepInput | undefined { return session.state?.[DEFERRED_STEP_INPUT_KEY] as StepInput | undefined; } @@ -1016,7 +666,6 @@ function buildToolResponsePartsForRequest( ]; } -/** Shared approval predicate: a request whose options are exactly `allow` / `cancel`. */ export function isApprovalRequest(request: InputRequest): boolean { return ( request.options?.length === 2 && @@ -1025,10 +674,6 @@ export function isApprovalRequest(request: InputRequest): boolean { ); } -// --------------------------------------------------------------------------- -// Tool call helpers -// --------------------------------------------------------------------------- - /** * Creates a runtime tool-call action shape from an AI SDK tool call. */ diff --git a/packages/eve/src/harness/tool-loop.ts b/packages/eve/src/harness/tool-loop.ts index d21b7cb56..673484492 100644 --- a/packages/eve/src/harness/tool-loop.ts +++ b/packages/eve/src/harness/tool-loop.ts @@ -105,8 +105,8 @@ import { import { createToolResultMessagePartFromToolError } from "#harness/action-result-helpers.js"; import { buildTelemetryRuntimeContext } from "#harness/instrumentation-runtime-context.js"; import { getApprovalAuditState } from "#harness/approval-candidates.js"; +import { authorizePendingApprovalResponse } from "#harness/authorize-approval-response.js"; import { - authorizePendingApprovalResponse, consumeDeferredStepInput, getApprovedTools, getPendingInputRequestIds, From 125a302f67d7470301e5ed34fe5ded7ae65fa070 Mon Sep 17 00:00:00 2001 From: benpankow Date: Wed, 29 Jul 2026 13:16:49 -0700 Subject: [PATCH 2/2] fix(eve): preserve approval authorization correlation Signed-off-by: benpankow --- .../eve/src/harness/approval-candidates.test.ts | 2 ++ packages/eve/src/harness/approval-candidates.ts | 3 +++ .../src/harness/authorize-approval-response.ts | 16 +++++++++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/packages/eve/src/harness/approval-candidates.test.ts b/packages/eve/src/harness/approval-candidates.test.ts index 498a284a7..e6ac2d94f 100644 --- a/packages/eve/src/harness/approval-candidates.test.ts +++ b/packages/eve/src/harness/approval-candidates.test.ts @@ -92,6 +92,7 @@ describe("approval candidate state", () => { it("tracks authorization-required state and provider expiry", () => { const first = create({ candidateId: "candidate-1", principalId: "U1" }); const state = markApprovalCandidateAuthorizationRequired({ + authorizationName: "candidate-1:github", candidateId: "candidate-1", expiresAt: 500, provider: "GitHub", @@ -99,6 +100,7 @@ describe("approval candidate state", () => { }); expect(getApprovalAuditState(state).activeCandidates[0]).toMatchObject({ + authorizationName: "candidate-1:github", expiresAt: 500, provider: "GitHub", status: "authorization-required", diff --git a/packages/eve/src/harness/approval-candidates.ts b/packages/eve/src/harness/approval-candidates.ts index b754d0139..78768b970 100644 --- a/packages/eve/src/harness/approval-candidates.ts +++ b/packages/eve/src/harness/approval-candidates.ts @@ -45,6 +45,7 @@ interface ActiveApprovalCandidate { readonly requestId: string; readonly responder: ApprovalResponderIdentity; readonly status: "pending" | "authorization-required"; + readonly authorizationName?: string; readonly createdAt: number; readonly expiresAt: number; readonly provider?: string; @@ -117,6 +118,7 @@ export function createApprovalCandidate(input: { /** Marks a candidate as waiting on a private authorization challenge. */ export function markApprovalCandidateAuthorizationRequired(input: { + readonly authorizationName: string; readonly candidateId: string; readonly expiresAt?: number; readonly provider?: string; @@ -127,6 +129,7 @@ export function markApprovalCandidateAuthorizationRequired(input: { if (candidate === undefined) return input.state; const nextCandidate: ActiveApprovalCandidate = { ...candidate, + authorizationName: input.authorizationName, expiresAt: input.expiresAt ?? candidate.expiresAt, provider: input.provider, status: "authorization-required", diff --git a/packages/eve/src/harness/authorize-approval-response.ts b/packages/eve/src/harness/authorize-approval-response.ts index 6e915f384..99b455893 100644 --- a/packages/eve/src/harness/authorize-approval-response.ts +++ b/packages/eve/src/harness/authorize-approval-response.ts @@ -7,6 +7,7 @@ import { import { cancelApprovalRequest, createApprovalCandidate, + expireApprovalCandidates, finishApprovalCandidate, getApprovalAuditState, markApprovalCandidateAuthorizationRequired, @@ -54,11 +55,20 @@ export async function authorizePendingApprovalResponse(input: { readonly stepInput?: StepInput; readonly tools: HarnessToolMap; }): Promise { + const now = input.now ?? Date.now(); + input = { + ...input, + session: { + ...input.session, + state: expireApprovalCandidates({ now, state: input.session.state }), + }, + }; const batch = getPendingInputBatch(input.session.state); const activeCandidate = getApprovalAuditState(input.session.state).activeCandidates.find( (candidate) => candidate.status === "authorization-required" && - getAuthorizationResult(candidate.candidateId) !== undefined, + candidate.authorizationName !== undefined && + getAuthorizationResult(candidate.authorizationName) !== undefined, ); const response = input.stepInput?.inputResponses?.find((entry) => @@ -87,7 +97,7 @@ export async function authorizePendingApprovalResponse(input: { const settled = cancelApprovalRequest({ actor: responder, requestId: response.requestId, - settledAt: input.now ?? Date.now(), + settledAt: now, state: input.session.state, }); return { @@ -117,7 +127,6 @@ export async function authorizePendingApprovalResponse(input: { "An authenticated responder is required.", ); } - const now = input.now ?? Date.now(); const candidateId = activeCandidate?.candidateId ?? approvalCandidateId(request.requestId, responder); const created = createApprovalCandidate({ @@ -221,6 +230,7 @@ export async function authorizePendingApprovalResponse(input: { session = { ...session, state: markApprovalCandidateAuthorizationRequired({ + authorizationName: authorization.challenges[0]?.name ?? candidateId, candidateId, expiresAt: providerExpiresAt, provider: authorization.challenges[0]?.challenge.displayName,