Skip to content

bus: handle_requests_with_context — expose the verified caller principal - #75

Merged
hartsock merged 2 commits into
mainfrom
feat/request-context-seam
Aug 12, 2026
Merged

bus: handle_requests_with_context — expose the verified caller principal#75
hartsock merged 2 commits into
mainfrom
feat/request-context-seam

Conversation

@hartsock

@hartsock hartsock commented Aug 12, 2026

Copy link
Copy Markdown
Member

What this PR does

  • add RequestContext and the *_with_context handler registration to the bus
  • keep the body-only API as a zero-context wrapper (back-compat)

Test plan

  • inbox::a_context_handler_receives_the_verified_caller_fingerprints — the
    handler sees the ACTUAL envelope signer's user+agent fingerprints, not a body
    value
  • bus::context_handler_sees_the_calling_agent_over_the_transport — full
    in-memory round-trip proving the responder learns the caller from the verified
    envelope
  • cargo test -p agent-mesh-bus 59 pass; clippy --all-targets -D warnings clean

Out of scope

  • binding the principal to the QUIC session key (conn.remote_id()) as an
    additional cross-check — the envelope signature already authenticates the
    signer; the connection cross-check is defense-in-depth, a follow-up.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com


Note

High Risk
Touches authz surface (caller principal handed to handlers) and inbound admission (session-binding drops mismatched envelopes), both security-critical paths.

Overview
Request handlers can now authorize on who called, not just the request body.

Adds RequestContext (authenticated caller_user_fp + caller_agent_fp from the verified envelope) and handle_requests_with_context / register_handler_with_context. The existing body-only handle_requests API remains as a zero-context wrapper for back-compat.

Also hardens iroh accept: envelope_matches_session drops envelopes whose signer is not the TLS-authenticated QUIC session key, blocking relay/replay of a validly-signed envelope over another peer's connection.

Reviewed by Cursor Bugbot for commit 8e4d608. Bugbot is set up for automated code reviews on this repo. Configure here.

…principal

Request handlers registered via `Bus::handle_requests` receive only the request
body: the authenticated caller identity, though known, is discarded. That is
fine for peers that serve any same-mesh caller, but a capability-gated responder
(e.g. a dock service that must authorize WHICH agent is calling) cannot express
its policy — authorization is forced onto the dialer, which is not complete
mediation.

Add an additive, wire-compatible seam:
- `RequestContext { caller_user_fp, caller_agent_fp }` — the verified principal.
  Every inbound envelope is already `verify()`-ed at the transport boundary, so
  `sender_agent_fp()` / `sender_user_fp()` are authenticated (not claimed); this
  is the request's *signer*, the correct principal for authorization (a relay
  can only deliver a request its signer already authorized).
- `Inbox::register_handler_with_context` + `Bus::handle_requests_with_context`,
  which hand the handler `(RequestContext, body)`. The existing body-only
  `handle_requests` / `register_handler` are unchanged (they wrap, discarding
  the context), so no caller churns.

No transport, wire, or protocol change: the caller fingerprints are already
computed in `on_envelope`; this threads them through `dispatch_request` to the
handler.

## What this PR does

- add `RequestContext` and the `*_with_context` handler registration to the bus
- keep the body-only API as a zero-context wrapper (back-compat)

## Test plan

- `inbox::a_context_handler_receives_the_verified_caller_fingerprints` — the
  handler sees the ACTUAL envelope signer's user+agent fingerprints, not a body
  value
- `bus::context_handler_sees_the_calling_agent_over_the_transport` — full
  in-memory round-trip proving the responder learns the caller from the verified
  envelope
- `cargo test -p agent-mesh-bus` 59 pass; `clippy --all-targets -D warnings` clean

## Out of scope

- binding the principal to the QUIC session key (`conn.remote_id()`) as an
  additional cross-check — the envelope signature already authenticates the
  signer; the connection cross-check is defense-in-depth, a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_3c98e0d5-b015-4cd7-b279-d5a21b145a37)

hartsock added a commit to Gilamonster-Foundation/newt-agent that referenced this pull request Aug 12, 2026
#1, #2, #3, #8)

Close the same-operator direct-dial bypass at the resource owner. Previously the
mesh dock responder served ANY caller the handshake admitted (same UserKey),
gated only by the local kill-switch — so a sibling AgentKey the operator never
approved could bind a DockClient and list / mirror / INJECT directly, bypassing
the hub-side gate entirely.

NewtDockService now uses agent-mesh's `handle_requests_with_context` (agent-mesh
PR #75) to get the VERIFIED caller principal (the envelope signer), and
`authorize_caller` resolves that agent fingerprint against THIS peer's OWN signed
dock registry before any disclosure or side effect. Same operator is
authentication, not authorization: an unapproved sibling is refused here.
Fail-closed by default; `NEWT_INSECURE_DOCK_NO_APPROVAL` is the named unsafe
opt-out.

Scope is now typed authority, not signed decoration: `DockScope { Mirror,
MirrorInject }` is enforced per operation — a Mirror dock may list + read
transcript but is refused inject. An unknown scope token fails to deserialize, so
a tampered/forward scope drops the whole record at load (fail-closed). CLI
defaults to least authority (`--scope mirror`).

Revocation linearization: the responder re-reads the registry on every request,
so once a revocation commits the next request from that caller is denied
(`approved()` excludes revoked rows). The false `verify_at(gen)` doc is removed.

Test plan (macOS/arm64, native):
- newt-mesh: `a_sibling_agent_the_operator_never_approved_is_denied_over_the_mesh`
  — REAL loopback QUIC, three agents / one UserKey: approved hub served,
  unapproved sibling denied on list/transcript/inject, its inject never lands;
  `dock_lifecycle_over_loopback_mesh` (approved caller) still green;
  `a_mirror_scope_caller_can_read_but_never_inject`,
  `a_revoked_caller_is_denied_on_the_next_request_linearization`,
  `an_unapproved_caller_is_refused_before_any_disclosure` (deterministic).
- newt-core: DockScope per-op + parse + unknown-token-drops-record; 17 tests.
- drive.sh 40/0; clippy -D warnings clean on newt-mesh/newt-core.

Depends on Gilamonster-Foundation/agent-mesh#75 (the RequestContext seam); CI
ref pinned in a follow-up commit.

Part of the PR #1643 adversarial security closure (fronts: responder authz,
scope, revocation).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
recv_envelope already verifies the envelope signature, but accept_conn forwarded the envelope without checking that its signer owns the QUIC session it arrived on. A validly-signed envelope replayed or relayed over a different peer's connection would then be authorized as its original signer. Add a pure envelope_matches_session(session_id, env) helper and reject at accept_conn any envelope whose cert_chain.agent_pubkey does not match conn.remote_id(). This binds the application principal to the transport session (defense in depth over the signature verify). Proven by an_envelope_is_bound_to_its_signers_quic_session and, end-to-end, by the newt-mesh live loopback-QUIC dock tests (approved hub still served, unapproved sibling still denied). Follow-up noted in #75.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@cursor

cursor Bot commented Aug 12, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_0d32a861-5836-41d5-90f8-a72157d3a140)

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