Skip to content

feat(eve): propagate remote-callee authorization events to the caller#1167

Draft
ruiconti wants to merge 5 commits into
mainfrom
rui/remote-agent-callback-notifications
Draft

feat(eve): propagate remote-callee authorization events to the caller#1167
ruiconti wants to merge 5 commits into
mainfrom
rui/remote-agent-callback-notifications

Conversation

@ruiconti

Copy link
Copy Markdown
Contributor

What

  • Session callbacks now carry an event envelope discriminated by event.status ("notification" | "termination"). Remote callees forward their authorization.required/authorization.completed events 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/tools exports the tool authorization API: requestAuthorization, getAuthorizationResult, getHookUrl, isAuthorizationSignal, with a new docs page.
  • Two harness fixes the new evals caught: remote dispatch forwarding a model-supplied empty outputSchema: {}, and a same-step approval + runtime action dropping the approval.
  • New agent-remote-callbacks e2e 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: termination resumes the parked parent (unchanged semantics, still the only durable resume path), notification is 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/tools authorization exports

harness/authorization.ts has declared these functions "Public API" since #225, but no package entrypoint exported them. The e2e evals need a deterministic authorization.required trigger, and nothing exportable today can do it: defineInteractiveAuthorization pins principalType: "user" (a remote-dispatched session isn't one), OpenAPI connections take a compile-time baseUrl, and out-of-band ConnectionAuthorizationRequiredError emits 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. The tool capability contract bumps 2→3 with the epoch-2 surface retained as a compatibility fixture.

Fix: empty outputSchema: {} fails remote calls

First live run, every remote call died with:

subagent-result:probe-remote status=failed error=The agent could not produce a result matching the requested schema.

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:

AI_MissingToolResultsError: Tool result is missing for tool call call_I4tnJrVHUKMpLUpQowurUKso.

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.requested was never emitted, and the action-result resume called the model with the gated call's tool_use unanswered. 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, the existing staged resolution in executeStepBody re-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_URL locally, VERCEL_URL on 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.

  • auth-propagation — callee parks on requestAuthorization; the forwarded authorization.required carries the webhookUrl, 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.
  • hitl-collision — one turn holds a pending approval and an in-flight remote call. The callee's auth completes via the deterministic <childSessionId>:auth hook while the caller is parked for input; both resolve independently.
  • notification-propagation — the callee's local subagent relays its authorization events two hops to the caller, each exactly once (count: 1) and in order required → completed → subagent.completed.

All three pass locally against openai/gpt-5.6-sol (34/34 gates). One caveat surfaced along the way: Client.fetch treats its whole path argument as pathname, so a ?code= query gets percent-encoded into the path and 404s. The hitl eval uses plain fetch; the client quirk is left for a follow-up.

ruiconti added 5 commits July 24, 2026 08:40
…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>
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
eve-docs Ready Ready Preview, Comment, Open in v0 Jul 24, 2026 12:46pm

@ruiconti
ruiconti marked this pull request as draft July 24, 2026 13:08
@ruiconti ruiconti closed this Jul 24, 2026
@ruiconti ruiconti reopened this Jul 24, 2026
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>
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant