diff --git a/packages/eve/src/harness/authorize-approval-response.ts b/packages/eve/src/harness/authorize-approval-response.ts index 99b455893..936686fd5 100644 --- a/packages/eve/src/harness/authorize-approval-response.ts +++ b/packages/eve/src/harness/authorize-approval-response.ts @@ -1,5 +1,7 @@ import type { SessionAuthContext } from "#channel/types.js"; import { buildCallbackContext } from "#context/build-callback-context.js"; +import { contextStorage } from "#context/container.js"; +import { AuthKey } from "#context/keys.js"; import { buildApprovalResponseAuth, handleApprovalResponseAuthorizationError, @@ -119,7 +121,10 @@ export async function authorizePendingApprovalResponse(input: { return { kind: "continue", session: input.session, stepInput: input.stepInput }; } - const responder = buildCallbackContext().session.auth.current; + const responder = + activeCandidate === undefined + ? buildCallbackContext().session.auth.current + : responderFromCandidate(activeCandidate.responder); if (responder === null) { return rejectWithoutCandidate( input, @@ -129,6 +134,9 @@ export async function authorizePendingApprovalResponse(input: { } const candidateId = activeCandidate?.candidateId ?? approvalCandidateId(request.requestId, responder); + if (activeCandidate !== undefined) { + contextStorage.getStore()?.setVirtualContext(AuthKey, responder); + } const created = createApprovalCandidate({ candidateId, createdAt: now, @@ -317,6 +325,12 @@ function removeInputResponse( }; } +function responderFromCandidate( + responder: Pick, +): SessionAuthContext { + return { ...responder, attributes: {} }; +} + function approvalCandidateId(requestId: string, responder: SessionAuthContext): string { const principal = [ responder.authenticator, diff --git a/packages/eve/src/harness/input-requests.test.ts b/packages/eve/src/harness/input-requests.test.ts index 3181766d7..7b5c46d6c 100644 --- a/packages/eve/src/harness/input-requests.test.ts +++ b/packages/eve/src/harness/input-requests.test.ts @@ -2,7 +2,8 @@ import { jsonSchema, type ModelMessage } from "ai"; import { describe, expect, it } from "vitest"; import { ContextContainer, contextStorage } from "#context/container.js"; -import { SessionKey } from "#context/keys.js"; +import { AuthKey, SessionKey } from "#context/keys.js"; +import { PendingAuthorizationResultKey } from "#harness/authorization.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"; @@ -1081,6 +1082,84 @@ describe("authorizePendingApprovalResponse", () => { ).toBe("unresolved"); }); + it("restores the persisted candidate responder before resumed policy runs", async () => { + const candidateState = { + "eve.runtime.hitl.approvalState": { + activeCandidates: { + "candidate-1": { + authorizationName: "candidate-1:github", + candidateId: "candidate-1", + createdAt: 100, + expiresAt: 700, + requestId: "approval-1", + responder: { + authenticator: "slack-webhook", + issuer: "slack:T1", + principalId: "U_ORIGINAL", + principalType: "user", + }, + status: "authorization-required", + }, + }, + candidateHistory: [], + settlements: {}, + }, + }; + const session = { + ...pendingSession(), + state: { ...pendingSession().state, ...candidateState }, + }; + const tools: HarnessToolMap = new Map([ + [ + "create_issue", + { + approval: { + authorizeResponse: ({ responder }) => + responder.principalId === "U_ORIGINAL" + ? "allowed" + : { safeReason: "wrong responder", status: "rejected" }, + policy: () => "user-approval", + }, + description: "Create issue", + inputSchema: jsonSchema({ type: "object" }), + name: "create_issue", + }, + ], + ]); + const ctx = new ContextContainer(); + ctx.set(AuthKey, { + attributes: {}, + authenticator: "slack-webhook", + issuer: "slack:T1", + principalId: "U_LATEST", + principalType: "user", + }); + ctx.set(PendingAuthorizationResultKey, [ + { + callback: { method: "GET", params: { code: "test" } }, + hookUrl: "https://eve.example/callback", + name: "candidate-1:github", + }, + ]); + ctx.set(SessionKey, { + auth: { current: ctx.require(AuthKey), initiator: null }, + sessionId: "sess-test", + turn: { id: "turn-test", sequence: 1 }, + }); + + const result = await contextStorage.run(ctx, () => + authorizePendingApprovalResponse({ + now: 200, + session, + stepInput: { inputResponses: [{ optionId: "approve", requestId: "approval-1" }] }, + tools, + }), + ); + + expect(result.kind).toBe("continue"); + expect(ctx.get(AuthKey)?.principalId).toBe("U_ORIGINAL"); + }); + it("cancels without running the response authorizer", async () => { const tools: HarnessToolMap = new Map([ [