Skip to content

Add MCP payment classification contract (full re-derivation foundation)#16

Merged
sftimeless merged 8 commits into
mainfrom
codex/mcp-payment-classification-fact
Jul 24, 2026
Merged

Add MCP payment classification contract (full re-derivation foundation)#16
sftimeless merged 8 commits into
mainfrom
codex/mcp-payment-classification-fact

Conversation

@sftimeless

Copy link
Copy Markdown
Member

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):

(connector_identity, canonical_tool_name)   [signed, server-owned]
  -> value_movement            [value_movement_classification.v1, hash-pinned]
  -> payment.execute           [action_classification_derivation.v1, normative algorithm]
  -> Permit-to-Pay             [existing presentation registry]

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

  • Four distinct signed facts, never collapsed: authorized_action / invocation / classification / control. invocation.operation stays call.tools; there is no signed field carrying the derived action.
  • transport_surface and target_surface are orthogonal signed facts (2b decoupling), tested.
  • control.mode = enforced_in_path is an explicitly-named issuer-attested fact; an offline verifier checks the signature, it cannot prove interception.
  • Strictly non-authorizing: a wrong derivation degrades a title, never escalates authorization.

Verification

check_permit_to_x_artifacts, check_public_hygiene, check_repo_integrity all 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

sftimeless and others added 8 commits July 24, 2026 07:32
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.
@sftimeless
sftimeless merged commit fcd09b9 into main Jul 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant