Skip to content

feat(proto)!: Universal Licensing Core + proto-native vocabulary (protovalidate + codegen) - #6

Merged
legendko merged 44 commits into
mainfrom
feature/license-terms
Jun 18, 2026
Merged

feat(proto)!: Universal Licensing Core + proto-native vocabulary (protovalidate + codegen)#6
legendko merged 44 commits into
mainfrom
feature/license-terms

Conversation

@KonstantinMirin

@KonstantinMirin KonstantinMirin commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Introduces the Universal Licensing Core — a structured, cross-domain licensing model for any digital resource — and a proto-native vocabulary mechanism for the open string axes it relies on. One resource carries many LicenseTerms (free for academic RAG, paid for commercial, reference-only for complex rights), the same shape at ingestion (ResourceEntry.terms) and emission (Offer.terms).

The design evolved over the branch (see commit history); this description reflects the final state. Closes #5.

Message model

message LicenseTerm {
  License               license      = 1;   // optional for ENUMERATED; required for REFERENCE_ONLY
  TermSemantics         semantics    = 2;   // ENUMERATED | REFERENCE_ONLY
  repeated Restriction  restrictions = 3;
  repeated Quota        quotas       = 4;
  repeated Obligation   obligations  = 5;
  Pricing               pricing      = 6;   // REQUIRED on every term (absent ≠ free)
  repeated string       scopes       = 7;   // Biscuit scope-gating; empty = public
  optional string       part_label   = 8;   // sub-part label
}
message License   { string uri = 1; optional string id = 2; optional string name = 3; optional bool immutable = 4; }
message Restriction { RestrictionKind kind = 1; repeated string permitted = 2; repeated string prohibited = 3; bool critical = 4; }
message Quota     { string metric = 1; int64 limit = 2; QuotaWindow window = 3; }
message Obligation { ObligationKind kind = 1; ObligationTrigger trigger = 2; optional string scope_license = 3; optional string detail = 4; }
  • LicenseTerm added to ResourceEntry (field 13) and Offer (field 19).
  • License is identity-shaped: uri (canonical, never URL-validated — data-labels TDLs use non-URL schemes), id (SPDX short-id / TollBit cuid), name, immutable.
  • Obligation carries scope_license (required for SHARE_ALIKE) and detail.

Pricing: charging structure (enum) + metering basis (vocabulary)

PricingModel was collapsed to the closed charging structure; the open-ended metering basis moved out of the enum into Pricing.unit (a vocabulary — see below).

enum PricingModel { PRICING_MODEL_UNSPECIFIED = 0; PRICING_MODEL_FREE = 1; PRICING_MODEL_PER_UNIT = 2; PRICING_MODEL_FLAT = 3; }
enum PricingMetering { PRICING_METERING_ONLINE = 0; PRICING_METERING_NONE = 1; PRICING_METERING_OFFLINE_SELF_REPORTED = 2; }
  • PER_UNITPricing.unit required (a registered token or a vendor: custom). FLAT = one-time fee. FREE must be explicit.
  • Pricing.metering (field 9) added. Pricing.revshare (field 6) and PRICING_MODEL_REVENUE_SHARE removed — settlement is off-protocol.

Proto-native vocabulary mechanism (the open axes)

The open string axes (Pricing.unit, Quota.metric, and the function/geography/user-type restriction axes) are defined in the proto and tooled by buf — no JSON registry, no enum-of-strings.

  • vocab.proto — custom options (ramp.v1.vocab) on FieldOptions (ext 50001) and (ramp.v1.vocab_enum) on EnumValueOptions (ext 50002). The registered token list is authored once, as these options (on the field for single-axis fields; on the RestrictionKind enum values for the kind-discriminated restriction axes).
  • Validationprotovalidate (adopted as a buf dep): a structural field CEL (empty / well-formed bare token / vendor:namespaced) plus message-level CEL for cross-field rules (PER_UNIT ⇒ unit != '', FREE ⇒ rate == 0). Enforced at the RPC boundary by the Connect validate interceptor, in any language, no generated validation code.
  • Constants — a small buf plugin (cmd/protoc-gen-rampvocab) reads (ramp.v1.vocab[_enum]) structurally off the descriptor and emits, into buf generate, a typed-constant package per axis under gen/go/vocab/: pricingunits, quotametrics, functiontokens, geographytokens, usertypes — each with token constants, All, and IsRegistered(). Application code branches on these; membership ("a bare token must be registered") is enforced from the same single source. Geography registers only the non-ISO specials (*, EU, EEA); ISO-3166 alpha-2 codes are structural.
  • Growing a registered list is an additive, non-breaking version bump — a new allowed string on a field that already holds strings. Vendor extension is ns:anything, no registry change.

This replaces the side-car vocab/*.json files, which are deleted in this PR.

Required enums carry _UNSPECIFIED = 0

TermSemantics, RestrictionKind, QuotaWindow, ObligationKind, ObligationTrigger, PricingModel each start at _UNSPECIFIED = 0 and are rejected if left unspecified (omission cannot silently default to a real value). PricingMetering is the deliberate exception — ONLINE = 0 is its real default.

Breaking changes / removals

  • AccessRestrictions removedLicenseTerm is the sole restriction model (no Offer.restrictions).
  • Pricing.revshare + PRICING_MODEL_REVENUE_SHARE removed; the old per-unit PRICING_MODEL_PER_* collapsed into PER_UNIT + unit.
  • PRICING_MODEL_ATTRIBUTION / CONTRIBUTION retired earlier in the branch (they are ObligationKinds).
  • Pre-v1: nothing reserved, enums renumbered cleanly.

Worked examples (final shapes)

News article — free for academic, paid for commercial

Field Term 1 (academic) Term 2 (commercial)
semantics ENUMERATED ENUMERATED
restrictions[FUNCTION].permitted ["ai-input"] ["ai-input","ai-index"]
restrictions[USER_TYPE].permitted ["academic"] ["commercial_entity"]
pricing {model: FREE} {model: PER_UNIT, unit: "accesses", rate: 0.05, currency: USD}
obligations[0] ATTRIBUTION / ON_USE ATTRIBUTION / ON_USE

Stock photo — perpetual license, impressions cap
{semantics: ENUMERATED, restrictions[FUNCTION].permitted: ["display"], restrictions[GEOGRAPHY].permitted: ["*"], pricing: {model: FLAT, rate: 1.20, currency: USD}, quotas[0]: {metric: "impressions", limit: 500000, window: TOTAL}, obligations[0]: ATTRIBUTION}

Patent/CAD — manufacture license, offline metering
{semantics: ENUMERATED, restrictions[FUNCTION].permitted: ["manufacture"], pricing: {model: PER_UNIT, unit: "units-manufactured", rate: 0.50, currency: USD, metering: OFFLINE_SELF_REPORTED}, quotas[0]: {metric: "units-manufactured", limit: 1000, window: TOTAL}, obligations[0]: CONTRIBUTION}

Tooling added to this repo

  • proto/buf.yaml dep on buf.build/bufbuild/protovalidate; cmd/protoc-gen-rampvocab wired as a local plugin in proto/buf.gen.yaml.
  • Generated gen/go/vocab/* constant packages.
  • vocab/*.json registry files deleted (the vocabulary now lives in the proto).

…s, quotas, obligations

Introduces a structured, cross-domain-portable licensing model that replaces
the flat AccessRestrictions structure. A resource now carries zero or more
LicenseTerm entries; each term is a complete access arrangement.

New messages: LicenseTerm, License, Restriction, Quota, Obligation (5 messages)
New enums: TermSemantics, RestrictionKind, QuotaWindow, ObligationKind,
           ObligationTrigger, PricingMetering (6 enums)

Wire additions:
- Offer.terms (field 19) — repeated LicenseTerm for agents at discovery
- ResourceEntry.terms (field 13) — publisher-declared terms at ingest
- Pricing.metering (field 9) — ONLINE/NONE/OFFLINE_SELF_REPORTED
- PushResourcesResponse.warnings (field 3) — non-fatal ingest warnings

Breaking change: PRICING_MODEL_ATTRIBUTION (6) and PRICING_MODEL_CONTRIBUTION (7)
removed from PricingModel. Both were behavioral obligations, not payment models.
Migrate to LicenseTerm with Obligation.kind = ATTRIBUTION / CONTRIBUTION.

AccessRestrictions retained for backward compatibility.

Closes #5
…antics

A license term with no Pricing is unactionable for an agent, so Pricing is
now REQUIRED on every term — including REFERENCE_ONLY, whose License governs
the human-readable terms but does not replace the machine-readable price.
model=FREE must still be stated explicitly (absent Pricing is not free; a
term may be FREE under an arbitrary license). Tightens the prior rule that
exempted REFERENCE_ONLY from carrying Pricing.

Comment-only contract change (proto3 has no required keyword); enforcement
lives in the Exchange's licenseterm.Validate.
A REFERENCE_ONLY term references its governing terms in an external License
document; one with no License.uri references nothing and is meaningless, so it
is rejected at ingest. Comment-only contract change; enforcement lives in the
Exchange's licenseterm.Validate.
…erm.scopes/part_label, Obligation.scope_license, UNSPECIFIED on required enums, drop revshare (RAMP-61)
These four registries (function, geography, user-type, quota-metrics) were
committed to a stranded branch with no open PR. ADR-014 defines the vocab
registry as part of the Universal Licensing Core, so they belong on this PR.
… = pricing-units vocabulary (RAMP-61)

PricingModel collapses to the closed charging structure {UNSPECIFIED, FREE,
PER_UNIT, FLAT}. The open-ended metering basis (per fetch/page/minute/record/
stream/...) moves out of the enum into Pricing.unit, governed by the new
vocab/pricing-units.json registry — same pattern as quota-metrics and the
restriction-value axes. New bases never touch the proto. No REVENUE_SHARE
(settlement is off-protocol).
RAMP-61. Introduce the (ramp.v1.vocab) custom field option (FieldOptions
extension 50001, ramp/v1/vocab.proto) carrying the 15 registered metering
tokens directly on Pricing.unit. Add protoc-gen-rampvocab, a buf plugin that
reads the option structurally via a dynamicpb extension resolver built from
the CodeGeneratorRequest descriptors and emits gen/go/vocab/pricingunits
(typed constants, All, IsRegistered).

Adopt protovalidate: structural field CEL on Pricing.unit (empty /
lowercase-dashed / vendor:namespaced) plus message-level CEL on Pricing
(PER_UNIT requires unit, FREE requires rate 0). Remove the obsolete side-car
vocab/pricing-units.json — the token list now lives solely in the option.

Additive, non-breaking (1.0.x): no message-shape change.
…b JSON (RAMP-61)

Extend the buf-native vocabulary mechanism from the Pricing.unit pilot to every
open vocabulary axis and remove all side-car JSON registries.

- vocab.proto: add (ramp.v1.vocab_enum), an EnumValueOptions extension (50002),
  the enum-value twin of the FieldOptions (ramp.v1.vocab) (50001). An extend
  block targets one options message, so a second extension is required.
- Quota.metric: annotate with the 8 metric tokens + a structural field CEL.
- RestrictionKind: annotate RESTRICTION_KIND_FUNCTION (23), _GEOGRAPHY (the
  non-ISO specials *, EU, EEA only) and _USER_TYPE (6) with their token lists.
- protoc-gen-rampvocab: read both options off field AND enum-value descriptors;
  emit one package per axis (quotametrics, functiontokens, geographytokens,
  usertypes), same shape as pricingunits. Special-case * -> Worldwide and
  all -> AllUses to avoid Go identifier collisions.
- Delete vocab/ entirely (quota-metrics.json, restriction-values/*.json,
  README.md). The proto options are now the sole authored source.

buf lint + buf generate clean; go build + go vet green.
@KonstantinMirin KonstantinMirin changed the title feat(proto)!: Universal Licensing Core — LicenseTerm, restrictions, quotas, obligations feat(proto)!: Universal Licensing Core + proto-native vocabulary (protovalidate + codegen) Jun 13, 2026

@legendko legendko left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

The core message model is sound (ingestion/emission symmetry, semantics split, dispute-chain integrity, comp.proto isolation, charging-structure-vs-metering-basis split all verified correct). But the branch ships an internally contradictory tree that does not match its own stated design, and it does not bring the documentation into conformance as the task requires. The single feature description (license-terms-impl.md) describes an idealized final state that the committed proto/ + cmd/ + gen/ + website/ do not collectively realize.

Two root causes explain ~70% of the findings:

  1. An incomplete/aborted "roll out vocabulary to all axes" step. The final commit 86c7962 and license-terms-impl.md claim all five axes were converted to a proto-native vocabulary (with a (ramp.v1.vocab_enum) extension #50002 and an upgraded plugin). The committed source shows only the pilot (Pricing.unit). git show 86c7962 on vocab.proto / ramp.proto / cmd/.../main.go is empty — only generated .go files and JSON deletions landed. Result: 4 of 5 generated vocab packages are orphans with no source in the tree, and the marquee deliverable is non-reproducible.
  2. Documentation was not migrated to the final proto. Three website docs and the proto changelog describe earlier design states (retired pricing models, wrong field names, a deleted JSON registry, a removed AccessRestrictions type).

Does it break existing flow? The intended breaking changes (remove AccessRestrictions/Offer.restrictions, remove Pricing.revshare, collapse/renumber PricingModel) are documented and expected — acceptable for pre-v1. The unintended breakage is: (a) the regenerate workflow no longer reproduces gen/; (b) 6+ doc files instruct implementers to use a type that no longer exists; (c) a dual-pricing + signature-coverage ambiguity that could affect offer integrity. These are not flagged as expected anywhere and must be resolved.

Severity counts

Severity Count IDs
Critical 1 C1
High 10 H1–H10
Medium 13 M1–M13
Low 7 L1–L7

Two items (H2-signature, H3-mirror) escalate toward Critical under conditions noted inline.


CRITICAL

C1 — Vocabulary toolchain is non-reproducible; 4 of 5 generated packages are orphans

Confidence: high. Flagged by all six lenses. Resolution: DECIDE scope, then fix code (see Decisions).

The committed cmd/protoc-gen-rampvocab/main.go provably cannot produce any of the five committed gen/go/vocab/ packages. Three independent proofs:

  • Header template mismatch (all 5). Plugin emit() writes // Source vocabulary: (ramp.v1.vocab) on field <X>. (main.go:202); every committed file's header reads // Source vocabulary: on <X>. — including the pilot pricingunits.go:3. Re-running buf generate rewrites pricingunits.go to the new header (verified) and leaves the other four untouched.
  • constName("*")* = "*", an invalid Go identifier, yet geographytokens.go ships Worldwide = "*" (main.go:250-262 has no */symbol special-casing).
  • constName("all")All, which collides with the emitted var All (main.go:219), yet functiontokens.go ships AllUses = "all" (special-case absent from the plugin).

Underlying source gaps: vocab.proto defines only extend FieldOptions { repeated string vocab = 50001; }no vocab_enum/50002, despite license-terms-impl.md §"Proto-native vocabulary" claiming it (grep/git log -S across all history: not present). ramp.proto annotates only Pricing.unit (1026-1032); RestrictionKind (719-725) and Quota.metric (824-826) carry no annotations. The plugin's axisPackage map (main.go:39-41) is {"unit":"pricingunits"} with the comment "Pilot scope is Pricing.unit only."

Impact. gen/ is a primary consumer-facing deliverable pulled directly via go get / the TS path export. It cannot be regenerated from committed source (RULE: "Always commit regenerated SDKs alongside .proto changes"). The headline "single source of truth / cannot drift" guarantee is inverted for 80% of axes: their token lists exist only as generated output with no input. (Note: this is a build-integrity/maintainability Critical, not a runtime one — go build ./... and go vet ./... both pass; the orphans compile.)


HIGH

H1 — Offer JWS signature coverage of terms is unspecified; dual pricing compounds it

Confidence: high (gap is real); severity escalates to CRITICAL if signature is field-scoped. Resolution: confirm against reference-impl, then fix doc (or code).
Offer carries both top-level pricing (field 3) and terms (field 19, each LicenseTerm with its own pricing). The signature comment (ramp.proto:366-367) says the JWS is "over offer fields (offer_id, package.id, pricing, identity)" — it does not list terms(19), and it cites package.id (a CoMP concept that design-history.md says was moved to an extension), so the comment is demonstrably stale. If that 4-field enumeration is normative, the entire licensing+pricing payload is unprotected and broker-tamperable, defeating the stated anti-tamper purpose. If the JWS actually signs the whole message, it is "only" a doc bug. The JWS construction lives in the reference-implementation repo and cannot be confirmed here. This is the highest-priority open question.

H2 — protocol/licensing-terms.mdx (new spec page) describes a different protocol

Confidence: high. Resolution: rewrite doc.
The brand-new 243-line spec page (all-additions in this branch) documents an early design: retired model: PER_ACCESS / SUBSCRIPTION in every example (:142,159,178,199,224); Obligation.description (proto field is detail); license: { uri, title } (proto field is name); a "Vocabulary registry" section (:234-243) presenting the deleted vocab/*.json as the live mechanism with no mention of the proto-native vocab; user-type tokens non-commercial/government that don't exist (real set: non_profit/news_publisher/broadcaster). This is the primary human-facing page for the feature and would cause incorrect implementations.

H3 — reference/proto-ramp.mdx mirror is wrong at the wire level

Confidence: high (field numbers verified). Resolution: regenerate/rewrite the mirror. Escalates toward Critical because following it yields wire-incompatible encodings.
This hand-maintained mirror must track the proto. It does not:

  • LicenseTerm: semantics/license field numbers swapped (mirror: semantics=1, license=2; proto: license=1, semantics=2); scopes(7) and part_label(8) missing.
  • License: documents title(2)/spdx_expression(3) instead of proto id(2)/name(3)/immutable(4).
  • Obligation: documents description(3) instead of scope_license(3)/detail(4).
  • PricingModel: Frankenstein table — UNSPECIFIED=0 added atop the old PER_ACCESS/PER_TOKEN/PER_FETCH/SUBSCRIPTION/REVENUE_SHARE; proto is FREE/PER_UNIT/FLAT.
  • Pricing: still lists revshare(6) (:197).
  • All new enums shown off-by-one (RestrictionKind FUNCTION=0, TermSemantics 0=ENUMERATED) — see also the security angle in H7/M-notes: teaching 0=ENUMERATED invites reading an omitted/UNSPECIFIED term as authoritative.
  • Restriction/Quota vocabulary sources point to deleted vocab/*.json.
  • Internal contradiction: "pricing MUST be present when ENUMERATED" vs the same table's "REQUIRED on every term regardless of semantics."

H4 — reference/changelog.mdx states wrong License fields and dead registry paths

Confidence: high. Resolution: fix doc.
Says License is (uri, title, spdx_expression) (proto: uri, id, name, immutable); cites vocab/restriction-values/ and vocab/quota-metrics.json (deleted). It also omits the PricingModel collapse and revshare/REVENUE_SHARE removal from the licensing entry.

H5 — proto/CHANGELOG.md mislabels the release and omits the headline feature

Confidence: high. Resolution: fix doc.
The "Unreleased" entry is headed "Additive, non-breaking (1.0.x)" while buf breaking reports four breaking removals/renumbers. It documents only the pilot Pricing.unit vocab mechanism and omits the entire Universal Licensing Core (LicenseTerm, License, Restriction, Quota, Obligation, six enums, Offer.terms, ResourceEntry.terms). It also references the never-on-main vocab/pricing-units.json. Notably, this changelog corroborates pilot scope — it disagrees with license-terms-impl.md's all-axes claim.

H6 — AccessRestrictions removal not propagated to docs (6+ files reference the deleted type)

Confidence: high. Resolution: update docs (migrate to LicenseTerm.restrictions).
Removal is intended (impl-doc). But the type still appears in: components/broker/selection-engine.mdx:154,233-261 (an entire "Stage 1: Filter by AccessRestrictions" section with Go code r *rampv1.AccessRestrictions), components/exchange/storage-model.mdx:68 (*rampv1.AccessRestrictions), components/agent-sdk/overview.mdx:339, components/exchange/request-flows.mdx:29, components/content-ingestion/catalog-compilation.mdx:59, protocol/extension-profiles.mdx:219. The branch's only edit to request-flows.mdx was unrelated (removed a "backwards compat" phrase). Implementers following these would reference a non-existent type.

H7 — Mandatory LicenseTerm invariants are prose-only; impl-doc overstates wire enforcement

Confidence: high. Resolution: add protovalidate CEL where expressible, or correct the doc.
The only buf.validate CEL in the proto is on Pricing (2 message rules + 1 field rule). Not wire-enforced: "Pricing required on every term," "REFERENCE_ONLY ⇒ License.uri non-empty," "SHARE_ALIKE ⇒ scope_license," "UNSPECIFIED enums rejected," unit-token membership, restriction/geography token validity. license-terms-impl.md frames validation as "Enforced at the RPC boundary by the Connect validate interceptor" — but the interceptor only runs the Pricing CEL; the rest is deferred to app ingest in the separate reference-impl. Any third-party implementation that trusts the impl-doc and skips app checks accepts malformed terms. protovalidate is already a dependency, so most of these are expressible on the wire.

H8 — Unvalidated, agent-fetched License.uri (SSRF / phishing surface)

Confidence: high. Resolution: add spec guidance + threat-model entry.
License.uri "MUST NOT be URL-validated" (ramp.proto:762-763) and REFERENCE_ONLY requires the agent to fetch and read it before use. No scheme allowlist, no "do not auto-fetch," no SSRF/metadata-endpoint guidance anywhere; website/.../security/threat-model.mdx received zero changes this branch. The non-validation is a deliberate accommodation for non-URL TDL schemes — the gap is the absence of consumer guidance, not the choice itself.

H9 — Restrictions fail open by default

Confidence: high. Resolution: document/decide safety posture.
Restriction.critical defaults false, making an unverifiable restriction advisory; an unknown restriction token is warned-and-ignored (robustness principle). A publisher's intended hard limit can therefore be silently downgraded — the opposite of the ext_critical (COSE crit) fail-closed posture used elsewhere. The asymmetry is undocumented.

H10 — gen/ SDK comments stale vs proto (regeneration contract violated)

Confidence: high. Resolution: regenerate + commit.
gen/go/ramp/v1/ramp.pb.go:476 and gen/ts/ramp/v1/ramp_pb.ts:4214 still cite (vocab/pricing-units.json) while the proto says (see vocab.proto). Shapes are in sync (the staleness is comment-only), but it proves committed gen/ ≠ a clean buf generate.


MEDIUM

  • M1 — Dual pricing source of truth. Offer.pricing(3) vs Offer.terms[].pricing with no documented precedence (ramp.proto:350,451). Interacts with H1. Fix: document precedence / which binds the transaction & signature.
  • M2 — optional Pricing can't express "REQUIRED." Field is optional Pricing pricing = 6 with no required/CEL; the schema contradicts its own "REQUIRED" comment (ramp.proto:908-914). Fix: add (buf.validate.field).required or message CEL.
  • M3 — Plugin axisPackage hardcoded + silent skip. Axis identity authored in two layers; genMessage silently skips any vocab-bearing field not in the map (main.go:120-133) — the direct mechanism by which the 4 axes orphaned. Bare-field-name keying is a latent collision hazard. Fix: derive package from the descriptor; error on unmapped vocab fields.
  • M4 — constName not robust + untested. Invalid identifiers for */empty/leading-digit; collisions for all and separator-variants; no _test.go, no diagnostic (main.go:250-262). Latent build break the moment any open axis is wired. Fix: sanitize + collision-guard + tests.
  • M5 — Tooling-only option in the wire package. (ramp.v1.vocab) lives in package ramp.v1, so vocab.pb.go / E_Vocab and vocab_pb.ts ship the extension descriptor into every consumer SDK (gen/go/ramp/v1/vocab.pb.go:37,58). buf convention (cf. protovalidate's own module) isolates annotations. Fix: move to a separate options package, or accept and document.
  • M6 — Licensing core is fully closed; asymmetric extensibility. License/Restriction/Quota/Obligation/LicenseTerm have no ext/ext_critical, while nested Pricing keeps ext(15)/ext_critical(90). Future licensing semantics are forced into core proto edits. Fix: decide deliberately and document; consider an ext seam.
  • M7 — cmd/ outside the documented license boundary. README enumerate Apache-2.0 as "everything under proto/ and gen/"; the new top-level cmd/ is under neither and not in README's structure list. Mitigated (root LICENSE is Apache-2.0, so no legal hole) but the enumerated docs are now inaccurate, and build tooling + the protovalidate dep ship inside the go get-able module. Fix: update the license/structure docs to include cmd/.
  • M8 — Proto comments cite deleted vocab/*.json. ramp.proto:721,723,795-797,821,825 — the root source of the dangling references that propagate into both SDKs. Fix: code (rewrite comments to the proto-native mechanism, or to the chosen final scope).
  • M9 — Scope-gating semantics under-specified. Empty scopes = public (fail-open on omission); hierarchical dist:* matching defined only in a comment (ramp.proto:916-920). Cross-implementation scope-escalation risk. Fix: specify matching + default posture.
  • M10 — REFERENCE_ONLY document has no content-integrity binding. The authoritative external uri is mutable and (per H1) likely outside the Offer signature; License.immutable is a bare bool with no hash, unlike ResourceIdentity.content_hash. Fix: add an optional content hash / digest for immutable references.
  • M11 — No CI gate. No .github/ workflows; amplify.yml builds the website only. Nothing enforces lint/breaking/build or a "regenerate → assert clean tree" check that would have caught C1/H10. Fix: add a regenerate-and-diff CI job.
  • M12 — ADR-014 / RAMP-61 / RAMP-62 cited but absent; design-history.md not updated. Commits and impl-doc reference an ADR that exists nowhere in the repo; design-history.md (90 lines; its stated purpose is recording wire-shaping decisions) has no licensing entry. "Faithful to ADR-014" is unverifiable and the core-vs-profile placement (M6) is undocumented. Fix: add the design-history/ADR record.
  • M13 — Threat model not updated. security/threat-model.mdx got no changes despite the new attack surface behind H1/H8/H9/M9/M10. Fix: add licensing threats.

LOW

  • L1 — Generated headers misrepresent provenance (cite enum-value sources that cannot exist) and assert a "cannot drift" guarantee that is already false. (execution F9)
  • L2 — readVocab swallows marshal/unmarshal errors as "no vocab" — silent failure mode (main.go:149-158). (execution F10)
  • L3 — unit regex rejects uppercase vendor namespaces (ACME:x fails; acme:X passes) — possibly unintended (ramp.proto:1036). (execution F6)
  • L4 — Cross-axis token overlap (accesses/tokens/seats/units-manufactured in both pricingunits and quotametrics) — intended; distinct billing-vs-cap axes; do not extract. (dry — verified non-issue)
  • L5 — buf.yaml ENUM_ZERO_VALUE_SUFFIX comment doesn't note PricingMetering.ONLINE=0 as the RAMP-native zero-value exception. (consistency F8)
  • L6 — Offer.signature comment cites package.id (a CoMP/extension concept) — stale wording independent of H1.
  • L7 — Codegen runs a local go run plugin (executes code at generate time); protovalidate is pinned by digest in buf.lock (good). Minor supply-chain note. (security SEC-10)

What passed (verified correct — credit)

  • Message-model symmetry: ResourceEntry.terms(13) and Offer.terms(19) are the same repeated LicenseTerm.
  • Semantics split (ENUMERATED vs REFERENCE_ONLY) is coherent.
  • Charging-structure vs metering-basis split is sound for the realized axis (PricingModel enum + Pricing.unit vocabulary).
  • Dispute chain intact: Offer → Transaction.id → UsageReport → UsageReportResponse.report_id → DisputeRequest unaffected.
  • comp.proto isolation intact: ramp.proto imports only struct/timestamp/duration/buf.validate/vocab — no comp coupling.
  • AccessRestrictions fully removed from proto/ + gen/ (the doc references in H6 are the only residue).
  • CEL is genuinely token-free — membership is delegated to generated IsRegistered; the core DRY argument holds for the one live axis. FREE ⇒ rate==0 and PER_UNIT ⇒ unit!='' are logically correct.
  • Generated vocab token lists match the (transient) deleted JSON exactly — zero token drift.
  • Builds green: buf build ✓, buf lint ✓, go build ./... ✓, go vet ./... ✓. buf breaking = exit 100, matching the documented intended pre-v1 break set.
  • protovalidate dependency is correctly used (blank import in ramp.pb.go) and pinned in buf.lock.

Decisions required from the maintainer (genuine ambiguities — not guessed)

  1. Vocabulary axis scope. Is the intended v1.0 state all five axes (then C1/H10/M3/M8 ⇒ finish the code: add vocab_enum/50002, annotate the enum values + Quota.metric, extend the plugin with constName sanitizing/collision-guarding and per-axis CEL for underscore/uppercase tokens, regenerate) or pilot Pricing.unit only (then ⇒ delete the 4 orphan packages, correct license-terms-impl.md + the 86c7962 narrative, fix proto comments)? The repo's own artifacts disagree: impl-doc + commit subject + 4 orphan packages say all-axes; proto + plugin + proto/CHANGELOG.md say pilot.
  2. Offer signature coverage (H1). Does the Offer JWS sign the whole serialized message (incl. terms(19)) or only the four enumerated fields? Determines whether H1 is a doc fix or a Critical integrity hole, and which pricing field binds the transaction (M1).

Items 1 and 2 are posed to the maintainer directly; the rest can proceed once the scope decision is made.


My thoughts on ADR-014 / RAMP-61 / RAMP-62 mentions in the commit messages: is not that critical because there are no mentions in the codebase, but for the future maybe it makes sense to omit such references to internal docs. A fix for current situation could be a rebase or a squash but that's your decision.
Regarding the proto/CHANGELOG.md - I think we should keep our idea of keeping everything under v1.0.0, besides the current changes are definitely not minor and not non-breaking (1.0.x) as currently stated.

…, doc sweep

Completes the all-axes proto-native vocabulary (vocab_enum 50002; function/
geography/user-type annotated on RestrictionKind, Quota.metric and Pricing.unit
on their fields) so buf generate reproduces gen/ byte-identically.

Hardening:
- Offer JWS documented as signing the whole canonical offer (terms + pricing)
- Offer.pricing is the term's pricing (one offer per term)
- Restriction.critical -> advisory (binding by default; fail closed)
- License.uri_digest pins the referenced document (required when uri present)
- regenerate-and-diff CI gate (.github/workflows/proto-ci.yml)

Docs brought into conformance: AccessRestrictions removed, pricing models
collapsed to FREE/PER_UNIT/FLAT, vocabulary now proto-native, License/Obligation
field names corrected, off-by-one enums fixed in the proto mirror; threat-model
gains a licensing section (T-LIC-1..4). CHANGELOG folded under v1.0.0.
@KonstantinMirin

Copy link
Copy Markdown
Contributor Author

Hi Yaroslav (@legendko) — thanks for the exceptionally thorough review. Pushed 02f1b6c addressing it end to end. Both of your escalated decisions are resolved, the vocabulary rollout is finished (so gen/ reproduces), and the docs are swept. ADR-014 lives in the sister repo and was updated there (agentic-content-access MR !12).

Your two maintainer decisions

  1. Vocabulary scope → all axes proto-native. This was the intended state; the tree was mid-rollout. Finished it: vocab_enum (50002) added, RESTRICTION_KIND_* values + Quota.metric + Pricing.unit all annotated, plugin handles every axis. buf generate now reproduces gen/go/vocab/* byte-identically (verified idempotent), and ADR-014 was amended to make all axes proto-native (the JSON-registry text was stale).
  2. Offer signature → whole canonical offer. Confirmed against the reference-impl: the JWS signs the entire deterministic-marshaled Offer including terms and pricing, excluding only signature/signature_algorithm/expires_at. So H1 is a doc bug, not a hole. Fixed the comment and dropped the stale package.id.

Critical

  • C1 — vocab non-reproducible / orphansResolved. All-axes rollout finished; buf generate reproduces gen/ with no drift (header template, *Worldwide, allAllUses all handled).

High

  • H1 — offer signature coverageFixed (doc). Comment now states the JWS covers the whole canonical offer (terms + pricing); package.id removed.
  • H2 — licensing-terms.mdxFixed. Retired pricing models, Obligation.descriptiondetail, license.titlename, user-type tokens (non_profit/news_publisher/…), and the proto-native vocab section.
  • H3 — proto-ramp.mdx mirrorFixed. Swapped LicenseTerm field numbers, added scopes/part_label, Licenseid/name/immutable(+uri_digest), Obligationscope_license/detail, removed revshare, replaced the PricingModel table, and corrected the off-by-one in all five enums (_UNSPECIFIED=0).
  • H4 — reference/changelog.mdxFixed. License fields, proto-native vocab, PricingModel collapse.
  • H5 — proto/CHANGELOG.mdFixed. Folded under v1.0.0 (pre-v1, breaking — dropped the "additive, non-breaking" label per your note) and documented the full Universal Licensing Core, not just the pilot.
  • H6 — AccessRestrictions in docsFixed. Swept ~24 docs → LicenseTerm.restrictions on Offer.terms[]; the broker/exchange "filter by restrictions" framing reframed to restrictions ride on the offer; the agent self-selects (matches the scope-only projection).
  • H7 — invariants prose-only / impl-doc overstates wire enforcementAddressed (by design). Per ADR-014, membership and most invariants are enforced at ingest (PushResources); the proto deliberately carries only the structural Pricing CEL. We are not adding more wire CEL — validation happens at push and everything downstream flows from already-valid, signed data, so it would be belts-and-suspenders. Doc framing corrected.
  • H8 — SSRF via License.uriDocumented at protocol level (threat-model T-LIC-1): scheme allowlist, block loopback/private/metadata IPs (resolve-then-check), client-side fetch via egress proxy, untrusted-content handling. "Never URL-validate" means don't reject non-URL TDL schemes, not blindly fetch.
  • H9 — restrictions fail openFixed (breaking). Inverted Restriction.criticalRestriction.advisory; restrictions are now binding/fail-closed by default (proto3 can't default a bool true, hence the rename). Diverges from COSE-crit deliberately so a forgotten flag fails safe.
  • H10 — stale gen/ commentsResolved by regeneration; no gen/ comment cites the deleted JSON (verified).

Medium

  • M1 — dual pricing precedenceFixed. One offer per term; Offer.pricing is that term's pricing (authoritative copy in terms[].pricing). Documented in proto + ADR.
  • M2 — optional Pricing can't express requiredBy design; enforced at ingest, not wire (see H7).
  • M3 — plugin axisPackage hardcoded / silent skipDeferred. All 5 axes are mapped; runtime membership stays a warning (open vocab — vendors must be able to supply their own values). Build-time error-on-unmapped-axis deferred until a 6th axis is added.
  • M4 — constName robustness/testsDeferred. * and all are special-cased; current tokens are collision-free. Intra-axis collision guard + tests are a nice-to-have, not a present bug.
  • M5 — tooling option in the wire packageAccepted as-is (the (ramp.v1.vocab*) extensions stay in ramp.v1; documented in vocab.proto).
  • M6 — licensing core closed (no ext seam)Deliberate; not adding an ext seam to the licensing messages now.
  • M7 — cmd/ outside the documented license boundaryOpen (minor). Root LICENSE (Apache-2.0) already covers it; the README structure list still needs cmd/ added — will fold into a docs pass.
  • M8 — proto comments cite deleted vocab/*.jsonFixed; rewritten to the proto-native mechanism (no vocab/*.json left in proto or gen/).
  • M9 — scope-gating under-specifiedDocumented (threat-model T-LIC-3): exact segment-wise matching via Biscuit Datalog, empty = public kept explicit, no sentinel backfill (it only relocates the "forgot it" failure).
  • M10 — REFERENCE_ONLY no content integrityFixed. Added License.uri_digest (method:hexdigest), required whenever uri is present — any semantics, mutable or not (MitM/swap protection); covered by the offer signature. Threat-model T-LIC-4.
  • M11 — no CI gateFixed. .github/workflows/proto-ci.yml: buf lintbuf generategit diff --exit-code (the drift gate that would have caught C1/H10) → go build/vet. buf breaking is non-blocking pre-v1.
  • M12 — ADR-014/tickets absentAddressed. ADR-014 is maintained in the sister repo (agentic-content-access, MR !12) and was updated there; internal ticket refs kept out of new commit messages per your suggestion.
  • M13 — threat model not updatedFixed. New §9 (T-LIC-1…4).

Low

  • L1 — generated header provenance → Resolved by regeneration.
  • L2 — readVocab swallows errors → Deferred (minor).
  • L3 — unit regex rejects uppercase vendor namespaces → Left as-is (lowercase-namespace convention); flag if you'd prefer case-insensitive.
  • L4 — cross-axis token overlap → Confirmed non-issue; no action (as you noted).
  • L5 — buf.yaml ENUM_ZERO_VALUE_SUFFIX comment → Open (minor); will note PricingMetering.ONLINE=0 as the exception.
  • L6 — Offer.signature cites package.id → Fixed (with H1).
  • L7 — local go run codegen plugin → Acknowledged; first-party plugin, protovalidate pinned by digest. No change.

Build stays green: buf lint/buf build ✓, go build/vet ✓, regenerate idempotent. Happy to split anything out or take the open minors (M7/L3/L5) in a follow-up if you'd like.

@KonstantinMirin

Copy link
Copy Markdown
Contributor Author

Follow-up on L5: addressed in 81c9403buf.yaml now documents PricingMetering.PRICING_METERING_ONLINE = 0 as the deliberate RAMP-native exception to ENUM_ZERO_VALUE_SUFFIX (it's an optional field with a safe default — absent = ONLINE = meter everything — so it needs no _UNSPECIFIED sentinel). Kept as a documented exception rather than normalized, since forcing UNSPECIFIED here would either add friction or reintroduce a silent default.

…ment cmd/ (L3, M7)

L3: the Pricing.unit and Quota.metric CEL accepted uppercase only AFTER the
colon (acme:X passed, ACME:x failed). Widen the namespace class to
[A-Za-z0-9._-] so vendor namespaces may be uppercase; bare registry tokens stay
lowercase-dashed.

M7: README now lists cmd/ in the repo structure and includes it in the
Apache-2.0 code boundary.
@KonstantinMirin

Copy link
Copy Markdown
Contributor Author

Follow-up on L3 and M7 — both addressed:

  • L3 (7fcb4f1): the Pricing.unit / Quota.metric CEL accepted uppercase only after the colon (acme:X passed, ACME:x failed). Widened the vendor-namespace class to [A-Za-z0-9._-] so namespaces may be uppercase; bare registry tokens stay lowercase-dashed. Regenerated (descriptor-embedded CEL); idempotent.
  • M7 (7fcb4f1): README now lists cmd/ in the repo structure and includes it in the Apache-2.0 code boundary.

That closes the review. Remaining items are the consciously-deferred maintainability nice-to-haves (M3 plugin error-on-unmapped-axis, M4 constName collision tests, L2 readVocab error surfacing) — happy to do those whenever you want, but none block.

…ons, decode failures (M3, M4, L2)

M3: key axis->package maps by full name (no short-name collisions) and ERROR
when a vocab-bearing field/enum value is unmapped, instead of silently emitting
nothing.

M4: constName validates its output is a unique exported identifier; invalid
idents (e.g. leading digit), reserved-name shadows (All/IsRegistered), and
token->ident collisions now error with a clear message. Adds unit tests.

L2: readVocab distinguishes 'no vocab option' from a decode failure and
propagates the latter instead of masking it as an empty axis.

Generated output is byte-identical for the current registries.
@KonstantinMirin

Copy link
Copy Markdown
Contributor Author

Follow-up on M3 / M4 / L2 — done in 7f137f7 (plugin hardening; generated output byte-identical for the current registries):

  • M3: axis→package maps are now keyed by full name (no short-name collisions), and an unmapped vocab-bearing field/enum value is a hard error instead of a silent skip. Kept the map in the Go plugin (Go package names are a Go-codegen concern, not the wire contract).
  • M4: constName validates its output is a unique exported identifier — invalid idents (e.g. leading digit), reserved-name shadows (All/IsRegistered), and two-tokens-one-ident collisions now error with a clear "add a special-case" message. Added main_test.go covering all of it.
  • L2: readVocab distinguishes "no vocab option" from a decode failure and propagates the latter instead of masking it as an empty axis.

That clears every actionable item in the review. Thanks again, @legendko.

@legendko legendko left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the above mentioned issues. We are almost there. The re-review found a few things that should be addressed before merge.

Verdict

The first review is substantially resolved. Of 31 original findings: 22 RESOLVED, 5 acceptably DEFERRED, 3 PARTIAL, 1 (H6) resolved-at-the-doc-layer-but-the-underlying-model-change-is-half-applied. No original finding is unaddressed.

However, the re-review surfaces two new HIGH issues that should be fixed before merge — one a regression introduced by the sweep, one a half-applied design change — plus six MEDIUM and several LOW items (a mix of newly-introduced and pre-existing-but-surfaced).

Does it break existing flow? (the primary-review bar)

  • The intended breaks (remove AccessRestrictions/Offer.restrictions/revshare; collapse PricingModel) remain documented and expected. ✓
  • The criticaladvisory inversion is a deliberate, well-documented in-branch behavioral change (fail-open → fail-closed). ✓ Acceptable.
  • The H6 discovery reframe ("Exchange no longer filters by requester attributes; the agent self-selects") is a real behavioral change that is only half-applied — it is explained in the new Restriction comment, but the proto still ships the old filtering enums and two docs still describe the old model (N1). This is exactly the "breaks existing flow without fully reconciling it" case the primary review guards against. Must be completed or the residue reconciled.

NEW issues (introduced or surfaced by the re-review)

HIGH

  • N1 — H6 discovery reframe is half-applied (internal proto contradiction). [orchestrator-verified]
    The new Restriction comment (ramp.proto:854-863) says "the Exchange does NOT filter terms by matching the requester's self-declared attributes (user_type / geography / intended_use); … the AGENT self-selects." But the same proto still ships the old filtering model: OfferAbsenceReason.{FUNCTION_PROHIBITED=3, GEO_RESTRICTED=4, USER_CATEGORY_PROHIBITED=5} (:256-261) and DenialReason.{FUNCTION_PROHIBITED=6, GEO_RESTRICTED=7} (:1664-1665), and discovery-paths.mdx:252 ("content blocked for your use case") + ext-c2pa.mdx:167 ("will not receive offers…") still describe attribute filtering. As written, the offer/denial enums and the Restriction model are mutually exclusive and nothing reconciles them. HIGH (not Critical: pre-v1, no consumers, dispute chain intact). Resolution: DECIDE — remove/repurpose those enums to reconcile with self-select, or narrow the Restriction claim. (Open question 1.)

  • N2 — REFERENCE_ONLY semantics regressed in the canonical mirror. [orchestrator-verified]
    The sweep rewrote proto-ramp.mdx:791 from the correct "machine fields are informational; the Exchange MUST NOT auto-enforce them" to "machine restrictions/quotas/obligations MUST be absent" — which contradicts ramp.proto:710-714 and licensing-terms.mdx:33 (both say informational, present-but-not-auto-enforced). A right→wrong regression in the authoritative reference page, introduced by this branch (git diff 86c7962..HEAD confirms). Fix (doc): restore "informational; not auto-enforced."

MEDIUM

  • N3 — Requester.intended_use orphaned by the reframe. The new model names intended_use (and user_type/geography) as things the Exchange does NOT consume, but Requester.intended_use (ramp.proto:1256-1258) keeps its old comment with no statement of what (if anything) now reads it. Tied to N1. (Also a pre-existing underscore-vs-hyphen token-form mismatch vs FUNCTION tokens.) Fix: clarify role (agent self-selection input? advisory/telemetry? vestigial) or remove.
  • N4 — uri_digest has no hash-algorithm constraint. Free-form method:hexdigest with no CEL accepts md5:/sha1:, making the new swap-protection collision-forgeable; and digest-present is prose/ingest-only though it underpins a new integrity guarantee. Fix: field-CEL strong-hash allowlist (sha256/sha384/sha512) + spec wording; consider wire-requiring it when uri is set.
  • N5 — CI drift gate misses untracked files. git diff --exit-code (.github/workflows/proto-ci.yml:44) does not see new generated files, so a future axis whose gen/go/vocab/<new>/ is generated-but-not-committed passes green — the exact C1 class the gate exists to seal (the author's roadmap anticipates a "6th axis"). Fix: git add -A && git diff --cached --exit-code, or test -z "$(git status --porcelain)".
  • N6 — Homepage code samples use retired PER_ACCESS. [orchestrator-caught; missed by agents] website/src/pages/index.astro:1407,1707 show model: PER_ACCESS (no longer exists). Likely missed because the sweep scoped to content/docs/, not pages/. Most-visible page. Fix (doc): PER_UNIT+unit or FLAT.
  • N7 — Offer replay: expires_at is unsigned with no signed time anchor. [orchestrator-verified; PRE-EXISTING, not a regression] expires_at is excluded from the JWS (:375) and there is no signed issued_at/nonce in Offer, so a signed offer's validity window is not integrity-protected; DENIAL_REASON_OFFER_EXPIRED=9 exists but enforcement lives in the reference-impl (not in this repo). The old signature comment also excluded expires_at, so this branch did not introduce it — but H1's "whole offer is now signed" framing makes expires_at the conspicuous unsigned exception worth hardening. Severity MEDIUM (security agent rated HIGH; downgraded — pre-existing + enforcement out-of-repo). Fix: signed issued_at + max-age check, or sign expires_at, or document the replay bound. (Open question 2.)
  • N8 — Open string axes still unbounded (original SEC-2, not addressed). Vendor-namespace CEL post-colon is .+ (whitespace/control chars), and Restriction.permitted/prohibited + Quota.metric carry no CEL or length bound → injection / log-forging / oversize-payload surface into billing/logs/display. No T-LIC entry. Pre-existing; surfaced again. Fix: length caps + charset tightening.

LOW

  • N9ramp.proto:687-701 worked-example comments use retired PER_ACCESS (canonical proto contradicts its own enum; comment-only, not in gen/).
  • N10ramp.proto:785 RESTRICTION_KIND_OTHER comment cites a non-existent Restriction.description field (Restriction has no free-text field; propagates to both SDKs).
  • N11constNameSpecial special-case values bypass isExportedIdent/reservedIdents validation; a future bad special-case would emit broken Go silently.
  • N12 — T-LIC-1 SSRF guidance is not cross-linked from the License.uri proto comment or licensing-terms.mdx; a proto-only implementer sees "MUST NOT URL-validate / must fetch" without the countermeasures.
  • N13CLAUDE.md "Licensing split" still omits cmd/ (README was fixed).
  • N14 — M6 closed-core rationale undocumented; design-history.md still 90 lines with no licensing entry, yet proto/CHANGELOG.md links to it "for the reasoning." One in-repo paragraph closes both M6 and M12.
  • N15exchange-manifest.mdx pricing_models_supported advertises retired strings (revenue_share/subscription/per_token). Pre-existing (identical on origin/main), not a regression, but now visibly stale.
  • N16 — "geography validated structurally (two-letter uppercase)" overstates the wire: Restriction.permitted/prohibited carry no CEL (consistent with H7's prose-only posture).

What the orchestrator verified directly

  • buf lint=0, buf build=0; go build ./...=0, go vet ./...=0, go test ./cmd/...=ok.
  • C1 reproducibility: a clean buf generate yields zero git diffgen/ (Go+TS SDKs + all 5 vocab packages) reproduces byte-identically.
  • buf breaking vs main = the same documented pre-v1 set (the criticaladvisory rename is invisible to main — Restriction is branch-new).
  • vocab.proto defines both extensions; all 5 axes annotated; License.uri_digest(5) and Restriction.advisory(4) present; Offer signature comment rewritten; CI workflow present.
  • Residual-token sweep across the live tree: spdx_expression/non-commercial/Restriction-critical/vocab/*.json(in proto+gen) all gone; AccessRestrictions only in the 2 changelogs (correct).
  • N1 and N2 contradictions confirmed against ramp.proto line-by-line; N7's unsigned-expires_at + no-issued_at confirmed.

Open questions / decisions for the maintainer

  1. (N1 — drives the only HIGH design item) Are OfferAbsenceReason.{FUNCTION_PROHIBITED, GEO_RESTRICTED, USER_CATEGORY_PROHIBITED} and DenialReason.{FUNCTION_PROHIBITED, GEO_RESTRICTED} meant to be removed (full commit to "agent self-selects"), or kept as reconcile/accept-phase reasons? A GEO_RESTRICTED discovery-phase OfferAbsenceReason cannot be salvaged by "reconcile-only" — it directly re-asserts Exchange-side discovery filtering. And what now consumes Requester.intended_use/user_type/geography (N3)?
  2. (N7) Intended offer-replay bound — should expires_at be signed or anchored by a signed issued_at, and is DENIAL_REASON_OFFER_EXPIRED wired in the reference-impl?
  3. (N4) Is a strong-hash (sha256+) mandatory for uri_digest?
  4. (M9) Multi-level scope-wildcard semantics (dist:* vs dist:US:CA).
  5. (N8) Is the open-axis injection/length surface (original SEC-2) accepted, or to be bounded?

Bottom line: the Critical is genuinely closed and the bulk of the review landed well. Before merge, address N1 (reconcile the half-applied discovery reframe) and N2 (REFERENCE_ONLY mirror regression); ideally also the MEDIUMs (N4 hash-algo, N5 CI gap, N6 homepage, plus the pre-existing N7/N8 hardening). The LOWs are cleanup.

…pires_at, bound open axes

- Reframe restrictions as agent-self-selected with an OPTIONAL Exchange/Broker
  convenience pre-filter; keep OfferAbsenceReason/DenialReason attribute reasons
  but make them coherent (convenience signal, not enforcement). Clarify
  Requester.intended_use as an advisory filter hint, not an entitlement.
- Correct REFERENCE_ONLY semantics: machine restrictions/quotas/obligations are
  optional, but when present must be accurate (no contradiction with the
  referenced document) and are enforced like ENUMERATED. Drops the wrong
  "must be absent" / "informational, not enforced" framings.
- Sign Offer.expires_at so the validity window is integrity-protected against
  replay (only signature/signature_algorithm now excluded).
- uri_digest: structural CEL allowlist — sha256/sha384/sha512 with matching hex
  length; reject forgeable md5/sha1.
- Bound the open string axes: length caps + charset tightening on
  Restriction.permitted/prohibited, Quota.metric, and Pricing.unit
  (vendor-namespace post-colon no longer ".+").
- CI drift gate stages files first so NEW untracked generated output is caught.
- Plugin: validate constNameSpecial values through the exported-ident/reserved
  guards so a bad special-case can't emit broken Go silently.
- Docs: restore REFERENCE_ONLY mirror, fix homepage + worked-example PER_ACCESS,
  retire stale manifest pricing strings, soften geography "validated
  structurally", cross-link SSRF guidance, add the design-history licensing
  entry (closed-core rationale).
- Regenerate gen/ (descriptor-embedded CEL + comments).
…ed licensing core

Pricing inherited an open google.protobuf.Struct ext (15) + ext_critical (90)
from the CoMP/COSE lineage. An untyped blob inside a signed, cross-exchange-
comparable pricing object defeats the comparability unit_cost exists for and
re-opens the unbounded-payload surface the vocabulary axes were closed to avoid.
Removed both so the entire licensing core (License/Restriction/Quota/Obligation/
LicenseTerm/Pricing) is uniformly closed; new commercial dimensions arrive as
typed fields or a vocab axis. The general ext/ext_critical mechanism stays on
transport/discovery messages where contextual, ignorable metadata is fine.

- proto: remove Pricing.ext / Pricing.ext_critical
- regenerate gen/ (Go + TS)
- docs: drop the Pricing ext row from the proto-ramp mirror; rewrite the
  design-history closed-core entry to "no ext anywhere in the licensing core"
- homepage: correct stale "12 pricing models" -> per unit / flat / free
…as a current-state snapshot

Multi-agent read-through of the proto source for comments that narrate how the
schema got here rather than what it is now. Removed/reworded 14 spots:

- "Replaces CoMP AISystem" (Requester field + message), "Replaces ContentQuality"
  (attestations), "(replaces eCPT)" (unit_cost)
- ATTRIBUTION/CONTRIBUTION "replace the retired PRICING_MODEL_*" narration
  (ObligationKind + Obligation)
- Pricing field-6 "formerly revshare, retired / Pre-v1: not reserved" parenthetical
- PricingModel "Pre-v1: renumbered cleanly, nothing reserved"
- ResourceAttestation "claims-schema migrated to ext (see CHANGELOG)" — reworded
  to present tense (and dropped the dangling CHANGELOG ref)
- DiscoveryMethod "v1 extension point for future..." framing
- "Relaxing the discovery anchor is intentionally deferred: revisit..." note
- vocab.proto protoc-evolution rationale ("modern protoc supports...") trimmed

The reasoning behind these choices lives in docs/design-history.md, which is the
file for it. Regenerated gen/ (comments propagate to the SDKs). No wire change.
…riction vocabulary across request/term/reason

Requester now carries identity + entitlements only (id, domain, type, name,
license_id, scopes, delegation). What is being asked for (uris) and the limits
the ask operates within (acceptable_restrictions) move to ResourceQuery and
RAMPRequest, where they belong. New AcceptableRestriction{axis, values} states
selection limits in the same open RestrictionKind vocabulary the terms use.

Reason enums unified onto the licensing core: the pre-licensing attribute fossils
(OfferAbsenceReason/DenialReason FUNCTION_PROHIBITED / GEO_RESTRICTED /
USER_CATEGORY_PROHIBITED) collapse to RESTRICTION_FILTERED and
RESTRICTION_NOT_SATISFIED, each carrying repeated RestrictionKind
(OfferGroup.restriction_filters, TransactionResult/Item.restriction_mismatches).
Request, term, and reason now all speak one vocabulary.

SCOPE_INSUFFICIENT reworded to cover subscription/scope-gated access generally,
not only enterprise deployments.

- proto: reshape Requester / ResourceQuery / RAMPRequest; add AcceptableRestriction;
  collapse + renumber reason enums; add RestrictionKind carriers
- regenerate gen/ (Go + TS)
- docs: mirror + 14 walkthrough/narrative pages swept to the new shape; reason
  tables and lists updated
Multi-agent consistency review of the Requester/ResourceQuery reshape surfaced
22 issues; all fixed:

Reshape-introduced:
- The doc sweep had placed uris/acceptable_restrictions into ExecuteTransaction
  (TransactionRequest) examples — discovery-phase fields that do not belong in a
  transaction commitment. Stripped from 8 transaction blocks across 6 walkthroughs
  (academic, due-diligence, medical-imaging, credit-report, eu-regulation,
  scenario) plus transaction-flow and walkthrough-v1.
- Fixed AcceptableRestriction.values / Usage tokens that used enum-style
  FUNCTION_AI_INPUT / SUB_FUNCTION_RAG instead of the real vocabulary
  (ai-input / ai_input / rag).
- Dropped the stale `uris` from the Requester signature tuple in 4 more docs
  (now (id, domain, scopes)).
- proto: DenialReason comment referenced a non-existent "TransactionResult" —
  corrected to TransactionResponse/TransactionResultItem.

Pre-existing mirror drift (proto-ramp.mdx) the review also caught:
- Offer field 2 package->title, field 9 exchange_signature->signature, +ext_critical
- Delegation: max_spend->max_spend_cents (int64), token string->bytes,
  +max_accesses/quota_period/issuer/ext_critical
- Usage: function/subfn typed as Function/SubFunction -> repeated string

Regenerated gen/; buf lint, go build/vet, website build all green.
…eaders (RFC 9421 multi-sig); drop IntermediaryHop

Request authentication is RFC 9421 HTTP Message Signatures (headers), never
message fields. Multi-hop forwarding is the same primitive: a stack of labeled
RFC 9421 signatures, each covering the request plus the prior hop's signature, so
the ordered set of signatures IS the forwarding chain (tamper-evident,
order-bound). The in-message hop chain is therefore removed.

Proto:
- Remove message IntermediaryHop and ResourceQuery.intermediaries (field 5);
  document the header multi-sig model on ResourceQuery.
- Keep RequestConstraints.max_hops and WellKnownManifest.max_intermediary_hops
  (now counted as signatures); reworded.
- Regenerate gen/ (Go + TS).

Docs (proto is source of truth):
- Strip request-side signature *fields* from all JSON examples and prose:
  requester.signature/signature_algorithm, agent_signature(_algorithm),
  offer_signature_algorithm, broker_signature(_algorithm). Reframe to RFC 9421
  HTTP Message Signatures in headers. Keep the real out-of-band artifacts:
  Offer JWS (signature/signature_algorithm), ResourceAttestation.signature,
  and the echoed offer_signature.
- Replace IntermediaryHop tables / intermediaries[] examples with the
  header signature-stack model across walkthroughs, components, authentication,
  threat-model, production-architecture, changelog, mirror, design-history.
- authentication.mdx "Signature Verification" rewritten to the RFC 9421 model.
- Fixed a stale REFERENCE_ONLY changelog line (machine fields optional but,
  when present, accurate and enforced).

buf lint, regen idempotent, go build/vet, website build all green.
…ation table + REFERENCE_ONLY changelog

The core Offer has no `package` field (field 2 is `title`); IAB CoMP
Package/Scope/Retrieval metadata lives as flat comp.* keys in Offer.ext per the
ramp-comp-v1 profile. Replaced every top-level `package` object in Offer JSON
examples across 11 walkthrough/profile docs with Offer.title + the documented
comp.* ext keys (comp.package_id / seller / citation_required / content_types /
retrieval_type), reframing prose that called Package a core Offer field.

Also:
- authentication.mdx Delegation table → max_spend_cents (int64), token (bytes),
  token_format "jwt" default, + issuer (match proto).
- changelog REFERENCE_ONLY wording → machine fields optional but, when present,
  accurate and enforced.

Website build green; docs-only (no proto/gen change).
…ing, unify scope matching, finish RFC 9421 auth model

Resolve the three security-documentation contradictions the latest review
surfaced (all docs/comments; no wire change):

- Holder binding: add the mandatory, non-skippable verification step — the
  RFC 9421 request-signing key MUST equal the token's holder/sealed key — to
  authentication.mdx and for-exchange-operators.mdx, and de-bearer the proto
  Delegation comment + threat-model T-DEL-1 (theft neutralized by binding;
  scope/time/spend caps are defense-in-depth). Name JWT proof-of-possession
  (cnf); declare the full JWT verification path deferred past v1 with no
  degraded mode (Biscuit v3 remains the default and only fully specified path).
- Scope matching: make the M9 segment-wise string rule the single normative
  algorithm protocol-wide; threat-model now cites it and frames Biscuit Datalog
  as a conformant implementation that must produce identical results. Propagate
  the rule to LicenseTerm.scopes and add the narrower-than-required case.
- Request auth: rewrite the residual in-message tuple-signature wording in the
  operator/architecture/broker docs to RFC 9421 over @method/@target-uri/
  content-digest.

Value-level doc drift and hardening:

- Complete the DenialReason table in event-types.mdx; broaden the
  DELEGATION_INVALID description everywhere.
- Point the duplicated delegation-claim vocab in proto-ramp.mdx at the single
  source; make its Delegation intro token-format-agnostic.
- Reserve the ramp_ claim-name prefix; bound scopes/uris with max_items.
- Rename code-sample exchange_signature to the real wire fields (signature on
  an Offer, offer_signature when echoed), keeping the term as a documented
  alias in prose; strip the biscuit-v3: prefix from opaque token samples;
  fix the stale CoMP Go path in multi-tenant.mdx.
- Record this round's decisions in design-history and the proto changelog.

Doc-conformance gate: also scan proto/ramp and assert positive facts (every
DenialReason value appears in the event-types table; registered ramp_ claims
appear in the auth spec) — a denylist alone cannot catch dropped values.

Stop tracking local review scratch (.claude/) and gitignore it.
… to the ext, not a pricing model

PricingModel covers only the charging structures an Exchange can quote, sign,
and compare at transaction time (FREE / PER_UNIT / FLAT). Revenue share has no
transaction-time price — the rate and its settlement are an off-protocol
agreement — so adding it as a pricing model would either be a rate-less label or
would pull commercial terms into the signed, comparable Pricing.

- design-history: record the decision and the reasoning.
- licensing-terms: add "Revenue-share arrangements" — express it as a FREE term
  gated by an agreement scope plus a reporting Obligation (same shape as a
  subscription); agent self-selects between a public per-unit term and the
  scope-gated revshare term.
- ext-comp: document comp.license[].revshare as carried verbatim in the ext and
  deliberately not mapped to a pricing model (parity by mapping, not duplication).
- doc gate: stop denylisting the bare word "revshare" (it is now a live CoMP ext
  identifier and scope prefix); the retired pricing model stays guarded via the
  enum-constant patterns.
@KonstantinMirin

Copy link
Copy Markdown
Contributor Author

@legendko — round 4 addressed. You were right that this round's findings were a different animal from the earlier stale-example drift — these were the security invariants contradicting each other, and the doc gate sailing past all three is exactly the "necessary but not sufficient" proof you predicted. So I took your three gate recommendations as well, not just the findings. All on feature/license-terms.

The three HIGH — the security-doc unit

I treated authentication.mdx + threat-model.mdx + the proto as one thing and reconciled them in the same commit, per your recommendation.

  • R4-1 — holder binding is now operationalized, not just asserted. Added the missing verification step in both authentication.mdx and for-exchange-operators.mdx: the RFC 9421 request-signing key MUST equal the key the token is sealed/attenuated to; reject otherwise. It's marked mandatory and non-skippable — an Exchange that omits it reduces the token to a bearer credential, and we say so. I also rewrote T-DEL-1: theft is neutralized by the binding (a thief with the bytes but not the signing key gets nothing); the scope/time/spend caps are reframed as defense-in-depth for the residual case where the holder's key is also compromised. That's why it can sit under "structurally preventable." The proto Delegation comment lost its old "damage is bounded by scope+time+spend" bearer framing too. The JWT proof-of-possession mechanism is now named (cnf, RFC 7800) — see the JWT note under your open questions.
  • R4-2 — one scope-matching rule, normatively. The M9 segment-wise string rule is now the spec. threat-model.mdx cites it instead of contradicting it, and frames Biscuit Datalog as a conformant implementation that MUST produce results identical to the string definition — not a second rule. No more "define which, once" (it's defined). Applied the same rule to LicenseTerm.scopes so the two scope surfaces can't diverge (R4-10).
  • R4-3 — the tuple-signature ghost is gone. Rewrote the (id, domain, scopes) body-signing wording in for-exchange-operators.mdx, production-architecture.mdx, and broker/overview.mdx to RFC 9421 over @method/@target-uri/content-digest (body bound via content-digest). These were the adjacent lines I missed when I fixed R3-1.

MEDIUM / value-level drift

R4-4 DELEGATION_INVALID description broadened everywhere (no longer just "expired"). R4-6 the second scenario-walkthrough offer-sig phrasing now matches the "entire Offer" guarantee. R4-8 proto-ramp.mdx no longer re-lists the delegation-claim vocab — it points at the single source and the binding row is no longer missing. R4-9 the event-types.mdx DenialReason table is complete (all values). R4-13 the round's decisions are now in design-history.md and the proto changelog, not only the user-facing changelog.

LOW / hygiene

R4-14 stripped the biscuit-v3: prefix from the opaque token samples (the version lives in token_format). R4-15 scopes/uris now carry max_items. R4-17 the proto-ramp Delegation intro is token-format-agnostic. R4-18 ramp_-prefixed claim names are now reserved against vendor shadowing. R4-19 the "grant narrower than the requirement doesn't cover it" case is specified. R4-20 — good catch, that was sloppy; I've stopped tracking the .claude/ review scratch and gitignored it.

Your open questions

  1. (R4-1) JWT holder binding / broker hops. Mechanism is cnf (RFC 7800) bound with DPoP or mTLS. The binding does survive hops: each attenuation re-seals the token to the next holder's key, so at every hop the bound key is whoever now signs the request — the Ed25519 block-signature chain carries it forward and it never reverts to bearer form. I documented this explicitly.
  2. (R4-2 / R4-10) string vs Datalog. String segment-matching (M9) is authoritative; Datalog is a conformant implementation that must match it. Same answer applied to LicenseTerm.scopes.
  3. (R4-5) JWT in v1. Wire-permitted, full verification path (cnf/DPoP + OIDC→JWKS) deferred past v1. Biscuit v3 is the default and the only fully specified path; an Exchange may accept JWT only if it can enforce the same mandatory holder binding and fail-closed rules — otherwise it MUST reject rather than fall back. No degraded mode.
  4. (R4-11 / R4-12 / R4-13) pre-existing debt. Swept now rather than deferred. exchange_signature is now defined once as the documented alias for the Offer's signature field, and every code sample (JSON keys, Go fields) uses the real wire name — signature on an Offer, offer_signature where it's echoed in a selection — while the term stays in prose as the concept. The stale Aisystem Go path in multi-tenant.mdx is fixed to req.Msg.Uris. Design-history/CHANGELOG updated as above.

On your gate recommendations

All three taken:

  • The gate now also greps proto/ramp (it would have caught the R3-6 orphan banner). Scoped off proto/comp deliberately — that mirrors the external CoMP standard, which has its own vocabulary (e.g. a legitimate revshare) the RAMP-removal denylist must not police.
  • It now asserts positive facts: every DenialReason value defined in the proto must appear in the event-types table, and the registered ramp_* claims must appear in the auth spec. That's the value-level/semantic class the denylist structurally can't see.
  • The security docs are reconciled in one commit, as a unit.

I left the compile-the-samples half out again — fragments don't compile standalone and the maintenance cost stays high — but the positive-fact assertions close part of the gap it would have covered, and a final grep sweep caught the last stray Go field (offer.ExchangeSignature in request-flows.mdx) that wasn't in the denylist.

One design call landed in the same branch — revenue share

Not from your review, but adjacent to the CoMP boundary and the Pricing-comparability work, so flagging it: I considered adding a revenue-share pricing model for parity with CoMP's License.revshare, and decided against it. A PricingModel is a charging structure an Exchange can quote, sign, and compare at transaction time (FREE/PER_UNIT/FLAT, each with a unit_cost); a revenue share has no transaction-time price — the rate and settlement are an off-protocol agreement. Adding it would either be a rate-less label or would pull commercial terms into the signed, comparable Pricing.

So it's expressed with existing primitives instead: a FREE term gated by an agreement scope (revshare:publisher-x) + a reporting Obligation — same shape as a subscription; "pay per crawl or take the revshare deal" is just two LicenseTerms and the agent self-selects. CoMP's revshare rate rides through verbatim in the ramp-comp-v1 ext (comp.license[].revshare), not mapped onto a pricing model — parity by mapping, not duplication. Written up in design-history.md, licensing-terms.mdx, and ext-comp.mdx.

Green

buf lint, regen idempotent (zero-diff buf generate), go build/vet/test, website build (76 pages), and the extended doc gate.

@legendko legendko left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We did a great job and I think we are almost done.


Verdict

This is the cleanest round of the five. No new HIGH or CRITICAL. No new breaking changes (buf breaking identical to round 4; the +46 proto lines are non-breaking comments + max_items options). Builds green, buf generate reproduces zero-diff, the extended doc gate runs clean.

The remaining issues are 3 MEDIUM + 8 LOW, all the familiar pattern — the central fix is correct but a few peripheral docs lag. The most consequential, R5-1, is that the round's headline security step (mandatory holder-binding verification) is in authentication.mdx but missing from the ~5 walkthrough/component docs that depict the verify flow.

NEW issues

MEDIUM

  • R5-1 — Holder-binding verification step not propagated to the verify-flow depictions. [orchestrator-verified] The R4-1 fix is airtight in authentication.mdx, but the docs that show an Exchange verifying a delegation still teach chain-only verification with no holder-binding check: request-flows.mdx:104 (step "2c… verifies the Biscuit token chain against the principal's published key" — not touched this round), exchange/overview.mdx:125 ("verifies the delegation chain"), walkthrough-v1.mdx:363, walkthrough-academic.mdx:193/833. An implementer following these omits exactly the step that makes the anti-theft guarantee real — fail-open-by-omission (the normative spec even says omitting it "reduces the token to a bearer credential"). Fix (doc): add the holder-binding step (or a one-line pointer to authentication.mdx#verification step 3) wherever delegation verification is depicted.
  • R5-2 — Holder binding under-specified for the brokered multi-signature case. [security] With two RFC 9421 signatures (agent + broker) and the agent's Biscuit passing through, the spec doesn't state which signature the binding check uses; the hop-survival paragraph (authentication.mdx:220) describes a re-attenuation/re-seal model that sits in mild tension with the default pass-through and with broker/overview.mdx:108 ("never modifies delegation") vs :181 ("MAY attenuate"). Intent is inferable but not stated at the binding step. Fix (doc): state, at the binding step, which request signature must match the bound key in the brokered/pass-through vs re-attenuated cases.
  • R5-3 — R4-12 PARTIAL: two compile-broken CoMP stragglers. [orchestrator-found + consistency + security] req.Aisystem.Aisysuse.Uri (removed CoMP path; should be req.Uris/req.Msg.Uris) survives in broker/overview.mdx:75 and broker/selection-engine.mdx:42 — the prose/Go siblings in the same two files were fixed this round; these two were missed. Pre-existing, won't compile, the recurring half-applied miss. Tree-wide sweep confirms these are the only two. Fix (doc).

LOW

  • R5-4 — Mirror scope-rule drift. [orchestrator-verified] proto-ramp.mdx:714 still calls LicenseTerm.scopes "hierarchical (dist:* covers dist:US)" — the terse framing the proto comment dropped this round; "hierarchical" is the wording T-LIC-3 warns against (the example is correct under the new rule; the word isn't). The mirror states the full segment-wise rule nowhere. Fix (doc): replace "hierarchical" with the segment-wise rule or a pointer.
  • R5-5 — for-ai-agents.mdx:182 DELEGATION_INVALID description still narrower than the broadened normative tables (an R4-4 sibling the sweep missed). Fix (doc).
  • R5-6 — Revshare prose imprecision. The off-protocol-revshare note says "a reporting Obligation," but there is no reporting ObligationKind; reporting is the separate offer-level ReportingObligation message. Design is fine; the type name is imprecise. Fix (doc).
  • R5-7 — revshare: scope not added to the authentication.mdx scope-example table (parity with the documented subscription: example). Fix (doc).
  • R5-8 — Doc-gate ramp_* positive-fact assertion is a hardcoded 3-item list (ramp_max_spend_cents/ramp_max_accesses/ramp_quota_period), not self-extending like the DenialReason loop — a maintenance dependency that will silently miss a future registered claim. Fix (script): derive the list, or add a comment tying it to the registry.
  • R5-9 — Doc gate still can't catch the Aisystem class (R5-3). A wholesale Aisystem denylist would false-positive on legitimate CoMP JSON keys, but a narrow req\.Aisystem/\.Aisysuse\. Go-path pattern would catch both stragglers with zero false-positive risk. Fix (script, optional).
  • R5-10 — enterprise.mdx:184 frames caps as theft protection without mentioning holder binding; borderline (it's selling attenuation and references the "compromised key" residual case). Acceptable / optional.
  • R5-11 — Transaction-log offer_signature event field is a mild misnomer (it signs the chain hash, not the offer); non-security. Optional.

Meta-finding (five rounds)

The proto / generated code / build tooling have been correct and reproducible throughout, and this round the documentation has substantially caught up: the security-doc contradictions are reconciled as a unit, the doc gate now covers proto/ramp + positive facts (both round-4 recommendations), and the AI-review scratch is gitignored. The only persistent residue is peripheral-doc lag — the round's central fix is right, but a handful of walkthrough/component/mirror files still depict the older flow (R5-1/R5-3/R5-4/R5-5). Two structural gaps remain in the gate-and-docs process: (a) it still doesn't compile the doc code samples (why the Aisystem Go stragglers and the chain-only verify depictions slip through), and (b) the positive-fact list is partly hardcoded. The recommendation stands: compile the doc code-samples against gen/ — it is the one check that would mechanically catch R5-1's chain-only verify code and R5-3's removed-field references.

Open questions for the maintainer

  1. (R5-2) In the brokered two-signature case, which RFC 9421 signature must match the token's bound key — and is the default broker behavior pass-through or re-attenuation? Reconcile broker/overview.mdx:108 vs :181.
  2. (R5-3) Broker examples: req.Uris (bare message) vs req.Msg.Uris (Connect wrapper)? Either way Aisysuse.Uri is wrong.
  3. (R5-4) Should proto-ramp.mdx inline the scope rule or link to authentication.mdx#scope-matching?

Bottom line: the branch is in strong shape — wire contract sound and reproducible, all prior findings resolved, the security-doc unit genuinely reconciled, the revshare decision well-reasoned and documented, the doc gate hardened per recommendation. No blocker remains. Before declaring R4-1 airtight end-to-end, propagate the holder-binding step to the verify-flow docs (R5-1) and disambiguate the brokered case (R5-2); fix the two Aisystem stragglers (R5-3); the rest are LOW polish.

…n; Biscuit optional

Flip the delegation token model to a holder-bound JWT by default and rewrite the
delegation story across the spec to match. The property RAMP depends on — a
leaked token is not bearer-usable — is proof-of-possession, not anything specific
to Biscuit, and a chain of cnf-bound JWTs delivers it with one fewer new
technology for adopters.

Model:
- token_format defaults to "jwt". The grant is bound to a key via the RFC 7800
  cnf claim (cnf.jkt = RFC 7638 thumbprint); the holder proves possession with
  the RFC 9421 request signature (verifier checks thumbprint(request key) ==
  cnf.jkt).
- Delegation is a chain of cnf-linked JWTs (owner -> principal -> agent): each
  child is signed by the key its parent named in cnf and narrows scope; the
  chain-linkage invariant rejects any token signed by a key not named upstream.
  Verified offline under the issuer's key alone; intermediate keys ride in the
  JOSE header jwk.
- "biscuit-v3" stays a permitted OPTIONAL alternative for deep multi-hop offline
  attenuation; the Delegation message shape is unchanged.

Updated: proto comments (Delegation, token_format, LicenseTerm scope-gating) and
regenerated SDKs; authentication.mdx (full delegation section rewrite, JWT
libraries, examples); threat-model (T-DEL theft/escalation, scope-gating);
proto-ramp; licensing-terms; broker/exchange/architecture component docs; the
walkthroughs and getting-started guides; design-history and changelogs.

Pre-v1, no shipped consumers. Builds green: buf lint, regen idempotent,
go build/vet/test, website build, doc-conformance gate.
… fix stragglers, harden gate

- R5-1: add the holder-binding step (request key hashes to cnf.jkt) to the
  delegation verify-flow depictions that taught chain-only (exchange/overview,
  walkthrough-academic).
- R5-2: state which signature binds in a brokered request — default broker
  pass-through, the AGENT remains the holder and the agent's signature must match
  cnf.jkt; reconcile broker/overview so narrowing is an opt-in only when the
  agent delegated to the broker (broker becomes the terminal holder).
- R5-3: fix the two remaining removed-CoMP Go paths (req.Aisystem.Aisysuse.Uri
  -> req.Uris) in broker/overview and broker/selection-engine.
- R5-4: proto-ramp scopes — drop "hierarchical", point to the segment-wise rule.
- R5-5: broaden DELEGATION_INVALID description in for-ai-agents.
- R5-6: revshare prose uses ReportingObligation (the offer-level message), not a
  reporting ObligationKind.
- R5-7: add the revshare: scope to the authentication scope-example table.
- R5-8: make the doc-gate ramp_ claim check self-extending — derive the
  registered claims from the auth registry and assert each maps to a Delegation
  proto field, no hardcoded list.
- R5-9: add narrow req.Aisystem / .Aisysuse. Go-path patterns to the doc gate.

Builds green: doc-conformance gate, website build.
…t it surfaces

Adds a Go conformance suite (conformance/) that performs the value- and
semantic-level checks the removed-identifier denylist structurally cannot:

- TestProtovalidateConstraints evaluates the embedded protovalidate CEL against
  valid/invalid instances (uri_digest strong-hash, Pricing PER_UNIT/FREE,
  unit format, charset + max_items). Until now nothing in the toolchain ever
  ran the constraints, so a wrong CEL shipped green.
- Doc-example checks over website/src: every Pricing unit / consumed_unit is a
  registered token, signature_algorithm is "EdDSA", and every LicenseTerm
  example carries the required semantics discriminator. Wired into proto-ci via
  `go test ./...`.

The harness surfaced the review findings (and two the manual pass missed) which
are fixed here:
- signature_algorithm "ed25519" -> "EdDSA" across the walkthroughs.
- unregistered Pricing units: articles->items, reports->records,
  studies->records (matches the offer's unit), and seconds is now a registered
  unit (added to the Pricing.unit vocab — per-second is the right media basis).
- ext-news max_display_words (deleted AccessRestrictions field) -> a Quota
  {metric:"display-words"}.
- 22 LicenseTerm examples gained the semantics discriminator.
- ai_input/ai_train/ai_index -> dashed ai-input/ai-train/ai-index (the
  registered vocabulary; CoMP's uppercase AI_INPUT is unaffected).
- DELEGATION_INVALID description unified across the tables; dropped the residual
  "attenuation" Biscuit-ism.

Gate hardening: scope the ramp_<field> delegation-claim check to the Delegation
message body (was matching any field in the proto); denylist the deleted/renamed
identifiers above.
@KonstantinMirin

Copy link
Copy Markdown
Contributor Author

@legendko — ran a multi-agent internal review of the branch (consistency / DRY / layering / testing lenses) before handing back. The wire contract came out clean — buf generate zero-diff, proto↔mirror in sync, the Biscuit→JWT switch consistent. All findings were in the prose layer and the guards meant to police it, and they cluster into three patterns. The headline: I built the doc-sample validation harness you've recommended every round, used it to surface the findings (it caught more than the manual pass), then fixed them.

The harness (the pattern-level fix)

New conformance/ Go suite, wired into proto-ci via go test ./...:

  • TestProtovalidateConstraints — actually evaluates the embedded protovalidate CEL (uri_digest strong-hash, Pricing PER_UNIT/FREE, unit format, charset + max_items) against valid/invalid instances. Until now nothing in the toolchain ran the constraints, so a mis-anchored regex shipped green. This closes that gap.
  • Doc-example checks over website/src — every Pricing.unit/consumed_unit is a registered token, signature_algorithm is "EdDSA", every LicenseTerm example carries the semantics discriminator. These are value/semantic checks a removed-identifier denylist structurally cannot do.

Patterns it surfaced (and fixes)

PAT-01 — drift lives in the denylist's blind spot. Example payloads that would be rejected at ingest but pass the grep gate: signature_algorithm: "ed25519" (→ EdDSA, ~21 sites); unregistered units articles/reports/studies (→ items/records/records); ext-news max_display_words (a deleted AccessRestrictions field → a Quota{metric:"display-words"}); 22 LicenseTerm examples missing semantics. The harness now fails the build on all of these. (It also caught two the manual review missed — a stray seconds unit and a quota-dimension false-lead.)

PAT-02 — normative text copied, not single-sourced, already drifting. DELEGATION_INVALID was worded three ways across four tables, two still carrying the "attenuation does not check out" Biscuit-ism. Unified, Biscuit-ism dropped.

PAT-03 — new logic / guards unverified. The CEL constraints (now evaluated by the harness) and a self-check bug: the ramp_<field> delegation-claim gate grepped the whole proto, so ramp_offer_id would have falsely passed — scoped it to the Delegation message body.

One decision worth flagging

seconds wasn't in the Pricing.unit vocabulary (only minutes), which is why the audio example tripped the harness. Rather than coerce the doc, I registered seconds — per-second is the correct fine-grained media metering basis. And per your earlier deferral (R3-8, pre-v1, no consumers), I did not add reserved guards — this is still evolution of the first proto version.

All green: buf lint, regen idempotent, go build/vet/test (incl. the new conformance suite), doc gate, website build (76 pages).

…d8y64, fc65j)

Move cross-field presence rules into protovalidate CEL on the canonical proto so
the shared SDK enforces them in every language (Go/TS/Python), not only in the
Go Exchange's hand-rolled validator:

- License: uri_digest is required whenever uri is set — any semantics. An
  undigested uri can be swapped after the offer is signed. (d8y64)
- LicenseTerm: REFERENCE_ONLY terms must carry a license with a non-empty uri.
  (fc65j)
- LicenseTerm: pricing is required on every term — (buf.validate.field).required
  on pricing. (fc65j)

Conformance suite gains valid/invalid cases for all three; regenerated SDKs.

Follow-up (agentic-content-access repo, separate): bump the proto go.mod pin,
regenerate TS/Python, and slim internal/licenseterm.Validate to drop the now-
duplicated presence checks (keep registry membership, canonicalization, and
lint-warnings, which CEL cannot express).
@KonstantinMirin

Copy link
Copy Markdown
Contributor Author

@legendko — consolidating what's landed since your round-5 review, because one change is big enough that your prior mental model needs updating.

The headline: the delegation token is now a JWT, not a Biscuit

You reviewed five rounds against a Biscuit-default model; we've since flipped it. The delegation token defaults to a holder-bound JWT (token_format: "jwt"); Biscuit ("biscuit-v3") stays as an optional alternative for deep multi-hop attenuation.

Why: the property we actually depend on — a leaked token isn't bearer-usable — is proof-of-possession, not anything Biscuit-specific. A chain of cnf-bound JWTs delivers it identically: the grant is bound to a key via RFC 7800 cnf.jkt (the RFC 7638 thumbprint), and the holder proves possession with its RFC 9421 request signature. Delegation is a chain of cnf-linked JWTs (owner → principal → agent), each child signed by the key its parent named, verified offline under the issuer's key alone. What Biscuit adds beyond this — in-token Datalog, deep in-place attenuation — RAMP doesn't use. The win is adoption: JWT is ubiquitous, so we ask implementers to take on no genuinely new token tech by default. Breaking, but pre-v1 with no shipped consumers. authentication.mdx has the full model; design-history.md records the decision.

(One consequence worth flagging since you've tracked the signature surface: there are now two algorithm names for the one Ed25519 key — ed25519 for the RFC 9421 request signature, EdDSA for the JOSE layer (offer JWS, delegation JWT, JWK). That's mandated by the two registries, not a choice. The holder binding spans them because cnf.jkt is computed over key material only — it excludes alg — so the same key has one thumbprint regardless of envelope.)

Round 5 (R5-1 … R5-11): resolved

Holder-binding step propagated to every verify-flow doc (R5-1); brokered case disambiguated — default pass-through, the agent's signature binds (R5-2); the two Aisystem Go stragglers fixed (R5-3); hierarchical→segment-wise (R5-4); DELEGATION_INVALID unified (R5-5, and the residual "attenuation" Biscuit-ism is gone); revshare prose → ReportingObligation (R5-6); revshare: scope added to the table (R5-7); the doc gate's ramp_ check scoped to the Delegation body and the Aisystem Go-path patterns added (R5-8/R5-9).

Your standing recommendation, built: a doc-sample validation harness

The thing you've asked for every round — compile/validate the doc samples against the contract — now exists as a Go conformance suite in CI (conformance/, run via go test ./...):

  • It evaluates the protovalidate CEL constraints against valid/invalid instances (until now nothing in the toolchain ran them — a wrong CEL shipped green).
  • It validates the doc example payloads: every Pricing.unit is registered, signature_algorithm is correct, every LicenseTerm example carries semantics.

It earned its keep immediately — caught a batch of example/vocab drift the denylist gate structurally can't see (unregistered units, missing semantics discriminators, a deleted AccessRestrictions field in an example), all fixed.

New: presence invariants moved into protovalidate CEL

Cross-field presence rules that were hand-rolled in the Go validator are now CEL on the proto, so the shared SDK enforces them in Go/TS/Python, not only at the Go Exchange:

  • License: uri_digest required whenever uri is set.
  • LicenseTerm: REFERENCE_ONLY must carry a license.uri; pricing required on every term.

All green: buf lint, regen idempotent, go build/vet/test (incl. the conformance suite), doc-conformance gate, website build.

Port the remaining CEL-expressible coherence rules from the Go licenseterm
validator into protovalidate CEL on the canonical proto, so the shared SDK
enforces them in Go/TS/Python:

- Restriction: permitted and prohibited must be disjoint (a token cannot be
  both permitted and prohibited on the same axis).
- LicenseTerm: at most one Restriction per kind (same-kind restrictions are
  AND-combined, so duplicates are an authoring error).
- Quota.limit >= 1 (a zero quota grants nothing).
- Obligation: SHARE_ALIKE requires scope_license.

Conformance suite gains valid/invalid cases for all four; regenerated SDKs.

Follow-up (agentic-content-access): bump the proto go.mod pin, regenerate, and
remove these now-duplicated checks from internal/licenseterm.Validate, leaving
only vocab membership/warnings, canonicalization, and business rules.
…_digest)

A SHARE_ALIKE scope_license can be a URI, and a referenced-license URI needs the
same swap-protection digest as any other license reference. Rather than guess
URI-vs-SPDX from a bare string, model scope_license as a `License`: the SPDX
short-id goes in `id`, the URI in `uri`, and the existing d8y64 rule
(uri present ⇒ uri_digest present) applies to it automatically — a scope_license
URI without a digest is now rejected.

- Obligation.scope_license: string → License.
- SHARE_ALIKE CEL: requires scope_license to identify a license (id or uri).
- Conformance: SPDX-id ok, uri+digest ok, uri-without-digest rejected, absent
  rejected. Docs updated (scope_license shown as { id: ... }).

Breaking (field type), pre-v1, no shipped consumers.
@KonstantinMirin

Copy link
Copy Markdown
Contributor Author

@legendko — update since the last note: the license-term coherence rules are now in protovalidate CEL as well, plus one refinement and an accuracy correction on the SDK-enforcement claim I made earlier.

Coherence rules → CEL (d332353)

Moved the remaining CEL-expressible license-term invariants out of hand-rolled Go and into the proto:

  • Restriction: permittedprohibited must be disjoint.
  • LicenseTerm: at most one Restriction per kind (same-kind restrictions AND-combine, so duplicates are an authoring error).
  • Quota.limit ≥ 1 (a zero quota grants nothing).
  • Obligation: SHARE_ALIKE requires scope_license.

scope_license refinement (3b0d9e8)

Obligation.scope_license changed from stringLicense. The old string was "SPDX id or URI," which a CEL can't cleanly tell apart to know when to demand a digest. As a License, the SPDX id goes in id, the URI in uri, and the existing uri ⇒ uri_digest rule applies automatically — a scope_license that references a URI without a digest is now rejected, same swap-protection as any other license reference. No heuristic, no second digest field.

Each rule ships with valid/invalid conformance cases (the suite proves they fire). Green throughout: buf lint, regen idempotent, go build/vet/test, doc gate, website.

Correction: what "SDK-enforced in Go/TS/Python" actually means today

I want to be precise rather than rosy. The constraints are defined once in the proto and embedded in every generated descriptor, so they're language-agnostic — but only one SDK actually ships generated and validation-tested right now:

Language Generated Validation wired + tested
Go ✅ (messages + Connect + vocab) protovalidate-go, run over valid/invalid instances in conformance/
TypeScript ✅ (protobuf-es) ⚠️ constraints carried in the descriptor, but no protovalidate-es wired/tested here
Python ❌ not generated

So "enforced everywhere" is true at the rule-definition level (any protovalidate runtime would apply them), but generated-and-proven only for Go today. Closing that — generating a Python SDK and wiring a TS validation harness that mirrors the Go conformance suite — is a tracked follow-up, not done here.

@legendko legendko left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict

The substance of this round is excellent; the headline flip left a HIGH-severity straggler cluster in the docs.

Three significant moves all landed architecturally sound and wire-clean: (1) the JWT holder-of-key default (Biscuit→JWT) is cryptographically sound — cnf.jkt binds the grant to the request-signing key, the cnf-chain reproduces attenuation/hop-survival/leaked-token-useless, no security property is lost, and it resolves the standing R4-5/R5-2 JWT/brokered-binding findings; (2) Obligation.scope_license string→License is a clean composition (SHARE_ALIKE target now inherits uri⇒uri_digest tamper-evidence — and the author's rationale is sound: a single string couldn't tell an SPDX id from a URI, so a CEL couldn't know when to demand a digest; as a License, the existing uri ⇒ uri_digest rule applies automatically); (3) the 5-round-old "prose-only invariants" critique is finally addressed — d8y64/fc65j/6z1v3 move presence + coherence rules into protovalidate CEL. Those constraints are defined once in the proto and embedded in every descriptor, so they are enforceable by any protovalidate runtime — but, per the author's own correction, they are generated-and-validation-tested only in Go today (TS is generated but has no protovalidate-es wired/tested; Python is not generated — a tracked follow-up). And the author built the doc-sample validation harness recommended every roundconformance/ genuinely evaluates the CEL (30+ valid/invalid cases) and validates doc examples; it is real, sound, CI-wired, test-scoped (no consumer dep), and it caught real drift. All R5 findings (R5-1…R5-11) are resolved.

But the Biscuit→JWT flip + the new two-algorithm-name distinction reintroduced 2 HIGH and several MEDIUM issues — all in the docs / proto-comments / enforcement-story layer, none in the wire/gen. The failures cluster precisely in the blind spots of the (otherwise good) automated guards: the RFC 9421 alg= value (the harness only checks the signature_algorithm field), PascalCase Go (SignatureAlgorithm = "…", a regex gap), proto comments, prose Biscuit-isms, the gate-excluded design-history.md, and unlabeled pseudocode fences.

NEW issues

HIGH

  • R6-1 — Algorithm-name swaps in a security primitive (both directions), incl. the canonical proto comment. [orchestrator-verified] The two-name convention (per the author: ed25519 = RFC 9421 request sig; EdDSA = JOSE / Offer JWS / delegation JWT / JWK) is violated in ~5 places:
    • RFC 9421 request-sig contexts wrongly using alg=EdDSA: proto/ramp/v1/ramp.proto:1419 (which contradicts ramp.proto:51 "Agent→Exchange: Signature header (alg=ed25519)" in the same file), broker/overview.mdx:106, for-exchange-operators.mdx:144.
    • JOSE/Offer-JWS contexts wrongly using ed25519: request-flows.mdx:194 (SignatureAlgorithm = "ed25519"), broker/selection-engine.mdx:436 (alg := "ed25519" // default for offer signatures).
      PAT-01's blanket ed25519→EdDSA sweep mismatched the distinction in both directions. An operator following for-exchange-operators.mdx:144 would verify RFC 9421 request signatures with alg=EdDSA and reject valid ed25519 requests at the auth boundary. Severity HIGH (the operator-step instance is arguably CRITICAL). Fix (doc + proto comment): ed25519 for RFC 9421, EdDSA for JOSE, per ramp.proto:51.
  • R6-2 — Phantom Biscuit URI-pattern delegation narrowing, claimed "cryptographically enforced." [security] enterprise.mdx:178/182 (+:188 "cryptographically enforced"), proto-ramp.mdx:132, and for-exchange-operators.mdx:157 ("evaluate all check conditions across all blocks") carry over Biscuit's per-resource/URI confinement, but the default JWT model has no wire field and no registered claim for URI/resource restriction — only scopes/caps/expiry. An operator who believes URI-confinement is enforced gets a fail-open gap. Severity HIGH. Open question 1 gates the fix: was URI-confinement intended to survive the flip (→ add a registered claim/field) or not (→ docs overclaim; remove/rewrite).

MEDIUM

  • R6-3 — Required-enum discriminators are NOT wire-enforced. [orchestrator-verified] The proto has zero enum constraints (grep -c "(buf.validate.field).enum" = 0; the only enum references in CEL are the conditional rules like PER_UNIT⇒unit, not an UNSPECIFIED-rejection), so protovalidate.Validate()even the Go runtime — accepts semantics: UNSPECIFIED (and kind/model/trigger/axis = 0). Only the Go Exchange application code rejects it, not the shared SDK constraint, despite every "// unset — rejected at ingest" comment. conformance/docexamples_test.go:125 still asserts UNSPECIFIED "would not validate," which is inaccurate at the protovalidate layer, and there is no validate_test.go case proving rejection (the harness gives false confidence here). Note: the author's "UPDATE 18-06-morning" correction proactively walked back the broader "enforced in Go/TS/Python" overstatement — verified accurate (gen/ is go+ts only, no Python; no protovalidate-es wired in TS) — which converts the cross-SDK half of this finding into an acknowledged, tracked limitation. The enum-has-no-CEL gap itself remains and is not addressed. Fix: add a this.<field> != …_UNSPECIFIED CEL per discriminator + conformance cases, and correct the test comment.
  • R6-4 — design-history.md self-contradiction on the headline decision. [orchestrator-verified] §195 "## Biscuit v3; JWT verification deferred" asserts present-tense "Biscuit v3 is the [v1] format" / "JWT … deferred past v1," left in place beside the new §254 "## JWT … is the default; Biscuit is optional," with no supersede marker. Severity MEDIUM (re-graded down from the consistency/architecture agents' HIGH: design-history.md is explicitly a non-normative history doc and the normative spec — authentication.mdx + proto — is correctly JWT-default, so it misleads readers but won't drive a wrong implementation). Fix: mark §195 superseded by §254 (or rewrite past-tense).
  • R6-5 — Mirror self-contradiction on pricing-required. [orchestrator-verified] proto-ramp.mdx:655-657 says pricing "MUST be present" only "when semantics = ENUMERATED," while :713 says "REQUIRED on every term regardless of semantics." The fc65j CEL enforces every-term (conformance-tested). Fix (doc): drop the ENUMERATED-only wording.
  • R6-6 — Biscuit-isms remain in default-JWT canonical sources. ramp.proto:1784 DELEGATION_INVALID still says "…or its attenuation does not check out" (the phrase PAT-02/R5-5 claimed fully removed — missed in the source the mirror derives from); threat-model.mdx:220 "sealed/attenuated to the holder's key" (default JWT is bound via cnf.jkt, not sealed); for-exchange-operators.mdx:157 "across all blocks." Fix (proto comment + docs).
  • R6-7 — Conformance/doc-gate coverage gaps (the harness is good but these let R6-1 through green). (a) TestDocSignatureAlgorithm keys only on the signature_algorithm field — it cannot see the RFC 9421 alg= value (R6-1's EdDSA-in-RFC9421) and its regex misses the PascalCase Go form SignatureAlgorithm = "…" (R6-1's ed25519-in-JOSE at request-flows.mdx:194). (b) The doc-example checks only parse ```json-fenced double-quoted JSON, so walkthrough pseudocode/JS-style fences escape all three checks — confirmed live at scenario-walkthrough.mdx:144-150 (a terms:[…] with no pricing and no semantics, a double-miss). (c) The quota.metric.format and restriction.prohibited.format CELs are never exercised by the suite. Fix (test): check RFC 9421 alg= context-aware, broaden example extraction beyond json fences, add the two missing CEL cases.

LOW

  • R6-8 stale "Token Attenuation" H2 header over JWT body (broker/overview.mdx:173).
  • R6-9 website changelog under-records the scope_license type change + the conformance/CEL additions.
  • R6-10 threat-model.mdx T-DEL-1 leads with Biscuit "sealed/attenuated" phrasing before the correct cnf description.
  • R6-11 consumed_unit registry membership is doc-test-only (no wire CEL) — a consistency nit vs Pricing.unit.
  • R6-12 the self-deriving ramp_* doc-gate check interpolates ${field} unescaped into a regex — harmless for today's [a-z_] claim names; latent.
  • R6-13 "hierarchical scope" format wording lingers near the "no implicit hierarchy" matching rule (terminology).

Standing invariants (all intact)

comp.proto not imported by core; dispute chain (Offer→Transaction→UsageReport→UsageReportResponse→DisputeRequest) unbroken; closed licensing core uniform; protovalidate runtime is test-only (no new dep on gen/go consumers); buf.lock/go.mod pin the same protovalidate commit (no version skew); R3-8 (reserved) deferral unchanged and still acceptable (no consumers; the scope_license field-3 string→message reuse is the same sharp-edge class but inside the branch-new Obligation).

What the orchestrator verified directly

buf lint/build=0; go build/vet/test=ok incl. conformance pass; clean buf generate=zero diff; doc gate clean; buf breaking=49 (unchanged). Read validate_test.go (genuine, 30+ valid/invalid CEL cases). Confirmed: the algorithm-name swaps (proto:1419 vs proto:51; the doc sites), (buf.validate.field).enum count = 0 (R6-3), design-history.md §195/§254 contradiction (R6-4), proto-ramp.mdx :655/:713 pricing contradiction (R6-5), proto:1784 attenuation Biscuit-ism (R6-6), JWT-default + cnf model in proto, scope_license as License (1077), seconds registered. Verified the author's SDK-coverage correction: gen/ contains only go + ts (no Python); no protovalidate-es dependency in gen/ts/package.json (only a descriptive comment) — so the CEL is wired-and-tested in Go only today, as the author now states.

Open questions for the maintainer

  1. (R6-2 — gates the fix) Was per-URI/resource delegation confinement intended to survive the Biscuit→JWT flip? If yes, it needs a registered claim/wire field (it has neither today); if no, enterprise.mdx/for-exchange-operators.mdx overclaim "cryptographically enforced" and must be rewritten.
  2. (R6-3 — partially answered) The author's UPDATE note clarified the cross-SDK reality (Go-only wired/tested; TS/Python a tracked follow-up), which settles the SDK-coverage half. Still open: was leaving the required-enum discriminators with no protovalidate CEL at all deliberate (so even Go's runtime accepts UNSPECIFIED and only the app rejects it), while the other presence invariants are CEL? If not, add the per-enum CEL.
  3. (R6-7) Is the harness intended to validate only json-fenced wire examples (treating walkthrough pseudocode as illustrative), or should it cover those too?

Bottom line: the protocol is in strong shape — the wire contract is clean and reproducible, the JWT flip is sound and loses nothing, the deepest 5-round critique (prose-only invariants) is finally on the wire (enforceable by any protovalidate runtime; wired+tested in Go today, with TS/Python a documented follow-up), and the recommended validation harness now exists and works. The blockers before merge are R6-1 (algorithm-name swaps in a security primitive, incl. the proto comment) and R6-2 (phantom "cryptographically enforced" URI-confinement) — both doc/comment-level but security-relevant; then the MEDIUMs (R6-3 enum-CEL gap, R6-4 design-history supersede, R6-5 mirror, R6-6 Biscuit-isms, R6-7 harness gaps). None touches the wire format itself.

Add protovalidate CEL so the required discriminator enums are rejected at
the wire/validation layer rather than only by application ingest:
LicenseTerm.semantics, Pricing.model, Restriction.kind, Obligation.kind
must not be *_UNSPECIFIED. PricingMetering.ONLINE=0 stays the deliberate
safe-default exception.

Add a format CEL to UsageReport.consumed_unit mirroring Pricing.unit
(bare registered token or vendor:namespaced), so the metering unit is
structurally validated on the wire instead of doc-test only.

Regenerate Go + TS SDKs. Extend the conformance suite with the
UNSPECIFIED-rejection cases and the previously-unexercised
quota.metric.format and restriction.prohibited.format rules.
Strengthen the doc-conformance guards to catch the value-level and
pseudocode-fence drift the prior denylist could not:

- Signature-algorithm check is now context-aware. It enforces the
  two-name convention (ed25519 = RFC 9421 request signatures; EdDSA =
  JOSE/JWS for the Offer signature, delegation JWTs, JWKs) across the
  signature_algorithm JSON field, the PascalCase Go SignatureAlgorithm
  form, and bare alg=… tokens classified by same-line context. Ambiguous
  lines are skipped, not guessed (57 sites checked, was a handful).

- LicenseTerm semantics check now scans all code fences (not just
  ```json) and matches quoted-or-unquoted keys, so term shapes in
  walkthrough pseudocode are covered (551 fences scanned).

Harden the delegation-claim doc-gate: guard the derived field name
against unexpected characters before interpolating it into the match
pattern.
…lt precision

- Fix algorithm-name swaps: ed25519 for RFC 9421 request signatures, EdDSA
  for JOSE/JWS (offer signatures). Touches broker/overview,
  broker/selection-engine, exchange/request-flows, for-exchange-operators.
- Qualify the "cryptographically enforced" URI-confinement claim: the
  default JWT model has no URI/resource claim — URI gating is via scopes;
  per-URI confinement is a biscuit-v3 / vendor-claim capability, not a
  wire guarantee. (enterprise, for-exchange-operators)
- Remove Biscuit-isms from JWT-default sources: holder binding is via
  cnf.jkt (threat-model T-DEL-1); drop "attenuation across blocks".
- Resolve proto-ramp pricing-required self-contradiction: pricing is
  required on every term regardless of semantics; rewrite the
  validation-rules block to cite the enforcing CEL ids.
- Fix walkthrough term examples missing semantics/pricing.
- design-history: mark the superseded "Biscuit v3 default" section.
- changelog: record Obligation.scope_license as a License and the
  wire-enforced CEL + conformance suite.
- Replace lingering "hierarchical scope" wording with the segment-wise rule.
@KonstantinMirin

Copy link
Copy Markdown
Contributor Author

Thanks Yaroslav — round 6 was a good catch on the algorithm-name distinction; the Biscuit→JWT flip left more residue than the diff suggested. All of R6 is addressed on feature/license-terms (e961ec4, 498514a, b5302f5). The theme this round, fittingly, is that two of your "blockers" turned out to be doc/comment-layer — the wire stayed clean throughout.

Your two open questions

  1. (R6-2) Did per-URI confinement survive the JWT flip? No — and it never lived at the wire layer. Delegation.token is opaque bytes and token_format is a free string; the protocol mandates neither JWT nor Biscuit and never inspects token internals. So the "default JWT" is a spec recommendation, not a wire change, and there's no registered claim for URI/resource confinement (only scopes/caps/expiry). The fix is therefore prose-only: I qualified the "cryptographically enforced" language in enterprise.mdx and for-exchange-operators.mdx so URI confinement reads as a biscuit-v3/vendor-claim capability, not a default-JWT guarantee. URI access in the default model is gated through scopes.
  2. (R6-3) Was leaving the required-enum discriminators with no CEL deliberate? No — overlooked. The conditional coherence CELs (per_unit⇒unit, reference_only⇒uri, share_alike⇒scope_license) are all vacuously satisfied when the discriminator is UNSPECIFIED, so the zero value sailed through. Fixed.

HIGH

  • R6-1 — algorithm-name swapsFixed. ed25519 for RFC 9421 request signatures, EdDSA for JOSE/JWS. Corrected the proto comment (ramp.proto:1419, now consistent with :51) and the four doc sites in both directions. The doc gate now catches this class (see R6-7).
  • R6-2 — phantom "cryptographically enforced" URI confinementFixed (doc). See open question 1.

MEDIUM

  • R6-3 — required-enum discriminators not wire-enforcedFixed. Added message-level CEL rejecting *_UNSPECIFIED on LicenseTerm.semantics, Pricing.model, Restriction.kind, Obligation.kind, with conformance cases. PricingMetering.ONLINE=0 stays the deliberate safe-default exception. Corrected the inaccurate docexamples_test.go comment — rejection now genuinely happens at the protovalidate layer (Go today; TS generated/not-yet-wired and Python remain the tracked follow-up).
  • R6-4 — design-history self-contradictionFixed. Marked §"Biscuit v3; JWT verification deferred" as superseded by the JWT-default section.
  • R6-5 — mirror pricing-required contradictionFixed. Unified to "required on every term regardless of semantics" and rewrote the validation-rules block to cite the enforcing CEL ids (fc65j/d8y64/6z1v3/semantics_specified).
  • R6-6 — Biscuit-isms in JWT-default sourcesFixed. ramp.proto:1784 denial-reason, threat-model.mdx T-DEL-1 (now leads with cnf.jkt), and the operator doc's "across all blocks".
  • R6-7 — conformance/doc-gate gapsFixed (all three). (a) The signature-algorithm check is now context-aware — it enforces the two-name convention across the JSON field, the PascalCase Go form, and bare alg=… tokens classified RFC 9421 vs JOSE by context (57 sites, was a handful). (b) The semantics check scans all code fences with quoted-or-unquoted keys, so walkthrough pseudocode is covered (551 fences) — it surfaced three drifted term examples, now fixed. (c) Added the missing quota.metric.format and restriction.prohibited.format cases.

LOW

  • R6-8 header → "Token Narrowing". R6-9 changelog now records Obligation.scope_license as a License + the CEL/conformance work. R6-10 resolved by the T-DEL-1 rewrite. R6-11 consumed_unit now carries a format CEL mirroring Pricing.unit. R6-12 doc-gate guards the derived field name before interpolation. R6-13 "hierarchical" wording replaced with the segment-wise rule.

Still deferred (your standing R3-8 note)

No reserved statements — pre-v1, zero shipped consumers, clean-cut breaks are acceptable. Keeping this as the standing call.

Build stays green: buf lint/generate (idempotent, zero gen drift), go build/vet/test ./..., doc-conformance gate all clean.

@legendko legendko left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR can be approved, the following gaps are non-blocking, just nice-to-fix.
Tell me whether you want to address them or leave.

Residual findings (all NON-BLOCKING)

MEDIUM

  • R7-MED-1 — R6-3 is incomplete: Quota.window and Obligation.trigger still accept UNSPECIFIED. [orchestrator-verified + both agents] The != …_UNSPECIFIED rejection CEL was added to four discriminators but not to Quota.window or Obligation.trigger, both of which carry the identical // unset — rejected at ingest contract (proto:836, :856). Proof it is real, not theoretical: the passing conformance case {"quota limit ok", Quota{Metric:"accesses", Limit:1}, true} (validate_test.go:87) has window unset → protovalidate provably accepts a quota with an ambiguous accumulation period; same for an obligation with an ambiguous firing trigger. Why non-blocking: the four highest-stakes discriminators (which select enforcement semantics and pricing structure) are wire-enforced, and a malformed window/trigger is still rejected by the Go Exchange at ingest — it is only the shared protovalidate constraint that is missing, and these are coherence fields, not a trust boundary. Fix: two this.<field> != …_UNSPECIFIED message CELs + two conformance cases (≈4 lines). AcceptableRestriction.axis (advisory request input) and PricingMetering.ONLINE=0 (real safe default) are correctly left alone.

LOW

  • R7-LOW-1 — Mirror not updated for consumed_unit. [consistency] e961ec4 added the format/length CEL to Usage.consumed_unit in the proto, but proto-ramp.mdx:353 still says only "defaults to tokens," while the sibling Pricing.unit/Quota.metric rows document their format in the same mirror. CLAUDE.md mandates the mirror track the proto. Fix: add the format/length constraint to that row.
  • R7-LOW-2 — Changelog over-states the enum fix. "required discriminator enums reject UNSPECIFIED" reads as complete, but two (window/trigger) are not yet enforced (R7-MED-1). Fix: name the four, or complete R7-MED-1 first.
  • R7-LOW-3 — Mixed citation style (cosmetic). The mirror's validation block says "cite the enforcing CEL ids," but three of four citations (fc65j/d8y64/6z1v3) are requirement tracking-codes (cross-referenced in proto comments + conformance tests), not CEL id:s. They resolve correctly; only the style is mixed.

(standards-layering.mdx:36 "Ed25519 (EdDSA)" was assessed and is benign — a crypto-stack key-type mention, not an alg= assignment.)

Round 6 added the !=UNSPECIFIED CEL to four discriminators; the round-7
re-review found two more left out (Quota.window, Obligation.trigger),
both marked "rejected at ingest" — the enumeration was done by hand,
twice, and was incomplete both times.

Add the two missing message CELs (quota.window_specified,
obligation.trigger_specified) and, more importantly, stop enumerating by
hand: TestRequiredEnumDiscriminatorsRejectZero reads the proto, collects
every enum whose zero value is marked "unset — rejected at ingest", walks
the descriptors, and fails unless each field of such an enum is either
covered by a zero-rejection rule or explicitly allow-listed with a reason.
A new discriminator now fails the build until enforced — the set can no
longer drift.

The guard immediately surfaced four advisory/diagnostic fields the manual
reviews never flagged (OfferGroup.restriction_filters, the two
restriction_mismatches, WellKnownManifest.pricing_models_supported); these
are Exchange-produced output / capability advertisements, not enforced
term discriminators, so they are allow-listed with reasons (same category
as AcceptableRestriction.axis). Conformance fixtures set window/trigger so
each case fails only for its named reason; added explicit window/trigger
UNSPECIFIED-rejection cases.
…tor set

- proto-ramp: document consumed_unit's wire-enforced token format (was only
  "defaults to tokens"); clarify that the validation-block parenthetical
  codes are requirement tracking-codes or CEL rule ids.
- changelog: name the six required discriminators (was the vaguer "required
  discriminator enums") and note the derive-from-proto guard.
@KonstantinMirin

Copy link
Copy Markdown
Contributor Author

Addressed — all four, plus a process fix so this class can't recur. On feature/license-terms (90c73f0, d499101).

The real question your R7-MED-1 raised

How did R6 and R7 both miss Quota.window/Obligation.trigger when they carry the identical // unset — rejected at ingest contract? Because coverage was being established by hand-enumerating the discriminators — I did it in R6 (missed two), and the natural next step was to hand-add two more. A third hand-count is not a fix.

So instead of point-fixing, I made the enumeration mechanical. New guard TestRequiredEnumDiscriminatorsRejectZero:

  1. reads the proto and collects every enum whose zero value is marked rejected at ingest,
  2. walks the descriptors and asserts each field of such an enum has a zero-rejection rule (message/field CEL referencing the field + UNSPECIFIED, or enum not_in:[0]),
  3. the only escape is an explicit zeroValueAllowed entry with a documented reason.

A newly added discriminator now fails CI until it's enforced or consciously allow-listed — the set can't drift out of sync with the contract comment again.

It immediately caught four more that both reviews missed

OfferGroup.restriction_filters, TransactionResponse.restriction_mismatches, TransactionResultItem.restriction_mismatches, WellKnownManifest.pricing_models_supported. All are advisory diagnostics / capability advertisements (Exchange-produced output, not enforced term discriminators) — the same category as AcceptableRestriction.axis that you flagged as correctly left alone — so they're allow-listed with reasons rather than CEL'd. (Verified the guard fails when a CEL is removed, so it has teeth.)

The four findings

  • R7-MED-1Fixed. Added quota.window_specified + obligation.trigger_specified CELs. Your example fixture {"quota limit ok", …} (and the obligation fixtures) now set window/trigger so each case fails only for its named reason; added explicit window/trigger UNSPECIFIED-rejection cases.
  • R7-LOW-1Fixed. The consumed_unit mirror row now documents the wire-enforced token format (matching the Pricing.unit/Quota.metric rows).
  • R7-LOW-2Fixed. Changelog now names the six required discriminators and the derive-from-proto guard, instead of the blanket "required discriminator enums."
  • R7-LOW-3Fixed. The mirror's validation block now states the parenthetical codes are requirement tracking-codes (fc65j/d8y64/6z1v3) or CEL rule ids (license_term.semantics_specified).

Also confirmed your standards-layering.mdx:36 "Ed25519 (EdDSA)" read — benign, left as-is.

Green: buf lint/generate (idempotent, zero drift), go build/vet/test incl. the new guard, doc-conformance. buf breaking = the expected pre-v1 set.

One command runs the full CI gating sequence locally (buf lint, buf
generate + drift check vs HEAD, go build/vet/test, doc-conformance), so
running only a subset can't silently pass a check CI will fail. buf
breaking is included as informational/non-blocking, matching the
workflow. Non-destructive: does not touch the git index.

(cherry picked from commit e1ebc84)

@legendko legendko left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Universal Licensing Core — LicenseTerm, restrictions, quotas, and obligations

2 participants