diff --git a/conformance/reachability_test.go b/conformance/reachability_test.go index d1ac9b7..a3f2c53 100644 --- a/conformance/reachability_test.go +++ b/conformance/reachability_test.go @@ -32,7 +32,7 @@ var outOfBandRoots = map[string]string{ "WBAFile": "served at /.well-known/http-message-signatures-directory — the pure WBA JWK Set (identity keys + revocation_url)", "ErrorDetail": "the transport-error envelope; carries the seven typed failure reasons", "KeyRevocationList": "served key-revocation document (thumbprint list), fetched out of band from WBAFile.revocation_url", - "AgentAcceptancePayload": "canonical signing structure for AgentAcceptance; never sent on the wire — the signer and verifier deterministically marshal it to derive byte-identical signed bytes", + "AgentAcceptancePayload": "canonical signing structure for AgentAcceptance; never sent on the wire — it fixes the field set the signer and verifier canonicalize (RFC 8785 JCS over canonical proto-JSON) to derive byte-identical signed bytes", } // refFields visits the message/enum each of md's fields references (following the diff --git a/docs/design-history.md b/docs/design-history.md index 37c0b98..8e802d9 100644 --- a/docs/design-history.md +++ b/docs/design-history.md @@ -37,6 +37,75 @@ Responses do not retrace the chain: the terminal Exchange returns directly to th originating agent, because the signed `retrieval_endpoint` is already bound to the agent's identity (`agent_identity_hash`) and needs no return-path relay. +## Canonical signing: JCS over proto-JSON, not deterministic protobuf + +The two signed RAMP payloads that cover a protobuf message — `Offer.signature` and +the agent's `AgentAcceptance.signature` — originally covered deterministic protobuf +*binary*: marshal the message with deterministic field order, sign those bytes. We +reversed that and moved both onto RFC 8785 JCS over canonical proto-JSON, +`JCS(protojson(msg with the signature fields cleared))`, under one pinned +proto-JSON option set (snake_case field names, enums as name strings, unpopulated +fields omitted). (`ResourceAttestation.signature` also outlives an HTTP exchange but +is out of scope here: it signs a hand-built JSON object — `{verifier, keyid, +attested_at, uri, claims}` — under its own rule, never a rendering of its proto +message.) + +Deterministic protobuf marshaling is not actually canonical. Protobuf's own +documentation disclaims byte stability across languages, across library versions, +and in the presence of unknown fields — it is a best-effort local property, not a +wire-format guarantee. That makes it unusable as the basis of a signature two +independent implementations must agree on, which is precisely the RAMP case: an +agent SDK signs, a broker relays, an Exchange verifies, and none of them need share +a language. It also forces every verifier — including an edge worker or a browser +client — to link a protobuf binary codec just to check a signature. JCS over +proto-JSON has neither problem: it is a published canonicalization of JSON, and any +language can reproduce the signed bytes from the JSON rendering alone. The +Go-emitted golden vector is the arbiter, and the TS and Python SDKs replay it +byte-for-byte. + +The form then changed a **second** time, within JCS. The first cut rendered +proto-JSON with its default camelCase `json_name` keys; we re-pinned it to +snake_case proto field names (`UseProtoNames=true`) so the wire, the conformance +corpus, the generated Pydantic/Zod clients, and the signed form all share one +naming — protojson accepts both spellings on input, so the duality was silently +hiding client divergence rather than absorbing it. That re-pin **re-signed the +golden vectors**: signatures produced over the camelCase-JCS form no longer verify, +and all three SDKs had to move in the same commit. + +One clause of that pinned option set carries more weight than it looks. +`EmitUnpopulated=false` means an empty field is *absent* from the JSON, not present +with an empty value — so the canonical bytes for an empty string field are never the +bytes for a populated one. Go inherits that from `protojson` for free. A port that +assembles the JSON object by hand does not, and has to enumerate the omission for +every field or it signs bytes Go never produces. The acceptance payload is the one +place the Python and TS ports hand-build the object — Go renders the +`AgentAcceptancePayload` message through the same protojson canonicalizer as the +offer — and it is exactly where the divergence +appeared: Python and TS dropped an empty `requester_domain` but emitted an empty +`requester_id`, which is wire-valid because `Requester.id` carries no `min_len`. Such +a mismatch fails closed, but it falsifies the byte-equivalence the canonical-bytes +accessors promise, so the shared corpus now carries a vector that holds every port to +the omission. + +The two reversals left a consequence worth naming, because it is the reason the +canonical bytes are a first-class SDK export rather than an internal detail. A +canonical form that has already changed twice can change again, and a verifier that +re-derives the bytes at verification time has silently pinned an *already-signed* +payload to whatever canonicalization the SDK implements *later* — the failure would +surface as "the signature does not verify", indistinguishable from "it was never +signed". So all three SDKs expose the exact signed bytes as a public accessor +(`CanonicalOfferBytes` / `CanonicalAcceptanceBytes` in Go, `canonical_offer_payload` +/ `jcs_acceptance_payload` in Python, `canonicalOfferPayload` / `acceptancePayload` +in TS). A party keeping evidence stores those bytes and re-verifies against them +verbatim, rather than trusting a future canonicalizer to reproduce the past. + +Storing them is necessary, not sufficient. A signature that verifies over stored bytes +proves only that the holder of that key signed *those bytes*; it says nothing about +which transaction they belong to. Whoever reads the evidence must also parse the stored +bytes and match their content — offer id, requester, idempotency key — against the +transaction under dispute. Skip that and any valid triple minted under the same key +passes as evidence for any transaction. + ## "Marketplace" → "Exchange" The selling intermediary was originally called a *marketplace*. We renamed it to diff --git a/docs/sdk-parity-matrix.md b/docs/sdk-parity-matrix.md index fa933c9..d2084eb 100644 --- a/docs/sdk-parity-matrix.md +++ b/docs/sdk-parity-matrix.md @@ -12,7 +12,7 @@ Go is the oracle (`sdk/go/{helpers,resolvers,core,connect,connectserver}`); Python and TS mirror it. This document is **generated** from the same two artifacts CI already enforces against the code, so it cannot drift from the real surface — a mismatch fails the API-surface gate or the corpus-completeness gate before it can reach this file. -**At a glance:** 72 symbols at cross-language parity · 14 documented divergences · 98 Go-idiomatic exclusions · 23 conformance corpora, each tri-replayed. +**At a glance:** 74 symbols at cross-language parity · 14 documented divergences · 99 Go-idiomatic exclusions · 23 conformance corpora, each tri-replayed. Layering (L1 pure trust core vs L2 I/O resolvers), the SSRF transport-wiring invariant, and naming conventions are recorded in [`design-history.md`](./design-history.md). @@ -27,6 +27,8 @@ Legend: a name = the public face in that language · `—` = intentionally none | `AcceptanceSignatureAlgorithm` | `ACCEPTANCE_SIGNATURE_ALGORITHM` | `ACCEPTANCE_SIGNATURE_ALGORITHM` | | `AppendSignature` | `append_signature` | `appendSignature` | | `ApplyScopes` | `apply_scopes` | `applyScopes` | +| `CanonicalAcceptanceBytes` | `jcs_acceptance_payload` | `acceptancePayload` | +| `CanonicalOfferBytes` | `canonical_offer_payload` | `canonicalOfferPayload` | | `CanonicalizeMoney` | `canonicalize_money` | `canonicalizeMoney` | | `CatalogRejectionDetail` | `catalog_rejection_detail` | `catalogRejectionDetail` | | `ConnectProtocolVersion` | `ConnectProtocolVersion` | `ConnectProtocolVersion` | @@ -226,6 +228,7 @@ Go constructs (functional-option builders, `errors.Is` sentinels, value types, c | `helpers.ErrURLMissingSignature` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.ErrURLNotAgentBound` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.ErrURLSignatureInvalid` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | +| `helpers.ErrUnknownFields` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.ErrUnsupportedAlgorithm` | Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels. | | `helpers.FromContext` | Go context.Context accessor; py/ts thread verified-request state explicitly. | | `helpers.NewContext` | Go context.Context accessor; py/ts thread verified-request state explicitly. | diff --git a/gen/descriptor.binpb b/gen/descriptor.binpb index e3c9ee6..3163f4c 100644 Binary files a/gen/descriptor.binpb and b/gen/descriptor.binpb differ diff --git a/gen/go/ramp/v1/ramp.pb.go b/gen/go/ramp/v1/ramp.pb.go index f5ad886..93dacd7 100644 --- a/gen/go/ramp/v1/ramp.pb.go +++ b/gen/go/ramp/v1/ramp.pb.go @@ -2556,6 +2556,29 @@ type Offer struct { // - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its // keys recursively, so the Struct case needs no special handling. // + // UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or + // PRESERVES it, and the rule follows from which: + // + // - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a + // canonicalizer CANNOT reproduce the signed bytes of a message carrying + // unknown fields — what it renders silently drops part of what the signer + // covered. It MUST refuse the message rather than emit the reduced bytes, + // and a verifier built on it MUST reject rather than verify over them. The + // refusal binds at EVERY depth: a nested message and each element of a + // repeated or map field carries its own unknown-field set. + // - PRESERVING (a canonicalizer that carries unrecognized members through): + // it reproduces the signed bytes faithfully, so there is nothing to refuse. + // + // Either way an APPENDED field cannot pass: an omitting canonicalizer refuses + // the message, and a preserving one renders the appended member into bytes the + // signer never covered, so the signature fails. Without the refusal the omitting + // case would fail OPEN — an intermediary could add unknown fields to an + // already-signed message and leave its signature verifying, smuggling + // unauthenticated content through a message the recipient treats as verified. + // + // Extensions therefore ride in `ext` / `ext_critical`, which are defined fields + // and inside the signed bytes — never as undeclared field numbers. + // // Because the signature covers `terms`, `pricing`, `expires_at`, and // `exchange`, an intermediary (Broker) cannot tamper with price, restrictions, // quotas, obligations, the expiry, the execute-routing target, or any @@ -4364,13 +4387,17 @@ func (x *Delegation) GetExtCritical() []string { // agent's key (RFC 7638 thumbprint of the acceptance key). // // `signature` is a hex-encoded detached Ed25519 signature (NOT a JWS) over the -// deterministic protobuf serialization of `AgentAcceptancePayload` — the same -// hex/Ed25519 convention the SDK uses for Offer.signature. `signature_algorithm` -// is "EdDSA". +// CANONICAL SIGNING form of `AgentAcceptancePayload` — RFC 8785 JCS over canonical +// proto-JSON. That form, including the pinned proto-JSON option set, is defined +// once on `Offer.signature` and is the single normative definition; there is no +// second recipe. `AgentAcceptancePayload` carries no signature fields, so the +// clear-then-render step of that definition reduces here to +// JCS(protojson(AgentAcceptancePayload)). Same hex/Ed25519 convention as +// `Offer.signature`; `signature_algorithm` is "EdDSA". type AgentAcceptance struct { state protoimpl.MessageState `protogen:"open.v1"` - // Hex-encoded detached Ed25519 signature over the deterministic-marshaled - // AgentAcceptancePayload bytes. + // Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload + // bytes (see the canonical-signing definition on Offer.signature). Signature string `protobuf:"bytes,1,opt,name=signature,proto3" json:"signature,omitempty"` // Signature algorithm; "EdDSA" for Ed25519. SignatureAlgorithm string `protobuf:"bytes,2,opt,name=signature_algorithm,json=signatureAlgorithm,proto3" json:"signature_algorithm,omitempty"` @@ -4425,8 +4452,10 @@ func (x *AgentAcceptance) GetSignatureAlgorithm() string { // AgentAcceptancePayload — the canonical signing structure for AgentAcceptance. // It is NEVER sent on the wire; it exists solely so the signer (SDK) and the // verifier (Exchange) derive BYTE-IDENTICAL signed bytes from the same proto -// schema via `proto.Marshal(Deterministic: true)`. Underspecifying these bytes -// is the top cross-implementation drift risk, so the field set is fixed here. +// schema. This message fixes the FIELD SET; the byte layout is the canonical +// signing form defined on Offer.signature — RFC 8785 JCS over canonical +// proto-JSON with the pinned option set. Underspecifying either half is the top +// cross-implementation drift risk, so both are pinned normatively. // // Field provenance when building the payload for an execute request: // - offer_sig = the accepted Offer.signature (the Exchange's hex @@ -4632,8 +4661,8 @@ type TransactionItem struct { Offer *Offer `protobuf:"bytes,3,opt,name=offer,proto3" json:"offer,omitempty"` // The agent's detached acceptance signature over this item's `offer`. // Optional on the wire; the Exchange enforces presence per - // item at the service layer for relayed batches. Signed bytes = - // deterministic AgentAcceptancePayload, with requester_* and idempotency_key + // item at the service layer for relayed batches. Signed bytes = the canonical + // AgentAcceptancePayload form, with requester_* and idempotency_key // taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature. AgentAcceptance *AgentAcceptance `protobuf:"bytes,4,opt,name=agent_acceptance,json=agentAcceptance,proto3,oneof" json:"agent_acceptance,omitempty"` unknownFields protoimpl.UnknownFields diff --git a/gen/python/wire/models.py b/gen/python/wire/models.py index 86d41c9..f5e3934 100644 --- a/gen/python/wire/models.py +++ b/gen/python/wire/models.py @@ -13,7 +13,7 @@ class AgentAcceptance(WireModel): signature: constr(min_length=1) = Field( ..., - description='Hex-encoded detached Ed25519 signature over the deterministic-marshaled\n AgentAcceptancePayload bytes.', + description='Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload\n bytes (see the canonical-signing definition on Offer.signature).', ) signature_algorithm: str | None = Field( '', description='Signature algorithm; "EdDSA" for Ed25519.' @@ -1630,7 +1630,7 @@ class Offer(WireModel): ) signature: str | None = Field( '', - description="CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.", + description="CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.", ) signature_algorithm: str | None = Field( '', @@ -1740,7 +1740,7 @@ class ResourceResponse(WireModel): class TransactionItem(WireModel): agent_acceptance: AgentAcceptance | None = Field( None, - description="The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes =\n deterministic AgentAcceptancePayload, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.", + description="The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes = the canonical\n AgentAcceptancePayload form, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.", ) offer: Offer = Field( ..., diff --git a/gen/ts/wire/schemas.ts b/gen/ts/wire/schemas.ts index 5c07348..ab56239 100644 --- a/gen/ts/wire/schemas.ts +++ b/gen/ts/wire/schemas.ts @@ -8,7 +8,7 @@ import { wire } from "./base.ts"; export const AcceptableRestrictionSchema = wire(z.object({ "axis": z.union([z.string().regex(new RegExp("^RESTRICTION_KIND_UNSPECIFIED$")), z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY /\n USER_TYPE / OTHER.").default(0), "values": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("The values the query operates within on this axis — same token vocabulary\n as the terms (e.g. FUNCTION [\"ai-train\"], GEOGRAPHY [\"US\", \"EU\"]).").optional() }).describe("AcceptableRestriction — the limits a query operates within on one restriction\n axis, expressed in the same RestrictionKind vocabulary that terms use. The\n Exchange/Broker MAY pre-select offers whose term restrictions fall within\n these as a convenience (see Restriction); it is NOT enforcement — the agent\n self-selects and bears compliance.")); -export const AgentAcceptanceSchema = wire(z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the deterministic-marshaled\n AgentAcceptancePayload bytes."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("`signature` is a hex-encoded detached Ed25519 signature (NOT a JWS) over the\n deterministic protobuf serialization of `AgentAcceptancePayload` — the same\n hex/Ed25519 convention the SDK uses for Offer.signature. `signature_algorithm`\n is \"EdDSA\".")); +export const AgentAcceptanceSchema = wire(z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload\n bytes (see the canonical-signing definition on Offer.signature)."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("`signature` is a hex-encoded detached Ed25519 signature (NOT a JWS) over the\n CANONICAL SIGNING form of `AgentAcceptancePayload` — RFC 8785 JCS over canonical\n proto-JSON. That form, including the pinned proto-JSON option set, is defined\n once on `Offer.signature` and is the single normative definition; there is no\n second recipe. `AgentAcceptancePayload` carries no signature fields, so the\n clear-then-render step of that definition reduces here to\n JCS(protojson(AgentAcceptancePayload)). Same hex/Ed25519 convention as\n `Offer.signature`; `signature_algorithm` is \"EdDSA\".")); export const AgentAcceptancePayloadSchema = wire(z.object({ "idempotency_key": z.string().describe("The transaction's idempotency key — binds the acceptance to a single\n execute so it cannot be replayed under a different transaction.").default(""), "offer_sig": z.string().describe("The accepted Offer's signature (Offer.signature). Anchors the whole signed\n offer without re-serializing its terms/pricing/expiry.").default(""), "requester_domain": z.string().describe("Requester domain (Requester.domain) the acceptance is bound to.").default(""), "requester_id": z.string().describe("Requester identity (Requester.id) the acceptance is bound to.").default("") }).describe("Field provenance when building the payload for an execute request:\n - offer_sig = the accepted Offer.signature (the Exchange's hex\n signature; transitively binds pricing, terms,\n expires_at, and — via the offer — the issuing Exchange)\n - requester_id = TransactionRequest.requester.id\n - requester_domain = TransactionRequest.requester.domain\n - idempotency_key = TransactionRequest.idempotency_key\n For batch mode, requester_* and idempotency_key come from the ENCLOSING\n TransactionRequest (a TransactionItem carries neither); offer_sig is the\n per-item Offer.signature.")); @@ -40,7 +40,7 @@ export const DiscoveryMethodSchema = wire(z.enum(["DISCOVERY_METHOD_EXCHANGE","D export const DiscoveryRequestSchema = wire(z.object({ "acceptable_restrictions": z.array(z.object({ "axis": z.union([z.string().regex(new RegExp("^RESTRICTION_KIND_UNSPECIFIED$")), z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY /\n USER_TYPE / OTHER.").default(0), "values": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("The values the query operates within on this axis — same token vocabulary\n as the terms (e.g. FUNCTION [\"ai-train\"], GEOGRAPHY [\"US\", \"EU\"]).").optional() }).describe("AcceptableRestriction — the limits a query operates within on one restriction\n axis, expressed in the same RestrictionKind vocabulary that terms use. The\n Exchange/Broker MAY pre-select offers whose term restrictions fall within\n these as a convenience (see Restriction); it is NOT enforcement — the agent\n self-selects and bears compliance.")).describe("The limits the agent will operate within, per restriction axis — see\n AcceptableRestriction. The Broker forwards these to Exchanges in\n ResourceQuery.acceptable_restrictions. Advisory selection inputs, not\n enforcement.").optional(), "constraints": z.object({ "budget_period": z.string().describe("Budget period (e.g. \"2592000s\" = 30 days; proto-JSON encodes Duration\n as seconds). Resets at period boundary.").optional(), "budget_scope": z.string().describe("Budget scope identifier for per-period tracking.\n E.g. \"user:u-12345\" for per-user budgets, \"team:eng\" for per-team.\n The Broker tracks cumulative spend per scope across sessions.").optional(), "delivery_preference": z.array(z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"])).describe("Preferred delivery methods, in order of preference.").optional(), "exchanges": z.array(z.string()).describe("Authorized Exchange domains. Broker queries only these.").optional(), "max_data_age": z.string().describe("Only relevant for DYNAMIC resources. Ignored for STATIC (content is\n immutable) and LIVE (content doesn't exist yet).\n\n Examples:\n 7 days — \"credit report updated within the last week\"\n 1 hour — \"stock snapshot from the last hour\"\n 30 days — \"drug interaction database updated this month\"").optional(), "max_hops": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum forwarding hops the agent will allow (Agent → Broker → … →\n Exchange), counted as the number of RFC 9421 HTTP Message Signatures on the\n request. Caps chain depth so a request is not relayed through more brokers\n than the agent is willing to trust or pay. A Broker MUST NOT forward a\n request whose signature count would exceed this. Absent = agent imposes no\n cap (the Exchange's max_intermediary_hops still applies).").optional(), "max_price": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Maximum price the agent is willing to pay.").optional(), "max_unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Maximum effective cost per unit, as an exact decimal string (not a float).").optional(), "period_budget": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Per-period budget limit. The Broker tracks spend against this\n for the budget_scope. Transactions that would exceed are denied.").optional(), "preferred_exchanges": z.array(z.string()).describe("Exchanges the agent has existing relationships with (subscriptions,\n contracts). The Broker SHOULD prefer these when resource is\n available — subscription resource has zero marginal cost.").optional(), "reporting_capable": z.boolean().describe("Whether the agent supports post-usage reporting.").optional() }).describe("Constraints for exchange filtering and offer selection.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "query": z.string().describe("Search query for Broker-side resource discovery.\n Used when the agent doesn't know specific URIs but wants the Broker\n to find matching resources across Exchanges.\n When present, the Broker interprets the query and discovers resources\n across Exchanges on the agent's behalf. Results returned as Offers\n in DiscoveryResponse, same as for specific URI requests.\n Can be used alongside uris (specific URIs + search in one request).").optional(), "requester": z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("When this delegation expires. Exchange MUST reject expired tokens.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Who granted this delegation (domain for public key lookup).").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).").optional(), "scopes": z.array(z.string()).describe("Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]*={0,2}$")).describe("Token bytes. A JWT (base64url-encoded JWS).").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent).").optional(), "domain": z.string().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").default(""), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().describe("Unique requester identifier (e.g., \"agent-research-bot-001\").").default(""), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("The Exchange filters its catalog to resources matching these scopes.\n Resources outside the scopes are not returned — the requester never\n learns they exist. This is the enforcement mechanism for both enterprise\n RBAC and open-market subscription entitlements.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester identity — who is making this request, what scopes they have.\n The Broker forwards this to Exchanges in ResourceQuery.requester.").optional(), "search_filters": z.record(z.string(), z.any()).describe("Structured search filters (optional, alongside or instead of query).\n Keys are profile-specific: \"academic.topic\", \"news.category\",\n \"legal.jurisdiction\", etc. The Broker maps these to Exchange-specific\n query parameters.").optional(), "supported_profiles": z.array(z.string()).describe("The Broker uses this to:\n 1. Route queries to Exchanges that support these profiles\n 2. Forward the profiles in ResourceQuery.supported_profiles\n 3. Include profile-specific ext fields when returning results\n\n Examples: [\"ramp-academic-v1\"] — agent working on literature review").optional(), "uris": z.array(z.string()).max(256).describe("Resource URIs the agent wants. The Broker forwards these to Exchanges in\n ResourceQuery.uris. Optional when `query` / `search_filters` drive\n Broker-side discovery instead.").optional(), "ver": z.string().describe("RAMP protocol version").default("") }).describe("DiscoveryRequest — Agent sends to Broker (Step 1).")); -export const DiscoveryResponseSchema = wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Existence-oracle note: an authorization-flavored reason (SCOPE_INSUFFICIENT,\n NOT_AUTHORIZED, NOT_IN_CATALOG, CONTENT_BLOCKED) confirms a resource exists\n and why access was refused. Resolve surfaces the same oracle at the broker\n that OfferGroup.absence_reason does at the Exchange, so the same mitigation\n applies: where existence itself must stay hidden, the Broker MAY omit the\n reason (leave this unset) rather than reveal it. See the threat model.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI — the sole offer representation in this\n response. One OfferGroup per URI the agent asked for (echoed in\n OfferGroup.uri); a group with no offers carries OfferGroup.absence_reason\n explaining why. Each contained Offer is the full signed Offer the Exchange\n issued (including Offer.exchange, the execute-routing target), forwarded by\n the Broker unchanged so the agent can verify the signature end to end.").optional(), "ver": z.string().default("") }).describe("Carries discovery results only: the offers the Broker gathered across\n Exchanges, grouped by the URI they were requested for. Committing to an offer\n is a separate exchange on the execute path; that per-transaction result\n (transaction_id, billing_id, cost, delivery_method, retrieval endpoint, …)\n is returned by TransactionResponse, not here.")); +export const DiscoveryResponseSchema = wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Existence-oracle note: an authorization-flavored reason (SCOPE_INSUFFICIENT,\n NOT_AUTHORIZED, NOT_IN_CATALOG, CONTENT_BLOCKED) confirms a resource exists\n and why access was refused. Resolve surfaces the same oracle at the broker\n that OfferGroup.absence_reason does at the Exchange, so the same mitigation\n applies: where existence itself must stay hidden, the Broker MAY omit the\n reason (leave this unset) rather than reveal it. See the threat model.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI — the sole offer representation in this\n response. One OfferGroup per URI the agent asked for (echoed in\n OfferGroup.uri); a group with no offers carries OfferGroup.absence_reason\n explaining why. Each contained Offer is the full signed Offer the Exchange\n issued (including Offer.exchange, the execute-routing target), forwarded by\n the Broker unchanged so the agent can verify the signature end to end.").optional(), "ver": z.string().default("") }).describe("Carries discovery results only: the offers the Broker gathered across\n Exchanges, grouped by the URI they were requested for. Committing to an offer\n is a separate exchange on the execute path; that per-transaction result\n (transaction_id, billing_id, cost, delivery_method, retrieval endpoint, …)\n is returned by TransactionResponse, not here.")); export const DisputeFailureSchema = wire(z.object({ "reason": z.enum(["DISPUTE_FAILURE_REASON_TRANSACTION_NOT_FOUND","DISPUTE_FAILURE_REASON_REPORT_NOT_FILED","DISPUTE_FAILURE_REASON_WINDOW_EXPIRED","DISPUTE_FAILURE_REASON_DUPLICATE","DISPUTE_FAILURE_REASON_INELIGIBLE"]).describe("The failure reason (defined-only, non-zero)") }).describe("DisputeFailure — a dispute could not be filed.")); @@ -88,11 +88,11 @@ export const ObligationKindSchema = wire(z.enum(["OBLIGATION_KIND_ATTRIBUTION"," export const ObligationTriggerSchema = wire(z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"])); -export const OfferSchema = wire(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")); +export const OfferSchema = wire(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")); export const OfferAbsenceReasonSchema = wire(z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"])); -export const OfferGroupSchema = wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")); +export const OfferGroupSchema = wire(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")); export const PreviewSchema = wire(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")); @@ -152,7 +152,7 @@ export const ResourceMutabilitySchema = wire(z.enum(["RESOURCE_MUTABILITY_STATIC export const ResourceQuerySchema = wire(z.object({ "acceptable_restrictions": z.array(z.object({ "axis": z.union([z.string().regex(new RegExp("^RESTRICTION_KIND_UNSPECIFIED$")), z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("Which axis (same enum as Restriction.kind): FUNCTION / GEOGRAPHY /\n USER_TYPE / OTHER.").default(0), "values": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("The values the query operates within on this axis — same token vocabulary\n as the terms (e.g. FUNCTION [\"ai-train\"], GEOGRAPHY [\"US\", \"EU\"]).").optional() }).describe("AcceptableRestriction — the limits a query operates within on one restriction\n axis, expressed in the same RestrictionKind vocabulary that terms use. The\n Exchange/Broker MAY pre-select offers whose term restrictions fall within\n these as a convenience (see Restriction); it is NOT enforcement — the agent\n self-selects and bears compliance.")).describe("The limits this query operates within, per restriction axis (function,\n geography, user-type, …) — see AcceptableRestriction. Advisory selection\n inputs the Exchange/Broker MAY pre-select offers against (convenience, not\n enforcement); the agent self-selects and bears compliance.").optional(), "deadline": z.string().describe("Maximum time the caller will wait for a response.\n Exchange SHOULD prioritize speed over completeness when tight.\n Absent = \"0.5s\" default (proto-JSON encodes Duration as seconds).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "requester": z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("When this delegation expires. Exchange MUST reject expired tokens.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Who granted this delegation (domain for public key lookup).").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).").optional(), "scopes": z.array(z.string()).describe("Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]*={0,2}$")).describe("Token bytes. A JWT (base64url-encoded JWS).").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent).").optional(), "domain": z.string().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").default(""), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().describe("Unique requester identifier (e.g., \"agent-research-bot-001\").").default(""), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("The Exchange filters its catalog to resources matching these scopes.\n Resources outside the scopes are not returned — the requester never\n learns they exist. This is the enforcement mechanism for both enterprise\n RBAC and open-market subscription entitlements.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester identity — who is making this request, what scopes they have,\n and optional delegation chain.").optional(), "supported_profiles": z.array(z.string()).describe("Declares which ext field vocabularies the caller can parse and act on.\n The Exchange SHOULD include profile-specific ext fields in Offers\n when the caller declares support. The Exchange MAY skip expensive\n metadata computation (e.g., retraction checking, consolidation\n verification) when the caller does not declare the relevant profile.\n\n Absence means \"send all available metadata\" — Exchange MUST NOT\n withhold ext fields solely because the caller omitted this field.\n\n Values match the Exchange's WellKnownManifest.supported_profiles entries.\n Examples: [\"ramp-news-v1\", \"ramp-academic-v1\", \"ramp-legal-v1\"]").optional(), "uris": z.array(z.string()).max(256).describe("Resource URIs being queried.").optional(), "ver": z.string().describe("RAMP protocol version.").default("") }).describe("Sent by a Broker or directly by an AI agent.\n The Exchange evaluates its access policies, available inventory,\n and reporting requirements before responding.")); -export const ResourceResponseSchema = wire(z.object({ "exchange": z.string().describe("Canonical domain of the responding Exchange.").default(""), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI (for multi-URI batch queries).\n When populated, `offers` SHOULD be empty to avoid ambiguity.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Flat list of offers (for single-URI queries).").optional(), "rate_limit": z.object({ "limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum requests allowed in the current window.").optional(), "remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Requests remaining in the current window.").optional(), "reset_at": z.string().datetime({ offset: true }).describe("When the current window resets (UTC). After this time, `remaining` resets to `limit`.").optional(), "window": z.string().describe("Duration of the rate limit window (e.g. 60s = per-minute limit).").optional() }).describe("Rate limit status for this caller.\n Present when the Exchange enforces per-caller rate limits on discovery.\n Enables agents/Brokers to throttle proactively rather than hitting\n hard limits. Particularly important when a Broker fans out the\n same batch query to multiple Exchanges — mid-batch rate limiting\n can cause partial results if not signaled early.").optional(), "ver": z.string().describe("Protocol version").default("") }).describe("When the ResourceQuery contains multiple URIs, offers are grouped by URI\n via OfferGroup. When a single URI is queried, the Exchange MAY use\n either the flat `offers` field or a single OfferGroup.")); +export const ResourceResponseSchema = wire(z.object({ "exchange": z.string().describe("Canonical domain of the responding Exchange.").default(""), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "offer_groups": z.array(z.object({ "absence_reason": z.enum(["OFFER_ABSENCE_REASON_NOT_IN_CATALOG","OFFER_ABSENCE_REASON_CONTENT_BLOCKED","OFFER_ABSENCE_REASON_RESTRICTION_FILTERED","OFFER_ABSENCE_REASON_TEMPORARILY_UNAVAILABLE","OFFER_ABSENCE_REASON_NOT_AUTHORIZED","OFFER_ABSENCE_REASON_SCOPE_INSUFFICIENT","OFFER_ABSENCE_REASON_UNKNOWN_CRITICAL_EXTENSION","OFFER_ABSENCE_REASON_BUDGET_EXCEEDED"]).describe("Why no offers are available for this URI.\n Present when `offers` is empty. Enables agents/Brokers to distinguish\n \"resource not in catalog\" from \"resource blocked for your use case\" without\n trial-and-error transactions. Analogous to OpenRTB nbr codes and\n Shutterstock per-item error metadata in batch responses.").optional(), "discovery_method": z.enum(["DISCOVERY_METHOD_EXCHANGE","DISCOVERY_METHOD_SEARCH","DISCOVERY_METHOD_RECOMMENDATION","DISCOVERY_METHOD_SYNDICATION"]).describe("How this URI was discovered by the Broker (v2 extension point).\n v1: always DISCOVERY_METHOD_EXCHANGE (Broker queried an Exchange).\n v2: may include DISCOVERY_METHOD_SEARCH (URI found via search engine like Exa),\n DISCOVERY_METHOD_RECOMMENDATION, etc. The Broker discovers URIs\n through any source, then routes through Exchange for pricing/transaction.\n The discovery method does not affect the transaction flow — it's metadata\n for the agent to understand how the resource was found.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Zero or more offers for this URI. Empty = resource not available.").optional(), "restriction_filters": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When absence_reason = RESTRICTION_FILTERED, the restriction axes that drove\n the convenience pre-filter, in the same RestrictionKind vocabulary the terms\n use (e.g. [GEOGRAPHY] when the requester's stated geography matched no term).\n Advisory diagnostics, not an enforcement verdict.").optional(), "uri": z.string().describe("The URI this group of offers is for (echoed from ResourceQuery.uris).").default("") }).describe("OfferGroup — Offers for a single requested URI.\n Enables multi-URI batch queries where the caller needs to know\n which offers correspond to which requested resource.")).describe("Offers grouped by requested URI (for multi-URI batch queries).\n When populated, `offers` SHOULD be empty to avoid ambiguity.").optional(), "offers": z.array(z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("Combines pricing, delivery method, resource identity, and reporting terms.\n CoMP-specific metadata (Package, Function) available via ramp-comp-v1 extension profile.")).describe("Flat list of offers (for single-URI queries).").optional(), "rate_limit": z.object({ "limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum requests allowed in the current window.").optional(), "remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Requests remaining in the current window.").optional(), "reset_at": z.string().datetime({ offset: true }).describe("When the current window resets (UTC). After this time, `remaining` resets to `limit`.").optional(), "window": z.string().describe("Duration of the rate limit window (e.g. 60s = per-minute limit).").optional() }).describe("Rate limit status for this caller.\n Present when the Exchange enforces per-caller rate limits on discovery.\n Enables agents/Brokers to throttle proactively rather than hitting\n hard limits. Particularly important when a Broker fans out the\n same batch query to multiple Exchanges — mid-batch rate limiting\n can cause partial results if not signaled early.").optional(), "ver": z.string().describe("Protocol version").default("") }).describe("When the ResourceQuery contains multiple URIs, offers are grouped by URI\n via OfferGroup. When a single URI is queried, the Exchange MAY use\n either the flat `offers` field or a single OfferGroup.")); export const RestrictionSchema = wire(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")); @@ -180,9 +180,9 @@ export const TermSemanticsSchema = wire(z.enum(["TERM_SEMANTICS_ENUMERATED","TER export const TransactionDenialSchema = wire(z.object({ "offer_id": z.string().describe("Batch mode: the offer this denial pertains to.").optional(), "reason": z.enum(["DENIAL_REASON_BILLING_REF_INACTIVE","DENIAL_REASON_INSUFFICIENT_BALANCE","DENIAL_REASON_RATE_LIMITED","DENIAL_REASON_CONTENT_UNAVAILABLE","DENIAL_REASON_RESTRICTION_NOT_SATISFIED","DENIAL_REASON_REPORTING_OVERDUE","DENIAL_REASON_OFFER_EXPIRED","DENIAL_REASON_SIGNATURE_INVALID","DENIAL_REASON_QUOTA_EXCEEDED","DENIAL_REASON_DELEGATION_INVALID","DENIAL_REASON_SCOPE_INSUFFICIENT","DENIAL_REASON_ENTITLEMENT_MISSING","DENIAL_REASON_ENTITLEMENT_MALFORMED","DENIAL_REASON_ENTITLEMENT_EXPIRED","DENIAL_REASON_ENTITLEMENT_WRONG_BUYER","DENIAL_REASON_SUBSCRIPTION_LAPSED","DENIAL_REASON_ENTITLEMENT_NOT_GRANTED"]).describe("The denial reason (defined-only, non-zero)"), "restriction_mismatches": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When reason = RESTRICTION_NOT_SATISFIED, the failed axes (same\n RestrictionKind vocabulary the terms use).").optional() }).describe("TransactionDenial — ExecuteTransaction could not complete. Carries the denial\n reason the response body no longer holds (denial_reason / restriction_mismatches\n move here in the response-shape normalization). Reuses the DenialReason vocab.")); -export const TransactionItemSchema = wire(z.object({ "agent_acceptance": z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the deterministic-marshaled\n AgentAcceptancePayload bytes."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes =\n deterministic AgentAcceptancePayload, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.").optional(), "offer": z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("The FULL signed Offer for this batch entry, reflected back exactly as\n received at discovery. The Exchange verifies `offer.signature` over these\n presented bytes — stateless, no reconstruct-from-catalog. REQUIRED: every\n batch item carries its offer.") }).describe("TransactionItem — A single offer commitment within a batch transaction.")); +export const TransactionItemSchema = wire(z.object({ "agent_acceptance": z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload\n bytes (see the canonical-signing definition on Offer.signature)."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes = the canonical\n AgentAcceptancePayload form, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.").optional(), "offer": z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("The FULL signed Offer for this batch entry, reflected back exactly as\n received at discovery. The Exchange verifies `offer.signature` over these\n presented bytes — stateless, no reconstruct-from-catalog. REQUIRED: every\n batch item carries its offer.") }).describe("TransactionItem — A single offer commitment within a batch transaction.")); -export const TransactionRequestSchema = wire(z.object({ "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "idempotency_key": z.string().min(1).max(255).describe("Idempotency key (REQUIRED). The server MUST dedupe on this: a replay returns\n the original result rather than re-executing. The transaction's durable\n identity is the Exchange-assigned transaction_id in the response.\n Uniqueness is scoped to the verified RFC 9421 signer: the server dedupes per\n (authenticated caller, key), never globally, so a key chosen by one caller\n cannot collide with another's cached result."), "items": z.array(z.object({ "agent_acceptance": z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the deterministic-marshaled\n AgentAcceptancePayload bytes."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes =\n deterministic AgentAcceptancePayload, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.").optional(), "offer": z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("The FULL signed Offer for this batch entry, reflected back exactly as\n received at discovery. The Exchange verifies `offer.signature` over these\n presented bytes — stateless, no reconstruct-from-catalog. REQUIRED: every\n batch item carries its offer.") }).describe("TransactionItem — A single offer commitment within a batch transaction.")).min(1).describe("The offers committed in this request (REQUIRED, min 1), each carrying its\n own reflected signed Offer + detached acceptance. A single offer is the\n degenerate 1-element list. The Exchange verifies each item's\n `offer.signature` (which covers pricing, terms, and expires_at) over the\n presented bytes against its own key — stateless, self-contained bearer\n tokens, with no reconstruct-from-catalog.").optional(), "requester": z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("When this delegation expires. Exchange MUST reject expired tokens.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Who granted this delegation (domain for public key lookup).").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).").optional(), "scopes": z.array(z.string()).describe("Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]*={0,2}$")).describe("Token bytes. A JWT (base64url-encoded JWS).").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent).").optional(), "domain": z.string().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").default(""), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().describe("Unique requester identifier (e.g., \"agent-research-bot-001\").").default(""), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("The Exchange filters its catalog to resources matching these scopes.\n Resources outside the scopes are not returned — the requester never\n learns they exist. This is the enforcement mechanism for both enterprise\n RBAC and open-market subscription entitlements.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester identity — forwarded for authorization and audit.").optional(), "ver": z.string().describe("Protocol version").default("") }).describe("After selecting offers, the caller commits by sending this to the\n Exchange. Supports both single-offer and batch (multi-offer) modes.\n The Exchange validates eligibility, authorizes billing, creates\n delivery, and logs each transaction.")); +export const TransactionRequestSchema = wire(z.object({ "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "idempotency_key": z.string().min(1).max(255).describe("Idempotency key (REQUIRED). The server MUST dedupe on this: a replay returns\n the original result rather than re-executing. The transaction's durable\n identity is the Exchange-assigned transaction_id in the response.\n Uniqueness is scoped to the verified RFC 9421 signer: the server dedupes per\n (authenticated caller, key), never globally, so a key chosen by one caller\n cannot collide with another's cached result."), "items": z.array(z.object({ "agent_acceptance": z.object({ "signature": z.string().min(1).describe("Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload\n bytes (see the canonical-signing definition on Offer.signature)."), "signature_algorithm": z.string().describe("Signature algorithm; \"EdDSA\" for Ed25519.").default("") }).describe("The agent's detached acceptance signature over this item's `offer`.\n Optional on the wire; the Exchange enforces presence per\n item at the service layer for relayed batches. Signed bytes = the canonical\n AgentAcceptancePayload form, with requester_* and idempotency_key\n taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature.").optional(), "offer": z.object({ "attestations": z.array(z.object({ "attested_at": z.string().datetime({ offset: true }).describe("When this attestation was created. Agents use this to assess freshness\n (e.g., \"I accept attestations up to N hours old for breaking news\").").optional(), "claims": z.record(z.string(), z.any()).describe("Signed claims about the resource (max 4KB). A JSON object containing\n whatever properties the attesting party can determine about the resource.\n Recommended claim names for interoperability:\n estimated_quantity (integer): estimated consumption quantity (e.g., token count for text)\n word_count (integer): word count (estimated_quantity ~ word_count * 1.32 for text)\n language (string): ISO 639-1 language code\n iab_categories (string[]): IAB Content Taxonomy 3.1 codes\n content_hash (string): hash of content in \"method:hexdigest\" format\n hash_method (string): algorithm used for content_hash\n Vendors MAY add vendor-specific claims (e.g., brand_safety, sentiment).\n The protocol does NOT define \"quality score\" — it is inherently subjective.\n If a vendor provides a proprietary score, the vendor defines what it means\n via their WellKnownManifest ext[\"ramp.attestation.claims_schema\"].").optional(), "keyid": z.string().describe("RFC 7638 JWK Thumbprint (the RFC 9421 keyid) of the verifier's\n attestation-signing key, resolved against the verifier's WBA directory\n (WBAFile.keys). Identifies which Ed25519 key signed this attestation.\n Enables key rotation: new keys are published with overlapping validity,\n new attestations use the new key's thumbprint, old attestations remain\n verifiable while the old key is still published.").default(""), "signature": z.string().describe("Ed25519 signature over JCS-canonicalized (RFC 8785) representation of\n {verifier, keyid, attested_at, uri, claims}. JCS (JSON Canonicalization\n Scheme) produces deterministic UTF-8 bytes: lexicographic key sorting,\n ECMAScript number serialization, strict string escaping, no whitespace.\n Each attestation is self-contained — new claim fields do not invalidate\n old attestations because the signature covers the specific claims instance.").default(""), "uri": z.string().describe("The resource URI this attestation covers. Must match the URI in the\n Offer or ResourceEntry this attestation is attached to.").default(""), "verifier": z.string().describe("Canonical domain of the attesting party (e.g., \"nytimes.com\" for\n self-attestation, \"doubleverify.com\" for third-party attestation).\n Used to look up the verifier's attestation-signing keys in its WBA\n directory (WBAFile.keys) at\n https://{verifier}/.well-known/http-message-signatures-directory").default("") }).describe("A provider or third-party verification vendor (GumGum, DoubleVerify, IAS)\n attests to properties of the resource at a specific URI at a specific time.\n The signature covers all fields, proving origin and integrity of the claims.\n\n Verification levels (determined by who the verifier is):\n Level 0: No attestation present. Resource may carry identifiers\n (DOI, IPTC GUID via ResourceIdentity) but nothing is cryptographically\n verifiable. Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): verifier == provider domain. Provider signs\n own claims with their Ed25519 key. Agent can independently verify\n content_hash by re-computing it from delivered bytes. Requires the\n provider to serve deterministic content at the delivery endpoint.\n Level 2 (third-party attested): verifier == verification vendor domain.\n Vendor independently crawled the resource and attested to its properties.\n Agent trusts the attestation — does NOT re-verify the content hash\n (agent lacks the vendor's extraction algorithm). The Ed25519 signature\n proves the vendor made the attestation; trust is binary (\"do I trust\n this vendor?\").\n\n Claims are limited to 4KB. Attestations are carried in-memory in the\n Exchange catalog and in Offer responses — strict size limits protect\n against payload poisoning and ensure catalog performance at scale.\n\n Verifiers MUST publish their attestation-signing keys in their WBA directory\n (WBAFile.keys) at:\n https://{verifier-domain}/.well-known/http-message-signatures-directory\n identified by RFC 7638 thumbprint. Verifiers publish the claims-schema\n structure at WellKnownManifest.ext[\"ramp.attestation.claims_schema\"].")).describe("Three verification levels determine what is independently verifiable:\n Level 0 (no attestations): Resource may carry identifiers (DOI, IPTC GUID)\n for identification, but nothing is cryptographically verifiable.\n Only CDN delivery failure is auto-disputable.\n Level 1 (self-attested): Provider signs own claims with Ed25519 key.\n Agent can independently verify content hash and token count.\n CDN delivery failure + content hash mismatch are auto-disputable.\n Level 2 (third-party attested): Independent verification vendor crawled\n the resource and attested to its properties. Agent trusts the attestation\n (does not re-verify hash). Token count discrepancy is auto-disputable\n when corroborated by CDN response size.\n\n Multiple attestations may be present (e.g., provider self-attestation\n plus a third-party verification). Agents choose which to trust.").optional(), "data_as_of": z.string().datetime({ offset: true }).describe("Not set for STATIC resources (content doesn't change) or LIVE\n resources (content doesn't exist yet).\n\n The Broker compares this against RequestConstraints.max_data_age\n to filter stale offers. Example: agent requests max_data_age = 7 days,\n Broker drops offers where now() - data_as_of > 7 days.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource will be delivered.").default(0), "exchange": z.string().describe("Canonical domain of the Exchange that issued this offer (e.g.\n \"exchange.example.com\"). This is the execute-routing target: the agent (or\n a relaying Broker) sends the ExecuteTransaction call for this offer to this\n Exchange. Because it is an ordinary Offer field it falls inside the signed\n bytes (see `signature` below — the signature covers every field except\n `signature` / `signature_algorithm`), so an intermediary cannot redirect\n the execute call to a different Exchange without invalidating the offer.\n This enables multi-Exchange fan-out routing from the offer itself,\n retiring the X-RAMP-Exchange-Endpoint transport header.").default(""), "expires_at": z.string().datetime({ offset: true }).describe("When this offer expires (ISO 8601).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "iab_categories": z.array(z.string()).describe("IAB Content Taxonomy category codes.\n Enables agents to filter offers by topic (e.g., \"only finance resources\").\n Uses IAB Content Taxonomy 3.1 codes.").optional(), "identity": z.object({ "c2pa_manifest": z.string().describe("Formats:\n Sidecar: HTTPS URI to a .c2pa manifest file\n Embedded: same URI as canonical_url (manifest is inside the asset)\n Content Credentials Cloud: https://contentcredentials.org/verify?uri=...").optional(), "c2pa_status": z.enum(["C2PA_STATUS_TRUSTED","C2PA_STATUS_VALID","C2PA_STATUS_INVALID","C2PA_STATUS_ABSENT"]).describe("The full C2PA validation details (signer identity, trust list,\n action history, training/mining status) are carried in a\n ResourceAttestation with c2pa.* claims — see ramp-c2pa-v1 profile.").optional(), "canonical_url": z.string().describe("Provider's authoritative URL for this resource (rel=\"canonical\").\n Always available. Different per provider for syndicated content.").optional(), "content_hash": z.string().describe("Level 1 (SimHash): computed by Exchange from extracted text.\n Agent verifies that fetched content is \"substantially similar.\"\n Tolerates dynamic page elements.\n\n Level 2 (SHA-256): computed by provider from deterministic payload.\n Agent verifies exact match. Requires provider to serve consistent\n content (e.g., API endpoint, static HTML, structured JSON).\n Mismatch = dispute. Commands premium pricing.").optional(), "doi": z.string().describe("Digital Object Identifier — persistent, never changes.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "hash_method": z.string().describe("Hash algorithm and verification level.\n Examples: \"simhash-v1\", \"minhash-v1\", \"sha256\", \"sha384\"").optional(), "iptc_guid": z.string().describe("IPTC NewsML-G2 globally unique identifier.\n Present when resource flows through news wire syndication (AP, Reuters).").optional(), "isni": z.string().describe("International Standard Name Identifier for the creator.").optional(), "resource_mutability": z.enum(["RESOURCE_MUTABILITY_STATIC","RESOURCE_MUTABILITY_DYNAMIC","RESOURCE_MUTABILITY_LIVE"]).describe("Drives hash verification behavior:\n STATIC: content_hash is stable. Agent SHOULD verify delivered content matches.\n DYNAMIC: content changes between offer and fetch (credit reports, drug databases).\n content_hash reflects state at offer generation time. Hash mismatch is\n expected and MUST NOT trigger automatic dispute.\n LIVE: content does not exist at offer time (streaming feeds, live broadcasts).\n content_hash is not applicable. The \"resource\" is the stream endpoint.\n\n Validated across 18 use cases: static content (articles, patents, legislation),\n dynamic data (credit reports, drug interactions, stock snapshots), and live\n streams (MarketData quotes, NPR broadcast, news monitoring feeds)."), "soft_binding": z.string().describe("Algorithm specified in soft_binding_method. Values are algorithm-specific\n (e.g., perceptual hash hex string, watermark identifier).").optional(), "soft_binding_method": z.string().describe("Algorithm used for soft_binding.\n Examples: \"phash-v1\" (perceptual hash), \"c2pa-watermark\" (C2PA invisible\n watermark), \"chromaprint\" (audio fingerprint).").optional() }).describe("Resource identity for cross-exchange deduplication.\n Enables Brokers to recognize the same resource offered by\n different Exchanges and compare pricing.").optional(), "offer_id": z.string().describe("Unique identifier for this offer, assigned by the Exchange.").default(""), "previews": z.array(z.object({ "duration": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Duration in seconds (for audio and video clips).").optional(), "height": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Height in pixels (images and video)").optional(), "media_type": z.string().describe("MIME type of the preview.\n Examples: \"image/jpeg\", \"image/webp\", \"audio/mpeg\", \"video/mp4\",\n \"text/plain\", \"application/json\"").default(""), "size": z.string().describe("Size category hint. Agents use this to select the right preview\n without fetching all of them.\n Standard values:\n \"thumbnail\" — smallest useful preview (100–150px or 5–10s)\n \"preview\" — mid-size for evaluation (300–500px or 15–30s)\n \"sample\" — larger / more detailed (for data: 1–3 sample records)").optional(), "url": z.string().describe("URL to a preview asset (thumbnail, clip, snippet, sample).\n Served by the provider's CDN, not by the Exchange.").default(""), "width": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Dimensions in pixels (for images and video).").optional() }).describe("The Exchange holds URLs (50–200 bytes per preview); the provider's\n CDN serves the actual bytes. This follows the universal pattern:\n Shutterstock (multi-size thumbnail URLs), Spotify (preview_url to\n 30s clip), IIIF (parameterized image URLs), OpenRTB (img.url + dims).\n\n Previews are free to fetch — no RAMP transaction required. They are\n the equivalent of looking at a book cover before buying. Providers\n MAY watermark visual previews or truncate text/audio previews.\n\n The Exchange populates preview URLs during catalog ingestion. Preview\n URLs MAY be signed with a short TTL to prevent hotlinking, or public\n (provider's choice). Agents fetch previews only when evaluating\n offers, not on every discovery query.")).describe("Per content type:\n Image: watermarked thumbnail (150–450px JPEG)\n Video: short clip (10–30s MP4, watermarked)\n Audio: short clip (15–30s MP3, low-bitrate or watermarked)\n Text: snippet or abstract (first 200 words as text/plain)\n Data: sample records (1–3 rows as application/json)\n Stream: optional frame capture or none (streams are priced by time)\n\n Modeled after Shutterstock (multi-size thumbnail URLs),\n Spotify (preview_url to 30s clip), IIIF (parameterized image URLs),\n and OpenRTB native (img.url + dimensions).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this offer. An offer represents a single licensing\n arrangement: each projected LicenseTerm yields its own offer, so this is\n that term's pricing (the authoritative copy lives in `terms[].pricing`).\n Used for cross-exchange comparison and Broker ranking. A resource with\n multiple alternative terms (e.g. dual-licensed) produces multiple separate\n offers, one per term — never one offer with a \"headline\" picked among them.").optional(), "reporting": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Post-usage reporting requirements for this offer.").optional(), "signature": z.string().describe("CANONICAL SIGNING (RFC 8785 JCS over canonical proto-JSON). The signed bytes\n are:\n\n signed_payload = JCS( protojson(msg with signature +\n signature_algorithm cleared) )\n\n i.e. render the message to canonical proto-JSON with the PINNED option set\n below, then apply RFC 8785 (JSON Canonicalization Scheme). Deterministic\n protobuf BINARY marshaling is explicitly NOT canonical across languages and\n versions (protobuf's own caveat), so it cannot be a cross-language signing\n primitive; JCS over proto-JSON can be reproduced by ANY language (Go, TS,\n Python) without a protobuf binary codec, so a broker/exchange/client in any\n language signs and verifies byte-identically. This same definition applies to\n the agent offer-acceptance signature (AgentAcceptance.signature).\n\n PINNED proto-JSON option set (the arbiter is the Go-emitted golden vector —\n whatever these options render MUST be byte-identical across all languages):\n - enum values as NAME strings (not numbers);\n - int64 / uint64 / fixed64 as decimal STRINGS;\n - bytes as standard (padded) base64;\n - google.protobuf.Timestamp / Duration per the proto-JSON WKT rules\n (RFC 3339 string for Timestamp);\n - unpopulated fields are OMITTED (never emitted as defaults);\n - field naming is snake_case (the proto field name, UseProtoNames=true),\n the naming every SDK target shares — wire, corpus, and signed form are all\n snake_case;\n - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its\n keys recursively, so the Struct case needs no special handling.\n\n UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or\n PRESERVES it, and the rule follows from which:\n\n - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a\n canonicalizer CANNOT reproduce the signed bytes of a message carrying\n unknown fields — what it renders silently drops part of what the signer\n covered. It MUST refuse the message rather than emit the reduced bytes,\n and a verifier built on it MUST reject rather than verify over them. The\n refusal binds at EVERY depth: a nested message and each element of a\n repeated or map field carries its own unknown-field set.\n - PRESERVING (a canonicalizer that carries unrecognized members through):\n it reproduces the signed bytes faithfully, so there is nothing to refuse.\n\n Either way an APPENDED field cannot pass: an omitting canonicalizer refuses\n the message, and a preserving one renders the appended member into bytes the\n signer never covered, so the signature fails. Without the refusal the omitting\n case would fail OPEN — an intermediary could add unknown fields to an\n already-signed message and leave its signature verifying, smuggling\n unauthenticated content through a message the recipient treats as verified.\n\n Extensions therefore ride in `ext` / `ext_critical`, which are defined fields\n and inside the signed bytes — never as undeclared field numbers.\n\n Because the signature covers `terms`, `pricing`, `expires_at`, and\n `exchange`, an intermediary (Broker) cannot tamper with price, restrictions,\n quotas, obligations, the expiry, the execute-routing target, or any\n licensing term without invalidating it.\n Agent SHOULD verify the signature (RFC 2119) against the Exchange's public\n key, and MUST reject an offer whose `expires_at` is in the past.").default(""), "signature_algorithm": z.string().describe("JWS algorithm. Always 'EdDSA' for Ed25519 via JWS Compact Serialization.").default(""), "subscription_id": z.string().describe("If set, this offer is available under an existing subscription/deal.\n No per-request billing — usage tracked against subscription quota.\n Pricing.rate = \"0\" for subscription offers (zero marginal cost).\n The Broker SHOULD prefer subscription offers when available.").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Subscription quota state, when this offer is under a subscription.\n Enables the agent to see remaining quota before committing.\n Multiple entries when the subscription has independent quotas\n (e.g., access count + spend cap).").optional(), "terms": z.array(z.object({ "license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("Governing license document. Authoritative for REFERENCE_ONLY terms, which\n MUST carry a License with a non-empty uri — a REFERENCE_ONLY term that\n references nothing is rejected at ingest.").optional(), "obligations": z.array(z.object({ "detail": z.string().describe("Free-form detail: attribution string, notice file URI, etc.\n OBLIGATION_KIND_OTHER without it → lint warning.").optional(), "kind": z.enum(["OBLIGATION_KIND_ATTRIBUTION","OBLIGATION_KIND_CONTRIBUTION","OBLIGATION_KIND_SHARE_ALIKE","OBLIGATION_KIND_NETWORK_COPYLEFT","OBLIGATION_KIND_NOTICE","OBLIGATION_KIND_OTHER"]).describe("What the agent must do."), "scope_license": z.object({ "id": z.string().describe("Stable short identifier: SPDX short-id (\"GPL-3.0-only\"), TollBit cuid,\n or catalog doc-id. Used by agents and the vocab linter for known-license\n lookup; SHARE_ALIKE derivatives default their scope_license to this.").optional(), "immutable": z.boolean().describe("Data-labels TDL: the document at uri is versioned and will not change.").optional(), "name": z.string().describe("Human-readable name (licenseType, schema.org node name).").optional(), "uri": z.string().describe("\"MUST NOT URL-validate\" means do not REJECT non-URL schemes — it does NOT\n mean fetch blindly. A consumer that dereferences this URI MUST apply the\n SSRF countermeasures in the security threat model (T-LIC-1): scheme\n allowlist, block loopback/private/metadata addresses (resolve-then-check),\n fetch via an egress proxy, and treat the response as untrusted content.\n Verify the fetched bytes against `uri_digest` before use.").optional(), "uri_digest": z.string().regex(new RegExp("^(sha256:[0-9a-f]{64}|sha384:[0-9a-f]{96}|sha512:[0-9a-f]{128})?$")).describe("The method MUST be a collision-resistant hash — sha256, sha384, or sha512.\n Legacy md5/sha1 are rejected on the wire: a forgeable digest would defeat\n the swap-protection this field exists for. The CEL is STRUCTURE ONLY\n (allowlisted prefix + matching hex length); presence (digest-when-uri) is\n enforced at ingest.").optional() }).describe("The license that derivatives must be released under. REQUIRED for\n SHARE_ALIKE (rejected if absent), where it MUST identify a license — set\n `id` (SPDX short-id, the common copyleft case, often the term's own\n License.id) and/or `uri`. Because it is a License, a referenced `uri`\n inherits the uri_digest swap-protection rule: a uri without a digest is\n rejected, exactly as for any other license reference.").optional(), "trigger": z.enum(["OBLIGATION_TRIGGER_ON_USE","OBLIGATION_TRIGGER_ON_DISTRIBUTION","OBLIGATION_TRIGGER_ON_NETWORK_SERVICE","OBLIGATION_TRIGGER_ON_DERIVATIVE"]).describe("When the obligation activates.") }).describe("Examples:\n Attribution on display: cite the author whenever content is shown to a user.\n Share-alike on derivative: AI-generated content that incorporates this work\n must be released under the same license.\n Notice on distribution: include the copyright notice when distributing copies.")).describe("Post-use behavioral requirements.").optional(), "part_label": z.string().describe("Informational human-readable name for this sub-part (sub-part terms).").optional(), "pricing": z.object({ "currency": z.string().describe("ISO 4217 currency code (e.g. \"USD\", \"EUR\").").default(""), "estimated_quantity": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Estimated quantity in the metering unit.\n For text: token count. For video: duration in seconds.\n For documents: page count. For data: record count.").optional(), "license_duration_months": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("License duration in months. How long the granted access remains valid.").optional(), "metering": z.enum(["PRICING_METERING_ONLINE","PRICING_METERING_NONE","PRICING_METERING_OFFLINE_SELF_REPORTED"]).describe("How usage is tracked for billing reconciliation.\n Absent = PRICING_METERING_ONLINE (default real-time tracking).\n NONE = one-time perpetual sale; no ReportUsage required after ExecuteTransaction.\n OFFLINE_SELF_REPORTED = agent self-reports physical-world consumption.").optional(), "model": z.enum(["PRICING_MODEL_FREE","PRICING_MODEL_PER_UNIT","PRICING_MODEL_FLAT"]).describe("Provider's pricing model."), "rate": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Price in the provider's model, as an exact decimal string — e.g. \"0.05\" =\n $0.05 per article. NOT a float: money is decimal to avoid binary rounding and\n to allow arbitrary sub-cent precision (e.g. \"0.0001234\"). Denominated in `currency`.").default(""), "unit": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)?$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare tokens. A buf plugin reads them structurally and emits the\n pricingunits constants + IsRegistered; ingest enforces membership from\n those. The CEL is STRUCTURE ONLY (empty / bare-form / vendor:namespaced) —\n it never lists the tokens, so it cannot drift from the registry.").optional(), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Normalized cost per unit — the universal comparison metric, exact decimal string.\n For text: cost per token. For video: cost per second.\n For data: cost per record. For APIs: cost per call.\n Denominated in the Exchange's base_currency (from its WellKnownManifest).").optional() }).describe("Pricing for this term. REQUIRED for every term regardless of semantics —\n an agent cannot act on a priceless term, so absent Pricing is a validation\n error at ingest. model = FREE must be stated explicitly (absent Pricing is\n not free). A REFERENCE_ONLY term states its price here too; its License\n governs the human-readable terms but does not replace the machine-readable\n price."), "quotas": z.array(z.object({ "limit": z.coerce.number().int().gte(1).describe("Maximum allowed value in the given window. A quota of 0 grants\n nothing — express \"no access\" by omitting the term, not a zero quota."), "metric": z.string().regex(new RegExp("^([a-z0-9-]+|[A-Za-z0-9._-]+:[A-Za-z0-9._-]+)$")).max(64).describe("The (ramp.v1.vocab) entries below are the SOLE authored source of the\n registered bare metric tokens. A buf plugin reads them structurally and\n emits the quotametrics constants + IsRegistered; ingest enforces membership\n from those. The CEL is STRUCTURE ONLY (non-empty bare token or\n vendor:namespaced) — it never lists the tokens, so it cannot drift.\n\n Token meanings:\n display-words Words of content text rendered to an end user.\n impressions Times the content is displayed to an end user.\n tokens LLM output tokens generated using this content.\n input-tokens LLM input tokens consumed from this content.\n units-manufactured Physical units manufactured from this design/pattern.\n accesses Distinct content access / retrieval events.\n copies Digital or physical copies produced.\n seats Distinct named users licensed to access the content."), "window": z.enum(["QUOTA_WINDOW_HOURLY","QUOTA_WINDOW_DAILY","QUOTA_WINDOW_MONTHLY","QUOTA_WINDOW_TOTAL"]).describe("Time window over which the limit accumulates.") }).describe("Quotas limit how much a licensee may consume before the term expires or\n must be renegotiated. They are NOT billing quantities — billing is in Pricing.\n\n The metric vocabulary is authored ONLY in the (ramp.v1.vocab) entries on\n Quota.metric below; the quotametrics constants + IsRegistered derive from it.")).describe("Usage caps. The agent must not exceed any individual Quota.").optional(), "restrictions": z.array(z.object({ "advisory": z.boolean().describe("Fail-closed by default. When false (the default), this restriction is\n BINDING: an agent that cannot evaluate every token in it — including an\n unknown vendor token — MUST decline the term. Set advisory = true to\n downgrade an unverifiable restriction to non-blocking. This deliberately\n inverts the COSE-`crit` opt-in default: a license restriction a consumer\n does not understand should stop it, not be silently ignored.").default(false), "kind": z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"]).describe("Which dimension this restriction applies to."), "permitted": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens allowed on this axis. Empty = all permitted.\n For FUNCTION: \"ai-input\", \"ai-train\", \"search\", \"editorial\", \"commercial\", …\n For GEOGRAPHY: \"US\", \"DE\", \"EU\", \"EEA\", \"*\", …\n For USER_TYPE: \"individual\", \"academic\", \"commercial_entity\", …").optional(), "prohibited": z.array(z.string().regex(new RegExp("^[A-Za-z0-9._:*-]+$")).min(1).max(64)).max(64).describe("Tokens blocked on this axis. Takes precedence over permitted[].").optional() }).describe("Restrictions model allowed and prohibited values on one axis (function,\n geography, or user-type). They are validated and normalized at ingest and\n RIDE ON THE OFFER: the AGENT is the responsible party — it self-selects the\n term whose restrictions it can honour and bears compliance, and enforcement\n happens downstream at accept → report → reconcile. Restrictions are NOT an\n Exchange-side gate the requester must pass to see a term.\n\n An Exchange or Broker MAY, purely as a CONVENIENCE, pre-filter the offers it\n returns against the limits the query states in ResourceQuery.acceptable_restrictions\n (the same RestrictionKind axes/vocabulary the terms use) — e.g. an agent that\n only wants US-eligible content can ask the Exchange to skip the rest so it\n doesn't pay to discover offers it would never accept. That filter is advisory and\n optional: a different Broker may not apply it, and it is a recommendation\n matched to the request, never an enforcement verdict. When an Exchange does\n drop offers this way it MAY signal it via OfferAbsenceReason.RESTRICTION_FILTERED\n (with the axes in OfferGroup.restriction_filters). Term visibility is otherwise\n gated only by resource_id/URI and delegation scope coverage — see\n LicenseTerm.scopes.\n\n Reading a restriction:\n A value is in-scope when it matches at least one permitted[] token\n AND matches none of the prohibited[] tokens.\n Empty permitted[] = any value is permitted on this axis.\n Empty prohibited[] = nothing is explicitly prohibited.\n\n Vocabulary sources (authored on the RestrictionKind enum values via\n (ramp.v1.vocab_enum); the functiontokens / geographytokens / usertypes\n constants + IsRegistered derive from them):\n FUNCTION — RSL 1.0 AI-use vocabulary + established IP/copyright terms\n GEOGRAPHY — ISO 3166-1 alpha-2 (structural) + the specials *, EU, EEA\n USER_TYPE — RAMP user/organization categories")).describe("Usage restrictions (function, geography, user-type).\n Multiple restrictions are AND-combined — the agent must satisfy all of them.").optional(), "scopes": z.array(z.string()).max(64).describe("Coverage uses the SAME matching rule as Requester/delegation scopes:\n segment-wise (\":\" separated), each granted segment must equal the\n corresponding required segment or be \"*\", a terminal \"*\" matches all\n remaining segments, and there is NO implicit prefix match (a grant\n narrower than the requirement does not cover it). \"dist:*\" covers\n \"dist:US\" and \"dist:US:CA\"; \"dist\" covers only \"dist\". There is exactly\n one scope-matching algorithm across the protocol.").optional(), "semantics": z.enum(["TERM_SEMANTICS_ENUMERATED","TERM_SEMANTICS_REFERENCE_ONLY"]).describe("How to interpret the machine fields.") }).describe("One LicenseTerm describes one complete access arrangement for a resource.\n A resource carries zero or more terms; having multiple terms is the normal\n case (one per use category, user type, or commercial arrangement).\n\n The same LicenseTerm shape appears at ingestion (ResourceEntry.terms) and\n at emission (Offer.terms). The Exchange stores what the publisher pushed\n and surfaces it on discovery, so agents see the same terms the publisher\n declared — no translation or reformulation.\n\n Validation rules:\n - Pricing MUST be present on EVERY term, regardless of semantics.\n Absent Pricing → reject at ingest: an agent cannot act on a term with\n no price. This holds for REFERENCE_ONLY too — its License governs the\n human-readable terms, but the machine-readable price is still stated\n here, not deferred to the document.\n - model=FREE must be explicit. Absent Pricing ≠ free. A term may be FREE\n under an arbitrary license; the agent still needs the price stated so it\n knows the access is free rather than unpriced.\n - REFERENCE_ONLY terms MUST carry a License with a non-empty uri. A\n REFERENCE_ONLY term that references no document is meaningless → reject\n at ingest.\n - Restriction tokens are validated against the vocab registry.\n Unknown tokens produce a PushResourcesResponse.warnings[] entry\n but do NOT cause rejection (forward-compatible).")).describe("Licensing terms for this offer, sourced from the publisher's ResourceEntry.\n Multiple terms when the resource has different arrangements by use case.\n See: Universal Licensing Core section.").optional() }).describe("The FULL signed Offer for this batch entry, reflected back exactly as\n received at discovery. The Exchange verifies `offer.signature` over these\n presented bytes — stateless, no reconstruct-from-catalog. REQUIRED: every\n batch item carries its offer.") }).describe("TransactionItem — A single offer commitment within a batch transaction.")).min(1).describe("The offers committed in this request (REQUIRED, min 1), each carrying its\n own reflected signed Offer + detached acceptance. A single offer is the\n degenerate 1-element list. The Exchange verifies each item's\n `offer.signature` (which covers pricing, terms, and expires_at) over the\n presented bytes against its own key — stateless, self-contained bearer\n tokens, with no reconstruct-from-catalog.").optional(), "requester": z.object({ "delegation": z.object({ "expires_at": z.string().datetime({ offset: true }).describe("When this delegation expires. Exchange MUST reject expired tokens.").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "issuer": z.string().describe("Token issuer. OIDC issuer URL or GNAP grant server URL.\n Exchange uses this for JWT validation (OIDC discovery → JWKS)\n or GNAP token introspection.").optional(), "max_accesses": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Maximum number of accesses allowed under this delegation.\n Exchange tracks cumulative access count against this cap.\n Deny with DENIAL_REASON_QUOTA_EXCEEDED when count >= limit.\n For subscriptions with \"10,000 accesses/month\", this carries the ceiling.").optional(), "max_spend_cents": z.coerce.number().int().describe("Maximum spend in currency minor units (e.g., cents for USD).\n Exchange tracks cumulative spend against this cap.").optional(), "principal_domain": z.string().describe("Who granted this delegation (domain for public key lookup).").default(""), "principal_id": z.string().describe("Principal's identifier (e.g., \"user@acme.com\", \"marketdata.example.com\").").default(""), "quota_period": z.string().describe("Quota reset period. How often the access/spend counters reset.\n Example: 30 days for monthly subscriptions — \"2592000s\" on the wire\n (proto-JSON encodes Duration as seconds; \"720h\" is not accepted).\n When absent, the quota is lifetime (bounded only by expires_at).").optional(), "revocation_uri": z.string().describe("Optional: URI for real-time revocation checking.\n Exchange MAY check this for high-value transactions.\n Not checked for routine low-value access (performance tradeoff).").optional(), "scopes": z.array(z.string()).describe("Scopes granted by this delegation. MUST be a subset of the\n principal's own scopes (attenuation — can only narrow, not widen).").optional(), "token": z.string().regex(new RegExp("^[A-Za-z0-9+/]*={0,2}$")).describe("Token bytes. A JWT (base64url-encoded JWS).").default(""), "token_format": z.string().describe("Token format: \"jwt\" (default). Empty is treated as \"jwt\". The field stays\n open for a future format.").default("") }).describe("Optional delegation — present when the requester acts on behalf of\n another entity (user, organization, upstream agent).").optional(), "domain": z.string().describe("Domain the requester belongs to — used for public key lookup.\n Keys published at {domain}/.well-known/ramp.json (WellKnownManifest, role=ROLE_AGENT).").default(""), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "id": z.string().describe("Unique requester identifier (e.g., \"agent-research-bot-001\").").default(""), "name": z.string().describe("Human-readable name (e.g., \"Acme Research Assistant\").").optional(), "scopes": z.array(z.string()).max(64).describe("The Exchange filters its catalog to resources matching these scopes.\n Resources outside the scopes are not returned — the requester never\n learns they exist. This is the enforcement mechanism for both enterprise\n RBAC and open-market subscription entitlements.\n\n Scope format: colon-separated segments, \"{domain}:{permission}\" or\n \"{profile}:{permission}\", optionally multi-segment (\"dist:US:CA\");\n matching is segment-wise per the rule below (no implicit hierarchy).\n Examples:\n \"credit:read\" — can access credit reports\n \"subscription:marketdata-2026\" — has active MarketData subscription\n \"academic:*\" — full access to academic resources\n \"internal:reports\" — can access internal reports\n \"*\" — unrestricted (public Exchange default)\n\n Matching is SEGMENT-WISE (\":\" separated). A granted scope G covers a\n required scope R iff, segment by segment, each G segment equals the\n corresponding R segment or is \"*\"; a terminal \"*\" matches all remaining\n segments. There is NO implicit prefix match, and a grant NARROWER than\n the requirement does not cover it (G must be equal-to-or-broader than R).\n Examples: \"dist:*\" covers \"dist:US\" and \"dist:US:CA\"; \"dist:US:*\" covers\n \"dist:US:CA\" but not \"dist:EU\"; bare \"dist\" covers only \"dist\"; granted\n \"dist:US:CA\" does NOT cover required \"dist:US\"; \"*\" covers everything.\n This same rule governs LicenseTerm.scopes — one algorithm protocol-wide.\n\n When empty, Exchange applies its default access policy (typically\n returns all publicly available resources).").optional(), "type": z.enum(["REQUESTER_TYPE_AGENT","REQUESTER_TYPE_HUMAN_TOOL","REQUESTER_TYPE_SERVICE","REQUESTER_TYPE_DELEGATED","REQUESTER_TYPE_RESEARCH"]).describe("What kind of entity is making this request.") }).describe("Requester identity — forwarded for authorization and audit.").optional(), "ver": z.string().describe("Protocol version").default("") }).describe("After selecting offers, the caller commits by sending this to the\n Exchange. Supports both single-offer and batch (multi-offer) modes.\n The Exchange validates eligibility, authorizes billing, creates\n delivery, and logs each transaction.")); export const TransactionResponseSchema = wire(z.object({ "agent_identity_hash": z.string().describe("Identity that a delivered retrieval_endpoint is bound to: the RFC 7638 JWK\n Thumbprint of the agent's Ed25519 request-signing key (see \"Retrieval-URL\n identity binding\" above). Shared across the request; set once.").default(""), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "items": z.array(z.object({ "billing_id": z.string().describe("Billing record identifier minted by the Exchange's billing adapter for\n this transaction (not the account handle — see RegisterResponse.billing_ref).").default(""), "cost": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Cost for this item.").optional(), "delivery_method": z.union([z.string().regex(new RegExp("^DELIVERY_METHOD_UNSPECIFIED$")), z.enum(["DELIVERY_METHOD_DIRECT","DELIVERY_METHOD_INSTRUCTIONS","DELIVERY_METHOD_STREAMING"]), z.coerce.number().int().gte(-2147483648).lte(2147483647)]).describe("How resource is delivered for this item.").default(0), "denial_reason": z.enum(["DENIAL_REASON_BILLING_REF_INACTIVE","DENIAL_REASON_INSUFFICIENT_BALANCE","DENIAL_REASON_RATE_LIMITED","DENIAL_REASON_CONTENT_UNAVAILABLE","DENIAL_REASON_RESTRICTION_NOT_SATISFIED","DENIAL_REASON_REPORTING_OVERDUE","DENIAL_REASON_OFFER_EXPIRED","DENIAL_REASON_SIGNATURE_INVALID","DENIAL_REASON_QUOTA_EXCEEDED","DENIAL_REASON_DELEGATION_INVALID","DENIAL_REASON_SCOPE_INSUFFICIENT","DENIAL_REASON_ENTITLEMENT_MISSING","DENIAL_REASON_ENTITLEMENT_MALFORMED","DENIAL_REASON_ENTITLEMENT_EXPIRED","DENIAL_REASON_ENTITLEMENT_WRONG_BUYER","DENIAL_REASON_SUBSCRIPTION_LAPSED","DENIAL_REASON_ENTITLEMENT_NOT_GRANTED"]).describe("Set if this specific item was denied (others may succeed).").optional(), "expires_at": z.string().datetime({ offset: true }).describe("When retrieval_endpoint expires.").optional(), "offer_id": z.string().describe("The offer_id this result is for.").default(""), "reporting_obligation": z.object({ "endpoint": z.string().describe("URL to submit the usage report to (if different from Exchange).").optional(), "ext": z.record(z.string(), z.any()).describe("Extension point").optional(), "ext_critical": z.array(z.string()).describe("Critical extension keys (COSE crit pattern, RFC 9052).\n Lists keys within ext that the consumer MUST understand.\n Unknown keys in this list → reject with UNKNOWN_CRITICAL_EXTENSION.\n Empty (default) → all ext keys are safe to ignore.").optional(), "required": z.boolean().describe("Whether post-usage reporting is required.").default(false), "required_fields": z.array(z.string()).describe("Field names that must be present in the report.").optional(), "window": z.string().describe("Duration within which the report must be submitted (e.g. \"86400s\" = 24\n hours; proto-JSON encodes Duration as seconds).").optional() }).describe("Reporting requirements for this item.").optional(), "resource_title": z.string().describe("Resource title echoed from the Offer.").optional(), "restriction_mismatches": z.array(z.enum(["RESTRICTION_KIND_FUNCTION","RESTRICTION_KIND_GEOGRAPHY","RESTRICTION_KIND_USER_TYPE","RESTRICTION_KIND_OTHER"])).describe("When denial_reason = RESTRICTION_NOT_SATISFIED, the restriction axes the\n request failed, in the same RestrictionKind vocabulary the terms use.").optional(), "retrieval_endpoint": z.string().describe("Signed retrieval URL for this item. Bound to the requesting agent's identity\n via the parent TransactionResponse.agent_identity_hash (shared across all\n batch items); expires at expires_at. Absent if this item was denied or its\n delivery_method is not signed-URL-based.").optional(), "subscription_id": z.string().describe("If under subscription, no per-request charge.").optional(), "subscription_unit_value": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Computed per-unit cost for financial attribution on subscription transactions.\n Even when cost.amount=\"0\" (subscription), this field carries the value\n of the access for accounting purposes (e.g., ASC 606 prepaid drawdown).").optional(), "transaction_id": z.string().describe("Exchange-assigned transaction identifier.").default("") }).describe("TransactionResultItem — Result for a single offer in a batch transaction.")).describe("Per-offer results (one entry per committed item, in original order).").optional(), "subscription_quota": z.array(z.object({ "quota_limit": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Total allowed in the current period.").optional(), "quota_remaining": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Remaining in the current period.").optional(), "quota_used": z.coerce.number().int().gte(-2147483648).lte(2147483647).describe("Used so far in the current period.").optional(), "resets_at": z.string().datetime({ offset: true }).describe("When the quota counter resets (UTC).").optional(), "subscription_id": z.string().describe("Subscription this quota applies to.").default(""), "unit": z.string().describe("What is being metered. Distinguishes access count quotas from\n spend quotas from burst limits.\n Standard values: \"accesses\", \"tokens\", \"spend_cents\", \"burst\"").optional() }).describe("Analogous to RateLimitInfo (which signals API request rate limits), this\n signals subscription consumption quotas. Enables agents to throttle\n proactively instead of discovering exhaustion via denial.\n\n Returned on Offer (per-offer quota visibility) and TransactionResponse\n (post-transaction remaining quota). A subscription may have multiple\n independent quotas (access count + spend cap + burst limit), so this\n message is used as a repeated field.\n\n Quota decrement timing: the counter increments at ExecuteTransaction\n (optimistic decrement, before delivery). If delivery fails, the agent\n files a DisputeTransaction which may reverse the decrement. This is\n consistent with the billing model (billing_id created at transaction time).")).describe("Post-transaction quota state. Tells the agent how much quota remains\n after this transaction. Enables proactive throttling (\"1 access left\").\n Multiple entries for multi-dimensional quotas.").optional(), "total_cost": z.object({ "amount": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).describe("Exact decimal string (not a float), e.g. \"19.99\". Denominated in `currency`.").default(""), "currency": z.string().default(""), "unit_cost": z.string().regex(new RegExp("^([0-9]+([.][0-9]+)?)?$")).max(32).optional() }).describe("Aggregate cost across all items.").optional(), "ver": z.string().describe("Protocol version").default("") }).describe("Items-only: every per-result datum lives in `items`\n (one TransactionResultItem per committed offer, in original order); the\n top-level fields carry only the shared aggregate state. A single offer is the\n degenerate 1-element `items`. The per-item denials remain in-body on\n TransactionResultItem as partial results of a successful request.")); diff --git a/proto/CHANGELOG.md b/proto/CHANGELOG.md index 31dd870..ad2c354 100644 --- a/proto/CHANGELOG.md +++ b/proto/CHANGELOG.md @@ -49,6 +49,91 @@ oracle. The SSRF-guarded transport is now a single env-driven client two flags (`SKIP_SSRF`, `ALLOW_INSECURE`). See `docs/sdk-parity-matrix.md` for the per-language surface. +**Go SDK: `helpers.CanonicalOfferBytes` exported (additive, no wire change).** +The offer-canonical-bytes accessor — RFC 8785 JCS over canonical proto-JSON with +`signature`/`signature_algorithm` cleared, `expires_at` included, byte-identical to +what `SignOffer` signs and `VerifyOffer` verifies — is now a public Go symbol. It +exposes the single canonicalization the signer and verifier already share, so a +caller can persist the signed offer as verbatim, independently re-verifiable +evidence. Python (`canonical_offer_payload`) and TS (`canonicalOfferPayload`) already +expose the equivalent public accessor; this brings the Go surface to parity. + +**Go SDK: `helpers.CanonicalAcceptanceBytes` exported (additive, no wire change).** +The acceptance-canonical-bytes accessor — RFC 8785 JCS over canonical proto-JSON of +`AgentAcceptancePayload{offer_sig, requester_id, requester_domain, idempotency_key}`, +byte-identical to what `SignOfferAcceptance` signs and `VerifyOfferAcceptance` +verifies — is now a public Go symbol, completing the pair with +`CanonicalOfferBytes`. A caller can persist an agent's acceptance as verbatim, +independently re-verifiable evidence rather than re-deriving the bytes at +verification time, which would pin an already-signed acceptance to whatever +canonicalization the SDK implements later. Python (`jcs_acceptance_payload`) and TS +(`acceptancePayload`) already expose the equivalent public accessor; this brings the +Go surface to parity. + +**Acceptance canonical-form text corrected (documentation only, no wire change).** +`AgentAcceptance` and `AgentAcceptancePayload` still described the RETIRED signing +form — "the deterministic protobuf serialization", "`proto.Marshal(Deterministic: +true)`" — contradicting the canonical-signing block on `Offer.signature` in the same +file, which already states that RFC 8785 JCS over canonical proto-JSON "applies to +the agent offer-acceptance signature". The acceptance text now points at that single +normative definition instead of restating a superseded recipe: +`AgentAcceptancePayload` fixes the field set, `Offer.signature` fixes the byte +layout. Implementations that followed the stale text would have produced +non-verifying signatures. No field, message, or wire change — comments only, with +`gen/` and the website mirror regenerated. + +**Python + TS SDK: the hand-built acceptance payload omits every unpopulated field +(bug fix, no wire change).** `jcs_acceptance_payload` (Python) and `acceptancePayload` +(TS) assemble the `AgentAcceptancePayload` JSON object key by key, and omitted only an +empty `requester_domain` — `requester_id` and `idempotency_key` were always emitted. +Go renders the same object through `protojson` with `EmitUnpopulated=false`, which omits +EVERY unpopulated field, so the three SDKs signed different bytes whenever `requester_id` +was empty. That input is wire-valid: `Requester.id` carries no `min_len`. Verification +failed closed on it (a byte mismatch, never a bypass), but the byte-equivalence the +canonical-bytes accessors promise did not hold. Both hand-built faces now drop each empty +string field, and two new vectors in `sdk/go/helpers/testdata/acceptance-vectors.json` — +`empty_requester_id` and `empty_idempotency_key`, one per omittable field left uncovered — +pin the agreement across Go, Python and TS. Without them the omission can be dropped in any +one language with every gate still green. The corpus change is purely additive — the +pre-existing vectors and their signatures are byte-identical, so no already-issued signature +is affected. + +**Canonical signing refuses messages carrying unknown fields (normative; Go SDK +behavior change, no wire change).** `Offer.signature` — the single normative definition of +the canonical form — now states the rule, which turns on whether a canonicalizer omits or +preserves content it has no schema for. An OMITTING canonicalizer (proto-JSON emits only +schema-defined fields) cannot reproduce the signed bytes of a message carrying unknown +fields, so it MUST refuse the message rather than emit the reduced bytes, at EVERY depth — +a nested message and each element of a repeated or map field carries its own unknown-field +set. A PRESERVING canonicalizer carries unrecognized members through, reproduces the +signed bytes faithfully, and has nothing to refuse. + +Either way an APPENDED field cannot pass, which is the point: the omitting case refuses +the message, and the preserving case renders the appended member into bytes the signer +never covered. Without the refusal the omitting case failed OPEN — an intermediary could +add unknown fields to an already-signed `Offer` **without invalidating its signature**, +smuggling unauthenticated content through a message the recipient treats as verified. That +is what the Go SDK now closes: `helpers.VerifyOffer` surfaces the refusal as +`ErrOfferSignatureInvalid` (a message that arrived carrying extra bytes is a tampered +offer, not an internal fault) wrapping the new `helpers.ErrUnknownFields`, so a caller can +branch on either; `helpers.CanonicalOfferBytes` and `helpers.SignOffer` return +`ErrUnknownFields` directly. + +Python (`from_wire_offer`) and TypeScript (`canonicalOfferPayload`) are preserving +canonicalizers and need no change — they already reject the appended-field case on a byte +mismatch. They are NOT expected to reject a message whose signer covered the unknown +member: they reproduce those bytes exactly and verify, which is the forward-compatible +outcome. Go, being an omitting canonicalizer, cannot reconstruct such a message at all and +refuses it; that asymmetry is inherent to the renderer, not new here — before this change +Go rejected the same message on a byte mismatch instead. + +No legitimate traffic regresses: an offer signed WITH a field this build cannot render +already failed to verify; the refusal only makes the reason explicit. Extensions are +unaffected — they ride in `ext` / `ext_critical`, defined fields that sit inside the signed +bytes, never undeclared field numbers. One new exported Go symbol (`ErrUnknownFields`, +registered as a Go-idiomatic exclusion in the parity map); no field, message, or wire +change — proto comments only, with `gen/` and the website mirror regenerated. + **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no `reserved` statement — pre-v1 the number returns to the free pool, and diff --git a/proto/ramp/v1/ramp.proto b/proto/ramp/v1/ramp.proto index 43f6362..119c35a 100644 --- a/proto/ramp/v1/ramp.proto +++ b/proto/ramp/v1/ramp.proto @@ -475,6 +475,29 @@ message Offer { // - google.protobuf.Struct (`ext`) → a plain JSON object; JCS then sorts its // keys recursively, so the Struct case needs no special handling. // + // UNKNOWN FIELDS. A canonicalizer either OMITS content it has no schema for or + // PRESERVES it, and the rule follows from which: + // + // - OMITTING (e.g. proto-JSON, which emits only schema-defined fields): such a + // canonicalizer CANNOT reproduce the signed bytes of a message carrying + // unknown fields — what it renders silently drops part of what the signer + // covered. It MUST refuse the message rather than emit the reduced bytes, + // and a verifier built on it MUST reject rather than verify over them. The + // refusal binds at EVERY depth: a nested message and each element of a + // repeated or map field carries its own unknown-field set. + // - PRESERVING (a canonicalizer that carries unrecognized members through): + // it reproduces the signed bytes faithfully, so there is nothing to refuse. + // + // Either way an APPENDED field cannot pass: an omitting canonicalizer refuses + // the message, and a preserving one renders the appended member into bytes the + // signer never covered, so the signature fails. Without the refusal the omitting + // case would fail OPEN — an intermediary could add unknown fields to an + // already-signed message and leave its signature verifying, smuggling + // unauthenticated content through a message the recipient treats as verified. + // + // Extensions therefore ride in `ext` / `ext_critical`, which are defined fields + // and inside the signed bytes — never as undeclared field numbers. + // // Because the signature covers `terms`, `pricing`, `expires_at`, and // `exchange`, an intermediary (Broker) cannot tamper with price, restrictions, // quotas, obligations, the expiry, the execute-routing target, or any @@ -1651,12 +1674,16 @@ enum ResourceMutability { // agent's key (RFC 7638 thumbprint of the acceptance key). // // `signature` is a hex-encoded detached Ed25519 signature (NOT a JWS) over the -// deterministic protobuf serialization of `AgentAcceptancePayload` — the same -// hex/Ed25519 convention the SDK uses for Offer.signature. `signature_algorithm` -// is "EdDSA". +// CANONICAL SIGNING form of `AgentAcceptancePayload` — RFC 8785 JCS over canonical +// proto-JSON. That form, including the pinned proto-JSON option set, is defined +// once on `Offer.signature` and is the single normative definition; there is no +// second recipe. `AgentAcceptancePayload` carries no signature fields, so the +// clear-then-render step of that definition reduces here to +// JCS(protojson(AgentAcceptancePayload)). Same hex/Ed25519 convention as +// `Offer.signature`; `signature_algorithm` is "EdDSA". message AgentAcceptance { - // Hex-encoded detached Ed25519 signature over the deterministic-marshaled - // AgentAcceptancePayload bytes. + // Hex-encoded detached Ed25519 signature over the canonical AgentAcceptancePayload + // bytes (see the canonical-signing definition on Offer.signature). string signature = 1 [(buf.validate.field).string.min_len = 1]; // Signature algorithm; "EdDSA" for Ed25519. @@ -1666,8 +1693,10 @@ message AgentAcceptance { // AgentAcceptancePayload — the canonical signing structure for AgentAcceptance. // It is NEVER sent on the wire; it exists solely so the signer (SDK) and the // verifier (Exchange) derive BYTE-IDENTICAL signed bytes from the same proto -// schema via `proto.Marshal(Deterministic: true)`. Underspecifying these bytes -// is the top cross-implementation drift risk, so the field set is fixed here. +// schema. This message fixes the FIELD SET; the byte layout is the canonical +// signing form defined on Offer.signature — RFC 8785 JCS over canonical +// proto-JSON with the pinned option set. Underspecifying either half is the top +// cross-implementation drift risk, so both are pinned normatively. // // Field provenance when building the payload for an execute request: // - offer_sig = the accepted Offer.signature (the Exchange's hex @@ -1750,8 +1779,8 @@ message TransactionItem { // The agent's detached acceptance signature over this item's `offer`. // Optional on the wire; the Exchange enforces presence per - // item at the service layer for relayed batches. Signed bytes = - // deterministic AgentAcceptancePayload, with requester_* and idempotency_key + // item at the service layer for relayed batches. Signed bytes = the canonical + // AgentAcceptancePayload form, with requester_* and idempotency_key // taken from the ENCLOSING TransactionRequest and offer_sig = offer.signature. optional AgentAcceptance agent_acceptance = 4; } diff --git a/sdk/go/helpers/acceptance.go b/sdk/go/helpers/acceptance.go index 2dd5727..25de9db 100644 --- a/sdk/go/helpers/acceptance.go +++ b/sdk/go/helpers/acceptance.go @@ -17,9 +17,11 @@ import ( // it stays valid no matter how many brokers relay the request, because it covers // the offer + requester + idempotency, not the HTTP envelope. // -// The signed bytes are pinned by the AgentAcceptancePayload proto message so the -// signer (agent) and the verifier (Exchange) derive them identically; both go -// through canonicalAcceptancePayload, the single source of the byte layout. +// The signed bytes are pinned in two halves, both normative: the +// AgentAcceptancePayload proto message fixes the FIELD SET, and the canonical +// signing form defined on Offer.signature fixes the BYTE LAYOUT. That is what +// lets the signer (agent) and the verifier (Exchange) derive them identically; +// both go through CanonicalAcceptanceBytes, the single implementation of the pair. // AcceptanceSignatureAlgorithm is the alg advertised on AgentAcceptance. // Always EdDSA for Ed25519. @@ -30,18 +32,43 @@ const AcceptanceSignatureAlgorithm = "EdDSA" // idempotency key). var ErrAcceptanceSignatureInvalid = errors.New("helpers: offer-acceptance signature invalid") -// canonicalAcceptancePayload is the canonical byte sequence an agent's offer -// acceptance covers: the accepted Offer.signature (which transitively binds the -// offer's pricing, terms, expiry, and issuing Exchange), plus the requester -// identity and the transaction's idempotency key. The offer must be signed — an -// empty anchor would let the acceptance float free of any concrete offer, so it -// is rejected fail-closed. +// CanonicalAcceptanceBytes returns the exact canonical byte sequence an agent's +// offer acceptance covers: the accepted Offer.signature (which transitively binds +// the offer's pricing, terms, expiry, and issuing Exchange), plus the requester +// identity and idempotency key of the ENCLOSING execute request — in batch mode +// both come from the TransactionRequest, never from the per-item TransactionItem, +// which carries neither. The returned bytes are +// byte-identical to what SignOfferAcceptance signs and VerifyOfferAcceptance +// verifies over — persist them to re-verify an acceptance verbatim, independent of +// how the canonical form later evolves. +// +// Re-verification also needs the persisted AgentAcceptance.signature and the signer's +// trusted public key: these bytes are the signed message, necessary but not by +// themselves sufficient. A passing signature proves only that the key holder signed +// THESE bytes, so a caller weighing them as evidence must also parse them and match +// their content against the transaction in question. // // The canonical form is RFC 8785 JCS over canonical proto-JSON — -// JCS(protojson(AgentAcceptancePayload)) — via canonicalSignPayload, the same -// primitive the offer signature uses, so any language reproduces the exact signed -// bytes without a protobuf binary codec. See canonicalsign.go for the option set. -func canonicalAcceptancePayload(offer *rampv1.Offer, requester *rampv1.Requester, idempotencyKey string) ([]byte, error) { +// JCS(protojson(AgentAcceptancePayload)) — the same definition the offer signature +// uses, stated normatively on Offer.signature in ramp.proto: snake_case proto field +// names, enums as name strings, unpopulated fields omitted. AgentAcceptancePayload +// carries no signature fields, so the clear-then-render step reduces to a plain +// render. Any language (Go/TS/Python) reproduces the exact bytes from that +// definition, without a protobuf binary codec. +// +// The payload is built here from the four scalars rather than taken from the caller, +// so it cannot carry the unknown fields CanonicalOfferBytes has to refuse; only the +// values read off the offer and requester reach the signed bytes. +// +// Unpopulated fields are OMITTED before JCS — EVERY empty string field, not just the +// domain: the bytes for an empty requester id are not the bytes for any populated +// one. A port that assembles this object by hand instead of rendering the proto must +// reproduce that omission per field, or it signs bytes this function never produces. +// +// Fails closed on a nil offer, a nil requester, or an unsigned offer (empty +// Offer.signature) — an empty anchor would let the acceptance float free of any +// concrete offer. +func CanonicalAcceptanceBytes(offer *rampv1.Offer, requester *rampv1.Requester, idempotencyKey string) ([]byte, error) { if offer == nil { return nil, errors.New("helpers: offer is nil") } @@ -68,7 +95,7 @@ func SignOfferAcceptance(priv ed25519.PrivateKey, offer *rampv1.Offer, requester if len(priv) != ed25519.PrivateKeySize { return "", fmt.Errorf("helpers: ed25519 private key must be %d bytes, got %d", ed25519.PrivateKeySize, len(priv)) } - payload, err := canonicalAcceptancePayload(offer, requester, idempotencyKey) + payload, err := CanonicalAcceptanceBytes(offer, requester, idempotencyKey) if err != nil { return "", err } @@ -86,7 +113,7 @@ func VerifyOfferAcceptance(offer *rampv1.Offer, requester *rampv1.Requester, ide if err != nil { return fmt.Errorf("helpers: decode acceptance signature: %w", err) } - payload, err := canonicalAcceptancePayload(offer, requester, idempotencyKey) + payload, err := CanonicalAcceptanceBytes(offer, requester, idempotencyKey) if err != nil { return err } diff --git a/sdk/go/helpers/acceptance_payload_test.go b/sdk/go/helpers/acceptance_payload_test.go index 476bccc..e4c6c28 100644 --- a/sdk/go/helpers/acceptance_payload_test.go +++ b/sdk/go/helpers/acceptance_payload_test.go @@ -4,14 +4,13 @@ import ( "testing" rampv1 "github.com/RAMP-Protocol/protocol/gen/go/ramp/v1" - "google.golang.org/protobuf/proto" ) -// The agent offer-acceptance wire envelope and its canonical -// signing payload exist on the modern execute shape. The acceptance must travel -// WITH the reflected Offer on both single-mode (TransactionRequest) and batch -// (TransactionItem), and the canonical payload must marshal deterministically -// (byte-identical across the SDK signer and the Exchange verifier). +// The agent offer-acceptance wire envelope and its canonical signing payload +// exist on the modern execute shape. The acceptance must travel WITH the +// reflected Offer on both single-mode (TransactionRequest) and batch +// (TransactionItem), so the Exchange can bind the agent to the exact offer it +// is executing no matter how many brokers relayed the request. func TestAgentAcceptance_wireFieldsExist(t *testing.T) { acc := &rampv1.AgentAcceptance{ Signature: "deadbeef", @@ -32,28 +31,28 @@ func TestAgentAcceptance_wireFieldsExist(t *testing.T) { } } -// The canonical signing structure is a dedicated message so the signer and -// verifier derive byte-identical signed bytes from the same proto. It MUST -// marshal deterministically. -func TestAgentAcceptancePayload_deterministicMarshal(t *testing.T) { - payload := &rampv1.AgentAcceptancePayload{ - OfferSig: "ex-offer-sig-hex", - RequesterId: "req-1", - RequesterDomain: "agent.example.com", - IdempotencyKey: "idem-1", - } - a, err := proto.MarshalOptions{Deterministic: true}.Marshal(payload) - if err != nil { - t.Fatalf("marshal: %v", err) - } - b, err := proto.MarshalOptions{Deterministic: true}.Marshal(payload) - if err != nil { - t.Fatalf("marshal (2): %v", err) - } - if len(a) == 0 { - t.Fatal("canonical payload marshalled to zero bytes") - } - if string(a) != string(b) { - t.Fatal("deterministic marshal is not stable across calls") +// AgentAcceptancePayload fixes the FIELD SET the acceptance signature covers; +// the byte layout is the canonical signing form defined on Offer.signature (RFC +// 8785 JCS over canonical proto-JSON). The two halves are pinned separately, and +// only this one is pinned by the message. +// +// Canonical proto-JSON omits unpopulated fields, so a field added to this message +// without a matching assignment in CanonicalAcceptanceBytes drops out of the +// signed bytes entirely: the golden vectors stay byte-identical and every gate +// stays green while the agent silently commits to less than the message claims. +// The set is therefore asserted here rather than left to review. +func TestAgentAcceptancePayload_fieldSetIsPinned(t *testing.T) { + want := []string{"offer_sig", "requester_id", "requester_domain", "idempotency_key"} + + fields := (&rampv1.AgentAcceptancePayload{}).ProtoReflect().Descriptor().Fields() + if fields.Len() != len(want) { + t.Fatalf("AgentAcceptancePayload has %d fields, want %d %v; a field the "+ + "canonicalizer does not assign is omitted from the signed bytes", + fields.Len(), len(want), want) + } + for i, name := range want { + if got := string(fields.Get(i).Name()); got != name { + t.Errorf("field %d = %q, want %q", i+1, got, name) + } } } diff --git a/sdk/go/helpers/acceptance_test.go b/sdk/go/helpers/acceptance_test.go index 67319c4..0ee4371 100644 --- a/sdk/go/helpers/acceptance_test.go +++ b/sdk/go/helpers/acceptance_test.go @@ -2,11 +2,13 @@ package helpers_test import ( "crypto/ed25519" + "encoding/hex" "errors" "testing" rampv1 "github.com/RAMP-Protocol/protocol/gen/go/ramp/v1" "github.com/RAMP-Protocol/protocol/sdk/go/helpers" + "google.golang.org/protobuf/proto" ) func acceptanceFixture() (*rampv1.Offer, *rampv1.Requester, string) { @@ -114,3 +116,70 @@ func TestSignOfferAcceptance_emptyOfferSignatureRejected(t *testing.T) { t.Fatal("expected error signing acceptance over an unsigned offer (empty anchor)") } } + +func TestCanonicalAcceptanceBytes_matchesSignOfferAcceptance(t *testing.T) { + // The bytes CanonicalAcceptanceBytes returns are exactly what + // SignOfferAcceptance signed: verifying that signature directly over them must + // hold. This is the property a caller relies on to persist verbatim, + // re-verifiable acceptance evidence. + pub, priv, _ := ed25519.GenerateKey(nil) + offer, requester, idem := acceptanceFixture() + sigHex, err := helpers.SignOfferAcceptance(priv, offer, requester, idem) + if err != nil { + t.Fatal(err) + } + canon, err := helpers.CanonicalAcceptanceBytes(offer, requester, idem) + if err != nil { + t.Fatal(err) + } + sig, err := hex.DecodeString(sigHex) + if err != nil { + t.Fatal(err) + } + if !ed25519.Verify(pub, canon, sig) { + t.Error("SignOfferAcceptance signature must verify over CanonicalAcceptanceBytes") + } +} + +// Byte-identity against the committed cross-language corpus is not asserted here: +// the golden emitter rebuilds testdata/acceptance-vectors.json through this same +// function and byte-compares the whole committed file on every default run, so a +// second harness over the same equality would only add a second schema to keep in +// step with the file. + +func TestCanonicalAcceptanceBytes_failsClosed(t *testing.T) { + offer, requester, idem := acceptanceFixture() + if _, err := helpers.CanonicalAcceptanceBytes(nil, requester, idem); err == nil { + t.Error("nil offer should error") + } + if _, err := helpers.CanonicalAcceptanceBytes(offer, nil, idem); err == nil { + t.Error("nil requester should error") + } + // An unsigned offer is an empty anchor: the acceptance would float free of any + // concrete offer, so the bytes are refused rather than produced. + unsigned := &rampv1.Offer{OfferId: "of_1"} + if _, err := helpers.CanonicalAcceptanceBytes(unsigned, requester, idem); err == nil { + t.Error("unsigned offer (empty offer signature) should error") + } +} + +func TestCanonicalAcceptanceBytes_doesNotMutateInputs(t *testing.T) { + // The payload is built from getters onto a fresh message, so a caller can hand + // in the live Offer/Requester it is about to persist and get them back + // untouched. Nothing is cloned on the way in, so the whole message is compared: + // a future refactor could reach any field, not only the four the canonical form + // reads. + offer, requester, idem := acceptanceFixture() + offerBefore := proto.Clone(offer) + requesterBefore := proto.Clone(requester) + + if _, err := helpers.CanonicalAcceptanceBytes(offer, requester, idem); err != nil { + t.Fatal(err) + } + if !proto.Equal(offer, offerBefore) { + t.Errorf("CanonicalAcceptanceBytes mutated the caller's offer\n got %v\n want %v", offer, offerBefore) + } + if !proto.Equal(requester, requesterBefore) { + t.Errorf("CanonicalAcceptanceBytes mutated the caller's requester\n got %v\n want %v", requester, requesterBefore) + } +} diff --git a/sdk/go/helpers/canonicalsign.go b/sdk/go/helpers/canonicalsign.go index 157cd1a..30c8bb6 100644 --- a/sdk/go/helpers/canonicalsign.go +++ b/sdk/go/helpers/canonicalsign.go @@ -1,11 +1,13 @@ package helpers import ( + "errors" "fmt" "github.com/gowebpki/jcs" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" ) // Canonical signing payload (ADR-020 §4, ramp.proto "canonical signing" note). @@ -47,10 +49,91 @@ var canonicalSignJSONOptions = protojson.MarshalOptions{ EmitUnpopulated: false, // omit unpopulated fields } +// ErrUnknownFields refuses a message the canonical form cannot faithfully +// represent. proto-JSON renders only fields the schema defines, so a message +// carrying unknown ones canonicalizes to bytes that silently omit them. Both +// directions of that gap are wrong: a signer built against a newer schema +// covered more than this build can reconstruct, and an on-path party that +// appends unknown fields to an already-signed message would otherwise leave the +// signature verifying over bytes it never saw. Refusing the message is what +// makes the canonical form's coverage total rather than schema-relative. +// +// CanonicalOfferBytes and SignOffer return it directly, so a caller persisting +// evidence can tell this refusal apart from a marshal fault. VerifyOffer wraps it +// alongside ErrOfferSignatureInvalid — a message that arrived carrying extra bytes +// is a tampered offer — so errors.Is matches either sentinel there. +var ErrUnknownFields = errors.New("helpers: message carries unknown fields; canonical bytes cannot represent them") + +// hasUnknownFields reports whether msg, or anything reachable from it, carries +// fields this build's schema does not define. +// +// The walk is explicit rather than delegated to a traversal helper: this is a +// signature-coverage guard, so its reachability rules are stated here where they +// can be audited against the message tree. Unknown-field sets are PER MESSAGE — +// a nested message and every element of a repeated or map field owns its own — +// so a top-level check alone would pass a payload whose tampering sits one level +// down. +func hasUnknownFields(msg proto.Message) bool { + if msg == nil { + return false + } + return messageHasUnknown(msg.ProtoReflect()) +} + +func messageHasUnknown(m protoreflect.Message) bool { + if !m.IsValid() { + return false // a typed nil carries nothing + } + if len(m.GetUnknown()) > 0 { + return true + } + found := false + m.Range(func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool { + switch { + case fd.IsMap(): + // Map KEYS are scalars; only a message-typed value can nest one. The + // synthetic ENTRY messages are not visited: protobuf-go discards unknown + // bytes planted on an entry at unmarshal, so they never reach here (nor + // the wire again). offer_test.go pins that, and fails loudly if a + // protobuf upgrade starts retaining them. + if fd.MapValue().Kind() == protoreflect.MessageKind || + fd.MapValue().Kind() == protoreflect.GroupKind { + v.Map().Range(func(_ protoreflect.MapKey, mv protoreflect.Value) bool { + if messageHasUnknown(mv.Message()) { + found = true + } + return !found + }) + } + case fd.IsList(): + if fd.Kind() == protoreflect.MessageKind || fd.Kind() == protoreflect.GroupKind { + list := v.List() + for i := 0; i < list.Len() && !found; i++ { + if messageHasUnknown(list.Get(i).Message()) { + found = true + } + } + } + case fd.Kind() == protoreflect.MessageKind, fd.Kind() == protoreflect.GroupKind: + if messageHasUnknown(v.Message()) { + found = true + } + } + return !found // stop the range as soon as one is found + }) + return found +} + // canonicalSignPayload renders msg to canonical proto-JSON (pinned options) and // applies RFC 8785 JCS, returning the exact UTF-8 bytes an Ed25519 signature // covers. Callers clear signature/signature_algorithm on a clone BEFORE calling. +// +// A message carrying unknown fields is refused rather than rendered — see +// ErrUnknownFields. func canonicalSignPayload(msg proto.Message) ([]byte, error) { + if hasUnknownFields(msg) { + return nil, ErrUnknownFields + } pj, err := canonicalSignJSONOptions.Marshal(msg) if err != nil { return nil, fmt.Errorf("helpers: canonical proto-JSON marshal: %w", err) diff --git a/sdk/go/helpers/gen_vectors_test.go b/sdk/go/helpers/gen_vectors_test.go index 99bb19d..0cb43a2 100644 --- a/sdk/go/helpers/gen_vectors_test.go +++ b/sdk/go/helpers/gen_vectors_test.go @@ -317,7 +317,7 @@ type offerVerifyVector struct { NowUnix int64 `json:"now_unix"` ExpectedVerified bool `json:"expected_verified"` // ExchangeSeedHex is the exchange offer-signing seed (fixedSeed 0x66), so a - // sign-face port re-signs canonicalOfferPayload(offer_json) and byte-matches + // sign-face port re-signs CanonicalOfferBytes(offer_json) and byte-matches // the signature already embedded in offer_json. ExchangeSeedHex string `json:"exchange_seed_hex"` } @@ -331,7 +331,7 @@ type offerVerifyDoc struct { } // offerCanonicalProtoJSON renders offer to the SAME pinned proto-JSON the signer -// canonicalizes over (camelCase, enums-as-names, omit-unpopulated). Emitting the +// canonicalizes over (snake_case, enums-as-names, omit-unpopulated). Emitting the // vector's offer_json through the identical option set is what lets the port // reproduce JCS(protojson(offer)) byte-for-byte. func offerCanonicalProtoJSON(t *testing.T, offer *rampv1.Offer) json.RawMessage { @@ -480,11 +480,12 @@ func buildOfferVerifyVectors(t *testing.T) []offerVerifyVector { } // wireCanonicalVector pins the wire-to-canonical conversion: wire_json is the -// offer exactly as the Connect codec emits it (camelCase json_names, enums as +// offer exactly as the Connect codec emits it (snake_case proto names, enums as // names, EmitUnpopulated zero-inflation; JCS-stabilized so the committed file is // deterministic), canonical_json is the byte sequence the offer signature covers -// (canonicalOfferPayload: signature/signature_algorithm cleared, snake_case, -// omit-unpopulated, JCS). A from-wire canonicalizer in any language must map +// (CanonicalOfferBytes: signature/signature_algorithm cleared, omit-unpopulated, +// JCS). Both sides share the snake_case naming, so what a from-wire canonicalizer +// must actually undo is the zero-inflation and the signature fields — it must map // wire_json to canonical_json exactly. type wireCanonicalVector struct { Name string `json:"name"` @@ -511,12 +512,13 @@ func buildWireCanonicalVectors(t *testing.T) []wireCanonicalVector { t.Fatalf("%s: wire proto-JSON marshal: %v", name, err) } // JCS-stabilize the committed wire form (protojson whitespace/order is not - // deterministic); key CASE is untouched, so the camel wire shape survives. + // deterministic); JCS sorts and re-encodes but never RENAMES a key, so the + // codec's snake_case wire shape survives intact. wireCanon, err := jcs.Transform(wirePJ) if err != nil { t.Fatalf("%s: wire JCS: %v", name, err) } - canonical, err := canonicalOfferPayload(offer) + canonical, err := CanonicalOfferBytes(offer) if err != nil { t.Fatalf("%s: canonical payload: %v", name, err) } @@ -994,9 +996,13 @@ func verifySignRequestVector(t *testing.T, v signRequestVector) { } // buildAcceptanceVectors signs a fixed set of offer acceptances with the REAL Go -// SignOfferAcceptance, seeded 0102..1f20 (shared with the app fixture). Includes -// the empty-domain case (proto3 field-3 default-skip). Records canonical bytes -// hex + signature hex + std-base64 pubkey. +// SignOfferAcceptance, seeded 0102..1f20 (shared with the app fixture). Records +// canonical bytes + signature hex + std-base64 pubkey. +// +// The specs cover each field the canonical form may omit, one per vector. Go omits +// them structurally (EmitUnpopulated=false); the Python and TS faces hand-build the +// object and have to drop empty members themselves, so an omission they miss shows +// up here as a byte mismatch and nowhere else. func buildAcceptanceVectors(t *testing.T) []acceptanceVector { t.Helper() seed, err := hex.DecodeString(acceptanceSeedHex) @@ -1017,13 +1023,24 @@ func buildAcceptanceVectors(t *testing.T) []acceptanceVector { specs := []spec{ {"all_present", "ex-offer-sig-hex", "agent-1", "agent.example.com", "idem-1"}, {"empty_domain", "sig2deadbeef", "agent-2", "", "idem-2"}, + // Requester.id carries no min_len, so an empty id is wire-valid. The + // canonical form omits EVERY unpopulated field, not only the domain — this + // vector is what holds the hand-built Python/TS payloads to that, since + // they enumerate the keys instead of inheriting EmitUnpopulated=false. + {"empty_requester_id", "sig3deadbeef", "", "agent.example.com", "idem-3"}, + // The last omittable field. TransactionRequest.idempotency_key carries + // min_len:1 so the wire rejects an empty one, but these accessors are the + // layer below that check and must still agree on the bytes — without this + // vector the omission guard can be dropped in any language with every gate + // still green. + {"empty_idempotency_key", "sig4deadbeef", "agent-4", "agent.example.com", ""}, } out := make([]acceptanceVector, 0, len(specs)) for _, s := range specs { offer := &rampv1.Offer{Signature: s.offerSig} requester := &rampv1.Requester{Id: s.requesterID, Domain: s.requesterDomain} - canon, err := canonicalAcceptancePayload(offer, requester, s.idempotencyKey) + canon, err := CanonicalAcceptanceBytes(offer, requester, s.idempotencyKey) if err != nil { t.Fatalf("%s: canonical: %v", s.name, err) } diff --git a/sdk/go/helpers/offer.go b/sdk/go/helpers/offer.go index 5054a25..f3b81b8 100644 --- a/sdk/go/helpers/offer.go +++ b/sdk/go/helpers/offer.go @@ -31,7 +31,7 @@ func SignOffer(priv ed25519.PrivateKey, offer *rampv1.Offer) (string, error) { if len(priv) != ed25519.PrivateKeySize { return "", fmt.Errorf("helpers: ed25519 private key must be %d bytes, got %d", ed25519.PrivateKeySize, len(priv)) } - payload, err := canonicalOfferPayload(offer) + payload, err := CanonicalOfferBytes(offer) if err != nil { return "", err } @@ -53,8 +53,17 @@ func VerifyOffer(offer *rampv1.Offer, signatureHex string, pub ed25519.PublicKey // signature must land on the same rejected path as a forged one. return fmt.Errorf("%w: decode hex: %v", ErrOfferSignatureInvalid, err) } - payload, err := canonicalOfferPayload(offer) + payload, err := CanonicalOfferBytes(offer) if err != nil { + if errors.Is(err, ErrUnknownFields) { + // An offer carrying fields this build cannot render is refused on the + // SAME path as a forged one. Callers branch on the sentinel to map a + // rejection to its denial reason, and "someone appended bytes to a + // signed offer" is a signature failure, not an internal fault. BOTH + // sentinels are wrapped: the denial mapping resolves through the first, + // a caller wanting the specific reason gets the second. + return fmt.Errorf("%w: %w", ErrOfferSignatureInvalid, err) + } return err } if !ed25519.Verify(pub, payload, sig) { @@ -63,21 +72,38 @@ func VerifyOffer(offer *rampv1.Offer, signatureHex string, pub ed25519.PublicKey return nil } -// canonicalOfferPayload is the canonical byte sequence the offer signature -// covers: the ENTIRE Offer (pricing, terms, expires_at, …) with ONLY the -// signature and signature_algorithm fields cleared, per ramp.proto Offer.signature. +// CanonicalOfferBytes returns the exact canonical byte sequence an Offer's +// signature is computed over: the ENTIRE Offer (pricing, terms, expires_at, …) +// with ONLY the signature and signature_algorithm fields cleared, per +// ramp.proto Offer.signature. The returned bytes are byte-identical to what +// SignOffer signs and VerifyOffer verifies over — persist them to re-verify an +// Offer signature verbatim, independent of how the canonical form, or the Offer +// message, later evolves. +// +// Re-verification also needs the persisted Offer.signature and the signer's trusted +// public key: these bytes are the signed message, necessary but not by themselves +// sufficient. A passing signature proves only that the key holder signed THESE bytes, +// so a caller weighing them as evidence must also parse them and match their content +// against the transaction in question. // // The canonical form is RFC 8785 JCS over canonical proto-JSON — -// JCS(protojson(offer with sig cleared)) — via canonicalSignPayload, so any -// language (Go/TS/Python) reproduces the exact signed bytes without a protobuf -// binary codec. See canonicalsign.go for the pinned proto-JSON option set. +// JCS(protojson(offer with sig cleared)) — rendered under the option set the +// Offer.signature comment in ramp.proto defines normatively: snake_case proto field +// names, enums as name strings, unpopulated fields omitted. That definition, not +// this implementation, is what lets any language (Go/TS/Python) reproduce the exact +// signed bytes without a protobuf binary codec. +// +// expires_at is covered (only signature/signature_algorithm are cleared), so a +// relaying Broker cannot extend or shorten a signed offer's TTL under an +// otherwise-valid signature. // -// NOTE (expires_at reconciliation): the protocol spec covers expires_at so a -// relaying Broker cannot extend/shorten a signed offer's TTL. The pre-SDK -// service-internal signer additionally cleared expires_at (a stateless-reissue -// workaround); this SDK follows the protocol. The platform adopts this behavior -// when it re-pins onto the SDK — see the filed reconciliation task. -func canonicalOfferPayload(offer *rampv1.Offer) ([]byte, error) { +// An Offer carrying UNKNOWN fields at ANY depth is REFUSED (ErrUnknownFields) rather +// than rendered — proto-JSON emits only what the schema defines, so those bytes would +// silently drop the unknown content. The rule, and the depths it reaches, are stated +// once in the ramp.proto Offer.signature comment; it is not restated here. VerifyOffer +// surfaces the refusal wrapped in ErrOfferSignatureInvalid, since a message that +// arrived carrying extra bytes is a tampered Offer, not an internal fault. +func CanonicalOfferBytes(offer *rampv1.Offer) ([]byte, error) { if offer == nil { return nil, errors.New("helpers: offer is nil") } diff --git a/sdk/go/helpers/offer_test.go b/sdk/go/helpers/offer_test.go index 1907571..c014f17 100644 --- a/sdk/go/helpers/offer_test.go +++ b/sdk/go/helpers/offer_test.go @@ -1,13 +1,18 @@ package helpers_test import ( + "bytes" "crypto/ed25519" + "encoding/hex" "errors" "testing" "time" rampv1 "github.com/RAMP-Protocol/protocol/gen/go/ramp/v1" "github.com/RAMP-Protocol/protocol/sdk/go/helpers" + "google.golang.org/protobuf/encoding/protowire" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -98,3 +103,318 @@ func TestVerifyOffer_nil(t *testing.T) { t.Error("nil offer should error") } } + +func TestCanonicalOfferBytes_matchesSignOffer(t *testing.T) { + // The bytes CanonicalOfferBytes returns are exactly what SignOffer signed: + // verifying the SignOffer signature directly over them must hold. This is the + // property a caller relies on to persist verbatim, re-verifiable offer evidence. + pub, priv, _ := ed25519.GenerateKey(nil) + offer := sampleOffer() + sigHex, err := helpers.SignOffer(priv, offer) + if err != nil { + t.Fatal(err) + } + canon, err := helpers.CanonicalOfferBytes(offer) + if err != nil { + t.Fatal(err) + } + sig, err := hex.DecodeString(sigHex) + if err != nil { + t.Fatal(err) + } + if !ed25519.Verify(pub, canon, sig) { + t.Error("SignOffer signature must verify over CanonicalOfferBytes") + } +} + +func TestCanonicalOfferBytes_ignoresSignatureFields(t *testing.T) { + // signature/signature_algorithm are cleared on a clone before canonicalizing, + // so an already-signed offer yields the same bytes as the unsigned one — the + // signature cannot cover itself. + offer := sampleOffer() + before, err := helpers.CanonicalOfferBytes(offer) + if err != nil { + t.Fatal(err) + } + offer.Signature = "deadbeef" + offer.SignatureAlgorithm = helpers.OfferSignatureAlgorithm + after, err := helpers.CanonicalOfferBytes(offer) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(before, after) { + t.Error("CanonicalOfferBytes must clear signature/signature_algorithm; bytes differ") + } + // The clear happens on a clone: the caller's offer keeps the fields it set. + if offer.Signature != "deadbeef" || offer.SignatureAlgorithm != helpers.OfferSignatureAlgorithm { + t.Error("CanonicalOfferBytes must not mutate the caller's offer (clears on a clone)") + } +} + +func TestCanonicalOfferBytes_coversExpiresAt(t *testing.T) { + // expires_at is inside the signed bytes (only the signature fields are + // cleared), so changing it changes the canonical output — the property that + // stops a relaying Broker from re-stretching a signed offer's TTL. + a := sampleOffer() + b := sampleOffer() + b.ExpiresAt = timestamppb.New(time.Unix(1799999999, 0)) + ba, err := helpers.CanonicalOfferBytes(a) + if err != nil { + t.Fatal(err) + } + bb, err := helpers.CanonicalOfferBytes(b) + if err != nil { + t.Fatal(err) + } + if bytes.Equal(ba, bb) { + t.Error("offers differing only in expires_at must produce different CanonicalOfferBytes") + } +} + +func TestCanonicalOfferBytes_nil(t *testing.T) { + if _, err := helpers.CanonicalOfferBytes(nil); err == nil { + t.Error("nil offer should error") + } +} + +// encodeWithUnknownField encodes msg and appends a field number no RAMP message +// defines — what a peer built against a newer schema emits, and equally what an +// on-path party appends to a message it did not author. +func encodeWithUnknownField(t *testing.T, msg proto.Message) []byte { + t.Helper() + raw, err := proto.Marshal(msg) + if err != nil { + t.Fatal(err) + } + raw = protowire.AppendTag(raw, 500, protowire.VarintType) + return protowire.AppendVarint(raw, 7) +} + +// embed appends msg's encoding to host as a length-delimited field, so an +// unknown field can be planted at a chosen depth of the Offer tree. +func embed(t *testing.T, host []byte, field protowire.Number, encoded []byte) []byte { + t.Helper() + host = protowire.AppendTag(host, field, protowire.BytesType) + return protowire.AppendBytes(host, encoded) +} + +// unknownFieldCase pairs an Offer with the SAME Offer carrying one unknown field. +// The two differ by nothing a renderer can see, which is the whole point: any +// rejection has to come from the unknown field itself, not from a visible +// difference that would break the signature anyway. +type unknownFieldCase struct { + clean *rampv1.Offer + tampered *rampv1.Offer +} + +// unknownFieldCases plants an unknown field at three depths: on the Offer itself, +// inside the nested Pricing message, and inside one element of the repeated +// attestations list. Unknown-field sets are per-message, so a guard that only +// checks the top level passes the first and misses the other two. +func unknownFieldCases(t *testing.T) map[string]unknownFieldCase { + t.Helper() + + decode := func(raw []byte) *rampv1.Offer { + var o rampv1.Offer + if err := proto.Unmarshal(raw, &o); err != nil { + t.Fatal(err) + } + return &o + } + encode := func(m proto.Message) []byte { + raw, err := proto.Marshal(m) + if err != nil { + t.Fatal(err) + } + return raw + } + + out := map[string]unknownFieldCase{} + + out["top level"] = unknownFieldCase{ + clean: sampleOffer(), + tampered: decode(encodeWithUnknownField(t, sampleOffer())), + } + + // Rebuild the offer from a shell plus a hand-encoded Pricing so the unknown + // field lands inside the nested message. The shell carries every other field, + // so clean and tampered render identically. + base := sampleOffer() + shell := encode(&rampv1.Offer{OfferId: base.OfferId, ExpiresAt: base.ExpiresAt}) + out["nested message"] = unknownFieldCase{ + clean: decode(embed(t, shell, offerPricingField, encode(base.Pricing))), + tampered: decode(embed(t, shell, offerPricingField, encodeWithUnknownField(t, base.Pricing))), + } + + // The attestation must be present in BOTH, or the tampered offer would differ + // visibly and be rejected on a byte mismatch — passing the test for a reason + // that has nothing to do with the unknown field. + full := encode(sampleOffer()) + att := &rampv1.ResourceAttestation{Verifier: "verifier.example.com", Uri: "https://example.com/a"} + out["repeated element"] = unknownFieldCase{ + clean: decode(embed(t, full, offerAttestationsField, encode(att))), + tampered: decode(embed(t, full, offerAttestationsField, encodeWithUnknownField(t, att))), + } + + return out +} + +// Offer field numbers used to plant an unknown field at depth. Kept as named +// constants so a proto renumbering breaks the fixture loudly instead of quietly +// planting the field somewhere else. +const ( + offerPricingField protowire.Number = 3 + offerAttestationsField protowire.Number = 14 +) + +func TestCanonicalOfferBytes_refusesUnknownFields(t *testing.T) { + // proto-JSON renders only what the schema defines, so a message carrying + // unknown fields would canonicalize to bytes that silently drop them. Producing + // those bytes is refused outright: they are not the bytes anyone signed, and + // handing them back as "the canonical form" is what would let an intermediary + // append content to a signed offer without disturbing its signature. + for name, c := range unknownFieldCases(t) { + t.Run(name, func(t *testing.T) { + if _, err := helpers.CanonicalOfferBytes(c.clean); err != nil { + t.Fatalf("control: the same offer without the unknown field must canonicalize: %v", err) + } + if _, err := helpers.CanonicalOfferBytes(c.tampered); err == nil { + t.Error("an offer carrying unknown fields must not yield canonical bytes") + } + }) + } +} + +func TestVerifyOffer_rejectsInjectedUnknownFields(t *testing.T) { + // The tamper case: a legitimately signed offer picks up unknown fields in + // transit. The signature still matches the fields this build can render, so + // only the refusal above stops it from verifying. It must land on the signature + // sentinel — callers map that to a signature-invalid denial, and an appended + // payload is a tampered offer, not an internal fault. + pub, priv, _ := ed25519.GenerateKey(nil) + for name, c := range unknownFieldCases(t) { + t.Run(name, func(t *testing.T) { + // Sign the CLEAN offer, then verify the tampered one. The signature is + // genuine and the two render identically, so the unknown field is the + // only thing that can cause a rejection. + sigHex, err := helpers.SignOffer(priv, c.clean) + if err != nil { + t.Fatal(err) + } + if err := helpers.VerifyOffer(c.clean, sigHex, pub); err != nil { + t.Fatalf("control: the clean offer must verify: %v", err) + } + if err := helpers.VerifyOffer(c.tampered, sigHex, pub); !errors.Is(err, helpers.ErrOfferSignatureInvalid) { + t.Errorf("want ErrOfferSignatureInvalid, got %v", err) + } + }) + } +} + +func TestCanonicalOfferBytes_refusesUnknownFieldsInsideExt(t *testing.T) { + // ext is a google.protobuf.Struct, whose `fields` is a map with MESSAGE values — + // the one place the walk's map branch is reachable on this schema, and it guards + // the sanctioned extension surface. An unknown field planted on a Struct Value + // sits two levels below the Offer and behind a map, so nothing shallower catches + // it. + st, err := structpb.NewStruct(map[string]any{"vendor.key": "v"}) + if err != nil { + t.Fatal(err) + } + clean := sampleOffer() + clean.Ext = st + if _, err := helpers.CanonicalOfferBytes(clean); err != nil { + t.Fatalf("control: an untampered ext must canonicalize: %v", err) + } + + tamperedVal := new(structpb.Value) + if err := proto.Unmarshal(encodeWithUnknownField(t, st.Fields["vendor.key"]), tamperedVal); err != nil { + t.Fatal(err) + } + if len(tamperedVal.ProtoReflect().GetUnknown()) == 0 { + t.Fatal("fixture is inert: the unknown field did not survive unmarshal") + } + st.Fields["vendor.key"] = tamperedVal + + if _, err := helpers.CanonicalOfferBytes(clean); !errors.Is(err, helpers.ErrUnknownFields) { + t.Errorf("want ErrUnknownFields for an unknown field inside ext, got %v", err) + } +} + +func TestCanonicalOfferBytes_mapEntryUnknownsAreDroppedBeforeTheWalk(t *testing.T) { + // The walk recurses into map VALUES, not into the synthetic entry messages that + // carry them. That is total only because protobuf-go discards unknown bytes + // planted on an entry at unmarshal, so they never reach the canonicalizer — and + // never reach the wire again either. This pins that assumption: if a protobuf + // upgrade started retaining entry-level unknowns, the walk would need a branch + // for them and this test is what would say so. + st, err := structpb.NewStruct(map[string]any{"vendor.key": "v"}) + if err != nil { + t.Fatal(err) + } + valRaw, err := proto.Marshal(st.Fields["vendor.key"]) + if err != nil { + t.Fatal(err) + } + // Struct.fields entry: key(1), value(2), then an undeclared field ON THE ENTRY. + entry := protowire.AppendTag(nil, 1, protowire.BytesType) + entry = protowire.AppendBytes(entry, []byte("vendor.key")) + entry = protowire.AppendTag(entry, 2, protowire.BytesType) + entry = protowire.AppendBytes(entry, valRaw) + entry = protowire.AppendTag(entry, 500, protowire.VarintType) + entry = protowire.AppendVarint(entry, 7) + structRaw := protowire.AppendTag(nil, 1, protowire.BytesType) + structRaw = protowire.AppendBytes(structRaw, entry) + + got := new(structpb.Struct) + if err := proto.Unmarshal(structRaw, got); err != nil { + t.Fatal(err) + } + reEncoded, err := proto.Marshal(got) + if err != nil { + t.Fatal(err) + } + clean, err := proto.Marshal(st) + if err != nil { + t.Fatal(err) + } + if len(reEncoded) != len(clean) { + t.Fatalf("protobuf-go now RETAINS entry-level unknown bytes (%d vs %d); the walk "+ + "must gain a map-entry branch", len(reEncoded), len(clean)) + } + + offer := sampleOffer() + offer.Ext = got + if _, err := helpers.CanonicalOfferBytes(offer); err != nil { + t.Errorf("entry-level unknowns are dropped before the walk, so this must canonicalize: %v", err) + } +} + +func TestVerifyOffer_rejectsANewerCanonicalFormWhicheverCheckFires(t *testing.T) { + // The other direction: the peer signed its OWN richer rendering, which includes + // the field it knows about and this build does not. The signed bytes cannot be + // reconstructed here at all, so the offer is rejected rather than verified over + // a reduced form. + // + // This pins the OUTCOME, not the mechanism: the rejection also follows from the + // plain signature mismatch, so the test passes with the unknown-field guard + // removed. The guard is gated by the injected-unknown tests above, where clean + // and tampered render identically and only the guard can reject. + pub, priv, _ := ed25519.GenerateKey(nil) + base := sampleOffer() + baseCanon, err := helpers.CanonicalOfferBytes(base) + if err != nil { + t.Fatal(err) + } + // JCS sorts keys, so a member sorting last is appended before the closing brace. + peerSigned := append(baseCanon[:len(baseCanon)-1:len(baseCanon)-1], []byte(`,"zz_new_field":"7"}`)...) + peerSig := hex.EncodeToString(ed25519.Sign(priv, peerSigned)) + + var newer rampv1.Offer + if err := proto.Unmarshal(encodeWithUnknownField(t, base), &newer); err != nil { + t.Fatal(err) + } + if err := helpers.VerifyOffer(&newer, peerSig, pub); !errors.Is(err, helpers.ErrOfferSignatureInvalid) { + t.Errorf("want ErrOfferSignatureInvalid, got %v", err) + } +} diff --git a/sdk/go/helpers/testdata/acceptance-vectors.json b/sdk/go/helpers/testdata/acceptance-vectors.json index 7ed6b20..f4b5655 100644 --- a/sdk/go/helpers/testdata/acceptance-vectors.json +++ b/sdk/go/helpers/testdata/acceptance-vectors.json @@ -22,6 +22,28 @@ "signature_hex": "a067c7d83dcf801f10389a5b2e2936756aae37006478b1eae5dbf70805fdf2feb0292e3ae3b5511a3921f4fe331c52bfc19894f25332f0b2ba2d741db0769506", "pubkey_b64": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=", "seed_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + }, + { + "name": "empty_requester_id", + "offer_sig": "sig3deadbeef", + "requester_id": "", + "requester_domain": "agent.example.com", + "idempotency_key": "idem-3", + "canonical_jcs": "{\"idempotency_key\":\"idem-3\",\"offer_sig\":\"sig3deadbeef\",\"requester_domain\":\"agent.example.com\"}", + "signature_hex": "5e9baefd90faae7edc44c8bcd3c6aec68fc3d9c253a6791ab83d6dcb6d638310053b5cb3607f19618cff96e5fc002a7d93d600ce16ba77019c73a2b7c50d1c01", + "pubkey_b64": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=", + "seed_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" + }, + { + "name": "empty_idempotency_key", + "offer_sig": "sig4deadbeef", + "requester_id": "agent-4", + "requester_domain": "agent.example.com", + "idempotency_key": "", + "canonical_jcs": "{\"offer_sig\":\"sig4deadbeef\",\"requester_domain\":\"agent.example.com\",\"requester_id\":\"agent-4\"}", + "signature_hex": "8230b1c9de146f70b9448adb2846a8e4db8a41172cb6b8e8da52c25be4d62d3d4b209eb77f85503eda3b2bd99f27fbd148bcfc7775a19a7f6830e0aa08dde606", + "pubkey_b64": "ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ=", + "seed_hex": "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20" } ] } diff --git a/sdk/parity/symbol-map.json b/sdk/parity/symbol-map.json index a19c0c6..4392f1c 100644 --- a/sdk/parity/symbol-map.json +++ b/sdk/parity/symbol-map.json @@ -76,6 +76,7 @@ "helpers.ErrURLMissingSignature": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", "helpers.ErrURLNotAgentBound": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", "helpers.ErrURLSignatureInvalid": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", + "helpers.ErrUnknownFields": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", "helpers.ErrUnsupportedAlgorithm": "Go errors.Is sentinel; py/ts express verification failures via typed failure unions / exception classes, not per-reason named sentinels.", "helpers.FromContext": "Go context.Context accessor; py/ts thread verified-request state explicitly.", "helpers.NewContext": "Go context.Context accessor; py/ts thread verified-request state explicitly.", @@ -210,6 +211,16 @@ "python": "apply_scopes", "ts": "applyScopes" }, + "helpers.CanonicalAcceptanceBytes": { + "allowlist_reason": null, + "python": "jcs_acceptance_payload", + "ts": "acceptancePayload" + }, + "helpers.CanonicalOfferBytes": { + "allowlist_reason": null, + "python": "canonical_offer_payload", + "ts": "canonicalOfferPayload" + }, "helpers.CanonicalizeMoney": { "allowlist_reason": null, "python": "canonicalize_money", diff --git a/sdk/python/ramp_sdk/core.py b/sdk/python/ramp_sdk/core.py index 79ba58c..7f12179 100644 --- a/sdk/python/ramp_sdk/core.py +++ b/sdk/python/ramp_sdk/core.py @@ -147,7 +147,7 @@ def canonical_offer_payload(offer: dict[str, Any]) -> bytes: """Reproduce the offer's signed bytes: clear signature + signature_algorithm from the canonical proto-JSON, then apply RFC 8785 JCS. - MUST stay byte-identical to the Go oracle (helpers.canonicalOfferPayload). The + MUST stay byte-identical to the Go oracle (helpers.CanonicalOfferBytes). The offer is already canonical proto-JSON (snake_case, enums-as-names, omit-unpopulated) — the core only clears the two signature keys and re-JCS-es. """ @@ -272,21 +272,27 @@ def jcs_acceptance_payload( """Canonical acceptance bytes = JCS(protojson(AgentAcceptancePayload)). The same JCS(protojson(...)) canonicalization the offer signature uses. proto-JSON - OMITS unpopulated fields, so an empty ``requester_domain`` is absent from the - object before JCS (matching the Go oracle's empty-domain vector). Fail-closed on - an empty ``offer_sig`` (mirror Go canonicalAcceptancePayload): an empty anchor - would let the acceptance float free of any concrete offer. + OMITS unpopulated fields, so EVERY empty string field is absent from the object + before JCS — not just ``requester_domain``. The Go oracle gets that structurally + from ``EmitUnpopulated=false``; this object is hand-built, so the omission is + applied once over the whole record rather than per key. A per-key guard is how the + rule went missing for ``requester_id`` — wire-valid, since ``Requester.id`` carries + no ``min_len`` — which signed bytes Go never produces. The filter tests for the + empty STRING, which covers every member ``AgentAcceptancePayload`` has (the field-set + guard in the Go suite pins that list), so a string field added to the message cannot + arrive without its omission. A non-string field would need its own zero-value test. + Fail-closed on an empty ``offer_sig`` (mirror Go CanonicalAcceptanceBytes): an + empty anchor would let the acceptance float free of any concrete offer. """ if offer_sig == "": raise ValueError("cannot accept an unsigned offer (empty offer signature)") - obj: dict[str, str] = { + payload: dict[str, str] = { "offer_sig": offer_sig, "requester_id": requester_id, + "requester_domain": requester_domain, "idempotency_key": idempotency_key, } - if requester_domain != "": - obj["requester_domain"] = requester_domain - return rfc8785.dumps(obj) + return rfc8785.dumps({k: v for k, v in payload.items() if v != ""}) def sign_offer_acceptance_jcs( diff --git a/sdk/python/ramp_sdk/wire_canon.py b/sdk/python/ramp_sdk/wire_canon.py index 62c563d..5a74ee8 100644 --- a/sdk/python/ramp_sdk/wire_canon.py +++ b/sdk/python/ramp_sdk/wire_canon.py @@ -1,13 +1,22 @@ """Wire-to-canonical offer conversion (the from-wire canonicalizer). -The RAMP Connect wire is camelCase protojson with ``EmitUnpopulated`` -(zero-valued scalars present) — pinned by the Go SDK codec test -(``sdk/go/connectserver/codec_test.go``: camelCase, NOT ``UseProtoNames``). The -offer SIGNATURE however covers the CANONICAL form: snake_case proto names, -omit-unpopulated, enums-as-names (sdk/go helpers ``canonicalSignJSONOptions``), -then RFC 8785 JCS. Every Python client verifying a wire offer must therefore -invert the wire emission before calling :func:`ramp_sdk.core.canonical_offer_payload` -— never call it on raw camelCase wire input. +The RAMP Connect wire is snake_case proto-JSON with ``EmitUnpopulated``: zero-valued +scalars, empty repeateds, null messages and ``*_UNSPECIFIED`` enums are all present. +That is what ``sdk/go/connectserver/codec.go`` marshals (``UseProtoNames: true``), and +what ``codec_test.go`` guards — a stray ``UseProtoNames=false`` is the regression it +catches. The offer SIGNATURE covers the CANONICAL form: the same snake_case proto +names, but omit-unpopulated, enums-as-names (sdk/go helpers +``canonicalSignJSONOptions``), then RFC 8785 JCS. Both sides share the naming, so what +this inversion undoes is the zero-inflation, NOT the naming. It does NOT clear the +signature fields — :func:`from_wire_offer` keeps ``signature`` / +``signature_algorithm`` verbatim, and +:func:`ramp_sdk.core.canonical_offer_payload` strips them on the way into JCS. Every +Python client verifying a wire offer must still perform the inversion before calling +that function — never call it on raw wire input. + +The retired proto-JSON lowerCamel ``json_name`` form is still TOLERATED on input (hence +:func:`_snake`, the ``offer_camel`` parameter name, and the pre-flip fixture the tests +keep). It is out of contract: accepted when it arrives, never emitted. :func:`from_wire_offer` performs that inversion SCHEMA-AWARE, driven by the generated ``wire.models`` classes (gen/python), whose field DEFAULTS encode @@ -21,13 +30,25 @@ data, never case-converted). ``*_UNSPECIFIED`` enum values are zero values and drop. -Byte-parity with the Go oracle is pinned by ``tests/test_wire_canon.py`` over a -live-captured wire/canonical fixture pair. - -KNOWN LIMITS (fail-closed, never fail-open): a wire offer carrying fields newer -than the pinned gen models is kept verbatim and will verify FALSE (rejected, -not silently accepted), and a non-optional int64 zero (emitted as ``"0"``) is -not dropped (no such field exists on the Offer tree today). +Byte-parity with the Go oracle is pinned by ``tests/test_wire_canon.py``: over the +drift-gated ``sdk/go/helpers/testdata/wire-canonical-vectors.json`` corpus for the +current snake_case wire, and over a live-captured fixture pair for the retired form. + +UNKNOWN FIELDS: a wire key the pinned gen models do not define is kept VERBATIM, +which makes this a PRESERVING canonicalizer in the sense the ``Offer.signature`` +canonical-signing block defines. Two consequences, and only the second is a limit: + +* a field APPENDED after signing lands in the canonical dict, so the bytes differ + from what the signer covered and verification fails — the tamper case is closed; +* a field the SIGNER covered (a peer built against a newer schema) is reproduced + exactly, so verification SUCCEEDS over a message this pin cannot fully interpret. + That is the forward-compatible outcome and is not a rejection. Go, whose + proto-JSON renderer OMITS what it has no schema for, cannot reconstruct such a + message at all and refuses it — an inherent difference between the two renderer + families, not a parity break. + +KNOWN LIMIT (fail-closed, never fail-open): a non-optional int64 zero (emitted as +``"0"``) is not dropped (no such field exists on the Offer tree today). """ from __future__ import annotations @@ -46,7 +67,11 @@ def _snake(key: str) -> str: - """Invert protojson's default lowerCamel json_name back to the proto name.""" + """Invert protojson's retired lowerCamel json_name back to the proto name. + + Idempotent on already-snake keys, which is what the current wire and the shared + corpus emit — so the inversion costs nothing and keeps the retired form working. + """ return _CAMEL_BOUNDARY.sub(r"_\1", key).lower() @@ -102,7 +127,7 @@ def _canon_field(field: FieldInfo, value: Any) -> Any: def _canon_message(model_cls: type[Any], wire: dict[str, Any]) -> dict[str, Any]: - """Reconstruct one message's canonical dict from its wire (camel) dict.""" + """Reconstruct one message's canonical dict from its wire dict.""" fields = model_cls.model_fields out: dict[str, Any] = {} for key, value in wire.items(): diff --git a/sdk/python/tests/test_acceptance_jcs_repin.py b/sdk/python/tests/test_acceptance_jcs_repin.py index 83f1f50..7eafba8 100644 --- a/sdk/python/tests/test_acceptance_jcs_repin.py +++ b/sdk/python/tests/test_acceptance_jcs_repin.py @@ -6,23 +6,25 @@ signed_payload = JCS(protojson(msg with sig/sig_algorithm cleared)) -For acceptance this changes Go canonicalAcceptancePayload (acceptance.go) from the -hand-rolled proto3 field-tag marshal (today's acceptance-vectors.json carries -`canonical_bytes_hex` beginning `0a10…` — proto field tags) to JCS UTF-8 bytes. +For acceptance this changed Go canonicalAcceptancePayload (acceptance.go, since +exported as CanonicalAcceptanceBytes) from the hand-rolled proto3 field-tag marshal +(acceptance-vectors.json then carried `canonical_bytes_hex` beginning `0a10…` — proto +field tags) to JCS UTF-8 bytes. The Go golden emitter therefore REGENERATES acceptance-vectors.json to the JCS form (new canonical bytes AND new signatures — the canonicalization itself changed, so this is the intended vector change, NOT a behavior-preservation violation). This re-pin suite asserts the Python core canonicalizes acceptance via JCS and produces byte-identical canonical bytes + signature to the REGENERATED Go oracle. -It is RED now for TWO expected reasons: - 1. sdk/python/ramp_sdk/core (the JCS acceptance canonicalizer + sign/verify) does +It was authored RED, for TWO expected reasons — both since resolved, so the suite is +green and the narrative below is history, not current state: + 1. sdk/python/ramp_sdk/core (the JCS acceptance canonicalizer + sign/verify) did not exist yet. - 2. The regenerated acceptance vectors do not exist yet — today's - acceptance-vectors.json is still the protobuf-binary form (no `canonical_jcs` - field, no `canonicalization: "jcs"` marker). The assertions below require the - JCS-regenerated shape, so they FAIL against the current file (and the import - fails outright), a clean TDD-red signal. + 2. The regenerated acceptance vectors did not exist yet — acceptance-vectors.json + was still the protobuf-binary form (no `canonical_jcs` field, no + `canonicalization: "jcs"` marker). The assertions below require the + JCS-regenerated shape, so they FAILED against the file as it then stood (and the + import failed outright), a clean TDD-red signal. Note: this is the acceptance sibling of test_core_offer_verify_parity.py; the two prove Go/TS/Python agree on the SAME JCS canonicalization for both signed payloads. diff --git a/sdk/python/tests/test_wire_canon.py b/sdk/python/tests/test_wire_canon.py index 3cde9de..5279bba 100644 --- a/sdk/python/tests/test_wire_canon.py +++ b/sdk/python/tests/test_wire_canon.py @@ -1,19 +1,22 @@ """sdk/python wire-to-canonical offer canonicalizer. -The RAMP Connect wire emitted by the Broker is camelCase protojson with -EmitUnpopulated (zero-valued scalars present). The offer SIGNATURE covers the -CANONICAL form: snake_case proto names, omit-unpopulated, enums-as-names, then -RFC 8785 JCS. ``from_wire_offer`` bridges the two forms; it belongs in sdk/python -so every RAMP client (MCP shim, future TS MCP, future Python broker) shares one -wire-normalization function proven byte-identical to the Go oracle. - -Three behaviors pinned here: - -(a) GO-ORACLE PARITY — ``from_wire_offer(offer_wire_camel)`` produces a canonical - dict whose JCS (with ``signature``/``signature_algorithm`` stripped) equals the - committed ``offer_canonical_go.json`` fixture byte-for-byte. The fixture pair - was captured from the live e2e stack (offer_wire_camel.json = real broker wire - output; offer_canonical_go.json = Go canonical form). +The wire form, the canonical form, and what the inversion does and does not undo are +described once, in :mod:`ramp_sdk.wire_canon`. That module docstring is the contract; +this one says only what the suite pins, so the two cannot drift apart the way they +already did once. The one consequence worth restating, because the assertions below +depend on it: ``from_wire_offer`` KEEPS the signature fields, so every comparison here +strips ``signature``/``signature_algorithm`` by hand, exactly as +``ramp_sdk.core.canonical_offer_payload`` does on the way into JCS. + +Four behaviors pinned here: + +(a) LEGACY camelCase TOLERANCE — ``offer_wire_camel.json`` is a PRE-FLIP capture, + taken from the live e2e stack before the Connect wire moved to snake_case proto + names (``UseProtoNames=true``); ``offer_canonical_go.json`` is the Go canonical + form of the same offer. It is kept because the inversion must go on tolerating + the retired lowerCamel form. ``from_wire_offer`` on it produces a canonical dict + whose JCS (with ``signature``/``signature_algorithm`` stripped) equals the + committed fixture byte-for-byte. The CURRENT wire is covered by (d). (b) UNSPECIFIED ENUM PRUNING — a wire offer carrying ``deliveryMethod='DELIVERY_METHOD_UNSPECIFIED'`` must emit a canonical dict @@ -28,13 +31,20 @@ this dedicated test makes the invariant explicit and survives future fixture rotation. -RED now: ``ramp_sdk.wire_canon`` does not exist yet. The module-level import -below causes a collection error, which is the established red style in this suite -(mirrors test_client_binding_smoke.py and test_core_offer_verify_parity.py: -module-level import with ``type: ignore[import-not-found]`` → ImportError on -collection → pytest ERRORS = confirmed RED). The implement step adds -``sdk/python/ramp_sdk/wire_canon.py`` (exporting ``from_wire_offer``); these -tests go green with no change to the assertions. +(d) DRIFT-GATED GO-ORACLE PARITY — the committed + ``sdk/go/helpers/testdata/wire-canonical-vectors.json`` corpus, emitted by the Go + golden emitter and replayed by Go, Python and TS. This is the authoritative + parity for the CURRENT snake_case wire; unlike (a) it cannot go silently stale. + +(b) and (c) feed camelCase synthetic dicts too, so they exercise the same tolerance +as (a). The rules they pin are naming-independent, and their snake_case twins are the +``unspecified_enum_pruned`` and ``set_empty_optional_unit`` vectors in (d). + +The suite was authored RED, before ``ramp_sdk.wire_canon`` existed: the module-level +import below raised on collection, the established red style here (mirrors +test_client_binding_smoke.py and test_core_offer_verify_parity.py). The module has +since landed and the assertions went green unchanged — the ``type: ignore`` on the +import is what remains of that step. """ from __future__ import annotations @@ -54,20 +64,25 @@ # --------------------------------------------------------------------------- -# (a) Go-oracle parity +# (a) Legacy camelCase tolerance — Go-oracle parity on the pre-flip fixture # --------------------------------------------------------------------------- def test_from_wire_offer_go_oracle_parity() -> None: """from_wire_offer(wire_camel) JCS-equals the committed Go canonical output. - The fixture pair was captured from the LIVE e2e stack: offer_wire_camel.json - is a real demo offer exactly as the Broker's Connect codec emitted it - (camelCase, EmitUnpopulated zero-inflation, ``unit:""`` set-empty optional), - and offer_canonical_go.json is the canonical form computed by the Go side + The fixture pair was captured from the LIVE e2e stack BEFORE the Connect wire + moved to snake_case proto names: offer_wire_camel.json is a real demo offer + exactly as the Broker's Connect codec emitted it back then (camelCase, + EmitUnpopulated zero-inflation, ``unit:""`` set-empty optional), and + offer_canonical_go.json is the canonical form computed by the Go side (protojson UseProtoNames + omit-unpopulated, then RFC 8785 JCS) — the byte sequence the exchange's offer signature covers. + The wire has since flipped to snake_case, so this pair no longer describes what + a Broker emits; it is retained as the tolerance case, pinning that a lowerCamel + wire offer still canonicalizes correctly. Parity for the CURRENT wire is (d). + A divergence here means from_wire_offer diverges from the Go oracle and every genuine offer signature would fail to verify in the Python shim. """ diff --git a/sdk/ts/core/verifier.ts b/sdk/ts/core/verifier.ts index 0da91f0..9da9268 100644 --- a/sdk/ts/core/verifier.ts +++ b/sdk/ts/core/verifier.ts @@ -144,7 +144,7 @@ function hexToBytes(hex: string): Uint8Array | undefined { /** * canonicalOfferPayload reproduces the signed bytes: clear signature + * signature_algorithm from the offer's canonical proto-JSON, then apply RFC 8785 - * JCS. MUST stay byte-identical to the Go oracle (helpers.canonicalOfferPayload). + * JCS. MUST stay byte-identical to the Go oracle (helpers.CanonicalOfferBytes). * The offer is already canonical proto-JSON (snake_case, enums-as-names, * omit-unpopulated) — the core only clears the two signature fields and re-JCS-es. */ diff --git a/sdk/ts/src/acceptance.ts b/sdk/ts/src/acceptance.ts index 7c133dc..da4ee19 100644 --- a/sdk/ts/src/acceptance.ts +++ b/sdk/ts/src/acceptance.ts @@ -25,10 +25,11 @@ import { utf8Bytes } from "./base64url.ts"; * (mirror helpers.AcceptanceSignatureAlgorithm). */ export const ACCEPTANCE_SIGNATURE_ALGORITHM = "EdDSA"; -/** The acceptance binding fields. requesterDomain is omitted from the canonical - * payload when empty (proto omit-unpopulated); an empty offerSig is rejected - * fail-closed — an empty anchor would let the acceptance float free of any - * concrete offer. */ +/** The acceptance binding fields. EVERY empty field is omitted from the canonical + * payload, not only requesterDomain (proto omit-unpopulated) — so an empty value and + * an absent one sign the same bytes, and neither signs the bytes of a populated one. + * An empty offerSig is rejected fail-closed — an empty anchor would let the + * acceptance float free of any concrete offer. */ export interface AcceptanceInput { offerSig: string; requesterId: string; @@ -52,9 +53,9 @@ function bytesToHex(bytes: Uint8Array): string { /** * acceptancePayload reproduces the canonical signed bytes: - * JCS(protojson(AgentAcceptancePayload)) with an empty requester_domain omitted. + * JCS(protojson(AgentAcceptancePayload)) with EVERY empty string field omitted. * Throws on an empty offer signature (fail-closed, mirror Go - * canonicalAcceptancePayload / Python jcs_acceptance_payload). + * CanonicalAcceptanceBytes / Python jcs_acceptance_payload). */ export function acceptancePayload(input: AcceptanceInput): Uint8Array { if (input.offerSig === "") { @@ -62,13 +63,22 @@ export function acceptancePayload(input: AcceptanceInput): Uint8Array = { + // proto omit-unpopulated: every empty string field is absent before JCS. The Go + // oracle gets that structurally from EmitUnpopulated=false; this record is + // hand-built, so the omission is applied once over the whole record rather than + // per key. A per-key guard is how the rule went missing for requester_id -- + // wire-valid, since Requester.id carries no min_len -- which signed bytes Go never + // produces. The filter tests for the empty STRING, which covers every member + // AgentAcceptancePayload has (the field-set guard in the Go suite pins that list), + // so a string field added to the message cannot arrive without its omission. A + // non-string field would need its own zero-value test. + const payload: Record = { offer_sig: input.offerSig, requester_id: input.requesterId, + requester_domain: input.requesterDomain, idempotency_key: input.idempotencyKey, }; - // proto omit-unpopulated: an empty requester_domain is absent before JCS. - if (input.requesterDomain !== "") obj.requester_domain = input.requesterDomain; + const obj = Object.fromEntries(Object.entries(payload).filter(([, v]) => v !== "")); const jcs = canonicalize(obj); if (jcs === undefined) { throw new Error("ramp/acceptance: payload is not JSON-serializable"); diff --git a/website/src/content/docs/reference/changelog.mdx b/website/src/content/docs/reference/changelog.mdx index 457d64e..4f042ed 100644 --- a/website/src/content/docs/reference/changelog.mdx +++ b/website/src/content/docs/reference/changelog.mdx @@ -32,6 +32,91 @@ TS signed-URL verify (`Ed25519Verifier`), and cross-language `ErrorDetail` reade in Python; `parseErrorDetail` / `errorDetailFrom` in TS). The SSRF-guarded transport is a single env-driven client governed by two flags (`SKIP_SSRF`, `ALLOW_INSECURE`). +**Go SDK: `helpers.CanonicalOfferBytes` exported (additive, no wire change).** +The offer-canonical-bytes accessor — RFC 8785 JCS over canonical proto-JSON with +`signature`/`signature_algorithm` cleared, `expires_at` included, byte-identical to +what `SignOffer` signs and `VerifyOffer` verifies — is now a public Go symbol. It +exposes the single canonicalization the signer and verifier already share, so a +caller can persist the signed offer as verbatim, independently re-verifiable +evidence. Python (`canonical_offer_payload`) and TS (`canonicalOfferPayload`) already +expose the equivalent public accessor; this brings the Go surface to parity. + +**Go SDK: `helpers.CanonicalAcceptanceBytes` exported (additive, no wire change).** +The acceptance-canonical-bytes accessor — RFC 8785 JCS over canonical proto-JSON of +`AgentAcceptancePayload{offer_sig, requester_id, requester_domain, idempotency_key}`, +byte-identical to what `SignOfferAcceptance` signs and `VerifyOfferAcceptance` +verifies — is now a public Go symbol, completing the pair with +`CanonicalOfferBytes`. A caller can persist an agent's acceptance as verbatim, +independently re-verifiable evidence rather than re-deriving the bytes at +verification time, which would pin an already-signed acceptance to whatever +canonicalization the SDK implements later. Python (`jcs_acceptance_payload`) and TS +(`acceptancePayload`) already expose the equivalent public accessor; this brings the +Go surface to parity. + +**Acceptance canonical-form text corrected (documentation only, no wire change).** +`AgentAcceptance` and `AgentAcceptancePayload` still described the RETIRED signing +form — "the deterministic protobuf serialization", "`proto.Marshal(Deterministic: +true)`" — contradicting the canonical-signing block on `Offer.signature` in the same +file, which already states that RFC 8785 JCS over canonical proto-JSON "applies to +the agent offer-acceptance signature". The acceptance text now points at that single +normative definition instead of restating a superseded recipe: +`AgentAcceptancePayload` fixes the field set, `Offer.signature` fixes the byte +layout. Implementations that followed the stale text would have produced +non-verifying signatures. No field, message, or wire change — comments only, with +`gen/` and the website mirror regenerated. + +**Python + TS SDK: the hand-built acceptance payload omits every unpopulated field +(bug fix, no wire change).** `jcs_acceptance_payload` (Python) and `acceptancePayload` +(TS) assemble the `AgentAcceptancePayload` JSON object key by key, and omitted only an +empty `requester_domain` — `requester_id` and `idempotency_key` were always emitted. +Go renders the same object through `protojson` with `EmitUnpopulated=false`, which omits +EVERY unpopulated field, so the three SDKs signed different bytes whenever `requester_id` +was empty. That input is wire-valid: `Requester.id` carries no `min_len`. Verification +failed closed on it (a byte mismatch, never a bypass), but the byte-equivalence the +canonical-bytes accessors promise did not hold. Both hand-built faces now drop each empty +string field, and two new vectors in `sdk/go/helpers/testdata/acceptance-vectors.json` — +`empty_requester_id` and `empty_idempotency_key`, one per omittable field left uncovered — +pin the agreement across Go, Python and TS. Without them the omission can be dropped in any +one language with every gate still green. The corpus change is purely additive — the +pre-existing vectors and their signatures are byte-identical, so no already-issued signature +is affected. + +**Canonical signing refuses messages carrying unknown fields (normative; Go SDK +behavior change, no wire change).** `Offer.signature` — the single normative definition of +the canonical form — now states the rule, which turns on whether a canonicalizer omits or +preserves content it has no schema for. An OMITTING canonicalizer (proto-JSON emits only +schema-defined fields) cannot reproduce the signed bytes of a message carrying unknown +fields, so it MUST refuse the message rather than emit the reduced bytes, at EVERY depth — +a nested message and each element of a repeated or map field carries its own unknown-field +set. A PRESERVING canonicalizer carries unrecognized members through, reproduces the +signed bytes faithfully, and has nothing to refuse. + +Either way an APPENDED field cannot pass, which is the point: the omitting case refuses +the message, and the preserving case renders the appended member into bytes the signer +never covered. Without the refusal the omitting case failed OPEN — an intermediary could +add unknown fields to an already-signed `Offer` **without invalidating its signature**, +smuggling unauthenticated content through a message the recipient treats as verified. That +is what the Go SDK now closes: `helpers.VerifyOffer` surfaces the refusal as +`ErrOfferSignatureInvalid` (a message that arrived carrying extra bytes is a tampered +offer, not an internal fault) wrapping the new `helpers.ErrUnknownFields`, so a caller can +branch on either; `helpers.CanonicalOfferBytes` and `helpers.SignOffer` return +`ErrUnknownFields` directly. + +Python (`from_wire_offer`) and TypeScript (`canonicalOfferPayload`) are preserving +canonicalizers and need no change — they already reject the appended-field case on a byte +mismatch. They are NOT expected to reject a message whose signer covered the unknown +member: they reproduce those bytes exactly and verify, which is the forward-compatible +outcome. Go, being an omitting canonicalizer, cannot reconstruct such a message at all and +refuses it; that asymmetry is inherent to the renderer, not new here — before this change +Go rejected the same message on a byte mismatch instead. + +No legitimate traffic regresses: an offer signed WITH a field this build cannot render +already failed to verify; the refusal only makes the reason explicit. Extensions are +unaffected — they ride in `ext` / `ext_critical`, defined fields that sit inside the signed +bytes, never undeclared field numbers. One new exported Go symbol (`ErrUnknownFields`, +registered as a Go-idiomatic exclusion in the parity map); no field, message, or wire +change — proto comments only, with `gen/` and the website mirror regenerated. + **`Requester.billing_ref` removed (breaking, pre-1.0).** The caller-written billing label on `Requester` is gone; the field is deleted outright with no `reserved` statement — pre-v1 the number returns to the free pool, and diff --git a/website/src/content/docs/reference/proto-ramp.mdx b/website/src/content/docs/reference/proto-ramp.mdx index d03dfe8..513cc3a 100644 --- a/website/src/content/docs/reference/proto-ramp.mdx +++ b/website/src/content/docs/reference/proto-ramp.mdx @@ -158,13 +158,13 @@ A single offer commitment within a batch transaction. ### AgentAcceptance -The agent's detached acceptance signature over an accepted Offer. It travels in the execute body alongside the reflected Offer and is independent of the transport (RFC 9421) request signature, so it survives any number of broker relays. The Exchange verifies it and binds the delivery URL to the agent's key (RFC 7638 thumbprint). `signature` is a hex-encoded detached Ed25519 signature over the deterministically marshaled [`AgentAcceptancePayload`](#agentacceptancepayload); `signature_algorithm` is `EdDSA`. +The agent's detached acceptance signature over an accepted Offer. It travels in the execute body alongside the reflected Offer and is independent of the transport (RFC 9421) request signature, so it survives any number of broker relays. The Exchange verifies it and binds the delivery URL to the agent's key (RFC 7638 thumbprint). `signature` is a hex-encoded detached Ed25519 signature over the JCS-canonicalized (RFC 8785) proto-JSON of [`AgentAcceptancePayload`](#agentacceptancepayload) — the same canonical signing form `Offer.signature` defines, reduced here to `JCS(protojson(payload))` because the payload carries no signature fields to clear; `signature_algorithm` is `EdDSA`. ::proto-message{name=AgentAcceptance} ### AgentAcceptancePayload -The canonical signing structure for [`AgentAcceptance`](#agentacceptance). It is **never sent on the wire** — both the signer and the verifier marshal it deterministically (`proto.Marshal` with deterministic field order) to derive byte-identical signed bytes, so the contract cannot drift between implementations. `offer_sig` is the accepted `Offer.signature` (which transitively binds pricing, terms, and expiry); `requester_id`, `requester_domain`, and `idempotency_key` come from the enclosing `TransactionRequest`. +The canonical signing structure for [`AgentAcceptance`](#agentacceptance). It is **never sent on the wire** — this message fixes the *field set*, and the *byte layout* is the canonical signing form defined on `Offer.signature`: RFC 8785 JCS over canonical proto-JSON with a pinned option set. Both halves are normative, so signer and verifier derive byte-identical signed bytes in any language without a protobuf binary codec, and the contract cannot drift between implementations. `offer_sig` is the accepted `Offer.signature` (which transitively binds pricing, terms, and expiry); `requester_id`, `requester_domain`, and `idempotency_key` come from the enclosing `TransactionRequest`. ::proto-message{name=AgentAcceptancePayload}