feat(eve): propagate remote-callee authorization events to the caller#1167
Draft
ruiconti wants to merge 5 commits into
Draft
feat(eve): propagate remote-callee authorization events to the caller#1167ruiconti wants to merge 5 commits into
ruiconti wants to merge 5 commits into
Conversation
…llbacks
Session callbacks now carry an event envelope discriminated by
event.status ("notification" | "termination"; "working" and
"input_required" are reserved and rejected). A remote callee forwards
its authorization.required/authorization.completed events to the caller
as notification callbacks, which the caller re-emits on its own stream
without resolving the pending call — the callback-URL analog of the
local subagent adapter's subagent-authorization-event proxying. A parent
that is itself a remote callee relays proxied child events one more hop
up its own callback URL.
Notification delivery is best-effort (an unreachable caller is logged on
the callee and never fails its turn); termination stays the single
durable resume path. The wire shape changed: mixed-version caller/callee
deployments reject each other's callbacks.
parseSessionCallback is renamed to parseCallbackMetadata to distinguish
the create-session callback metadata from the new callback events.
Signed-off-by: Rui Conti <ruiconti@gmail.com>
requestAuthorization, getAuthorizationResult, getHookUrl, and isAuthorizationSignal (plus their types) were documented as public in harness/authorization.ts but reachable only from framework internals. A tool's execute can now suspend the turn on an external authorization callback and read the parsed callback params on re-execution — the same lifecycle interactive connection auth uses, without requiring a user-principal session or a connection definition. Documented at docs/tools/authorization.md. The tool capability contract gains the new exports: epoch 2 -> 3, with the epoch-2 authoring surface retained as a compatibility fixture. Signed-off-by: Rui Conti <ruiconti@gmail.com>
Models routinely emit outputSchema: {} for the optional per-call param
on the lowered subagent tool. Local delegation filters non-empty objects
(subagent-tool.ts) but remote dispatch forwarded the empty object, which
forced the callee into structured-output task completion and failed the
call with OUTPUT_SCHEMA_NOT_FULFILLED even when the callee produced a
correct prose result. Remote dispatch now applies the same non-empty
filter, falling back to the definition's declared schema.
Signed-off-by: Rui Conti <ruiconti@gmail.com>
When one model step requested both an approval-gated tool call and a runtime action (subagent or remote-agent call), the park classification returned on the runtime-action batch before the input-request park ran, silently dropping the approval: input.requested was never emitted, and the action-result resume called the model with the gated call's tool_use still unanswered — AI_MissingToolResultsError, failing the turn. The classification now persists both batches (the input batch with empty responseMessages, since the action batch owns the step's assistant messages) and emits input.requested immediately. On the action-result resume, executeStepBody's existing staged resolution re-parks on the still-unanswered batch, committing the resolved action results to history and closing the turn with the standard waiting boundary. Signed-off-by: Rui Conti <ruiconti@gmail.com>
… HITL collision New agent-remote-callbacks fixture whose probe-remote subagent calls the fixture's own deployment (WORKFLOW_LOCAL_BASE_URL locally, VERCEL_URL on Vercel; loopback callback hosts are allowed by the SSRF guard), keeping the suite self-contained while exercising the full remote dispatch and session-callback wire. Three evals: - auth-propagation: the callee parks on requestAuthorization, the forwarded authorization.required surfaces on the caller's stream, the eval completes the webhookUrl with a ?code, and the code round-trips through the callee's resume into the caller's final reply. - hitl-collision: one turn holds a pending release_gate approval and an in-flight remote call; the callee's auth completes (deterministic <childSessionId>:auth hook) while the caller is parked for input, and both resolve independently. Caught the dropped-approval harness bug. - notification-propagation: the callee's local credential-probe subagent relays its authorization events two hops to the caller, each exactly once and in order, without resolving the pending call. Also caught the empty-outputSchema dispatch bug. The hitl eval uses plain fetch for the hook: Client.fetch treats its whole path argument as pathname, so a ?code= query would be percent-encoded into the path and 404 the hook. Signed-off-by: Rui Conti <ruiconti@gmail.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
ruiconti
marked this pull request as draft
July 24, 2026 13:08
ruiconti
added a commit
that referenced
this pull request
Jul 24, 2026
…on normalization The doc jumped from stream-backbone framing straight into the v1 design without saying what the proposal is. Lead with it now: the harness funnels every event a single way and all consumers, channels included, consume from a durable delivery step, trading one extra hop per event for durable redelivery. The failure bullets become an explicit Why list and gain the missing redelivery reason (failed or refused deliveries are logged and dropped; nothing durable exists to redeliver from). A new Comparison artifacts section names the two runnable references for the decision: reopened #1167 as the baseline that pipes notifications through today's continuation entry point (to be stripped of its eve/tools authorization exposure), and a prototype of the consumer design against the same evals. Signed-off-by: Rui Conti <ruiconti@gmail.com>
This was referenced Jul 24, 2026
ruiconti
added a commit
that referenced
this pull request
Jul 24, 2026
"Notification" is #1167's wire vocabulary — one status class on the callback envelope — not the unit this proposal normalizes. Organizing the plan around it undersold the change: the funnel and the durable delivery step apply to events as such; which classes are admitted (authorization.* in v1, working/HITL behind the frequency rule) is an implementation detail of a given lane. Rename research/notification-consumers.md to event-consumers.md and sweep the design identifiers with it: the companion run is workflow//eve//eventConsumer, the hook is <sessionId>:events, the stream section is "The propagated event". "notification" survives only where it is factual: the status field on #1167's callback envelope. Signed-off-by: Rui Conti <ruiconti@gmail.com>
ruiconti
added a commit
that referenced
this pull request
Jul 24, 2026
Every failure bullet now carries commit-pinned code references (413ce48, the #1167 baseline) plus a concrete example: the drop window is the authorization.completed POST racing the terminal callback, verifiable with one curl against an unparked session's callback URL; rendering-only is defined by example (a child's authorization.required becoming the channel's sign-in prompt). Two claims changed under the trace. "Channels implement the same rendering twice" was wrong: both transports converge on one proxy step at the caller's inbox (turn-workflow.ts:297), so rendering happens once — what exists twice is the forwarding (subagent-adapter.ts vs session-callback-notification.ts, self-described as its "callback-URL analog"), with divergent failure semantics. "Nothing redelivers" holds, but the reason is subtler than "dropped at the producer": producers do emit from journaled steps, yet delivery failures are deliberately swallowed before the journal sees them (adapter.ts:246, session-callback-notification.ts:61), and a producer-side retry could not fix a deterministic "not pending" anyway. Also adds an event-indexing open question: the consumer lane is where an indexer would attach, but v1's best-effort callee wire means a parent-stream index cannot claim cross-deployment completeness. Signed-off-by: Rui Conti <ruiconti@gmail.com>
ruiconti
added a commit
that referenced
this pull request
Jul 25, 2026
…on normalization The doc jumped from stream-backbone framing straight into the v1 design without saying what the proposal is. Lead with it now: the harness funnels every event a single way and all consumers, channels included, consume from a durable delivery step, trading one extra hop per event for durable redelivery. The failure bullets become an explicit Why list and gain the missing redelivery reason (failed or refused deliveries are logged and dropped; nothing durable exists to redeliver from). A new Comparison artifacts section names the two runnable references for the decision: reopened #1167 as the baseline that pipes notifications through today's continuation entry point (to be stripped of its eve/tools authorization exposure), and a prototype of the consumer design against the same evals. Signed-off-by: Rui Conti <ruiconti@gmail.com>
ruiconti
added a commit
that referenced
this pull request
Jul 25, 2026
"Notification" is #1167's wire vocabulary — one status class on the callback envelope — not the unit this proposal normalizes. Organizing the plan around it undersold the change: the funnel and the durable delivery step apply to events as such; which classes are admitted (authorization.* in v1, working/HITL behind the frequency rule) is an implementation detail of a given lane. Rename research/notification-consumers.md to event-consumers.md and sweep the design identifiers with it: the companion run is workflow//eve//eventConsumer, the hook is <sessionId>:events, the stream section is "The propagated event". "notification" survives only where it is factual: the status field on #1167's callback envelope. Signed-off-by: Rui Conti <ruiconti@gmail.com>
ruiconti
added a commit
that referenced
this pull request
Jul 25, 2026
Every failure bullet now carries commit-pinned code references (413ce48, the #1167 baseline) plus a concrete example: the drop window is the authorization.completed POST racing the terminal callback, verifiable with one curl against an unparked session's callback URL; rendering-only is defined by example (a child's authorization.required becoming the channel's sign-in prompt). Two claims changed under the trace. "Channels implement the same rendering twice" was wrong: both transports converge on one proxy step at the caller's inbox (turn-workflow.ts:297), so rendering happens once — what exists twice is the forwarding (subagent-adapter.ts vs session-callback-notification.ts, self-described as its "callback-URL analog"), with divergent failure semantics. "Nothing redelivers" holds, but the reason is subtler than "dropped at the producer": producers do emit from journaled steps, yet delivery failures are deliberately swallowed before the journal sees them (adapter.ts:246, session-callback-notification.ts:61), and a producer-side retry could not fix a deterministic "not pending" anyway. Also adds an event-indexing open question: the consumer lane is where an indexer would attach, but v1's best-effort callee wire means a parent-stream index cannot claim cross-deployment completeness. Signed-off-by: Rui Conti <ruiconti@gmail.com>
ruiconti
added a commit
that referenced
this pull request
Jul 25, 2026
…on normalization The doc jumped from stream-backbone framing straight into the v1 design without saying what the proposal is. Lead with it now: the harness funnels every event a single way and all consumers, channels included, consume from a durable delivery step, trading one extra hop per event for durable redelivery. The failure bullets become an explicit Why list and gain the missing redelivery reason (failed or refused deliveries are logged and dropped; nothing durable exists to redeliver from). A new Comparison artifacts section names the two runnable references for the decision: reopened #1167 as the baseline that pipes notifications through today's continuation entry point (to be stripped of its eve/tools authorization exposure), and a prototype of the consumer design against the same evals. Signed-off-by: Rui Conti <ruiconti@gmail.com>
ruiconti
added a commit
that referenced
this pull request
Jul 25, 2026
"Notification" is #1167's wire vocabulary — one status class on the callback envelope — not the unit this proposal normalizes. Organizing the plan around it undersold the change: the funnel and the durable delivery step apply to events as such; which classes are admitted (authorization.* in v1, working/HITL behind the frequency rule) is an implementation detail of a given lane. Rename research/notification-consumers.md to event-consumers.md and sweep the design identifiers with it: the companion run is workflow//eve//eventConsumer, the hook is <sessionId>:events, the stream section is "The propagated event". "notification" survives only where it is factual: the status field on #1167's callback envelope. Signed-off-by: Rui Conti <ruiconti@gmail.com>
ruiconti
added a commit
that referenced
this pull request
Jul 25, 2026
Every failure bullet now carries commit-pinned code references (413ce48, the #1167 baseline) plus a concrete example: the drop window is the authorization.completed POST racing the terminal callback, verifiable with one curl against an unparked session's callback URL; rendering-only is defined by example (a child's authorization.required becoming the channel's sign-in prompt). Two claims changed under the trace. "Channels implement the same rendering twice" was wrong: both transports converge on one proxy step at the caller's inbox (turn-workflow.ts:297), so rendering happens once — what exists twice is the forwarding (subagent-adapter.ts vs session-callback-notification.ts, self-described as its "callback-URL analog"), with divergent failure semantics. "Nothing redelivers" holds, but the reason is subtler than "dropped at the producer": producers do emit from journaled steps, yet delivery failures are deliberately swallowed before the journal sees them (adapter.ts:246, session-callback-notification.ts:61), and a producer-side retry could not fix a deterministic "not pending" anyway. Also adds an event-indexing open question: the consumer lane is where an indexer would attach, but v1's best-effort callee wire means a parent-stream index cannot claim cross-deployment completeness. Signed-off-by: Rui Conti <ruiconti@gmail.com>
ruiconti
added a commit
that referenced
this pull request
Jul 25, 2026
…on normalization The doc jumped from stream-backbone framing straight into the v1 design without saying what the proposal is. Lead with it now: the harness funnels every event a single way and all consumers, channels included, consume from a durable delivery step, trading one extra hop per event for durable redelivery. The failure bullets become an explicit Why list and gain the missing redelivery reason (failed or refused deliveries are logged and dropped; nothing durable exists to redeliver from). A new Comparison artifacts section names the two runnable references for the decision: reopened #1167 as the baseline that pipes notifications through today's continuation entry point (to be stripped of its eve/tools authorization exposure), and a prototype of the consumer design against the same evals. Signed-off-by: Rui Conti <ruiconti@gmail.com>
ruiconti
added a commit
that referenced
this pull request
Jul 25, 2026
"Notification" is #1167's wire vocabulary — one status class on the callback envelope — not the unit this proposal normalizes. Organizing the plan around it undersold the change: the funnel and the durable delivery step apply to events as such; which classes are admitted (authorization.* in v1, working/HITL behind the frequency rule) is an implementation detail of a given lane. Rename research/notification-consumers.md to event-consumers.md and sweep the design identifiers with it: the companion run is workflow//eve//eventConsumer, the hook is <sessionId>:events, the stream section is "The propagated event". "notification" survives only where it is factual: the status field on #1167's callback envelope. Signed-off-by: Rui Conti <ruiconti@gmail.com>
ruiconti
added a commit
that referenced
this pull request
Jul 25, 2026
Every failure bullet now carries commit-pinned code references (413ce48, the #1167 baseline) plus a concrete example: the drop window is the authorization.completed POST racing the terminal callback, verifiable with one curl against an unparked session's callback URL; rendering-only is defined by example (a child's authorization.required becoming the channel's sign-in prompt). Two claims changed under the trace. "Channels implement the same rendering twice" was wrong: both transports converge on one proxy step at the caller's inbox (turn-workflow.ts:297), so rendering happens once — what exists twice is the forwarding (subagent-adapter.ts vs session-callback-notification.ts, self-described as its "callback-URL analog"), with divergent failure semantics. "Nothing redelivers" holds, but the reason is subtler than "dropped at the producer": producers do emit from journaled steps, yet delivery failures are deliberately swallowed before the journal sees them (adapter.ts:246, session-callback-notification.ts:61), and a producer-side retry could not fix a deterministic "not pending" anyway. Also adds an event-indexing open question: the consumer lane is where an indexer would attach, but v1's best-effort callee wire means a parent-stream index cannot claim cross-deployment completeness. Signed-off-by: Rui Conti <ruiconti@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
event.status("notification" | "termination"). Remote callees forward theirauthorization.required/authorization.completedevents as notification callbacks, and the caller re-emits them on its own stream — so a remote subagent's sign-in prompt surfaces exactly like a local subagent's.eve/toolsexports the tool authorization API:requestAuthorization,getAuthorizationResult,getHookUrl,isAuthorizationSignal, with a new docs page.outputSchema: {}, and a same-step approval + runtime action dropping the approval.agent-remote-callbackse2e fixture with three evals: auth propagation end-to-end, HITL/callback collision, and two-hop notification propagation.Callback envelope
Before this, the callback wire had exactly one message: the terminal result. A callee that parked on authorization mid-run had no way to tell its caller, so the sign-in affordance never reached the channel where the person actually is. Each callback POST now carries one event classed by
status:terminationresumes the parked parent (unchanged semantics, still the only durable resume path),notificationis re-emitted without resolving the call, best-effort — an unreachable caller is logged on the callee and never fails its turn."working"and"input_required"are declared so the vocabulary is stable, but callers reject them for now.Wire break: caller and callee must both run this version; a mixed-version remote-agent pair rejects each other's callbacks. Pre-1.0, per repo policy.
A parent that is itself a remote callee relays proxied child events one more hop up its own callback URL, so authorization events reach the root conversation from any depth.
eve/toolsauthorization exportsharness/authorization.tshas declared these functions "Public API" since #225, but no package entrypoint exported them. The e2e evals need a deterministicauthorization.requiredtrigger, and nothing exportable today can do it:defineInteractiveAuthorizationpinsprincipalType: "user"(a remote-dispatched session isn't one), OpenAPI connections take a compile-timebaseUrl, and out-of-bandConnectionAuthorizationRequiredErroremits the event without suspending. Exporting the existing API is smaller than any workaround and is useful on its own — device-code and push-to-approve flows in authored tools. Thetoolcapability contract bumps 2→3 with the epoch-2 surface retained as a compatibility fixture.Fix: empty
outputSchema: {}fails remote callsFirst live run, every remote call died with:
Models routinely emit
outputSchema: {}for the optional param on the lowered subagent tool. Local delegation filters it (subagent-tool.ts); remote dispatch forwarded it, forcing the callee into structured-output completion. Dispatch now applies the same non-empty filter.Fix: same-step approval + runtime action drops the approval
With the schema fix in, the collision eval still failed the turn:
When one model step requests an approval-gated tool call and a subagent/remote call together, the park classification returned on the runtime-action batch before the input-request park ran:
input.requestedwas never emitted, and the action-result resume called the model with the gated call'stool_useunanswered. The classification now persists both batches — the input batch with emptyresponseMessages, since the action batch owns the step's assistant messages — and emitsinput.requestedimmediately. On the action-result resume, the existing staged resolution inexecuteStepBodyre-parks on the still-unanswered approval with the action results committed to history.E2E fixture and evals
agent-remote-callbacks' remote subagent points at the fixture's own deployment (WORKFLOW_LOCAL_BASE_URLlocally,VERCEL_URLon Vercel), so the suite stays self-contained while exercising the real dispatch, park, and callback wire. Loopback callback hosts are already allowed by the SSRF guard for exactly this.requestAuthorization; the forwardedauthorization.requiredcarries thewebhookUrl, the eval fetches it with a?code=, and the code round-trips through the callee's resume into the caller's final reply. Also asserts the notification resolved nothing and created no input requests.<childSessionId>:authhook while the caller is parked for input; both resolve independently.count: 1) and in orderrequired → completed → subagent.completed.All three pass locally against
openai/gpt-5.6-sol(34/34 gates). One caveat surfaced along the way:Client.fetchtreats its wholepathargument as pathname, so a?code=query gets percent-encoded into the path and 404s. The hitl eval uses plainfetch; the client quirk is left for a follow-up.