Skip to content

feat(schema): admit WCM as a marketplace stack - #141

Merged
imran-siddique merged 1 commit into
mainfrom
feat/wcm-stack-support
Aug 27, 2026
Merged

feat(schema): admit WCM as a marketplace stack#141
imran-siddique merged 1 commit into
mainfrom
feat/wcm-stack-support

Conversation

@imran-siddique

Copy link
Copy Markdown
Member

Why

A Weight Custody Manifest integration was not merely unlisted in the Marketplace, it was unrepresentable. integrates_with is an enum of three values, so schema validation rejects wcm; and generate_marketplace_catalog.py maps that field through STACK_LABELS, so a fourth value raises KeyError and takes the whole catalog build down. The public Marketplace derives its stack filter from that catalog, which means this repository is the only place the gap can be closed.

This PR adds no integration. It makes the next fourteen possible.

What changed

Change File
integrates_with accepts wcm schema/integration.schema.json
wcm maps to the WCM stack label scripts/generate_marketplace_catalog.py
wcm_roles and wcm_conformance_levels schema/integration.schema.json
Model & weight custody category schema/integration.schema.json
weight-custody-manifest floor at 0.26.0 compatibility.yaml
WCM declaration rules and a link rule CONTRIBUTING.md

Two decisions worth reviewing

Levels are a list of layer identifiers, not a number. trace_conformance_level is a single integer because TRACE conformance is cumulative. WCM is four independent layers (L1 manifest and joint signature, L2 attestation-gated release, L3 runtime custody, L4 derivative lineage), and a registry adapter that verifies manifests has no business claiming the release gate. wcm_conformance_levels: ["L1", "L4"] says exactly what was run.

Two roles are exempt from declaring a level. attestation-source hands evidence to somebody else's verifier; evidence-consumer reads custody output downstream. Neither verifies a manifest. This is the same carve-out external-evidence-source already has on the TRACE side, and it exists so an adapter cannot be read as having passed a suite it never ran.

The link rule

agentrust-io/weight-custody-manifest is still private (issue #40). Every github.com/agentrust-io/weight-custody-manifest URL 404s for anonymous readers, and PyPI renders such links live regardless. WCM's own pyproject.toml carries a comment recording that v0.26.0 shipped with exactly those dead links. CONTRIBUTING now makes citing the docs site and PyPI a hard rule rather than a convention.

Tests

Seven new cases across tests/test_integration_schema.py and tests/test_generate_marketplace_catalog.py: role requirement, level requirement, the attestation-source carve-out, rejection of a numeric level, tested_against recording, the new category, and the stack label reaching the catalog. Full suite: 23 passed. validate_integrations clean on all 18 existing integrations, and generate_marketplace_catalog --check reports the committed catalog is current, so no existing listing shifts.

🤖 Generated with Claude Code

https://claude.ai/code/session_014NL8o3PXq6kfs2SdmBv6ak

Nothing in this repository could describe a Weight Custody Manifest
integration. `integrates_with` was an enum of three values, the catalog
generator would KeyError on a fourth, and the Marketplace derives its stack
facet from that generator, so a WCM listing was not merely unlisted but
unrepresentable.

Four changes make one possible:

- `integrates_with` accepts `wcm`, and the generator labels it `WCM`.
- `wcm_roles` and `wcm_conformance_levels` mirror the TRACE role/level pair.
  Levels are per-layer identifiers (L1-L4) rather than one number, because WCM
  is four layers and an integration may verify a manifest without brokering a
  key. `attestation-source` and `evidence-consumer` verify no manifest and are
  therefore excluded from the level requirement, the same carve-out
  `external-evidence-source` already gets on the TRACE side.
- A `Model & weight custody` Marketplace category, so custody integrations are
  browsable as a group instead of scattered across Security & policy.
- A `weight-custody-manifest` floor of 0.26.0, the current PyPI release.

CONTRIBUTING gains a hard rule about links: the WCM source repository is still
private, so any github.com/agentrust-io/weight-custody-manifest URL 404s for
every anonymous reader. Integrations cite the docs site and PyPI instead. The
repository's own pyproject already records shipping that exact dead link once.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NL8o3PXq6kfs2SdmBv6ak
@imran-siddique
imran-siddique merged commit f8c31f1 into main Aug 27, 2026
18 checks passed
@imran-siddique
imran-siddique deleted the feat/wcm-stack-support branch August 27, 2026 06:51
imran-siddique added a commit that referenced this pull request Aug 27, 2026
* feat(wcm): first four Weight Custody Manifest integrations

WCM had no integrations at all. These four cover the layers a deployment
actually touches first: evidence out (TRACE), the manifest travelling through
supply-chain tooling (in-toto), the manifest gating a download (Hugging Face),
and the cluster preconditions it implies (Kyverno).

Each is built and tested against the published PyPI release, 0.26.0, and links
only to the docs site and PyPI, per the rule added in #141.

wcm-trace
  Layer 2 release decisions and Layer 3 custody state as TRACE v0.2 records.
  runtime.platform names hardware only when the broker's own cpu_quote_verified
  check passed, so SoftwareProvider evidence produces software-only and no
  argument can override it. A refusal is a record too: appraisal.status
  contraindicated, with the failed checks named. policy.bundle_hash digests the
  signing pre-image rather than the document, so countersigning does not move a
  hash consumers compare terms by.

wcm-in-toto
  Statement v1 wrapper. The predicate embeds the jointly-signed manifest whole
  rather than summarizing it: a DSSE signature answers who built the envelope,
  which is a different question from who agreed the custody terms, and a
  summary would leave a consumer trusting the transcription. Verification keeps
  the subject binding and the manifest signatures as separate reported facts,
  because a manifest stapled to a different artifact is the attack this format
  invites.

wcm-huggingface
  Refuses a snapshot whose bytes do not hash to the bound weights_hash. Layer 1
  only, and the README says so: a public Hub repository is not an enclave. An
  immutable commit sha is required unless the caller opts out, because
  verifying a snapshot fetched from a branch is not a claim anyone can
  re-check. Failed downloads are left on disk rather than deleted, so the
  evidence of what was served survives.

wcm-kyverno
  The interesting one is what it refuses to do. Checking pod image digests
  against required_serving_image.accepted_measurements looks obvious and is
  wrong: those are launch measurements from a quote, not OCI digests. The
  failure mode is not a broken policy but someone "fixing" the mismatch by
  putting registry digests into the field the broker compares against a quote.
  So the generator enforces what admission genuinely can (confidential runtime
  class, no process-inspection escape hatches, digest-pinned images, a
  manifest-hash binding) and prints the boundary as a header comment in the
  emitted YAML.

The artifact digest recipe in wcm-huggingface is duplicated from the WCM
examples repository rather than imported, because it is not in the published
SDK. It belongs there; this is flagged in the README so a third dialect does
not appear.

Tests: 82 across the four, all against PyPI 0.26.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NL8o3PXq6kfs2SdmBv6ak

* feat(wcm): telemetry, registry and Azure key-release integrations

Three more, and two of them exist mainly to refuse something.

wcm-opentelemetry
  Release decisions and custody state as OTel spans and metrics, under a wcm.*
  attribute set because GenAI semconv has no vocabulary for weight custody and
  bending gen_ai.* into meanings its authors did not give it would be worse
  than a namespace. Every attribute is described in ATTRIBUTES and a test
  rejects emitting one that is not. Both the claimed platform and whether the
  broker verified it are exported: a dashboard showing only the claim presents
  it as a fact. Metrics carry a narrow attribute set on purpose, because a
  counter keyed by manifest hash is one time series per revision. The README
  says plainly that telemetry is not evidence and points at wcm-trace.

wcm-oci
  Referrer artifact, and the difficulty is a digest-domain gap: an OCI subject
  digest names a registry manifest, weights_hash covers weight bytes, and no
  registry can derive one from the other. So weights_binding is a three-valued
  string (layer-digest / annotation-only / unbound) rather than a boolean, and
  `trusted` requires layer-digest. annotation-only may be good enough for a
  given deployment, but that has to be decided rather than inherited from a
  property that quietly accepted less.

wcm-azure-skr
  SKR is Layer 2 in Azure's encoding, so the translation is natural until the
  measurements. WCM's HashValue is 256-bit; x-ms-sevsnpvm-launchmeasurement is
  384-bit. They cannot be compared, and on Azure the WCM path binds a SHA-256
  PCR 23 digest, a different value from a different chain. A generator that
  emitted the obvious condition would produce a policy that never matches and
  an engineer who concludes the CVM is broken; the plausible fix, widening the
  manifest field, breaks the broker's binding. So measurement_claim is required
  and its width is checked at generation time. evidence_from_maa_claims takes a
  claims mapping rather than a JWT so it cannot be mistaken for a verifier.

Tests: 68 across the three, against PyPI 0.26.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NL8o3PXq6kfs2SdmBv6ak

* feat(wcm): Trustee, NVIDIA GPU and Confidential Space integrations

wcm-coco-trustee
  Rego resource policy for the confidential-containers key broker. Defaults to
  the azsnpvtpm/aztdxvtpm TEE names rather than snp/tdx, because that is where
  a 256-bit WCM measurement and a Trustee claim are the same width; the bare
  metal names carry 384-bit launch measurements a WCM HashValue cannot hold.
  Every value interpolated into the Rego is a validated hex digest, a fixed TEE
  name, or a dotted-identifier claim path: a generator that concatenated
  arbitrary input into the policy deciding who gets model keys would be an
  injection vector, and there are tests that try.

wcm-nvidia-nras
  Promoted from the WCM repository's validated hardware tooling rather than
  written from documentation: the required-claim list is what a real H100 in CC
  mode produced under nvattest --verifier local. The design point is that two
  appraisals run and neither replaces the other. NVIDIA checks the RIMs and
  live OCSP against its own reference data; WCM independently verifies the raw
  report chain, signature and nonce, which is why the raw evidence is carried
  through in quote_b64 rather than summarized. Both nonces are checked
  separately, since replay only needs the weaker check to be missing.

wcm-gcp-confidential-space
  The counterpoint to the Azure integration. Confidential Space identifies a
  workload by its OCI image digest, which is sha256 and 64 hex, the same shape
  as a WCM HashValue, so the comparison is meaningful and the generator can
  emit it. The assumption that a manifest's measurements are image digests is
  stated in the generated command rather than left implicit, and a measurement
  of another shape is refused with that reason.

Tests: 116 across the three, against PyPI 0.26.0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NL8o3PXq6kfs2SdmBv6ak

* feat(wcm): serving runtimes, Agent Manifest binding, ML-BOM, and CI

The last four of the wave, plus the CI that runs all fourteen.

wcm-vllm
  The honest part is what it does not claim. Layer 3 says a runtime that loses
  authorization wipes the key. In Python, half of that is achievable: zeroize()
  clears the SDK's own buffer, but decrypted weights live in heap copies,
  pinned host buffers and GPU device memory that no Python code reaches. So
  on_lapse terminates the process via os._exit, and the README says plainly
  that termination is the wipe. sys.exit would let a framework catch it and
  serve the next request anyway. The lease is checked per request, not per
  token, because vLLM has no cheap per-forward hook, and the README says to
  size the cadence for that. A lapsed lease and an exhausted operation budget
  are handled differently: one halts the process, the other asks for
  re-attestation, and conflating them would take a healthy server down.

wcm-triton
  Not a repository agent and could not be one: those are C. This is the step
  before Triton starts. It takes a decrypt callable rather than choosing a
  cipher, because WCM specifies no container format and inventing one inside an
  integration is how a project ends up with three dialects of the same idea.
  It removes the staging directory on failure, the opposite of the Hugging Face
  gate, because Triton scans its repository and a wrong directory is loadable,
  whereas a failed download is evidence of what a registry served.

  It also carries the test that keeps wcm-artifact-digest/v1 a single recipe:
  it imports wcm-huggingface and asserts byte equality over the same tree. Two
  implementations exist because the recipe is not in the published SDK, and the
  failure mode of that is silent divergence presenting as tampered weights.

wcm-agent-manifest
  Both SDKs define HashValue with the identical pattern, so weights_hash is a
  valid model_hash verbatim, and hash_grammars_agree() asserts that rather than
  assuming it. verify_binding returns three separate facts and a property named
  describes_the_same_weights, at length on purpose: two documents carrying the
  same digest is not custody enforcement, and a name like `verified` would
  invite reading one into the other.

wcm-cyclonedx
  Gets custody terms in front of licence review. Fields with no CycloneDX home
  are dropped rather than stuffed into properties as free text, which would
  produce a BOM that reads as enforcement and carries strings. Output is
  reproducible: no self-stamped clock, because that makes every rebuild look
  like a change.

CI runs all fourteen on 3.11-3.13 against pinned PyPI 0.26.0, with the digest
parity check split into its own job so that failure is legible rather than
buried in a suite of 344.

Marketplace: 18 native integrations to 32, with a WCM stack facet and a
Model & weight custody category.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NL8o3PXq6kfs2SdmBv6ak

* fix(wcm): name public keys as public, and build CLI reports from primitives

Two CI findings from #142.

The generated README integration index was stale. Regenerated; fourteen rows.

CodeQL raised two high-severity "logs sensitive data as clear text" alerts, on
the verify report in wcm-in-toto and wcm-oci. No key material was ever printed,
but the analyser is not wrong to be unable to tell. The taint runs from a file
read behind an argument called --key, through a VerificationContext, into a
result object whose attributes are then spread into a dict and serialized. From
outside, that is indistinguishable from a path that prints a key.

Both halves of that are worth fixing on their own terms.

The material is PUBLIC key material, and nothing in these modules reads, holds
or emits a private key. Calling the flag --key made a reader work that out from
context. It is now --public-key, and _load_context is _public_key_context with
a docstring saying so. wcm-huggingface gets the same treatment for consistency,
even though CodeQL did not flag it: leaving one --key behind is how the next
person concludes the three disagree on purpose.

The reports are now built by a _report() helper that coerces every value to
bool, str or None by name. Previously they spread result attributes into a dict
literal, which was correct only because of what those results happen to
contain. Now an attribute added later cannot reach stdout by being swept into a
dict comprehension somebody wrote in a hurry, and the analyser can see the
boundary.

344 tests still pass, and the CLI flag rename is carried through the tests and
the three READMEs. These integrations are unreleased, so no caller breaks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NL8o3PXq6kfs2SdmBv6ak

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
imran-siddique added a commit to agentrust-io/agentrust-io.github.io that referenced this pull request Aug 27, 2026
Every link from agentrust-io.com/wcm/ into the WCM repository 404s. The
repository is private, and a launch page's entire audience is anonymous
readers, so all six were broken for exactly the people the page is for:

  /blob/main/THREAT-MODEL.md   "Full threat model"
  /issues/78 and /issues/79    the two disclosed open limitations
  /issues                      "Review open questions", for security researchers
  /actions                     the "574 tests passing" evidence claim
  /tree/main/conformance/vectors  the "91 conformance vectors" claim

This is the same trap agentrust-io/integrations#141 made a hard CONTRIBUTING
rule about, arriving from the other direction: fourteen integration READMEs now
point here as "specification and documentation", so a reader following one to
find the spec landed on a page whose own links were dead.

Each replacement is a real destination rather than a redirect of the problem:

  Vectors and test count now point at PyPI, and say why in a title attribute.
  Both are checkable from the published package, because the sdist ships tests/
  and the conformance vectors. That is a stronger claim than a CI badge: a
  reader can reproduce the numbers instead of being shown a green tick.

  The two limitations lose their hyperlinks and keep their prose. The paragraph
  already states both in full; the disclosure was always the point and the link
  only ever pointed into a tracker nobody outside the org can open.

  "Full threat model" points at the on-page limits section, which is the public
  statement of the same thing.

  Security researchers get a mailto, matching the two personas that already use
  one. An issue tracker they cannot open is not a way in.

The evidence strip had also drifted: pinned at a pre-0.27.0 commit, showing
0.26.0 and 574 tests. Now a952cb6, 0.27.0 and 599, with proof.json regenerated
from that commit.

launch-page.test.js gains the durable part. It previously asserted the issue
LINKS were present, which is how these survived: the check enforced the dead
link rather than the disclosure. It now asserts the limitation prose, and
separately that no link into the private repository exists anywhere on the
page. Verified it fails on a reintroduced link. Delete that check when the
repository goes public (weight-custody-manifest#40).


Claude-Session: https://claude.ai/code/session_014NL8o3PXq6kfs2SdmBv6ak

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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