From e20c45b0381278c35a1979f28c2bd29a1e39e218 Mon Sep 17 00:00:00 2001 From: benpankow Date: Wed, 29 Jul 2026 14:20:25 -0700 Subject: [PATCH] feat(eve): proxy authorized approvals from subagents Signed-off-by: benpankow --- packages/eve/src/channel/types.ts | 12 +++-- .../eve/src/execution/subagent-adapter.ts | 6 +++ .../subagent-auth-proxy.integration.test.ts | 46 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/packages/eve/src/channel/types.ts b/packages/eve/src/channel/types.ts index b21987e82..07a78d1d7 100644 --- a/packages/eve/src/channel/types.ts +++ b/packages/eve/src/channel/types.ts @@ -1,6 +1,6 @@ import type { UserContent } from "ai"; -import type { UnstampedMessageStreamEvent, MessageStreamEvent } from "#protocol/message.js"; +import type { MessageStreamEvent, UnstampedMessageStreamEvent } from "#protocol/message.js"; import type { CancelTurnStatus } from "#protocol/cancel-turn.js"; import type { RunMode } from "#shared/run-mode.js"; import type { RuntimeActionResult } from "#runtime/actions/types.js"; @@ -198,10 +198,16 @@ export interface SubagentInputRequestHookPayload { readonly subagentName: string; } -/** Authorization lifecycle event forwarded from a delegated child. */ +/** Responder-specific lifecycle event forwarded from a delegated child. */ export type SubagentAuthorizationEvent = Extract< UnstampedMessageStreamEvent, - { type: "authorization.required" | "authorization.completed" } + { + type: + | "approval.candidate" + | "approval.settled" + | "authorization.required" + | "authorization.completed"; + } >; /** diff --git a/packages/eve/src/execution/subagent-adapter.ts b/packages/eve/src/execution/subagent-adapter.ts index 7661a356c..e90620da2 100644 --- a/packages/eve/src/execution/subagent-adapter.ts +++ b/packages/eve/src/execution/subagent-adapter.ts @@ -73,6 +73,12 @@ export function isSubagentAdapterState(value: unknown): value is SubagentAdapter */ export const SUBAGENT_ADAPTER: ChannelAdapter = { kind: SUBAGENT_ADAPTER_KIND, + async "approval.candidate"(data, ctx) { + await forwardSubagentAuthorizationEvent({ data, type: "approval.candidate" }, ctx); + }, + async "approval.settled"(data, ctx) { + await forwardSubagentAuthorizationEvent({ data, type: "approval.settled" }, ctx); + }, async "authorization.required"(data, ctx) { await forwardSubagentAuthorizationEvent({ data, type: "authorization.required" }, ctx); }, diff --git a/packages/eve/src/execution/subagent-auth-proxy.integration.test.ts b/packages/eve/src/execution/subagent-auth-proxy.integration.test.ts index 6a6c43c2c..3a8f045e6 100644 --- a/packages/eve/src/execution/subagent-auth-proxy.integration.test.ts +++ b/packages/eve/src/execution/subagent-auth-proxy.integration.test.ts @@ -136,6 +136,52 @@ function decodeEvent(chunk: Uint8Array): MessageStreamEvent { } describe("subagent authorization proxy", () => { + it("preserves approval candidate and settlement events", async () => { + const parentSessionId = "parent-approval-session"; + const session = createSession(parentSessionId); + const { ctx } = buildContext({ adapter: authorizationAdapter, sessionId: parentSessionId }); + const chunks: Uint8Array[] = []; + const parentWritable = createCapturingWritable(chunks); + const candidateEvent: SubagentAuthorizationEvent = { + data: { + candidateId: "candidate-1", + outcome: "pending", + requestId: "approval-1", + responderPrincipalId: "slack:T1:U1", + sequence: 0, + stepIndex: 1, + turnId: "child-turn", + }, + type: "approval.candidate", + }; + const settledEvent: SubagentAuthorizationEvent = { + data: { + outcome: "approved", + requestId: "approval-1", + responderPrincipalId: "slack:T1:U1", + sequence: 0, + stepIndex: 2, + turnId: "child-turn", + }, + type: "approval.settled", + }; + + await emitProxiedSubagentEvent({ + ctx, + durableSession: projectToDurableSession(session), + hookPayload: authorizationPayload(candidateEvent), + parentWritable, + }); + await emitProxiedSubagentEvent({ + ctx, + durableSession: projectToDurableSession(session), + hookPayload: authorizationPayload(settledEvent), + parentWritable, + }); + + expect(decodeEvent(chunks[0]!)).toMatchObject(candidateEvent); + expect(decodeEvent(chunks[1]!)).toMatchObject(settledEvent); + }); it("preserves events and parent adapter state across required/completed steps", async () => { const parentSessionId = "parent-session"; const session = createSession(parentSessionId);