Add MCP payment classification contract (full re-derivation foundation)#16
Merged
Merged
Conversation
Foundation for offline re-derivation of a payment identity minted from a
classified tool call. Under full re-derivation, the verifier trusts nothing the
issuer asserts about the classification — it re-derives the whole chain from
signed facts plus these versioned, hash-pinned artifacts:
(connector_identity, canonical_tool_name) [signed, server-owned]
-> value_movement [value_movement_classification.v1]
-> payment.execute [action_classification_derivation.v1]
-> Permit-to-Pay [existing presentation registry]
value_movement_classification.v1 is the 31 (connector, tool) pairs enforcement
already classifies value-movement, GENERATED from the enforcement map rather
than hand-transcribed, so it byte-matches by construction. A parity guard in
keel-api will hold it identical thereafter, so a single evaluation backs both
enforcement and verification — not two lookups that could diverge under
normalization.
action_classification_derivation.v1 records the verifier obligations explicitly:
the classification tuple must appear in the pinned artifact at the referenced
version+digest (offline, no trust in the issuer's result); control.mode must be
enforced_in_path (enforcement qualifies issuance, it does not define the action);
invocation.operation stays call.tools while authorized_action is payment.execute
as two distinct signed facts, never one field impersonating both; transport and
target surfaces stay separate; no provider outcome is implied.
Both registered in the manifest with hash pins; the pin bites on tamper by name.
Only the value-movement slice moves, not the full tool taxonomy — the verifier
adjudicates the authorization chain, not every tool's risk class.
Non-authorizing throughout: a wrong derivation degrades a title, never escalates
authorization.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…est-vector corpus Contract-shape review (2nd opinion) found the first cut named fields without binding them to conventions the system already establishes, and shaped the derivation as a single special-case rule. Revised so two independent verifiers cannot honestly diverge. Bound to existing conventions rather than reinvented: - input_digest canonicalization -> keel.canonical_json.payload.v1 (the system's one profile), over a logical object whose own profile string is a field, so two different structures cannot collide. - digests -> sha256: hex, matching every other artifact (not base64url). - historical resolution -> the keel-verifier v3.7.0 version-aware model: resolve each dependency by the permit's embedded version+digest, never substitute a newer artifact; unavailable -> unverifiable, not fallback. - identifiers -> a named ASCII grammar profile; "no normalization" becomes "normalized by the issuer's authority at issuance, compared byte-for-byte". Reshaped and tightened: - derivation is now a ruleset with match_policy exactly_one (never first-match); a second Permit-to-X is a new rule entry, not a schema change. - classification.subject is a tagged union; registered_tool has required/forbidden fields; an unknown kind is no_derivation (no title), not invalid (rejection). - a closed predicate vocabulary (membership, registry-digest, input-digest); no expression language; an unknown predicate is unverifiable. - explicit outcome taxonomy: valid / no_derivation / invalid / unverifiable. - classification result is fixed in the artifact body, never inferred from the filename; entries unique + sorted. Adds the normative contract: a 10-vector test corpus covering all four outcomes (valid, observed-not-enforced, absent-tool, unknown-kind, wrong-input-digest, wrong-registry-digest, malformed identifier, whitespace, artifact-unavailable). The corpus carries REAL digests computed from the canonicalization profile and is self-consistent: its registry digest matches the artifact, and every positive vector is a provable registry member with a correct input_digest. That self-check caught a hand-picked positive (payments.purchase) that is NOT value-movement — exactly the drift the corpus exists to stop. Parity with keel-api enforcement still holds byte-for-byte after the rewrite. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tion, tighten resolution Second contract-shape review found real defects; all fixes are bounded, no reshape. Blocking, fixed: - operation namespace collision: the rule derived both authorized_action AND operation=payment.execute while an invariant said invocation.operation stays call.tools. Removed operation from `derives`; the rule emits ONLY authorized_action, fed to the selector as an internal input, never a signed field named operation. The two facts can no longer be confused. - Decision procedure was three leaf predicates plus prose. Added a normative, ordered decision_algorithm (structural -> subject-kind -> grammar -> dependency resolution -> integrity -> internal-digest -> input-digest -> rule-cardinality -> requires -> derive), which fixes failure precedence: the first terminal step wins, deterministically. - invalid vs unverifiable was verifier-inventory-dependent. Tightened: resolve ONLY by embedded digest; absent -> unverifiable (environmental, resolves once supplied); present-but-not-the-pinned-bytes -> invalid; possession of a different artifact is irrelevant and must never flip the outcome. Corpus vectors now carry `given.available_artifacts` so resolution outcomes are deterministic per vector rather than dependent on the runner's environment. - Duplicate registry digest (header vs vector) now has an explicit digest_consistency rule: disagreement without an explicit store override invalidates the corpus artifact, never silently changes a vector's outcome. Honesty fixes: - enforced_in_path is now named an ISSUER-ATTESTED fact: an offline verifier checks the issuer signed it, it cannot independently prove interception. The trust boundary is explicit; presentation is non-authorizing so it cannot escalate. - Dangling input profile keel.registered_tool_classification_input.v1 is now normatively defined here (logical_object_schema) and separated from the serialization profile keel.canonical_json.payload.v1 (payload vs serialization are distinct concerns, separately named). - "corpus = contract" downgraded to NECESSARY, NOT SUFFICIENT: the algorithm + profiles are the contract; the corpus is mandatory minimum evidence. Coverage: - Surface fields (transport_surface, target_surface) now on every vector, with a positive-surfaces-are-orthogonal vector proving they do not affect the outcome (the 2b decoupling, made testable). - deferred_coverage names what is intentionally not yet covered (multiple-rule overlap needs a synthetic 2-rule fixture; property tests) so absence is explicit, co-designed with the reference verifier, not silent. Corpus self-consistent (real digests, positive vectors provable members), all keel-permit gates pass, enforcement parity byte-for-byte. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…clean A reference executor runs the ruleset's decision_algorithm against every corpus vector purely from the spec — the strongest test that two implementers get identical outcomes without an undocumented choice. Building it forced two real corrections: 1. The corpus `given` model was `available_artifacts` (objects with resolve/ bytes flags), which is not what the algorithm consumes. Replaced with `trusted_registry_digests`: the set of classification_registry digests the verifier pins. Step-4 resolution resolves ONLY a digest in that set; outside it is unverifiable regardless of what bytes anyone can supply. Every resolution outcome is now a function of (signed record, trusted set), never incidental local inventory. 2. invalid-registry-digest-disagrees was mis-modeled. A well-formed-but-untrusted registry digest is unverifiable (environmental), not invalid — exactly the distinction the review insisted on. Split into two record-determined vectors: invalid-malformed-registry-digest (syntactically bad sha256 -> invalid) and unverifiable-registry-digest-untrusted (well-formed, not pinned -> unverifiable, an attacker's forged bytes do not help). All 11 vectors now execute to their expected outcome, 0 mismatch, straight from the spec. The executor is a reference tool, not a pinned contract artifact. Gates pass; enforcement parity holds.
…ectors Round 3 (executor-driven) found real determinism defects. All fixed; the executor now runs 14 vectors, 0 mismatch, including three that lock the fixes. Blocking, fixed: - Step 9 internal precedence (the definite two-executor-disagreement): eliminated BY CONSTRUCTION rather than merely ordered. Integrity/digest checks are now GLOBAL algorithm steps (3 resolution, 4 dependency validation, 6 input digest), run before rule matching. The rule's per-rule `requires` holds only the two SOFT conditions (membership, enforcement), both -> no_derivation, so no valid/invalid conflict inside step 8 is constructible. - Unknown-kind-before-grammar masking: reordered so the COMMON envelope (incl. digest syntax) is validated at step 1, BEFORE subject-kind dispatch at step 2. An unknown kind can no longer launder a malformed common binding. Locked by invalid-unknown-kind-plus-malformed-common-digest. - "Integrity by construction" hole: step 3 is now a normative hash-verifying resolution (require digest in trusted set; require sha256(bytes)==digest) and step 4 validates the resolved registry's schema/version/uniqueness. Trusting a digest is not trusting arbitrary bytes offered under it, nor is pinning bytes validating them as a registry. Locked by invalid-trusted-digest-corrupt-bytes and invalid-trusted-digest-malformed-registry. - Unevaluable predicate (unknown operator / operand out of domain) is now explicitly invalid (step 8a), not a false predicate. Honesty: - Determinism claim qualified: outcomes are deterministic GIVEN the same record, the same immutable trusted_registry_digests, validated bytes, and algorithm version. Trust configuration is an explicit immutable verification input the verifier must report, not ambient mutable state. Offline verification requires trust anchors; a pinned registry digest is one. - Step 5 (internal digest agreement) is a named no-op only because the v1 schema forbids a second authoritative registry reference; named, not ceremonial, so a future field cannot be silently ignored. Deferred (named, not silent): multiple-rule / rule-order-permutation need a synthetic 2-rule fixture (with the second Permit-to-X rule); full canonicalization/Unicode/resolver-multiplicity suites are property tests co-designed with the reference verifier. Gates pass; enforcement parity holds byte-for-byte.
…lobal steps 3/4/6) Left-over from moving integrity to global steps: step 8 prose still described an 8b evaluating digest predicates the rule no longer carries. The executor was already correct (membership + enforcement only). Aligned the prose to it; step 8 is now 8a evaluability, 8b membership, 8c enforcement, all deterministic.
Fourth contract-review round converged: no remaining determinism hole (the Step-8 conflict is designed out and no same-input/same-trust/same-bytes divergence is constructible), shape sound enough to build the reference verifier against. Captured the one carry-forward: canonicalization boundaries and unsupported-version handling are deferrable to verifier construction but MUST be finalized as part of completing it, since they govern digest/signature validity.
Building the reference verifier with real bytes surfaced that the corrupt/ malformed integrity vectors used abstract flags (store_corrupt_digests / store_malformed_digests) that are not realizable under content-addressing: bytes hashing to the pinned registry digest ARE the pinned registry. Replaced with a byte-concrete of (digest, content_utf8) pairs. corrupt-bytes now references a digest its supplied content does not hash to (-> step-3 dependency_integrity); malformed-registry references the real hash of not-a-registry content (-> step-3 passes, step-4 dependency_artifact_invalid). This is the round-4 'store rules finalized during verifier build' obligation, handled. Executor 14/0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Foundation for offline re-derivation of an AI Permit-to-Pay identity minted from an in-path-enforced MCP payment tool call. The verifier owns the whole chain and trusts nothing the issuer asserts about the classification (full re-derivation):
Artifacts
semantics/permit/value_movement_classification_v1.json— 31(connector, tool)pairs, generated from keel-api's enforcement map so it byte-matches by construction; a parity guard in keel-api holds it identical, so a single evaluation backs both enforcement and verification.semantics/permit/action_classification_derivation_v1.json— the derivation ruleset with a normative, ordered decision algorithm (9 steps), a closed predicate vocabulary, an explicit outcome taxonomy (valid / no_derivation / invalid / unverifiable), and the verifier-build obligations.test-vectors/action_classification_derivation/v1/corpus.json— 14 vectors, real digests, all four outcomes, including three that lock the round-3 precedence fixes.test-vectors/.../reference_executor.py— runs the algorithm against every vector from the spec (14/0).Reviewed to convergence
Four external contract-review rounds. Round 3 found and fixed a real determinism defect (Step-9 precedence, now designed out — integrity is global, before rule matching). Round 4 verdict: no remaining determinism hole, no same-input/same-trust/same-bytes divergence constructible, build-ready. Building the reference executor caught two further spec bugs no reading would have.
Design invariants
authorized_action/invocation/classification/control.invocation.operationstayscall.tools; there is no signed field carrying the derived action.control.mode = enforced_in_pathis an explicitly-named issuer-attested fact; an offline verifier checks the signature, it cannot prove interception.Verification
check_permit_to_x_artifacts,check_public_hygiene,check_repo_integrityall pass; hash pins bite on tamper by name; enforcement parity byte-for-byte; executor 14/0.Consumers (keel-verifier, then keel-api, then dashboard) are built against this contract next. Hold merge pending review of the contract shape.
🤖 Generated with Claude Code