Relay proto: well-known endpoint resolver + RAMP* prefix purge - #14
Closed
KonstantinMirin wants to merge 9 commits into
Closed
Relay proto: well-known endpoint resolver + RAMP* prefix purge#14KonstantinMirin wants to merge 9 commits into
KonstantinMirin wants to merge 9 commits into
Conversation
…e body (RAMP-102)
Add AgentAcceptance{signature,signature_algorithm} carried on both
TransactionRequest (single mode, field 9) and TransactionItem (batch,
field 4), optional on the wire. It is the agent's DETACHED, content-bound
acceptance over an accepted Offer — topology-independent (survives broker
relays) and distinct from the transport RFC 9421 request signature.
The canonical signed bytes are pinned by a dedicated AgentAcceptancePayload
message {offer_sig, requester_id, requester_domain, idempotency_key} that is
never sent on the wire: signer and verifier deterministically marshal it to
derive byte-identical bytes, so the contract cannot drift. Registered as an
out-of-band reachability root for that reason.
Regenerated gen/{go,ts,python}+descriptor, validation corpus, and the
reference page; ci-local green.
…AMP-56) Extend L1 signing/verification from single 'sig1' to N labels, where single-sig is exactly the N=1 case of one shared code path (byte-for-byte identical sig1 output, pinned by golden signature-base tests). - sigbase.go: CoveredComponent/ComponentParam + renderComponent; widen sigParams.Covered to []CoveredComponent; chainLinkValue re-encodes the referenced predecessor member canonically (whitespace-drift proof). - sign.go: AppendSignature(signer) chains sig(N+1) over the prior label via a 'signature';key='sigN' covered link; BrokerKeyIDPrefix; shared signWithParams set/append modes. - verify.go: multi-label parsers, enforceSignatureChain (structural) + per-sig Ed25519 (crypto) binding, VerifyMultisigRequest, MaxSignatures hop bound, ErrTooManyHops/ErrBrokenSignatureChain. - keyresolver.go: VerifyMultisigRequestResolved (per-hop key resolution). - context.go: pure context plumbing (multisig slot + single-slot fallback); no Middleware (L1 stays IO-free per ADR-020). Re-expressed from the proven platform internal/httpsig core; does not fork it. ci-local green; existing single-signer tests unchanged. NOTE: VerifiedRequest.Signature now carries the per-label base64 value (was the whole Signature header) — platform consumers re-pinning should account for it.
…AMP-102)
The {verified,rejected} primitive for the agent's detached offer acceptance.
Both sign and verify go through canonicalAcceptancePayload — a deterministic
marshal of the AgentAcceptancePayload proto message {offer_sig, requester_id,
requester_domain, idempotency_key} — so the agent signer (R8/MCP) and the
Exchange verifier (R4) derive byte-identical bytes and cannot drift. Mirrors
the offer.go SignOffer/VerifyOffer family (raw Ed25519 key, hex signature).
Fail-closed on an unsigned offer (empty offer signature would let the
acceptance float free of a concrete offer). Tamper of offer_sig/requester/
domain/idempotency or a wrong key -> ErrAcceptanceSignatureInvalid.
…MP-56) The Broker's BrokerService/Resolve becomes discovery-only on the modern two- phase flow: it discovers and ranks Exchange-signed Offers but does not execute (the agent originates the execute itself via the relay route). Add repeated OfferGroup offer_groups = 2 to RAMPResponse to carry the ranked discovered offers; the execute-shaped fields stay for the legacy path but are not populated by a discovery-only Resolve. Additive; regen + ci-local green.
…MP-101) Cherry-picks the single Offer.exchange field from PR #11 (feature/discovery- offer-list) onto the relay line so multi-Exchange fan-out (RAMP-102 R10) can route the per-offer ExecuteTransaction from the offer itself instead of the X-RAMP-Exchange-Endpoint transport header. exchange = 8 (free; matches PR #11). As an ordinary Offer field it is inside the signed bytes (canonicalOfferPayload clears only signature/signature_algorithm), so an intermediary cannot redirect the execute to a different Exchange without invalidating the offer. Regen + ci-local green.
…e (RAMP-102)
Close the defined-but-inert WellKnownManifest.endpoint gap so an agent/broker
can reach an arbitrary Exchange from a signed Offer.exchange + standard
well-known alone, and purge the redundant RAMP* prefixes inside the ramp.v1
namespace (ramp.v1 / rampv1 / rampv1connect kept as the legitimate binding).
- sdk: host-keyed WellKnownEndpointResolver (ResolveEndpoint(ctx, host) ->
WellKnownManifest.endpoint) sharing a single full-manifest fetch/decode with
WellKnownKeyResolver (jscpd-zero); WellKnownOptions gains Scheme; ErrNoEndpoint
distinguishes a reachable-but-endpointless manifest from transport failure.
- sdk: rename module path sdk/go/ramphelpers -> sdk/go/helpers (package helpers).
- proto: RAMPRequest/RAMPResponse -> DiscoveryRequest/DiscoveryResponse; regen
gen/{go,ts,python} + descriptor + conformance corpus + docs.
- tests: host-keyed resolver coverage (two-host isolation, cache, TTL, non-200,
decode, missing-endpoint); offer Exchange-tamper + acceptance economics-rebound
audit regressions.
…proto Final hop of the cascade — main + protocol-unification + SDK types + L1 + stateless-offer-redemption flow into the relay-proto branch. Clean auto-merge of ramp.proto (RAMP-102 relay changes coexist); gen/ and both corpora regenerated and unchanged.
…P-102) Remove single-offer mode from the execute contract (epic 6afpc, pre-release, no backward-compat). A single offer is now the degenerate 1-element items[]. - TransactionRequest: delete offer=8 + agent_acceptance=9 + the offer_xor_items message CEL; add repeated.min_items=1 to items=7; reserve 8,9. - TransactionResponse: delete the single-mode top-level result fields (transaction_id=3, billing_id=4, resource_title=5, cost=6, delivery_method=7, reporting_obligation=8, expires_at=9, subscription_id=12, subscription_unit_value=16, retrieval_endpoint=18); reserve them. Per-result data lives only in TransactionResultItem; the top level keeps the shared aggregate (ver, agent_identity_hash, items, total_cost, subscription_quota, ext). - regenerate gen/go + descriptor + validation corpus; migrate the conformance + SDK tests off the removed fields/rule (items-min-1 replaces offer_xor_items). ci-local.sh green (buf lint, zero gen/corpus drift, go build/vet/test, doc conformance, docs guards).
This was referenced Jun 23, 2026
…02-relay-proto # Conflicts: # conformance/corpus/cases.json # conformance/descriptor_invariants_test.go # conformance/reachability_test.go # conformance/validate_test.go # gen/descriptor.binpb # gen/go/ramp/v1/ramp.pb.go # gen/go/ramp/v1/rampv1connect/ramp.connect.go # gen/python/wire/models.py # gen/ts/wire/schemas.ts # proto/ramp/v1/ramp.proto # sdk/go/ramphelpers/constants_test.go # website/src/content/docs/components/broker/deployment.mdx # website/src/content/docs/components/broker/overview.mdx # website/src/content/docs/components/broker/selection-engine.mdx # website/src/content/docs/protocol/authentication.mdx # website/src/content/docs/protocol/discovery-paths.mdx # website/src/content/docs/protocol/extension-profiles.mdx # website/src/content/docs/protocol/scenario-walkthrough.mdx
Contributor
Author
|
Superseded by and included in #17. This PR's commits are ancestors of |
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.
Relay proto: well-known endpoint resolution, items-only execute, RAMP* prefix purge
Brings the two-phase agent→Broker→Exchange relay onto the modern protocol shape: an agent reaches an arbitrary Exchange from a signed
Offer.exchange+ standard well-known alone, the execute contract collapses to a single items-only mode, and the redundantRAMP*prefixes inside theramp.v1namespace are purged.Why this exists / motivation
A signed
Offer.exchange(the discovery-remodel field from PR #11) tells an agent which Exchange issued an offer, but the protocol had no key-free way to turn that domain into a reachable address.WellKnownManifest.endpoint(field 12) was defined but inert — nothing read it. A Broker relaying an agent'sExecuteTransactiontherefore still depended on an out-of-band endpoint hint (theext['ramp.broker.offer_routes']/X-RAMP-Exchange-Endpointplaceholder the platform ships today). That breaks the moment offers from more than one Exchange appear in a single discovery result: routing must derive from the signature-coveredOffer.exchange, not from a side channel the agent or broker can tamper with.This PR closes that gap on the protocol/SDK side. Routing now flows purely from the signed
Offer.exchange: resolve the host's/.well-known/ramp.json, read its self-advertisedendpoint, dial that. The pre-seeded registry becomes a trust overlay (an Allow hook), never the source of the endpoint — the endpoint always comes from the Exchange's own manifest.Two contract simplifications ride along, both pre-release with no backward-compat obligation:
TransactionRequest/TransactionResponsecarried a single-offer mode and a batch mode, gated by aoffer_xor_itemsCEL rule, with the per-result data duplicated between top-level scalars and per-item entries. Two shapes for one concept is a permanent tax on every consumer (Exchange, Broker, MCP shim, SDK, conformance corpus). A single offer is just the degenerate 1-elementitemslist, so the single-offer mode is removed outright.RAMP*prefix purge. Inside a package already namedramp.v1,RAMPRequest/RAMPResponsestutter. They becomeDiscoveryRequest/DiscoveryResponse. Theramp.v1namespace and itsrampv1/rampv1connectbindings are deliberately kept — those are the legitimate package identity, not redundant.Stacking / dependency note
This PR is stacked, not based on
main:Offeras a stateless bearer token; the items-only collapse here builds directly on that reflected-offer shape.sdk/go/ramphelpers; this PR renames that module tosdk/go/helpers.Offer.exchangeandoffer_groups; the endpoint resolver here is what finally makesOffer.exchangeactionable.Review #13 first; this PR's diff is cleanest read on top of it. Each parent must merge and the next re-base before this one merges.
Data flow — what this PR changes
The relay is: agent discovers signed offers → self-selects → signs an acceptance → Broker co-signs and relays the execute to the issuing Exchange → Exchange verifies and returns a signed URL. This PR changes the routing leg (how the Broker finds the Exchange) and the execute message shape.
The key shift: the Exchange address is now derived inside the SDK from the signed offer, sharing the exact fetch/decode path the key resolver already uses, so the endpoint and key faces of a manifest cannot drift. No production endpoint hint is on the protocol's critical path anymore.
Concrete changes (reading the diff)
Proto —
proto/ramp/v1/ramp.protoTransactionRequest: deleteoffer = 8andagent_acceptance = 9; delete thetransaction_request.offer_xor_itemsmessage CEL; add(buf.validate.field).repeated.min_items = 1toitems = 7;reserved 8, 9.TransactionResponse: delete the single-mode top-level result fields (transaction_id=3,billing_id=4,resource_title=5,cost=6,delivery_method=7,reporting_obligation=8,expires_at=9,subscription_id=12,subscription_unit_value=16,retrieval_endpoint=18);reserved 3,4,5,6,7,8,9,12,16,18. Per-result data lives only initems[](TransactionResultItem); the top level keeps the shared aggregate (ver,agent_identity_hash,items,total_cost,subscription_quota,ext). Per-item denials remain in-body as partial results of a successful request.RAMPRequest/RAMPResponse→DiscoveryRequest/DiscoveryResponse(theResolveRPC signature and doc comments follow).SDK — new
sdk/go/helpers/endpointresolver.goWellKnownEndpointResolverwithResolveEndpoint(ctx, host) → endpoint. Host-keyed (unlike the single-URLWellKnownKeyResolver): per-host cache, TTL freshness, single-flight coalescing — because a Broker resolves an arbitrary signature-coveredOffer.exchangeper request.fetchWellKnownDoc+wellKnownDocdecode the whole manifest once, serving both the key face and the endpoint face (jscpd-zero — no duplicated HTTP/status/decode logic).ErrNoEndpointdistinguishes a reachable-but-endpointless manifest from a transport/decode failure; the Allow overlay refuses a disallowed host before any network call.WellKnownOptionsgainsScheme(defaults tohttps).SDK — wide mechanical rename
sdk/go/ramphelpers/ → sdk/go/helpers/(packagehelpers)acceptance,offer,sign,verify,keyresolver,multisig_chain,signedurl,thumbprint, … and their tests). Diff shows these as git renames with import/package-line churn only — no behavior change.sdk/go/README.mdanddoc.goupdated to the new path.Offer.exchange-tamper anti-redirect and acceptance economics-rebound audit regressions.Generated + docs
gen/{go,ts,python}+gen/descriptor.binpb+ conformance corpus (conformance/corpus/*.json,corpusgen) + Astro docs underwebsite/. The conformance/SDK tests migrate off the removed fields and theoffer_xor_itemsrule (items-min-1 replaces it).Out of scope / follow-ups
agentic-content-access) re-pin theirgo.modto this commit once merged, and migrate execute call sites from single-offer fields toitems[]and from the endpoint hint toWellKnownEndpointResolver. Tracked separately on the platform side.Offer.exchangefield andoffer_groupsthemselves landed in PR #11; this PR consumes them, it does not introduce them.