From 6a2904ac63aba86210e4eb9bcdd42e026a8b8fa9 Mon Sep 17 00:00:00 2001 From: benpankow Date: Wed, 29 Jul 2026 12:58:18 -0700 Subject: [PATCH] feat(eve): correlate private approval authorization Signed-off-by: benpankow --- packages/eve/src/execution/workflow-steps.ts | 21 +++++++++++++++---- packages/eve/src/harness/authorization.ts | 2 ++ packages/eve/src/harness/input-requests.ts | 8 ++++++- packages/eve/src/harness/tool-loop.ts | 1 + packages/eve/src/protocol/message.ts | 10 +++++++++ .../public/channels/slack/defaults.test.ts | 13 ++++++++++++ .../eve/src/public/channels/slack/defaults.ts | 4 ++-- 7 files changed, 52 insertions(+), 7 deletions(-) diff --git a/packages/eve/src/execution/workflow-steps.ts b/packages/eve/src/execution/workflow-steps.ts index c4b98ff26..1bba1a804 100644 --- a/packages/eve/src/execution/workflow-steps.ts +++ b/packages/eve/src/execution/workflow-steps.ts @@ -161,11 +161,19 @@ export async function turnStep(rawInput: TurnStepInput): Promise + | Array<{ + candidateId?: string; + name: string; + authorization: ConnectionAuthorizationChallenge; + }> | undefined; if (pendingAuth && input.input?.kind === "deliver") { const authResults: Array<{ name: string } & AuthorizationResult> = []; - const completed: Array<{ name: string; authorization: ConnectionAuthorizationChallenge }> = []; + const completed: Array<{ + candidateId?: string; + name: string; + authorization: ConnectionAuthorizationChallenge; + }> = []; const remainingPayloads: DeliverPayload[] = []; for (const payload of input.input.payloads) { const cb = payload["authorizationCallback"] as @@ -180,7 +188,11 @@ export async function turnStep(rawInput: TurnStepInput): Promise ({ + candidateId: entry.candidateId, name: entry.name, challenge: entry.challenge, hookUrl: entry.hookUrl, diff --git a/packages/eve/src/harness/input-requests.ts b/packages/eve/src/harness/input-requests.ts index 95135da7e..5dbd1f0fc 100644 --- a/packages/eve/src/harness/input-requests.ts +++ b/packages/eve/src/harness/input-requests.ts @@ -290,7 +290,13 @@ async function authorizePendingApprovalResponseInternal(input: { }), }; return { - authorization, + authorization: { + ...authorization, + challenges: authorization.challenges.map((challenge) => ({ + ...challenge, + candidateId, + })), + }, candidateId, kind: "authorization-required", requestId: response.requestId, diff --git a/packages/eve/src/harness/tool-loop.ts b/packages/eve/src/harness/tool-loop.ts index d3e23e2ac..d21b7cb56 100644 --- a/packages/eve/src/harness/tool-loop.ts +++ b/packages/eve/src/harness/tool-loop.ts @@ -594,6 +594,7 @@ export function createToolLoopHarness(config: ToolLoopHarnessConfig): StepFn { await emit( createAuthorizationRequiredEvent({ authorization: challenge.challenge, + candidateId: authorized.candidateId, description: challenge.challenge.instructions ?? `Authorization required for ${challenge.name}`, name: challenge.name, diff --git a/packages/eve/src/protocol/message.ts b/packages/eve/src/protocol/message.ts index 9cfaa4d31..4762eab1a 100644 --- a/packages/eve/src/protocol/message.ts +++ b/packages/eve/src/protocol/message.ts @@ -529,6 +529,7 @@ export interface CompactionCompletedStreamEvent { export interface AuthorizationRequiredStreamEvent { data: { authorization?: ConnectionAuthorizationChallenge; + candidateId?: string; description: string; name: string; sequence: number; @@ -560,6 +561,7 @@ export type ConnectionAuthorizationOutcome = AuthorizationOutcome; */ export interface AuthorizationCompletedStreamEvent { data: { + candidateId?: string; /** * The challenge from the matching `authorization.required` event, * journaled across the park. Lets channels keep rendering the @@ -970,6 +972,7 @@ export function createActionsRequestedEvent(input: { */ export function createAuthorizationRequiredEvent(input: { readonly authorization?: ConnectionAuthorizationChallenge; + readonly candidateId?: string; readonly description: string; readonly name: string; readonly sequence: number; @@ -987,6 +990,9 @@ export function createAuthorizationRequiredEvent(input: { if (input.authorization !== undefined) { data.authorization = input.authorization; } + if (input.candidateId !== undefined) { + data.candidateId = input.candidateId; + } if (input.webhookUrl !== undefined) { data.webhookUrl = input.webhookUrl; } @@ -1003,6 +1009,7 @@ export function createAuthorizationRequiredEvent(input: { */ export function createAuthorizationCompletedEvent(input: { readonly authorization?: ConnectionAuthorizationChallenge; + readonly candidateId?: string; readonly name: string; readonly outcome: AuthorizationOutcome; readonly reason?: string; @@ -1020,6 +1027,9 @@ export function createAuthorizationCompletedEvent(input: { if (input.authorization !== undefined) { data.authorization = input.authorization; } + if (input.candidateId !== undefined) { + data.candidateId = input.candidateId; + } if (input.reason !== undefined) { data.reason = input.reason; } diff --git a/packages/eve/src/public/channels/slack/defaults.test.ts b/packages/eve/src/public/channels/slack/defaults.test.ts index 38cabe867..8c9b62402 100644 --- a/packages/eve/src/public/channels/slack/defaults.test.ts +++ b/packages/eve/src/public/channels/slack/defaults.test.ts @@ -136,6 +136,19 @@ describe("defaultEvents authorization.required", () => { expect(channel.state.pendingAuthMessageTs).toEqual({ notion: "ts1" }); }); + it("does not post a public status for candidate-scoped authorization", async () => { + const { channel, post, postEphemeral } = buildChannelStub({ triggeringUserId: "U777" }); + + await defaultEvents["authorization.required"]!( + { ...authRequiredEvent(), candidateId: "candidate-1" }, + channel, + sessionCtx, + ); + + expect(post).not.toHaveBeenCalled(); + expect(postEphemeral).toHaveBeenCalledTimes(1); + }); + it("targets the current Slack caller instead of stale channel state", async () => { const { channel, postEphemeral } = buildChannelStub({ triggeringUserId: "U_FIRST" }); const currentCaller = sessionContext({ diff --git a/packages/eve/src/public/channels/slack/defaults.ts b/packages/eve/src/public/channels/slack/defaults.ts index e92ccae4f..6ad851bf0 100644 --- a/packages/eve/src/public/channels/slack/defaults.ts +++ b/packages/eve/src/public/channels/slack/defaults.ts @@ -300,7 +300,7 @@ export const defaultEvents: SlackChannelInternalEvents = { // the session is blocked and later see it complete. The challenge // itself remains private. const pending = channel.state.pendingAuthMessageTs ?? {}; - if (pending[event.name] === undefined) { + if (event.candidateId === undefined && pending[event.name] === undefined) { const publicText = buildAuthRequiredPublicText({ displayName, hasUser: triggeringUserId !== null, @@ -350,7 +350,7 @@ export const defaultEvents: SlackChannelInternalEvents = { async "authorization.completed"(event, channel, _ctx) { const displayName = event.authorization?.displayName ?? formatConnectionDisplayName(event.name); - if (event.outcome === "authorized") { + if (event.outcome === "authorized" && event.candidateId === undefined) { await channel.thread.startTyping(`Connected to ${displayName}. Resuming...`); }