diff --git a/.github/workflows/integrity-gate.yml b/.github/workflows/integrity-gate.yml index 79b3e6d..8072636 100644 --- a/.github/workflows/integrity-gate.yml +++ b/.github/workflows/integrity-gate.yml @@ -96,7 +96,7 @@ jobs: run: pip show mcp | grep -i '^version' - name: Capture / parity / pagination tests under mcp 1.x - run: pytest -q tests/test_capture_model_dump.py tests/test_capture_pagination.py tests/test_e2e_pin_check.py tests/test_capture_http.py + run: pytest -q tests/test_capture_model_dump.py tests/test_capture_pagination.py tests/test_e2e_pin_check.py tests/test_capture_http.py tests/test_tool_integrity.py tests/test_tool_integrity_guard.py # -------------------------------------------------------------------------- # Job 1b: lint gate (ruff) diff --git a/CHANGELOG.md b/CHANGELOG.md index dd4814e..758d821 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,30 @@ Streamable HTTP; the v0.3 `guard` proxy adds deterministic runtime *result* insp ## [Unreleased] +### Fixed — release validation + +- Refresh the dev/CI and Action dependency locks from PyJWT 2.13.0 to 2.15.1 + with artifact hashes; the audited closure passes without advisory suppression. +- Re-capture the three committed public examples at schema level 4 using the same + pinned server versions. Their prior fields are unchanged; new commitments are + reviewed example baselines, not transferred historical signatures. + +### Added — tool metadata integrity (DSE-1539) + +- Schema level **4** commits complete tool annotations and output schemas, including + output structural skeletons. Hint flips/removal and output changes cause drift; + schema-out-* classes distinguish structural/cosmetic changes. Python/TypeScript + share the vectors. The runtime tools/list gate compares new commitments for v4. +- Missing/null hashes JSON null; {} is distinct. Invalid object fields and v4 locks + omitting commitments are refused. Raw annotations are not rendered, and hints + grant no capability or proof of behavioral safety. +- V1–v3 locks remain readable. Approved legacy locks retain unapproved-change plus + migration; unapproved old locks also fail migration rather than claiming coverage. + Review/re-pin/re-approve for v4; historical signatures do not carry across. + Lock rotation preserves recorded schema. +- The human checkpoint and protocol-neutral Warden direction over prompts, retrieval, + code, and serverless adapters are documented proposals, not shipped guarantees. + ### Changed - **`mcp` SDK 2.x is now supported — the `<2` cap from #92 is lifted to `<3` (supersedes diff --git a/DOCUMENTATION_INDEX.md b/DOCUMENTATION_INDEX.md index e694d83..ece86a8 100644 --- a/DOCUMENTATION_INDEX.md +++ b/DOCUMENTATION_INDEX.md @@ -1,5 +1,7 @@ # Documentation Index — mcp-warden +Last Updated: 2026-10-02 + Master index of every document in this repository. The `docs/` files are the **security contract and source of truth** for all algorithms; the three core docs describe and visualize the implementation that satisfies that contract. @@ -16,13 +18,17 @@ describe and visualize the implementation that satisfies that contract. ## Lock Format v1 conformance (`vectors/` + `@mcp-warden/lock` — DSE-1513) +Schema level 4 adds annotation/output commitments (DSE-1539). The +[upgrade plan and Warden checkpoint proposal](docs/plans/2026-10-02-tool-integrity-upgrade.md) +separates implemented surface integrity from proposed protocol-neutral enforcement. + The format is a standard, not a tool: a language-neutral corpus defines conformance and two implementations (Python reference, zero-dependency TypeScript) prove it in CI. | Artifact | Purpose | |----------|---------| | [`vectors/README.md`](vectors/README.md) | Consumer contract: manifest schema, the four vector kinds, the surface document shape, how a third implementation runs the corpus | -| [`vectors/manifest.json`](vectors/manifest.json) + [`vectors/cases/`](vectors/cases/) | 77 generated vectors — canonical (RFC 8785), digest, drift (every `WRD-DRIFT-*` class), malformed | +| [`vectors/manifest.json`](vectors/manifest.json) + [`vectors/cases/`](vectors/cases/) | 111 generated vectors — canonical (RFC 8785), digest, drift (every `WRD-DRIFT-*` class), malformed | | [`vectors/tools/generate.py`](vectors/tools/generate.py) | Regenerates the corpus from the Python reference; a corpus diff = a hashed-derivation change = a `schema_version` bump (SPEC §14.2) | | [`tests/test_spec_vectors.py`](tests/test_spec_vectors.py) | Python harness over the manifest (honours `MCP_LOCK_VECTORS_DIR`) | | [`packages/lock-ts/`](packages/lock-ts/README.md) | `@mcp-warden/lock` — verify-only TypeScript: hand-written JCS, SHA-256, capability + skeleton derivation, drift classifier; `npm test` runs the same corpus | @@ -156,7 +162,7 @@ scope-honesty box and makes no compliance/regulatory claim. | [`docs/AGENT_TRUST_KERNEL.md`](docs/AGENT_TRUST_KERNEL.md) | **(DSE-714, design contract)** Normative invariants for the future deterministic Agent Trust Kernel: trust boundaries, complete mediation, default deny, non-overridable critical classes, evidence-before-effect, offline operation, residual risks, and bindings for DSE-715 through DSE-717. MCP-Warden v1.1 is explicitly not yet ATK-conformant | | [`docs/CONTENT_ENVELOPE.md`](docs/CONTENT_ENVELOPE.md) | **(DSE-715, implemented foundation)** Strict immutable V1 content envelope, domain-separated exact-byte digests, canonical metadata boundary, bounded one-hop lineage, monotonic taint, stable code-only errors, and secret-safe public projection. Evidence only; no authority or whole-ATK conformance claim | | [`docs/POLICY_ENFORCEMENT.md`](docs/POLICY_ENFORCEMENT.md) | **(DSE-716, implemented foundation)** Versioned signed policy/runtime/adapter/executable-bundle activation, exact adapter/bundle-bound leases, mechanically derived frozen handler identity, deterministic default-deny PDP, evidence-gated structural PEP, stable reason/recovery matrix, caps, and non-optional fixed-corpus adapter harness. DSE-717 durable evidence is in progress but remains required for any whole-ATK claim | -| [`docs/WARDEN_LOCK_SCHEMA.md`](docs/WARDEN_LOCK_SCHEMA.md) | **mcp-warden implementation of [`docs/SPEC.md`](docs/SPEC.md) (MCP Lock Format v1).** `warden.lock` format, RFC 8785 canonicalization + SHA-256 hashing, field/entry/overall digests, the normative drift definition + severities; **§5.1/§6.2 structural schema diff** (normalized per-tool `schema_skeleton`, `schema_version` 3 — skeleton added at v2, in-document `$ref` resolution at v3 (#29), granular `WRD-DRIFT-SCHEMA-*` taxonomy + severities, v1 fallback); **§8.1/§8.2 (v0.3, #19)** structured out-of-digest provenance (`pinner` / `attestations` / `rotation_count`, `PROVENANCE_VERSION`, B4 `bound_digest` format) + `lock rotate` digest-invariant semantics + the #16 signing implication; **§11 (v0.2)** optional per-tool inspection policy (`expected_output_charset` / `may_return_urls` / `secret_echo_applies`, fail-safe defaults, digest impact) | +| [`docs/WARDEN_LOCK_SCHEMA.md`](docs/WARDEN_LOCK_SCHEMA.md) | **mcp-warden implementation of [`docs/SPEC.md`](docs/SPEC.md) (MCP Lock Format v1).** `warden.lock` format, RFC 8785 canonicalization + SHA-256 hashing, field/entry/overall digests, the normative drift definition + severities; **§5.1/§6.2 structural schema diff** (normalized per-tool `schema_skeleton`, `schema_version` 4 — annotations/output added at v4, skeleton added at v2, in-document `$ref` resolution at v3 (#29), granular `WRD-DRIFT-SCHEMA-*` taxonomy + severities, v1 fallback); **§8.1/§8.2 (v0.3, #19)** structured out-of-digest provenance (`pinner` / `attestations` / `rotation_count`, `PROVENANCE_VERSION`, B4 `bound_digest` format) + `lock rotate` digest-invariant semantics + the #16 signing implication; **§11 (v0.2)** optional per-tool inspection policy (`expected_output_charset` / `may_return_urls` / `secret_echo_applies`, fail-safe defaults, digest impact) | | [`docs/WARDEN_LOCK_EXAMPLE.md`](docs/WARDEN_LOCK_EXAMPLE.md) | Illustrative full `warden.lock` + a post-`lock rotate` `pin` block (archived from WARDEN_LOCK_SCHEMA §9 to keep that core doc under the line cap) | | [`docs/CHECKS.md`](docs/CHECKS.md) | The deterministic `WRD-*` static-check catalog (capability/secret/supply/robustness), the shared tokenizer, severity→SARIF mapping, redaction rule, CUT list. **Reused by v0.2** `WRD-RES-SECRET-ECHO` (the `WRD-SEC-*` patterns + redaction) | | [`docs/POLICY_MODEL.md`](docs/POLICY_MODEL.md) | Policy schema, the four high-risk shapes, constraint vocabulary, fail-closed defaults, SSRF deny ranges, lint + single-sample eval semantics. **Enforced at runtime by v0.2 `guard`** on live `tools/call` requests | @@ -167,6 +173,8 @@ scope-honesty box and makes no compliance/regulatory claim. ## Non-normative design and implementation plans +[Runtime CLI examples](docs/archive/2026-10-02-runtime-cli-examples.md) retain the detailed guard commands moved from README. + | Plan | Purpose | |---|---| | [`docs/plans/2026-07-18-agent-trust-kernel-design.md`](docs/plans/2026-07-18-agent-trust-kernel-design.md) | **Non-normative execution record.** Records the DSE-714 design decision and verification plan; binding requirements live in `docs/AGENT_TRUST_KERNEL.md` | @@ -217,6 +225,7 @@ scope-honesty box and makes no compliance/regulatory claim. | `src/mcp_warden/signing.py` | **(#16)** Sigstore keyless sign/verify primitives (guarded import; `build_statement` / `sign_statement` / `verify_statement` — verify raises on failure, returns None on success) | SIGNING.md | | `src/mcp_warden/cli_sign.py` | **(#16)** `pin --sign` / `check --verify` CLI control flow: fixed-sidecar verify, atomic bundle write, fail-closed exits | SIGNING.md | | `src/mcp_warden/drift.py` | Per-class drift/diff engine + severities | WARDEN_LOCK_SCHEMA §6.2 | +| `src/mcp_warden/drift_tool_metadata.py` | v4 annotation drift and structural output-schema classification without raw annotation disclosure | SPEC v4 extension | | `src/mcp_warden/schema_diff.py` | Deterministic structural `inputSchema` skeleton extractor + per-fact diff classifier (`WRD-DRIFT-SCHEMA-*`; `$ref`/cyclic/malformed-safe) | WARDEN_LOCK_SCHEMA §5.1, §6.2 | | `src/mcp_warden/checks.py` | Static-check orchestrator (deterministic sort) | CHECKS §4–§5 | | `src/mcp_warden/checks_secret.py` | `WRD-SEC-*` vendor + entropy + redaction | CHECKS §4.2 | @@ -260,7 +269,7 @@ scope-honesty box and makes no compliance/regulatory claim. | `tests/test_capture_http.py` | **(#74, DSE-57)** Async/sync Streamable HTTP capture, protocol/list normalization, timeout handling, and connection errors | | `tests/test_diff.py` | **(v0.3)** `warden diff` renderer: identical→"no differences", tool add/remove + schema change rows, **redaction-leak guard** (secret in `server.args` absent from human/`--json`/`--sarif` incl. parsed-JSONL `detail`), provenance-only section vs empty integrity drift, `--exit-code` (1 on integrity drift / 0 on provenance-only), `--no-provenance` M6 message, fail-closed on missing/invalid lock | | `tests/test_result_inspection.py` | **(v0.2)** `WRD-RES-*`: ANSI codepoint match (incl. extended/binary-ok), secret-echo reuse + redaction, exfil host/subdomain boundary + path-qualified, injection exact-phrase (no broad-regex FP), URL/uninspectable notes | -| `tests/test_inspection_policy.py` | **(v0.2)** §11 per-tool policy fail-safe defaults, byte-identical-to-v0.1 digest when absent, inspection-policy drift, pin-time validation, reader fallback + LOCK-INVALID | +| `tests/test_inspection_policy.py` | **(v0.2)** §11 per-tool policy fail-safe defaults, inspection omission/None digest parity in the current format, inspection-policy drift, pin-time validation, reader fallback + LOCK-INVALID | | `tests/test_wire_block.py` | **(v0.2)** `-32001` error-response shape, block-mode mapping, ANSI strip-in-place `_meta.warden.modified`, secret redact-in-place | | `tests/test_framing.py` | **(v0.2)** newline + Content-Length framing, chunk-split reads, original-bytes pass-through, malformed-frame parse capture | | `tests/test_guard_posture.py` | **(v0.2/v0.3)** fail-open (inspector exception/malformed → pass-through) vs fail-closed (policy deny → block under `armed_policy`), audit-only precedence over default-on | diff --git a/README.md b/README.md index a13dd2c..60672c3 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # mcp-warden +Last Updated: 2026-10-02 + [![CI](https://github.com/DataScience-EngineeringExperts/mcp-warden/actions/workflows/integrity-gate.yml/badge.svg)](https://github.com/DataScience-EngineeringExperts/mcp-warden/actions/workflows/integrity-gate.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/downloads/) @@ -10,11 +12,22 @@ tool/resource/prompt surface into a signed `warden.lock`, then fails CI when that surface drifts.** `pin` and `check` support stdio and Streamable HTTP; `guard` is stdio-only. +**Schema level 4** also locks complete tool annotations and output schemas. Changing +destructiveHint, removing a result schema, or widening its types now triggers drift; +the existing runtime tools/list gate checks these fields against v4 locks too. +Older locks stay readable, but require review and re-pinning for this coverage. +Hints are server claims, not proof of safety, and schemas do not certify content. + +The proposed next direction is a protocol-neutral **Warden**: human-approved tool +versions and bounded actions, with untrusted inputs kept separate from authority. +The [upgrade plan and checkpoint proposal](docs/plans/2026-10-02-tool-integrity-upgrade.md) +maps prompts, retrieval, code execution, and serverless adapters to existing Agent +Trust Kernel work. Those broader checkpoints are proposals, not shipped guarantees. + > ⚠️ **Install `mcp-warden-cli`, not `mcp-warden`.** The PyPI name `mcp-warden` is > an **unrelated package by a different author** — it is not this project. The > correct install is `pip install mcp-warden-cli` (the CLI command is still -> `mcp-warden`). Or use the [GitHub Action](#github-action-one-step-drop-in) / a -> git-pinned install. +> `mcp-warden`). Or use the [GitHub Action](#github-action-one-step-drop-in) / a git-pinned install. If you already follow the published guidance — *pin versions, hash tool definitions, alert on drift* — mcp-warden is the deterministic tool that does it. @@ -380,51 +393,13 @@ For stdio, `` is passed to the OS as an **argv array, never throu shell.** `--url` instead connects to an already-running Streamable HTTP endpoint and is mutually exclusive with a server command. Set `WARDEN_LOG_LEVEL=INFO` for diagnostics. -### Runtime result inspection (v0.3 — blocks by default) - -`guard` sits transparently between an MCP client and server and inspects tool *results*. -**As of v0.3 the deterministic tier blocks out of the box** (council-established field -false-positive rate ~0): - -```bash -# Default: ANSI is stripped in place; echoed secrets + exfil domains are error-replaced; -# a mid-session tools/list swap that diverges from warden.lock is blocked (needs --lock); -# an argument-policy deny is blocked (needs --policy). The fuzzy injection tier stays log-only. -mcp-warden guard node ./build/index.js --lock warden.lock --policy policy.yaml --sarif guard.sarif - -# Observe-first rollout: --audit-only restores full v0.2 shadow in one flag (detect + log only). -mcp-warden guard node ./build/index.js --lock warden.lock --audit-only - -# Opt a single category back to shadow (still detected/logged/SARIF, frame forwarded): -mcp-warden guard node ./build/index.js --no-block-ansi --allow-exfil-domain -# Or shadow the whole deterministic tier + both gates: -mcp-warden guard node ./build/index.js --no-block-deterministic -# Opt INTO the fuzzy injection tier (never default): -mcp-warden guard node ./build/index.js --block-inject-phrase - -# Fail-CLOSED (high-security): TERMINATE the session (exit 3, -32003 to the client) if an -# internal inspection (result / argument-policy / tools-list) cannot complete, instead of the -# default fail-open pass-through. Opt-in; integrity over availability. -mcp-warden guard node ./build/index.js --lock warden.lock --policy policy.yaml --strict - -# Re-analyze a recorded session offline with the identical rule catalog (always report-only): -mcp-warden inspect session.trace.jsonl --lock warden.lock --sarif inspect.sarif -``` +### Runtime result inspection -**Flag scheme:** opt-out is canonical `--no-block-` -(`ansi|secret-echo|exfil-domain|list-changed|policy`, plus `--no-block-deterministic` for the -whole tier); `--allow-exfil-domain` is the sole affirmative alias. Precedence: -`--audit-only` > `--no-block-*` > default-block / `--block-inject-phrase`. The v0.2 -`--block-*` enable flags are accepted but **inert no-ops** (one-line stderr deprecation note), -so old scripts keep working. **`--strict`** (opt-in, default off) trades availability for -integrity: an internal inspection error at the result / argument-policy / tools-list layer -**terminates the session** (exit `3`, `-32003` non-retriable error to the client) instead of -failing open — framing/EOF/over-cap stay fail-open in all modes (known limitation). Reserved -error codes: **`-32001`** (policy/result block), **`-32002`** (transport/lifecycle), **`-32003`** -(`--strict` abort, non-retriable). See -[`docs/RESULT_INSPECTION.md`](docs/RESULT_INSPECTION.md), -[`docs/GUARD_PROXY.md`](docs/GUARD_PROXY.md), and -[`docs/GUARD_PROXY_V3.md`](docs/GUARD_PROXY_V3.md). +`guard` inspects stdio tool results and blocks deterministic hazards by default. +`--audit-only` restores observation; `--strict` terminates on inspection errors. +Framing errors still fail open. Prompt-injection phrase matching remains monitor-only. +See [runtime examples](docs/archive/2026-10-02-runtime-cli-examples.md) and the +[guard contract](docs/GUARD_PROXY_V3.md) for flags, reserved errors, and limitations. --- @@ -487,7 +462,8 @@ fallback evidence, rollback-resistant state, the recovery latch, and any whole-k claim remain incomplete. See [`docs/POLICY_ENFORCEMENT.md`](docs/POLICY_ENFORCEMENT.md) and [`docs/AGENT_TRUST_KERNEL.md`](docs/AGENT_TRUST_KERNEL.md). -See [`DOCUMENTATION_INDEX.md`](DOCUMENTATION_INDEX.md). The security-contract specs +See [`DOCUMENTATION_INDEX.md`](DOCUMENTATION_INDEX.md) and +[`SYSTEM_CONTEXT_DIAGRAM.md`](SYSTEM_CONTEXT_DIAGRAM.md). The security-contract specs under `docs/` (including [`GUARD_PROXY_V3.md`](docs/GUARD_PROXY_V3.md) for the v0.3 default-block + lifecycle contract) are the source of truth for every algorithm; the schemas in `warden.lock` and the SARIF output match them byte-for-byte. @@ -495,7 +471,7 @@ schemas in `warden.lock` and the SARIF output match them byte-for-byte. ## Tests ```bash -.venv/bin/python -m pytest -q +PATH="$PWD/.venv/bin:$PATH" .venv/bin/python -m pytest -q ``` The headline test is a real stdio round-trip: spawn the clean fixture → `pin` → diff --git a/SYSTEM_CONTEXT_DIAGRAM.md b/SYSTEM_CONTEXT_DIAGRAM.md index cb16d72..c246179 100644 --- a/SYSTEM_CONTEXT_DIAGRAM.md +++ b/SYSTEM_CONTEXT_DIAGRAM.md @@ -1,5 +1,15 @@ # mcp-warden — System Context Diagram +Last Updated: 2026-10-02 + +**Schema level 4** extends capture/lock/check to complete tool annotations and output +schemas, with structural output drift and Python/TypeScript parity. The existing +tools/list gate compares those commitments for v4 locks; legacy locks retain narrower +runtime coverage and require re-pin for the new fields. Annotations grant no authority. +The [Warden checkpoint proposal](docs/plans/2026-10-02-tool-integrity-upgrade.md) +shows a future kernel beneath prompt/retrieval/code/serverless adapters with signing +authority outside agent-editable state. It does not expand current runtime claims. + Where mcp-warden sits, what it talks to, and where its outputs go. The **definition-only path introduced in v0.1** (`pin`/`check`/`policy`) is read-only: it captures the *declared* surface and writes a baseline + machine reports — no proxy, no runtime @@ -177,8 +187,8 @@ sequenceDiagram end ``` -> `compute_drift` structurally classifies tool `inputSchema` changes via the normalized -> `schema_skeleton` stored in the lock (`schema_version` 3 — skeleton added at v2, in-document +> `compute_drift` structurally classifies tool `inputSchema` and v4 `outputSchema` changes via the normalized +> `schema_skeleton` stored in the lock (`schema_version` 4 — annotations/output added at v4, skeleton added at v2, in-document > `$ref` resolution at v3, #29): each security-relevant mutation is a per-fact > `WRD-DRIFT-SCHEMA-*` item (`docs/WARDEN_LOCK_SCHEMA.md` §6.2). v1 locks fall > back to a single high-severity `schema-modified` until re-pinned. diff --git a/action/requirements.lock b/action/requirements.lock index 0b66d15..cb3ad8b 100644 --- a/action/requirements.lock +++ b/action/requirements.lock @@ -359,9 +359,9 @@ pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via rich -pyjwt==2.13.0 \ - --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ - --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 +pyjwt==2.15.1 \ + --hash=sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193 \ + --hash=sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8 # via mcp python-multipart==0.0.32 \ --hash=sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e \ diff --git a/docs/GUARD_PROXY.md b/docs/GUARD_PROXY.md index acc403a..9e3525a 100644 --- a/docs/GUARD_PROXY.md +++ b/docs/GUARD_PROXY.md @@ -1,5 +1,10 @@ # mcp-warden — Guard Proxy Contract (v0.3) +**V4 lock addendum (DSE-1539):** The existing inline tools/list drift gate also +compares complete annotation/output-schema hashes for schema-level-4 baselines. +Legacy locks retain description/input-schema coverage until reviewed and re-pinned. +The same strict/audit-only/opt-out semantics apply; annotations grant no permissions. + **Status:** v0.3 security contract. Implementation-ready. **Extends — does not replace —** the v0.2 contract below; v0.2 statements hold except where a `v0.3` note overrides them (default posture in §5; proxy hardening in [`GUARD_PROXY_V3.md`](GUARD_PROXY_V3.md)). diff --git a/docs/SIGNING.md b/docs/SIGNING.md index 4bb0b33..beace28 100644 --- a/docs/SIGNING.md +++ b/docs/SIGNING.md @@ -1,5 +1,11 @@ # Sigstore signing + verification of `warden.lock` (#16) +Schema level 4 includes tool annotation/output commitments in the signed surface +digest. Older signatures authenticate their old digest only; v4 requires review, +re-pin, and re-sign. Signature verification against a pinned signer identity/issuer +does not certify code, policy, findings, or returned content as safe, or grant runtime +permission. See the [human checkpoint proposal](plans/2026-10-02-tool-integrity-upgrade.md). + mcp-warden can **Sigstore-sign** the identity of a pinned surface and later **cryptographically verify** that signature in CI — keyless (Fulcio short-lived certs + Rekor transparency log), no long-lived private keys to manage. diff --git a/docs/SPEC.md b/docs/SPEC.md index 1242e41..baec485 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -157,6 +157,22 @@ hashes. This keeps the file small, reviewable, and free of any secret material. ### 7.1 Tool entry (array sorted by `name`) +Schema level 4 commits `name`, `description`, `inputSchema`, the complete `annotations` +object, and `outputSchema`, together with derived capability/skeleton/inspection +fields. Top-level `title`, `icons`, and `_meta` remain excluded. Levels 1–3 committed +only name/description/inputSchema from the raw Tool; missing old commitments MUST +NOT be treated as historical null metadata. + +`annotations_hash = hash(annotations)`; `output_schema_hash = hash(outputSchema)`. +Absent/null values MUST hash JSON **null**, not `{}`; empty objects are distinct. +Non-null values MUST be objects, otherwise capture/verification refuses them. +`output_schema_skeleton` is the §7.5 extraction of a present schema, or null when +absent. It participates in entry_digest. V4 readers MUST require both hash strings +and the skeleton field (which may be null). Raw metadata is not added to reports. +Annotations are untrusted server declarations, never authority or proof of safety; +output schemas do not certify returned content. + + ```jsonc { "name": "read_file", @@ -164,6 +180,9 @@ hashes. This keeps the file small, reviewable, and free of any secret material. "input_schema_hash": "sha256:...", // §5.1 "capabilities": ["fs-read"], // §7.4 derived flags, sorted, deduped "schema_skeleton": { ... }, // §7.5 structural facts, or null + "annotations_hash": "sha256:...", // v4; absent/null -> hash(null) + "output_schema_hash": "sha256:...", // v4; absent/null -> hash(null) + "output_schema_skeleton": { ... }, // v4; absent/null -> null "inspection": { ... }, // §11 OPTIONAL per-tool inspection block "entry_digest": "sha256:..." // §7.3 } @@ -226,6 +245,14 @@ blob-level schema-modified classification until re-pinned. ## 8. Overall digest and drift +V4 annotation changes emit high `tool-annotations-modified`. Output addition/removal +emits high `schema-out-added`/`schema-out-removed`; other changes reuse §8.3 +structural classes with `schema-out-` replacing `schema-`. Cosmetic-only changes +emit low `schema-out-cosmetic-modified`; missing skeletons fall back to high +`schema-out-modified`. SARIF prefixes these with `WRD-DRIFT-`, including +`WRD-DRIFT-SCHEMA-OUT-TYPE-BROADENED`. + + ### 8.1 Overall digest ``` @@ -522,6 +549,11 @@ skeleton of affected tools → `entry_digest` → `overall_digest`, so each was silent surface change. A producer MUST NOT change any hashed field's derivation without bumping `schema_version`. +**3→4** adds annotations_hash, output_schema_hash, and output_schema_skeleton to +every tool entry, including canonical null commitments. Fresh entry digests change +and require review/re-attestation. Python and TypeScript implement the same level; +conformance includes genuine v3 bytes and malformed v4 omissions. + **14.3 How consumers are notified / how old locks are handled.** Because `schema_version` lives **inside** `overall_digest`, a format bump deterministically changes the digest, and on an approved baseline a verifier surfaces it as the **same** high `unapproved-change` @@ -534,6 +566,11 @@ degrade gracefully: a baseline lacking a `schema_skeleton` falls back to the coa `schema-modified` (high) until re-pinned (§7.5, §8.3). Additive migration advisories never DOWNGRADE a finding. +For v4, an **unapproved** older baseline also receives a migration finding and fails: +missing old commitments cannot be reported as v4 coverage. Approved locks retain +high unapproved-change plus the advisory. Reading or rotating old documents preserves +their recorded schema rather than silently converting them to v4. + **14.4 Newer levels.** A reader MUST reject (fail closed) a lock whose `schema_version` is **above** the level it implements. It cannot reproduce a derivation it does not know, and comparing such a lock under an older level's rules would silently mis-verify — the diff --git a/docs/WARDEN_LOCK_SCHEMA.md b/docs/WARDEN_LOCK_SCHEMA.md index dabf0fc..1b90d3b 100644 --- a/docs/WARDEN_LOCK_SCHEMA.md +++ b/docs/WARDEN_LOCK_SCHEMA.md @@ -2,6 +2,23 @@ **Status:** v0.1 security contract + v0.2 per-tool inspection addendum (§11). Implementation-ready. + +**Schema level 4 (DSE-1539):** Fresh tool entries add annotations_hash, +output_schema_hash, and output_schema_skeleton to their hashed body. The hashes +commit complete wire objects with JCS/SHA-256; absent/null is JSON null, distinct +from {}. Non-null non-object values are refused. The output skeleton uses the input +extraction, participates in entry_digest, and is null when the schema is absent. +V4 readers require both hash strings and the skeleton field. Historical v1–v3 +documents retain their fields and remain readable; fresh v4 bodies extend the +earlier formulas below. [SPEC §7.1](SPEC.md#71-tool-entry-array-sorted-by-name) +defines the current field set and [§8](SPEC.md#8-overall-digest-and-drift) the new +annotation/output drift classes. + +Annotations remain untrusted declarations and grant no permissions. Output schemas +do not certify content. Raw metadata is not stored; top-level title/icons/_meta stay +excluded. Migration requires review/re-pin: approved legacy locks keep high +unapproved-change plus the advisory; unapproved legacy locks also get a migration +finding. Historical signatures cannot extend approval to the new commitments. **Purpose:** Define the on-disk baseline that `pin` writes and `check` verifies, the exact canonicalization + hashing so the two are bit-reproducible, and the precise definition of "drift." **§11 (v0.2)** adds optional, deterministic per-tool inspection diff --git a/docs/archive/2026-10-02-runtime-cli-examples.md b/docs/archive/2026-10-02-runtime-cli-examples.md new file mode 100644 index 0000000..c3fb5c4 --- /dev/null +++ b/docs/archive/2026-10-02-runtime-cli-examples.md @@ -0,0 +1,51 @@ +# Runtime CLI examples + +Moved from README on 2026-10-02 to keep the core overview within 500 lines. + +### Runtime result inspection (v0.3 — blocks by default) + +`guard` sits transparently between an MCP client and server and inspects tool *results*. +**As of v0.3 the deterministic tier blocks out of the box** (council-established field +false-positive rate ~0): + +```bash +# Default: ANSI is stripped in place; echoed secrets + exfil domains are error-replaced; +# a mid-session tools/list swap that diverges from warden.lock is blocked (needs --lock); +# an argument-policy deny is blocked (needs --policy). The fuzzy injection tier stays log-only. +mcp-warden guard node ./build/index.js --lock warden.lock --policy policy.yaml --sarif guard.sarif + +# Observe-first rollout: --audit-only restores full v0.2 shadow in one flag (detect + log only). +mcp-warden guard node ./build/index.js --lock warden.lock --audit-only + +# Opt a single category back to shadow (still detected/logged/SARIF, frame forwarded): +mcp-warden guard node ./build/index.js --no-block-ansi --allow-exfil-domain +# Or shadow the whole deterministic tier + both gates: +mcp-warden guard node ./build/index.js --no-block-deterministic +# Opt INTO the fuzzy injection tier (never default): +mcp-warden guard node ./build/index.js --block-inject-phrase + +# Fail-CLOSED (high-security): TERMINATE the session (exit 3, -32003 to the client) if an +# internal inspection (result / argument-policy / tools-list) cannot complete, instead of the +# default fail-open pass-through. Opt-in; integrity over availability. +mcp-warden guard node ./build/index.js --lock warden.lock --policy policy.yaml --strict + +# Re-analyze a recorded session offline with the identical rule catalog (always report-only): +mcp-warden inspect session.trace.jsonl --lock warden.lock --sarif inspect.sarif +``` + +**Flag scheme:** opt-out is canonical `--no-block-` +(`ansi|secret-echo|exfil-domain|list-changed|policy`, plus `--no-block-deterministic` for the +whole tier); `--allow-exfil-domain` is the sole affirmative alias. Precedence: +`--audit-only` > `--no-block-*` > default-block / `--block-inject-phrase`. The v0.2 +`--block-*` enable flags are accepted but **inert no-ops** (one-line stderr deprecation note), +so old scripts keep working. **`--strict`** (opt-in, default off) trades availability for +integrity: an internal inspection error at the result / argument-policy / tools-list layer +**terminates the session** (exit `3`, `-32003` non-retriable error to the client) instead of +failing open — framing/EOF/over-cap stay fail-open in all modes (known limitation). Reserved +error codes: **`-32001`** (policy/result block), **`-32002`** (transport/lifecycle), **`-32003`** +(`--strict` abort, non-retriable). See +[`docs/RESULT_INSPECTION.md`](../RESULT_INSPECTION.md), +[`docs/GUARD_PROXY.md`](../GUARD_PROXY.md), and +[`docs/GUARD_PROXY_V3.md`](../GUARD_PROXY_V3.md). + +--- diff --git a/docs/plans/2026-10-02-tool-integrity-upgrade.md b/docs/plans/2026-10-02-tool-integrity-upgrade.md new file mode 100644 index 0000000..a2b6c38 --- /dev/null +++ b/docs/plans/2026-10-02-tool-integrity-upgrade.md @@ -0,0 +1,127 @@ +# Tool Integrity Upgrade Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Detect changes to MCP tool annotations and output schemas, keeping human approval bound to the declared surface actually observed. + +**Architecture:** Extend the existing capture → canonical hashes → signed lock → drift pipeline at schema level 4. Reuse the structural schema classifier for output schemas and keep Python and TypeScript on one conformance corpus. Existing locks remain readable, but migration requires review and re-attestation; it cannot silently carry approval to newly covered fields. + +**Tech Stack:** Python/Pydantic, RFC 8785, SHA-256, MCP SDK 1.x/2.x, zero-dependency TypeScript verifier, pytest, Node test runner. + +--- + +## Brief and scope + +Ernest approved DSE-1539's integrity upgrade in the current session. The broader goal is to help agents protect humans and their own execution from malicious external software and content. This release strengthens declared-surface integrity; it does not claim semantic safety, full runtime mediation, or infection-free code/results. + +- Hash the complete `annotations` object and `outputSchema`; missing/null hashes canonical JSON null, while `{}` remains distinct. +- Preserve their wire fields through capture. Non-object non-null values are rejected rather than normalized into an empty trusted surface. +- Store annotation/output hashes and the output structural skeleton in every v4 tool entry, including absent-value hashes. Raw annotations/output schema are not added to reports. +- Annotation drift emits `tool-annotations-modified` (high); output changes reuse structural classes with the `schema-out-` prefix, plus added/removed/cosmetic/fallback classes. +- No new permission is derived from hints such as `readOnlyHint` or `destructiveHint`. +- Accept v1–v3 documents with legacy defaults; reject malformed v4 entries missing new hash fields. Keep existing unapproved-change and migration advisory semantics. +- Update TypeScript, shared vectors, committed fixture locks, docs, and meaningful regression tests together. +- Exclude pagination tuning, DSE-1538 capture error handling, HTTP guard, fleet services, signing-key deployment, and ongoing DSE-717 work. + +## Task 1: Reproduce and freeze the missing coverage + +**Files:** Create `tests/test_tool_integrity.py` and `tests/fixtures/tool_integrity_server.py`; reference `tests/fixtures/_sdk_compat.py`. + +1. Build baseline and changed surfaces with identical name/description/input schema but changed annotations or output schema. Assert the overall digest changes and drift is present. +2. Run `.venv/bin/python -m pytest tests/test_tool_integrity.py -q`; expect the new drift tests to fail under v3. +3. Cover absence/null equivalence, absent versus empty object, annotation removal, output-schema removal, structural versus cosmetic changes, redaction, and combined changes. +4. Add a real stdio fixture that reads its declared definition from a test file. Pin/check the same argv before/after changing only the declaration; assert exit 1 and the expected SARIF rules. + +## Task 2: Capture and hash the additional fields + +**Files:** Modify `src/mcp_warden/{models.py,capture.py,lockfile.py,__init__.py}`. + +1. Add `CapturedTool.annotations` and `CapturedTool.output_schema` as nullable object fields; preserve wire `annotations`/`outputSchema` with a raw-dictionary tools/list result before SDK model projection (including extensions and explicit nested nulls). +2. Set schema level to 4. Extend hashed entries with `annotations_hash = hash_value(tool.annotations)`, `output_schema_hash = hash_value(tool.output_schema)`, and `output_schema_skeleton = extract_skeleton(tool.output_schema)` when present, otherwise null. +3. Add optional entry fields for legacy parsing and require v4 hash fields during top-level validation. Preserve legacy serialization without injecting new null fields into old entries. +4. Recompute consistency and surface digests using the document's recorded schema version when inspecting existing locks; fresh builds use version 4. +5. Run focused capture/lock tests. Verify SDK default nulls do not become phantom annotation changes. + +## Task 3: Explain changes without granting authority + +**Files:** Modify `src/mcp_warden/drift.py` and `src/mcp_warden/guard_list_gate.py`; create `src/mcp_warden/drift_tool_metadata.py` if needed to keep changes readable. + +1. Compare annotation hashes only when the baseline has the v4 field; legacy coverage is represented by migration, not an invented historical annotation value. +2. Output hashes likewise compare only when baseline coverage exists. Null-hash transitions classify added/removed; structural skeleton changes reuse `diff_skeletons` and prefix `schema-` with `schema-out-`. +3. Use fixed annotation messages, structural details from the existing redaction path, and no raw annotation values. +4. Migration never weakens unapproved-change. Review old approved lock → new identical surface and old lock → changed surface separately. +5. Run drift/SARIF regression tests; commit the Python implementation after focused tests pass. +6. Extend the existing runtime list gate to the same v4 annotation/output hashes, retaining its legacy-lock and strict/audit/opt-out behavior. Prove a real metadata-only list change is blocked. This extends existing integrity coverage; it adds no new mediation or permission mechanism. + +## Task 4: Cross-language contract and independent examples + +**Files:** Modify `packages/lock-ts/src/{lock.ts,drift.ts}`, `vectors/tools/generate.py`, `tests/test_spec_vectors.py`, and `packages/lock-ts/test/vectors.test.ts`. + +1. Mirror object/null validation, hashing, stored field requirements, output skeletons, and drift rules exactly in TypeScript. Keep its public `verify` refusal as `LockFormatError`. +2. Extend both Python surface adapters to preserve the new fields. +3. Preserve a genuine approved v3 baseline with historical entry digests; do not merely relabel a v4 lock as v3. +4. Add vectors for hint flips/removal, output-schema add/remove/relax/cosmetic/refs, absent/null/empty semantics, malformed v4 fields, and real v3 migration. Retain independent assertions on expected classes so regeneration cannot hide missing detection. +5. Regenerate deliberately with `.venv/bin/python vectors/tools/generate.py`; run `.venv/bin/python -m pytest tests/test_spec_vectors.py -q` and `npm test --prefix packages/lock-ts`. Expect identical digests and ordered findings. + +## Task 5: Fixtures, documentation, and release evidence + +**Files:** Modify `tests/fixtures/clean.warden.lock`, additional current-version fixture locks if required, `docs/{SPEC.md,WARDEN_LOCK_SCHEMA.md,SIGNING.md}`, `README.md`, `SYSTEM_CONTEXT_DIAGRAM.md`, `DOCUMENTATION_INDEX.md`, and `CHANGELOG.md`. + +1. Re-pin the benign committed fixture explicitly at v4; retain historical fixtures used to prove migrations. +2. Document current hashed fields and exclusions (`title`, `icons`, `_meta`), null normalization, new drift classes, and re-attestation requirements. Clarify annotations are untrusted declarations and output schemas do not certify returned content. +3. Document the human-checkpoint proposal below as a proposal linked to existing content-envelope/PDP/PEP/receipt work, not a shipped capability. +4. Run full pytest with CI coverage configuration and the sigstore extra, pinned Ruff, TypeScript conformance, and clean-fixture gate pass/mutated-fixture block. +5. Obtain independent CSO verdict on the exact head; repair any findings, classify the final diff, and prepare the PR. Release must follow the existing security receipt/required-reviewer controls without inventing approval evidence. + +## Human–agent checkpoint proposal (not implemented by this upgrade) + +Ernest selected: **a reviewed tool version and its allowed actions until it changes**. + +SHA-256 fingerprints exact bytes; it does not encrypt them or identify an approver. A digital signature can bind an approved manifest to an explicitly trusted human/DSE signing identity. The private signing capability must remain outside the model and its tool-accessible environment; distributed verifiers receive public trust material, not a shared signing secret. The shipped Sigstore path already verifies lock surface signatures against a pinned identity and issuer, but it does not sign executable code, findings, policy, or tool results. + +The next design should bind a manifest to: exact executable/artifact digest and launch identity; tool-surface digest; allowed actions and bounded arguments; policy/rule-bundle digest; approver identity; generation, expiry, and revocation state. Verify these in a deterministic enforcement boundary before effects. Mismatch, missing evidence, expiry, or revocation must deny or quarantine. Tool-returned content remains untrusted data and cannot grant permissions or change trust roots. A signed result can establish provenance and exact bytes, never freedom from malicious intent. + +Reuse DSE-715 content envelopes, DSE-716 verified adapters/PDP/PEP, and DSE-717's unfinished signed receipts and protected state. Do not claim their whole-kernel enforcement is live or mutate the existing started ticket. A checkpoint is an authorization boundary, not a claim that a model is conscious or that all harmful behavior can be prevented. + +Primary references: [MCP annotations and their limits](https://blog.modelcontextprotocol.io/posts/2026-03-16-tool-annotations/), [Sigstore security model](https://docs.sigstore.dev/about/security/), [Sigstore verification](https://docs.sigstore.dev/cosign/verifying/verify/). + +## Growth direction: a protocol-neutral Warden (proposal) + +Ernest's intended boundary includes prompts, tool definitions/results, retrieval, executable code, segmented execution, and serverless handlers. Keep the shipped MCP-Warden name and compatibility today. Build toward a small shared deterministic kernel with protocol adapters, rather than embed MCP-specific assumptions in the trust model or launch a new fleet service prematurely. + +```mermaid +flowchart LR + Inputs[Prompts / retrieval / tools / code / serverless results] --> Adapters[Protocol and execution adapters] + Adapters --> Envelopes[Typed envelopes: origin, digest, classification, taint] + Envelopes --> Model[Agent planning: untrusted content stays data] + Human[Human-controlled approval and public trust roots] --> Policy[Verified manifests / bounded policy] + Model --> Intent[Typed requested action] + Intent --> Kernel[Deterministic decision and enforcement boundary] + Policy --> Kernel + Kernel --> Evidence[Durable decision evidence] + Evidence --> Effects[Permitted effects in isolated execution] + Effects --> Adapters +``` + +| Surface | Proposed checkpoint | What that checkpoint does not prove | +|---|---|---| +| Prompts and retrieval | Preserve source/taint; separate trusted instructions from retrieved content; prevent content from modifying authority | Semantic truth or complete prompt-injection detection | +| MCP and other tools | Verify approved surface and implementation digests before registration and use; bound arguments and allowed effects | That an author or tool will behave honestly | +| Generated/local executable code | Treat generated code as untrusted; verify execution identity and enforce filesystem/network/resource boundaries outside the model | That signing makes arbitrary code safe | +| Serverless execution | Verify exact deployed artifact, handler, role, policy, and invocation boundary; mediate effects where they occur | That a function name, URL, or successful TLS connection authenticates deployed code | +| Returned results | Preserve producer/transform provenance and untrusted taint; inspect deterministic hazards before model ingestion or release | That encrypted or signed content is non-malicious | +| Human approval | Bind approved tool version, exact allowed actions/policy, generation, expiry, and revocation; require new approval after material changes | Blanket future permission, permission to modify its own trust roots, or irreversible safety guarantees | + +The enforcement boundary must be outside model-editable prompts/state. Segmentation reduces consequences: retrieval workers do not inherit signing authority or unrestricted execution/network privileges; effectful workers receive only bounded capabilities. An agent may request approval or explain a denial, but cannot mint approvals, disable required checks, or strip taint merely by summarizing content. Runtime measurements must be bound to the exact artifact actually executed; checking a file and later executing a substituted one is not sufficient. + +Sequence: this v4 declared-surface upgrade → qualify the existing content-envelope and adapter/PDP/PEP foundations → complete signed receipts, anti-replay/rollback, revocation, and recovery evidence → qualify one non-MCP adapter → consider fleet control only after local enforcement is proved. Existing started DSE-717 and dependent DSE-1076 remain separately owned work. This plan does not implement those stages or claim that the shipped guard completely mediates every input or effect. + +## Adversarial pass and rollback + +- A malicious server can lie consistently: signatures and hashes catch unauthorized changes, not truthfulness. Keep behavioral inspection and bounded effects separate. +- A shared symmetric signing secret in agents would let a compromised agent impersonate the human approver. Distribute verification material only. +- Absent fields must not become `{}` or SDK-default hints; raw annotations must not leak into reports. +- Old locks cannot silently inherit v4 approval. Use genuine v3 bytes and keep migration as a blocking review boundary for approved locks. +- Output skeleton tampering must not excuse approved digest mismatch; malformed v4 field omission must be refused in both readers. +- Keep future-schema rejection and canonical depth bounds intact. +- Roll back package/code through the existing release path; retain old approved locks/signatures as historical evidence. A v3 verifier must reject v4 locks rather than pretend to verify them. No automatic downgrade or re-approval. diff --git a/examples/pinned-servers/server-everything/README.md b/examples/pinned-servers/server-everything/README.md index b8e0328..d6c4a39 100644 --- a/examples/pinned-servers/server-everything/README.md +++ b/examples/pinned-servers/server-everything/README.md @@ -41,3 +41,8 @@ Exit 0 = surface matches the lock. Exit 1 = drift. Exit 2 = capture error. capability annotation, not drift — `check` reproduces it identically. - `command_digest` hashes the literal launch argv (`npx … @2026.1.26`), not the resolved binary, so this lock verifies identically on any machine. + +The committed example was re-captured at **schema level 4** on 2026-10-02, +including complete tool annotations and output schemas. The approver is an +obviously synthetic example identity; this lock is a reproducible demonstration, +not an authenticated human authorization receipt. diff --git a/examples/pinned-servers/server-everything/warden.lock b/examples/pinned-servers/server-everything/warden.lock index 20d1943..e0529ab 100644 --- a/examples/pinned-servers/server-everything/warden.lock +++ b/examples/pinned-servers/server-everything/warden.lock @@ -1,12 +1,13 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-everything@2026.1.26" ], + "url": null, "command_digest": "sha256:898034042c967913541d5b6e16f114b0ea1c6fabc21f4160b9d3a87cc082d653" }, "tools": [ @@ -39,7 +40,10 @@ } } }, - "entry_digest": "sha256:d22a7a6753697be7ca343b021b35af35757193823a87415a9e82d6673fc7d328" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:dce8914f8b582cb75d56da8bd8fc916abe06e5df26d7ad95dbd4346aba64a9bf" }, { "name": "get-annotated-message", @@ -84,7 +88,10 @@ } } }, - "entry_digest": "sha256:7964848eae36b809db099dbb715712a481310fd2d89fa277260b2e08b6d543a2" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:2777d1a49e6520b25e78b7d1c79809a72490da9e129310ad1a2ecb33ac66f57c" }, { "name": "get-env", @@ -105,7 +112,10 @@ } } }, - "entry_digest": "sha256:e459a105b99abfe16880dfb8126a94df1ec7b4b2814347f5be6f72cd6e1ed0cc" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:61ac09535bba78dbd05532657bbdae15919ff4e24d3e5941d0617ac34dcf231e" }, { "name": "get-resource-links", @@ -138,7 +148,10 @@ } } }, - "entry_digest": "sha256:26d498066afc20e5d7f358ff612b379a14423654a2c30ac440b01cf4b542bd43" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:8ec58ba06f82099d72d2e51cd46ba86c2d1a9ed409bed5c5531449c63fd705ca" }, { "name": "get-resource-reference", @@ -182,7 +195,10 @@ } } }, - "entry_digest": "sha256:92c65d2b1a76ea987b0da05f3a837062bc07f6f63f993a646eca5c5b5162241f" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:6911300313d4ce64fb6a19d18ce4c23971c0ee2115c59887c3adb42ef9a5022f" }, { "name": "get-structured-content", @@ -217,7 +233,53 @@ } } }, - "entry_digest": "sha256:9144fefef5e8907ac56fc43e8db0963d8a52d63fdf1fa17ac2fccdeec42f511f" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:c2b0083215a57373f29510b3ef2b41bf44c11505693062311190519759aba475", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "conditions": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "humidity": { + "type": [ + "number" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "temperature": { + "type": [ + "number" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:24c63ca2bd835f24a70ee485b646cee937dac2ec1a41e4a72b3706f713fa89a3" }, { "name": "get-sum", @@ -258,7 +320,10 @@ } } }, - "entry_digest": "sha256:aba41d49788f6a26131200bb4b2b85f8d0b223a7da811d462230643dc9abc3e6" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:cdd26b883fefcf70cdd7f5c11c05dfec8552928158a84d5ab5484543e75b6aaa" }, { "name": "get-tiny-image", @@ -279,7 +344,10 @@ } } }, - "entry_digest": "sha256:0e36b63338a6be99547aba091ff43817b4fcbd0d563f5824d6315c3b969c34b9" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:bfe06016d952b0e86bdeb7df4a55e35737365515e6bbed8e16070baaa0d9bd39" }, { "name": "gzip-file-as-resource", @@ -334,7 +402,10 @@ } } }, - "entry_digest": "sha256:6035f8ec75eb9957c78dffb5a8eac15fb62644d62aac09aea0f0f029c6b3f3c1" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:916ffa6bd1c0877833a7f4bd810b082518a9a08ae8f7fa3ae9a218fbb6beb07d" }, { "name": "simulate-research-query", @@ -377,7 +448,10 @@ } } }, - "entry_digest": "sha256:63edd49a9dad361fbfcec4b2b041ea5b4eafa4cbbc10e0325ddcf81e084cdddd" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:e8acf38d9849a2dbc27848994424f959814f8cdeea47534e9ed53f334fe80d45" }, { "name": "toggle-simulated-logging", @@ -398,7 +472,10 @@ } } }, - "entry_digest": "sha256:a3c18cf55f3830356e5b41427564573c986d6b6ce1db448d687f02ca4337945d" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:2b772d676235f6436af536c0e421b31b7b3ff93c0ac3a1db0e55134dfe9553db" }, { "name": "toggle-subscriber-updates", @@ -419,7 +496,10 @@ } } }, - "entry_digest": "sha256:2c2788b6edf991c3feb6b1a7e86563684f0b350deb5d07269c729b03c0aa6bed" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:666dccc13c710289519d6540cc60d4e7d6f36473c76c252612a54a204c90fe93" }, { "name": "trigger-long-running-operation", @@ -460,7 +540,10 @@ } } }, - "entry_digest": "sha256:7221da574c6c603d5b2ab0da019b316bb1296994558d841fc876dbd04d0d5987" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:61c1529d6d5817a86fe2291ca55f5addd73c4b513e36b6965f3a954248c834e9" } ], "resources": [ @@ -549,19 +632,19 @@ "snippet": "name token 'query'" } ], - "overall_digest": "sha256:c0d636a27f360f148534042e7fd39ccc22f93ac545a9f996c2e1f3b08d680d76", + "overall_digest": "sha256:59be2ada2e6ff15c6a749ba29a8a265015b73acc2ce4b9967d795549ef7b6c49", "pin": { - "created_at": "2026-06-10T23:46:45Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:57:26Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "examples@mcp-warden.invalid", - "approved_at": "2026-06-10T23:46:45Z", - "approved_digest": "sha256:c0d636a27f360f148534042e7fd39ccc22f93ac545a9f996c2e1f3b08d680d76", + "approved_at": "2026-10-02T02:57:26Z", + "approved_digest": "sha256:59be2ada2e6ff15c6a749ba29a8a265015b73acc2ce4b9967d795549ef7b6c49", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -570,9 +653,10 @@ "actor": "examples@mcp-warden.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-10T23:46:45Z", - "bound_digest": "sha256:c0d636a27f360f148534042e7fd39ccc22f93ac545a9f996c2e1f3b08d680d76", - "note": null + "created_at": "2026-10-02T02:57:26Z", + "bound_digest": "sha256:59be2ada2e6ff15c6a749ba29a8a265015b73acc2ce4b9967d795549ef7b6c49", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/examples/pinned-servers/server-memory/README.md b/examples/pinned-servers/server-memory/README.md index 18b7e43..2576506 100644 --- a/examples/pinned-servers/server-memory/README.md +++ b/examples/pinned-servers/server-memory/README.md @@ -45,3 +45,8 @@ Exit 0 = surface matches the lock. Exit 1 = drift. Exit 2 = capture error. digest does not depend on the host environment (timezone, locale, etc.). - `command_digest` hashes the literal launch argv (`npx … @2026.1.26`), not the resolved binary, so this lock verifies identically on any machine. + +The committed example was re-captured at **schema level 4** on 2026-10-02, +including complete tool annotations and output schemas. The approver is an +obviously synthetic example identity; this lock is a reproducible demonstration, +not an authenticated human authorization receipt. diff --git a/examples/pinned-servers/server-memory/warden.lock b/examples/pinned-servers/server-memory/warden.lock index 1b5b1a7..5ca81e4 100644 --- a/examples/pinned-servers/server-memory/warden.lock +++ b/examples/pinned-servers/server-memory/warden.lock @@ -1,12 +1,13 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-memory@2026.1.26" ], + "url": null, "command_digest": "sha256:586e40c1ab55ec9c6c0a471e55753aae13181a3d543d7c3af7f4fda5875b010a" }, "tools": [ @@ -79,7 +80,73 @@ } } }, - "entry_digest": "sha256:92f47f32935461fb5aa4bed3b4341b1e768a41b17c77d90b46db247b9a508771" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:c0a91d3961f360b126a83ba1e51ecb9bfe45d3b9f1e5105fc441b655575e8d60", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "results": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "results[]": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "results[].addedObservations": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "results[].addedObservations[]": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "results[].entityName": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:b39d41b7225375035e5744916253e4df4d244ddc30f902493fa65745b40821bc" }, { "name": "create_entities", @@ -160,14 +227,9 @@ } } }, - "entry_digest": "sha256:278407f89c2b86c9b9116e6ae10d7f132480df4313ba44c11debb58132dcd997" - }, - { - "name": "create_relations", - "description_hash": "sha256:e2dd4540b9030148fe495bef81941f9524224115944a44a27a1b24c7b22054c1", - "input_schema_hash": "sha256:5df4fdc93cbf199dff73ccc802d5cca510bd37eb7d99e0949fc0a17b261a65d3", - "capabilities": [], - "schema_skeleton": { + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:69d0a98c9c47b02782d7e7933246ab1e0608b6394f93a862e81f9bf1a0e4491c", + "output_schema_skeleton": { "props": { "$root": { "type": [ @@ -176,10 +238,10 @@ "required": false, "enum": null, "constraints": { - "additionalProperties": true + "additionalProperties": false } }, - "relations": { + "entities": { "type": [ "array" ], @@ -189,17 +251,17 @@ "additionalProperties": true } }, - "relations[]": { + "entities[]": { "type": [ "object" ], "required": false, "enum": null, "constraints": { - "additionalProperties": true + "additionalProperties": false } }, - "relations[].from": { + "entities[].entityType": { "type": [ "string" ], @@ -209,7 +271,7 @@ "additionalProperties": true } }, - "relations[].relationType": { + "entities[].name": { "type": [ "string" ], @@ -219,24 +281,34 @@ "additionalProperties": true } }, - "relations[].to": { + "entities[].observations": { "type": [ - "string" + "array" ], "required": true, "enum": null, "constraints": { "additionalProperties": true } + }, + "entities[].observations[]": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } } } }, - "entry_digest": "sha256:ea6bebbf3b63ae5477282cbc739f06c7f75cd501933cdb5c76b3a0966f9f1ce7" + "entry_digest": "sha256:5510f7f453a489541ff9326a1b01f399fd02295700252a02c2b3e9a67a31e305" }, { - "name": "delete_entities", - "description_hash": "sha256:086222eb6cb791391cb6ad23a4edca11ec2879f4fa993c079ae8906113fd05a6", - "input_schema_hash": "sha256:a1bac527df1c3bd311c9c2bf2e9a9cf8ae5e266b690bd6c97d4a62e4af2dc731", + "name": "create_relations", + "description_hash": "sha256:e2dd4540b9030148fe495bef81941f9524224115944a44a27a1b24c7b22054c1", + "input_schema_hash": "sha256:5df4fdc93cbf199dff73ccc802d5cca510bd37eb7d99e0949fc0a17b261a65d3", "capabilities": [], "schema_skeleton": { "props": { @@ -250,7 +322,7 @@ "additionalProperties": true } }, - "entityNames": { + "relations": { "type": [ "array" ], @@ -260,26 +332,51 @@ "additionalProperties": true } }, - "entityNames[]": { + "relations[]": { "type": [ - "string" + "object" ], "required": false, "enum": null, "constraints": { "additionalProperties": true } + }, + "relations[].from": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[].relationType": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[].to": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } } } }, - "entry_digest": "sha256:8eefdb47816e921409a737708f22f737be4f75da910dfb2373d19f1e1718fdc4" - }, - { - "name": "delete_observations", - "description_hash": "sha256:30ffb2113bd8f0f022ccbb7a31d2ca610b5bb961cfd2677c4ebe492fc8f5fc97", - "input_schema_hash": "sha256:6b96624fc9e8cfbb92d8c7672e16fe06218216634815f892c8027cf7162f8ea7", - "capabilities": [], - "schema_skeleton": { + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:e08835c944f747c9202c316eb7c8fa583dfd4d9c8a0f29b7d2b64e7ab059daea", + "output_schema_skeleton": { "props": { "$root": { "type": [ @@ -288,10 +385,10 @@ "required": false, "enum": null, "constraints": { - "additionalProperties": true + "additionalProperties": false } }, - "deletions": { + "relations": { "type": [ "array" ], @@ -301,17 +398,17 @@ "additionalProperties": true } }, - "deletions[]": { + "relations[]": { "type": [ "object" ], "required": false, "enum": null, "constraints": { - "additionalProperties": true + "additionalProperties": false } }, - "deletions[].entityName": { + "relations[].from": { "type": [ "string" ], @@ -321,9 +418,9 @@ "additionalProperties": true } }, - "deletions[].observations": { + "relations[].relationType": { "type": [ - "array" + "string" ], "required": true, "enum": null, @@ -331,11 +428,11 @@ "additionalProperties": true } }, - "deletions[].observations[]": { + "relations[].to": { "type": [ "string" ], - "required": false, + "required": true, "enum": null, "constraints": { "additionalProperties": true @@ -343,12 +440,12 @@ } } }, - "entry_digest": "sha256:f028917cf289eb3b539e6fb3b6f016cc6ce9f60843fc79f494f41dad4c644eb6" + "entry_digest": "sha256:663d314e670bf13e7e1ba674abd399e1880c4058656b589174c09b412db3efe2" }, { - "name": "delete_relations", - "description_hash": "sha256:ee6a0e5cd6554a9a84afd7fe11f50a474aec5c3fa0e989d431abcaa078144f37", - "input_schema_hash": "sha256:17985b44074691e832f8548bf2efc55af3256f9c52d3398c8ddc20141f2b0697", + "name": "delete_entities", + "description_hash": "sha256:086222eb6cb791391cb6ad23a4edca11ec2879f4fa993c079ae8906113fd05a6", + "input_schema_hash": "sha256:a1bac527df1c3bd311c9c2bf2e9a9cf8ae5e266b690bd6c97d4a62e4af2dc731", "capabilities": [], "schema_skeleton": { "props": { @@ -362,7 +459,7 @@ "additionalProperties": true } }, - "relations": { + "entityNames": { "type": [ "array" ], @@ -372,27 +469,33 @@ "additionalProperties": true } }, - "relations[]": { + "entityNames[]": { "type": [ - "object" + "string" ], "required": false, "enum": null, "constraints": { "additionalProperties": true } - }, - "relations[].from": { + } + } + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:63668c025b3b06891519aa283e0a1a7c29822865cda9308585b4d8a0d72dd553", + "output_schema_skeleton": { + "props": { + "$root": { "type": [ - "string" + "object" ], - "required": true, + "required": false, "enum": null, "constraints": { - "additionalProperties": true + "additionalProperties": false } }, - "relations[].relationType": { + "message": { "type": [ "string" ], @@ -402,9 +505,9 @@ "additionalProperties": true } }, - "relations[].to": { + "success": { "type": [ - "string" + "boolean" ], "required": true, "enum": null, @@ -414,12 +517,12 @@ } } }, - "entry_digest": "sha256:d255bf676b17b0cc772b093ea4a1b3885e9612d5d9922928bac5f658245834f5" + "entry_digest": "sha256:cf2127372189b68a3ee2987124be38b34ca8ba59cafcb4ba0c0fdb019af72641" }, { - "name": "open_nodes", - "description_hash": "sha256:d4df7ff09e4b7e42a029d505efe4b8881e10f24f16ca9e33c5f3c3cea6484e48", - "input_schema_hash": "sha256:70aeb8f52abf632f4798453c1b731e1b3e0c8f967e1fdbfb4b00a8d193e5a1c0", + "name": "delete_observations", + "description_hash": "sha256:30ffb2113bd8f0f022ccbb7a31d2ca610b5bb961cfd2677c4ebe492fc8f5fc97", + "input_schema_hash": "sha256:6b96624fc9e8cfbb92d8c7672e16fe06218216634815f892c8027cf7162f8ea7", "capabilities": [], "schema_skeleton": { "props": { @@ -433,7 +536,7 @@ "additionalProperties": true } }, - "names": { + "deletions": { "type": [ "array" ], @@ -443,7 +546,37 @@ "additionalProperties": true } }, - "names[]": { + "deletions[]": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "deletions[].entityName": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "deletions[].observations": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "deletions[].observations[]": { "type": [ "string" ], @@ -455,14 +588,9 @@ } } }, - "entry_digest": "sha256:a9861a45ac8ed95b5f5dbef6d7849a5191e8849de3527b97c81bcc58168d86f3" - }, - { - "name": "read_graph", - "description_hash": "sha256:9701aab08ebf30e3248ebf4cfb80b17ff537834c2ccb4edbfeb24a48e5e42875", - "input_schema_hash": "sha256:7a014b717a77aac971ea0ab5c0ff47bb13e6cae7ef9442d9dedb15ce36381b15", - "capabilities": [], - "schema_skeleton": { + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:63668c025b3b06891519aa283e0a1a7c29822865cda9308585b4d8a0d72dd553", + "output_schema_skeleton": { "props": { "$root": { "type": [ @@ -470,21 +598,39 @@ ], "required": false, "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "message": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "success": { + "type": [ + "boolean" + ], + "required": true, + "enum": null, "constraints": { "additionalProperties": true } } } }, - "entry_digest": "sha256:333004822337e7a2767f8e243deba0730e7bdf990c6dd9191c812e24b64bc425" + "entry_digest": "sha256:878ba19cc377fa65683643a9cf3c1f2444a381423129eae2b6c3e5b1e81546c4" }, { - "name": "search_nodes", - "description_hash": "sha256:17d75cae4af864a9fcc3adf96f9d209af1f758cf6849e3cb4088b3f1786b6631", - "input_schema_hash": "sha256:71205bf235256b270ba5b0e20bdc6729c241f73ac81828077c76630fbcbaf6b6", - "capabilities": [ - "sql-query" - ], + "name": "delete_relations", + "description_hash": "sha256:ee6a0e5cd6554a9a84afd7fe11f50a474aec5c3fa0e989d431abcaa078144f37", + "input_schema_hash": "sha256:17985b44074691e832f8548bf2efc55af3256f9c52d3398c8ddc20141f2b0697", + "capabilities": [], "schema_skeleton": { "props": { "$root": { @@ -497,7 +643,556 @@ "additionalProperties": true } }, - "query": { + "relations": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[]": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[].from": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[].relationType": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[].to": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:63668c025b3b06891519aa283e0a1a7c29822865cda9308585b4d8a0d72dd553", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "message": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "success": { + "type": [ + "boolean" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:561df016fc0c3093f22a294e1a34a2e0dd9246da5a3bd3db8d7d838afdccc887" + }, + { + "name": "open_nodes", + "description_hash": "sha256:d4df7ff09e4b7e42a029d505efe4b8881e10f24f16ca9e33c5f3c3cea6484e48", + "input_schema_hash": "sha256:70aeb8f52abf632f4798453c1b731e1b3e0c8f967e1fdbfb4b00a8d193e5a1c0", + "capabilities": [], + "schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "names": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "names[]": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:b9d498d6e8a81d411860e12b23145781b1487d0f70ac9f20e9ee4e40597dcde5", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "entities": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "entities[]": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "entities[].entityType": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "entities[].name": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "entities[].observations": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "entities[].observations[]": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[]": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "relations[].from": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[].relationType": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[].to": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:69ef6f8b6f6592c934347f64bc2740c48dc9ee48b03b739f96fe86117d3fd10d" + }, + { + "name": "read_graph", + "description_hash": "sha256:9701aab08ebf30e3248ebf4cfb80b17ff537834c2ccb4edbfeb24a48e5e42875", + "input_schema_hash": "sha256:7a014b717a77aac971ea0ab5c0ff47bb13e6cae7ef9442d9dedb15ce36381b15", + "capabilities": [], + "schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:b9d498d6e8a81d411860e12b23145781b1487d0f70ac9f20e9ee4e40597dcde5", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "entities": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "entities[]": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "entities[].entityType": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "entities[].name": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "entities[].observations": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "entities[].observations[]": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[]": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "relations[].from": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[].relationType": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[].to": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:8a68dd1367e11c5c6a66c444cfdb3bc2f40591dda53ef3e50294e2da280f92d9" + }, + { + "name": "search_nodes", + "description_hash": "sha256:17d75cae4af864a9fcc3adf96f9d209af1f758cf6849e3cb4088b3f1786b6631", + "input_schema_hash": "sha256:71205bf235256b270ba5b0e20bdc6729c241f73ac81828077c76630fbcbaf6b6", + "capabilities": [ + "sql-query" + ], + "schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "query": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:b9d498d6e8a81d411860e12b23145781b1487d0f70ac9f20e9ee4e40597dcde5", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "entities": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "entities[]": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "entities[].entityType": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "entities[].name": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "entities[].observations": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "entities[].observations[]": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[]": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "relations[].from": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[].relationType": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "relations[].to": { "type": [ "string" ], @@ -509,7 +1204,7 @@ } } }, - "entry_digest": "sha256:ac0728d41083b126dc13793fad4fb182a341b6abdf4c4aaa5e9a5ea963b34c4b" + "entry_digest": "sha256:37e41da2ee680bc3ceda6e241e17e081fe3d588bccd454125dc1b79f00b6a72c" } ], "resources": [], @@ -523,19 +1218,19 @@ "snippet": "property 'query'" } ], - "overall_digest": "sha256:99953175766c987bac7763bbf17ff9808781375b6b19c31e50862ac134f96385", + "overall_digest": "sha256:f8dfe8da6ca9a4103887c9e8a8f3014b3d652a27debe1560fa5744102ab923ab", "pin": { - "created_at": "2026-06-11T00:12:20Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:57:56Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "examples@mcp-warden.invalid", - "approved_at": "2026-06-11T00:12:20Z", - "approved_digest": "sha256:99953175766c987bac7763bbf17ff9808781375b6b19c31e50862ac134f96385", + "approved_at": "2026-10-02T02:57:56Z", + "approved_digest": "sha256:f8dfe8da6ca9a4103887c9e8a8f3014b3d652a27debe1560fa5744102ab923ab", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -544,9 +1239,10 @@ "actor": "examples@mcp-warden.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-11T00:12:20Z", - "bound_digest": "sha256:99953175766c987bac7763bbf17ff9808781375b6b19c31e50862ac134f96385", - "note": null + "created_at": "2026-10-02T02:57:56Z", + "bound_digest": "sha256:f8dfe8da6ca9a4103887c9e8a8f3014b3d652a27debe1560fa5744102ab923ab", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/examples/pinned-servers/server-sequential-thinking/README.md b/examples/pinned-servers/server-sequential-thinking/README.md index b53feb2..bfb5515 100644 --- a/examples/pinned-servers/server-sequential-thinking/README.md +++ b/examples/pinned-servers/server-sequential-thinking/README.md @@ -37,3 +37,8 @@ Exit 0 = surface matches the lock. Exit 1 = drift. Exit 2 = capture error. it never registers as drift. - `command_digest` hashes the literal launch argv, so this lock verifies identically on any machine. + +The committed example was re-captured at **schema level 4** on 2026-10-02, +including complete tool annotations and output schemas. The approver is an +obviously synthetic example identity; this lock is a reproducible demonstration, +not an authenticated human authorization receipt. diff --git a/examples/pinned-servers/server-sequential-thinking/warden.lock b/examples/pinned-servers/server-sequential-thinking/warden.lock index 6e5bbea..4cefbb2 100644 --- a/examples/pinned-servers/server-sequential-thinking/warden.lock +++ b/examples/pinned-servers/server-sequential-thinking/warden.lock @@ -1,12 +1,13 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "npx", "args": [ "-y", "@modelcontextprotocol/server-sequential-thinking@2025.12.18" ], + "url": null, "command_digest": "sha256:96a06b285fe3292c53860204b94bcadfe7ce68fbd79bd77baee6eaaf41111d6d" }, "tools": [ @@ -127,7 +128,83 @@ } } }, - "entry_digest": "sha256:2a4b66d61c405ac5be578cae31b0b41246afa751b4746fbfdce1f538c93a065a" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:b157dfbca04cf3213646a212215d93d3e049b333e61643eb5c7a9c628979ac30", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": false + } + }, + "branches": { + "type": [ + "array" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "branches[]": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "nextThoughtNeeded": { + "type": [ + "boolean" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "thoughtHistoryLength": { + "type": [ + "number" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "thoughtNumber": { + "type": [ + "number" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "totalThoughts": { + "type": [ + "number" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:04d37aae84ad9914578d20f64782469b62cd0967175c7040746a57f2374a0447" } ], "resources": [], @@ -141,19 +218,19 @@ "snippet": "mode…(len=47)" } ], - "overall_digest": "sha256:714be5f26176d84f3804d5f91643b5d782f2a3b82d7e5e1e41f986b833395d1d", + "overall_digest": "sha256:c635c79cc9382a28736668b7b7d4a2f75a752e951d95f1affa434ed1b24b6b80", "pin": { - "created_at": "2026-06-10T23:49:05Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:57:57Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "examples@mcp-warden.invalid", - "approved_at": "2026-06-10T23:49:05Z", - "approved_digest": "sha256:714be5f26176d84f3804d5f91643b5d782f2a3b82d7e5e1e41f986b833395d1d", + "approved_at": "2026-10-02T02:57:57Z", + "approved_digest": "sha256:c635c79cc9382a28736668b7b7d4a2f75a752e951d95f1affa434ed1b24b6b80", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -162,9 +239,10 @@ "actor": "examples@mcp-warden.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-10T23:49:05Z", - "bound_digest": "sha256:714be5f26176d84f3804d5f91643b5d782f2a3b82d7e5e1e41f986b833395d1d", - "note": null + "created_at": "2026-10-02T02:57:57Z", + "bound_digest": "sha256:c635c79cc9382a28736668b7b7d4a2f75a752e951d95f1affa434ed1b24b6b80", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/packages/lock-ts/README.md b/packages/lock-ts/README.md index f9f4ab6..2031dbf 100644 --- a/packages/lock-ts/README.md +++ b/packages/lock-ts/README.md @@ -1,5 +1,10 @@ # @mcp-warden/lock +Implements schema level **4**, including complete tool annotations and output schemas. +Missing/null metadata hashes JSON null; empty objects differ. Hint changes and output +schema drift fail verification. Older locks remain readable but require review/re-pin +to acquire the new coverage; annotations are declarations and grant no authority. + Zero-dependency **verifier** for [MCP Lock Format v1](../../docs/SPEC.md) — the `warden.lock` baseline that [mcp-warden](https://github.com/DataScience-EngineeringExperts/mcp-warden) pins an MCP server's declared tool/resource/prompt surface into. diff --git a/packages/lock-ts/src/drift.ts b/packages/lock-ts/src/drift.ts index b8820fc..9fe6b3b 100644 --- a/packages/lock-ts/src/drift.ts +++ b/packages/lock-ts/src/drift.ts @@ -8,6 +8,7 @@ import type { Lock, LockPromptEntry, LockResourceEntry, LockToolEntry, BuiltLock, BuiltTool } from "./lock.js"; import { cmpCodepoint, deepEqual, pyJsonDumps, pyRepr, pyStr } from "./py.js"; import type { PropFacts, Skeleton } from "./skeleton.js"; +import { hashValue } from "./digest.js"; export interface SchemaChange { path: string; @@ -226,6 +227,7 @@ function diffTools(baseline: LockToolEntry[], current: BuiltTool[]): DriftItem[] const schemaChanged = b.input_schema_hash !== c.input_schema_hash; if (schemaChanged) items.push(...diffToolSchema(name, target, b, c)); + items.push(...diffToolMetadata(name, target, b, c)); const bCaps = new Set(b.capabilities); const cCaps = new Set(c.capabilities); @@ -246,6 +248,29 @@ function diffTools(baseline: LockToolEntry[], current: BuiltTool[]): DriftItem[] return items; } +function diffToolMetadata(name: string, target: string, b: LockToolEntry, c: BuiltTool): DriftItem[] { + const items: DriftItem[] = []; + const add = (cls: string, severity: string, message: string, detail: string | null = null) => + items.push(item(cls, severity, target, `Tool '${name}' ${message}`, detail)); + if (b.annotations_hash !== null && b.annotations_hash !== c.annotations_hash) { + add("tool-annotations-modified", "high", "annotations changed (server declarations, not authority)"); + } + if (b.output_schema_hash === null || b.output_schema_hash === c.output_schema_hash) return items; + const nullHash = hashValue(null); + if (b.output_schema_hash === nullHash) add("schema-out-added", "high", "outputSchema added"); + else if (c.output_schema_hash === nullHash) add("schema-out-removed", "high", "outputSchema removed"); + else if (b.output_schema_skeleton === null || c.output_schema_skeleton === null) add("schema-out-modified", "high", "outputSchema changed"); + else { + const changes = diffSkeletons(b.output_schema_skeleton, c.output_schema_skeleton); + if (!changes.length) add("schema-out-cosmetic-modified", "low", "outputSchema changed cosmetically (no structural change)"); + for (const ch of changes) { + const cls = ch.change_class.replace(/^schema-/, "schema-out-"); + add(cls, ch.severity, `outputSchema ${cls} at '${ch.path}'`, ch.detail); + } + } + return items; +} + function diffResources(baseline: LockResourceEntry[], current: LockResourceEntry[]): DriftItem[] { const items: DriftItem[] = []; const base = indexBy(baseline, (r) => r.uri); @@ -287,6 +312,9 @@ export function computeDrift(baseline: Lock, current: BuiltLock): DriftItem[] { items.push(...diffTools(baseline.tools, current.tools)); items.push(...diffResources(baseline.resources, current.resources)); items.push(...diffPrompts(baseline.prompts, current.prompts)); + if (baseline.schema_version < 4 && current.schema_version >= 4 && !baseline.pin.approved) { + items.push(item("schema-version-migrated", "low", "pin/approved_digest", "Legacy lock does not commit tool annotations/outputSchema; review and re-pin under schema v4")); + } const approvedDigest = baseline.pin.approved_digest; if (baseline.pin.approved && approvedDigest !== null && approvedDigest !== current.overall_digest) { diff --git a/packages/lock-ts/src/lock.ts b/packages/lock-ts/src/lock.ts index 52d52dc..9db146f 100644 --- a/packages/lock-ts/src/lock.ts +++ b/packages/lock-ts/src/lock.ts @@ -9,7 +9,7 @@ import { checkDepth, cmpCodepoint, isPlainObject } from "./py.js"; import { extractSkeleton, skeletonFromJson, type Skeleton } from "./skeleton.js"; /** The format level this verifier implements (SPEC.md §14). */ -export const SCHEMA_VERSION = 3; +export const SCHEMA_VERSION = 4; export class LockFormatError extends Error {} @@ -20,6 +20,9 @@ export interface LockToolEntry { capabilities: string[]; inspection: Record | null; schema_skeleton: Skeleton | null; + annotations_hash: string | null; + output_schema_hash: string | null; + output_schema_skeleton: Skeleton | null; entry_digest: string; } @@ -55,7 +58,7 @@ export interface Surface { command?: string; args?: string[]; url?: string | null; - tools?: Array<{ name: string; description?: string | null; inputSchema?: unknown }>; + tools?: Array<{ name: string; description?: string | null; inputSchema?: unknown; annotations?: unknown; outputSchema?: unknown }>; resources?: Array<{ uri: string; name?: string | null; description?: string | null; mimeType?: string | null }>; prompts?: Array<{ name: string; description?: string | null; arguments?: unknown[] | null }>; } @@ -67,6 +70,9 @@ export interface BuiltTool { input_schema_hash: string; capabilities: string[]; schema_skeleton: Skeleton; + annotations_hash: string; + output_schema_hash: string; + output_schema_skeleton: Skeleton | null; entry_digest: string; } @@ -110,9 +116,16 @@ function obj(v: unknown, where: string): Record { return v; } -function toolEntry(raw: unknown, i: number): LockToolEntry { +function toolEntry(raw: unknown, i: number, schemaVersion: number): LockToolEntry { const o = obj(raw, `tools[${i}]`); const w = `tools[${i}]`; + if (schemaVersion >= 4) { + for (const key of ["annotations_hash", "output_schema_hash"]) str(o, key, w); + if (!("output_schema_skeleton" in o)) fail(`${w}.output_schema_skeleton is required`); + } + for (const key of ["annotations_hash", "output_schema_hash"]) { + if (o[key] !== undefined && o[key] !== null && typeof o[key] !== "string") fail(`${w}.${key} must be a string or null`); + } const caps = arr(o, "capabilities", w); if (!caps.every((c) => typeof c === "string")) fail(`${w}.capabilities must be strings`); const inspection = o["inspection"]; @@ -130,6 +143,9 @@ function toolEntry(raw: unknown, i: number): LockToolEntry { capabilities: caps as string[], inspection: inspection === undefined || inspection === null ? null : inspection, schema_skeleton: skeleton, + annotations_hash: (o["annotations_hash"] ?? null) as string | null, + output_schema_hash: (o["output_schema_hash"] ?? null) as string | null, + output_schema_skeleton: skeletonFromJson(o["output_schema_skeleton"]), entry_digest: str(o, "entry_digest", w), }; } @@ -202,7 +218,7 @@ function parseLockStrict(doc: unknown): Lock { url: url === undefined ? null : url, command_digest: str(server, "command_digest", "server"), }, - tools: arr(o, "tools", "lock").map(toolEntry), + tools: arr(o, "tools", "lock").map((entry, i) => toolEntry(entry, i, sv)), resources: arr(o, "resources", "lock").map(resourceEntry), prompts: arr(o, "prompts", "lock").map(promptEntry), findings: arr(o, "findings", "lock"), @@ -239,12 +255,18 @@ export function buildFromSurface(surface: Surface): BuiltLock { const tools: BuiltTool[] = (surface.tools ?? []) .map((t) => { const schema = isPlainObject(t.inputSchema) ? t.inputSchema : null; + for (const key of ["annotations", "outputSchema"] as const) { + if (t[key] !== undefined && t[key] !== null && !isPlainObject(t[key])) fail(`tool.${key} must be an object or null`); + } const body = { name: t.name, description_hash: hashDescription(t.description), input_schema_hash: hashInputSchema(schema), capabilities: deriveCapabilities(t.name, schema), schema_skeleton: extractSkeleton(t.inputSchema), + annotations_hash: hashValue(t.annotations ?? null), + output_schema_hash: hashValue(t.outputSchema ?? null), + output_schema_skeleton: t.outputSchema === undefined || t.outputSchema === null ? null : extractSkeleton(t.outputSchema), }; return { ...body, entry_digest: hashValue(body) }; }) diff --git a/packages/lock-ts/test/vectors.test.ts b/packages/lock-ts/test/vectors.test.ts index ca143c9..9b62808 100644 --- a/packages/lock-ts/test/vectors.test.ts +++ b/packages/lock-ts/test/vectors.test.ts @@ -90,6 +90,30 @@ test("manifest shape", () => { assert.equal(new Set(manifest.vectors.map((v) => v.id)).size, manifest.vectors.length); }); +test("metadata-only mutation blocks through the public verifier", () => { + const v = load(manifest.vectors.find((e) => e.id === "digest/tool-metadata-full")!); + const lock = lockFromDigestVector(v); + const surface = v["surface"] as Surface; + const changed = structuredClone(surface); + changed.tools![0].annotations = { destructiveHint: true, readOnlyHint: true, title: "Record" }; + assert.ok(verify(lock, changed).findings.some((d) => d.drift_class === "tool-annotations-modified")); + changed.tools![0].annotations = surface.tools![0].annotations; + changed.tools![0].outputSchema = { type: "object", properties: { value: { type: ["string", "number"], maxLength: 64 } }, required: ["value"] }; + assert.ok(verify(lock, changed).findings.some((d) => d.drift_class === "schema-out-type-broadened")); +}); + +test("malformed observed metadata cannot silently normalize to absence", () => { + const v = load(manifest.vectors.find((e) => e.id === "digest/tool-metadata-full")!); + const lock = lockFromDigestVector(v); + for (const key of ["annotations", "outputSchema"] as const) { + for (const value of [[], "not-an-object", true]) { + const surface = structuredClone(v["surface"]) as Surface; + surface.tools![0][key] = value; + assert.throws(() => verify(lock, surface), LockFormatError); + } + } +}); + for (const entry of manifest.vectors) { test(entry.id, () => { const v = load(entry); diff --git a/requirements-dev.lock b/requirements-dev.lock index 38f3397..df17c9f 100644 --- a/requirements-dev.lock +++ b/requirements-dev.lock @@ -542,9 +542,9 @@ pygments==2.20.0 \ # via # pytest # rich -pyjwt==2.13.0 \ - --hash=sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423 \ - --hash=sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728 +pyjwt==2.15.1 \ + --hash=sha256:42d59d631f7768a1028a64c7ff581a9bf7519804daf91fc5b6c56e30eec5e193 \ + --hash=sha256:4f259e80cdfb6b3fc18a7de51fd1ef9ec79652f25019bae68975ca2468a34df8 # via mcp pytest==9.1.1 \ --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ diff --git a/src/mcp_warden/__init__.py b/src/mcp_warden/__init__.py index 6491254..f3d3df0 100644 --- a/src/mcp_warden/__init__.py +++ b/src/mcp_warden/__init__.py @@ -1,21 +1,23 @@ """mcp-warden — CI-first MCP supply-chain integrity gate. mcp-warden pins and verifies the *declared* tool/resource/prompt surface of an -MCP server (the ``(name, description, inputSchema)`` metadata returned by +MCP server (name/description/inputSchema plus v4 annotations/outputSchema from ``tools/list`` / ``resources/list`` / ``prompts/list``), then fails CI when that surface drifts from an approved baseline. It operates on **definitions**, never on runtime tool behavior or tool results. See ``docs/THREAT_MODEL.md``. """ __version__ = "1.2.0" -#: Lock schema version. Bumped 2 → 3 for #29 (in-document ``$ref`` resolution in +#: Lock schema version. Bumped 3 → 4 for annotation/outputSchema commitments +#: (DSE-1539). Missing/null metadata hashes JSON null, not an empty object. +#: Earlier 2 → 3 migration was #29 (in-document ``$ref`` resolution in #: ``schema_diff.extract_skeleton``). Following refs changes the skeleton of any #: ref-using tool → its ``entry_digest`` and the ``overall_digest`` (which embeds #: ``schema_version``, lockfile.py:167). The bump makes that digest change a #: declared schema-format migration rather than a silent surface change; drift.py #: emits an additive ``schema-version-migrated`` advisory alongside (never in #: place of) the ``unapproved-change`` finding so re-attestation is required. -SCHEMA_VERSION = 3 +SCHEMA_VERSION = 4 #: Provenance-block version (#19). Lives INSIDE the ``pin`` block, OUTSIDE the #: ``overall_digest`` payload, so it can evolve for #16/#23 without changing any #: server's digest. Deliberately distinct from ``SCHEMA_VERSION`` (which is in diff --git a/src/mcp_warden/capture.py b/src/mcp_warden/capture.py index 8979064..6afd262 100644 --- a/src/mcp_warden/capture.py +++ b/src/mcp_warden/capture.py @@ -15,10 +15,12 @@ from typing import Any import anyio -from mcp import ClientSession, StdioServerParameters +from mcp import ClientSession as SDKClientSession +from mcp import StdioServerParameters from mcp import types as mcp_types from mcp.client.stdio import stdio_client from mcp.client.streamable_http import streamable_http_client +from pydantic import BaseModel, Field, field_validator from .models import ( CapturedPrompt, @@ -41,6 +43,38 @@ class CaptureError(Exception): """ +class _WireToolsResult(BaseModel): + """Preserve complete tool objects before SDK ToolAnnotations projection.""" + + tools: list[dict[str, Any]] + next_cursor: str | None = Field(default=None, alias="nextCursor") + + @field_validator("tools") + @classmethod + def validate_tools(cls, tools): + # Retain SDK validation without retaining its lossy projection. In + # particular a malformed later page must never produce a partial pin. + for tool in tools: + mcp_types.Tool.model_validate(tool) + return tools + + +class ClientSession(SDKClientSession): + """Capture declarations, including fields unknown to the installed SDK. + + SDK 2.x's ToolAnnotations discards extension keys. A raw result model also + preserves explicit nulls inside annotations and schemas. This session never + calls tools, so SDK output-validation caches and tool filtering are not used. + """ + + async def list_tools(self, *, params=None): + request = mcp_types.ListToolsRequest(params=params) + # SDK 1.x wraps requests in a RootModel; SDK 2.x uses a union alias. + if isinstance(mcp_types.ClientRequest, type): + request = mcp_types.ClientRequest(request) + return await self.send_request(request, _WireToolsResult) + + def _model_dump(obj: Any) -> dict[str, Any]: """Wire-format dict view of an MCP SDK model: camelCase keys, no SDK-default nulls. @@ -53,6 +87,8 @@ def _model_dump(obj: Any) -> dict[str, Any]: ``by_alias=True, exclude_none=True`` reproduces what was on the wire and is identical on 1.x and 2.x (tests/test_capture_model_dump.py). """ + if isinstance(obj, dict): + return obj if hasattr(obj, "model_dump"): return obj.model_dump(by_alias=True, exclude_none=True) # pydantic v2 if hasattr(obj, "dict"): @@ -178,6 +214,8 @@ async def _list_tools(session: ClientSession) -> list[CapturedTool]: name=str(data.get("name", "")), description=data.get("description"), input_schema=data.get("inputSchema"), + annotations=data.get("annotations"), + output_schema=data.get("outputSchema"), ) ) return out diff --git a/src/mcp_warden/cli_lock.py b/src/mcp_warden/cli_lock.py index 5c674c3..3bcaff1 100644 --- a/src/mcp_warden/cli_lock.py +++ b/src/mcp_warden/cli_lock.py @@ -61,7 +61,8 @@ def rotate( # OWN stored entries. A mismatch means the lock was hand-edited / tampered # — rotate must never "launder" it by re-stamping; refuse and re-pin. recomputed = compute_overall_digest( - lock_doc.server, lock_doc.tools, lock_doc.resources, lock_doc.prompts + lock_doc.server, lock_doc.tools, lock_doc.resources, lock_doc.prompts, + schema_version=lock_doc.schema_version, ) if recomputed != lock_doc.overall_digest: err_console.print( diff --git a/src/mcp_warden/drift.py b/src/mcp_warden/drift.py index e6d5946..15a4432 100644 --- a/src/mcp_warden/drift.py +++ b/src/mcp_warden/drift.py @@ -10,6 +10,7 @@ import logging from dataclasses import dataclass +from .drift_tool_metadata import metadata_changes from .models import ( PromptEntry, ResourceEntry, @@ -126,6 +127,10 @@ def _diff_tools(baseline: list[ToolEntry], current: list[ToolEntry]) -> list[Dri schema_changed = b.input_schema_hash != c.input_schema_hash if schema_changed: items.extend(_diff_tool_schema(name, target, b, c)) + items.extend( + DriftItem(cls, severity, target, f"Tool '{name}' {message}", detail) + for cls, severity, message, detail in metadata_changes(b, c) + ) added_caps = sorted(set(c.capabilities) - set(b.capabilities)) removed_caps = sorted(set(b.capabilities) - set(c.capabilities)) @@ -232,6 +237,14 @@ def compute_drift(baseline: WardenLock, current: WardenLock) -> list[DriftItem]: items.extend(_diff_resources(baseline.resources, current.resources)) items.extend(_diff_prompts(baseline.prompts, current.prompts)) + # An unapproved legacy lock also lacks commitments to the newly covered + # metadata. Never report a clean v4 check against that unknown baseline. + if baseline.schema_version < 4 <= current.schema_version and not baseline.pin.approved: + items.append(DriftItem( + "schema-version-migrated", "low", "pin/approved_digest", + "Legacy lock does not commit tool annotations/outputSchema; review and re-pin under schema v4", + )) + # Unapproved-change finding (§8): approved baseline whose attested digest no # longer matches the recomputed surface. if baseline.pin.approved and baseline.pin.approved_digest not in (None, current.overall_digest): diff --git a/src/mcp_warden/drift_tool_metadata.py b/src/mcp_warden/drift_tool_metadata.py new file mode 100644 index 0000000..44b845b --- /dev/null +++ b/src/mcp_warden/drift_tool_metadata.py @@ -0,0 +1,37 @@ +"""Additional v4 tool commitments: hints never grant authority.""" + +from __future__ import annotations + +from .hashing import hash_value +from .models import ToolEntry +from .schema_diff import diff_skeletons + +NULL_HASH = hash_value(None) + + +def metadata_changes(b: ToolEntry, c: ToolEntry) -> list[tuple[str, str, str, str | None]]: + """Return (class, severity, message, safe detail) without raw annotation values. + + Legacy entries have no commitments to these fields. Their review boundary is + the schema migration and unchanged unapproved-change finding, not fictional + differences against a historical null value. + """ + items = [] + if b.annotations_hash is not None and b.annotations_hash != c.annotations_hash: + items.append(("tool-annotations-modified", "high", "annotations changed (server declarations, not authority)", None)) + if b.output_schema_hash is None or b.output_schema_hash == c.output_schema_hash: + return items + if b.output_schema_hash == NULL_HASH: + items.append(("schema-out-added", "high", "outputSchema added", None)) + elif c.output_schema_hash == NULL_HASH: + items.append(("schema-out-removed", "high", "outputSchema removed", None)) + elif b.output_schema_skeleton is None or c.output_schema_skeleton is None: + items.append(("schema-out-modified", "high", "outputSchema changed", None)) + else: + changes = diff_skeletons(b.output_schema_skeleton, c.output_schema_skeleton) + if not changes: + items.append(("schema-out-cosmetic-modified", "low", "outputSchema changed cosmetically (no structural change)", None)) + for change in changes: + cls = change.change_class.replace("schema-", "schema-out-", 1) + items.append((cls, change.severity, f"outputSchema {cls} at '{change.path}'", change.detail)) + return items diff --git a/src/mcp_warden/guard_list_gate.py b/src/mcp_warden/guard_list_gate.py index 2c79306..807a1ec 100644 --- a/src/mcp_warden/guard_list_gate.py +++ b/src/mcp_warden/guard_list_gate.py @@ -9,7 +9,8 @@ This reuses ``pin``'s hashing (``hashing.py``) over the live ``tools/list`` result rather than re-spawning the server: the inline result already carries the live -``(name, description, inputSchema)`` triples for every tool. +definitions for every tool. V4 locks additionally commit the complete annotations +and outputSchema; legacy locks retain their narrower coverage until re-pinned. """ from __future__ import annotations @@ -27,7 +28,8 @@ def diverges_from_lock(result: dict[str, Any], lock: WardenLock, *, strict: bool """Compare an inline ``tools/list`` result's tool surface to the lock. Compares the set of tool names plus each tool's ``(description, inputSchema)`` - hashes (reusing :mod:`hashing`) against the pinned ``ToolEntry`` digests. Any + hashes (and annotations/outputSchema for v4, reusing :mod:`hashing`) against + the pinned ``ToolEntry`` digests. Any added/removed tool, or any changed description/schema hash, is divergence. Args: @@ -51,7 +53,7 @@ def diverges_from_lock(result: dict[str, Any], lock: WardenLock, *, strict: bool return False, "" try: - live = _hash_live_tools(tools) + live = _hash_live_tools(tools, metadata=lock.schema_version >= 4) except Exception as exc: # malformed entry if strict: # Strict: a hash failure means we could not gate this surface. Re-raise @@ -61,6 +63,11 @@ def diverges_from_lock(result: dict[str, Any], lock: WardenLock, *, strict: bool return False, "" baseline = {t.name: (t.description_hash, t.input_schema_hash) for t in lock.tools} + if lock.schema_version >= 4: + baseline = { + t.name: (t.description_hash, t.input_schema_hash, t.annotations_hash, t.output_schema_hash) + for t in lock.tools + } added = sorted(set(live) - set(baseline)) removed = sorted(set(baseline) - set(live)) @@ -80,9 +87,9 @@ def diverges_from_lock(result: dict[str, Any], lock: WardenLock, *, strict: bool return True, reason -def _hash_live_tools(tools: list[Any]) -> dict[str, tuple[str, str]]: - """Hash each live tool entry to ``name -> (description_hash, input_schema_hash)``.""" - out: dict[str, tuple[str, str]] = {} +def _hash_live_tools(tools: list[Any], *, metadata: bool = False) -> dict[str, tuple]: + """Hash live declarations; append v4 metadata commitments when requested.""" + out: dict[str, tuple] = {} for tool in tools: if not isinstance(tool, dict): continue @@ -92,4 +99,9 @@ def _hash_live_tools(tools: list[Any]) -> dict[str, tuple[str, str]]: desc_hash = hashing.hash_description(tool.get("description")) schema_hash = hashing.hash_input_schema(tool.get("inputSchema")) out[name] = (desc_hash, schema_hash) + if metadata: + for key in ("annotations", "outputSchema"): + if tool.get(key) is not None and not isinstance(tool[key], dict): + raise ValueError(f"tools/list {key} must be an object or null") + out[name] += (hashing.hash_value(tool.get("annotations")), hashing.hash_value(tool.get("outputSchema"))) return out diff --git a/src/mcp_warden/lockfile.py b/src/mcp_warden/lockfile.py index 33db350..595f687 100644 --- a/src/mcp_warden/lockfile.py +++ b/src/mcp_warden/lockfile.py @@ -99,7 +99,7 @@ def _tool_entry(tool: Any, inspection: dict[str, Any] | None = None) -> ToolEntr """Build a hashed tool entry (§5.1/§5.3, §11) from a captured tool. Args: - tool: A captured tool (``name``/``description``/``input_schema``). + tool: A captured tool including annotations and output_schema. inspection: Optional §11 inspection block. When ``None`` it is excluded from the hashed body, so the digest is byte-identical to v0.1. @@ -116,6 +116,12 @@ def _tool_entry(tool: Any, inspection: dict[str, Any] | None = None) -> ToolEntr "description_hash": hash_description(tool.description), "input_schema_hash": hash_input_schema(schema), "capabilities": derive_capabilities(tool.name, schema), + "annotations_hash": hash_value(tool.annotations), + "output_schema_hash": hash_value(tool.output_schema), + "output_schema_skeleton": ( + extract_skeleton(tool.output_schema).model_dump(mode="json") + if tool.output_schema is not None else None + ), } if inspection is not None: _validate_inspection(tool.name, inspection) @@ -158,6 +164,8 @@ def compute_overall_digest( tools: list[ToolEntry], resources: list[ResourceEntry], prompts: list[PromptEntry], + *, + schema_version: int | None = None, ) -> str: """Compute ``overall_digest`` per §6.1. @@ -174,7 +182,7 @@ def compute_overall_digest( The ``sha256:`` overall digest. """ payload = { - "schema_version": SCHEMA_VERSION, + "schema_version": SCHEMA_VERSION if schema_version is None else schema_version, "server": {"command_digest": server.command_digest}, "tools": [t.entry_digest for t in tools], "resources": [r.entry_digest for r in resources], @@ -193,7 +201,7 @@ def surface_digest(lock: WardenLock) -> str: It is derived, never stored, and can be recomputed from any lock. """ payload = { - "schema_version": SCHEMA_VERSION, + "schema_version": lock.schema_version, "tools": [t.entry_digest for t in lock.tools], "resources": [r.entry_digest for r in lock.resources], "prompts": [p.entry_digest for p in lock.prompts], @@ -203,7 +211,9 @@ def surface_digest(lock: WardenLock) -> str: def lock_is_self_consistent(lock: WardenLock) -> bool: """True when the lock's entries reproduce its stored ``overall_digest``.""" - return compute_overall_digest(lock.server, lock.tools, lock.resources, lock.prompts) == lock.overall_digest + return compute_overall_digest( + lock.server, lock.tools, lock.resources, lock.prompts, schema_version=lock.schema_version + ) == lock.overall_digest def build_lock( @@ -279,10 +289,15 @@ def lock_to_pretty_json(lock: WardenLock) -> str: data = lock.model_dump(mode="json") # §11.4: a tool with no inspection policy must serialize EXACTLY as in v0.1 # (the key is simply absent). Only drop the key when it is None — present - # inspection blocks are kept and are part of the digest. + # inspection blocks are kept and are part of the digest. Legacy locks omit + # the v4 defaults so rotating historical evidence never rewrites its shape. for tool in data.get("tools", []): if tool.get("inspection") is None: tool.pop("inspection", None) + if lock.schema_version < 4: + for key in ("annotations_hash", "output_schema_hash", "output_schema_skeleton"): + if tool.get(key) is None: + tool.pop(key, None) text = json.dumps(data, indent=2, ensure_ascii=False, sort_keys=False) return text + "\n" diff --git a/src/mcp_warden/models.py b/src/mcp_warden/models.py index 1e29438..ee0c003 100644 --- a/src/mcp_warden/models.py +++ b/src/mcp_warden/models.py @@ -8,7 +8,7 @@ from typing import Any -from pydantic import BaseModel, ConfigDict, Field, field_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from . import SCHEMA_VERSION @@ -26,6 +26,8 @@ class CapturedTool(BaseModel): name: str description: str | None = None input_schema: Any | None = None + annotations: dict[str, Any] | None = None + output_schema: dict[str, Any] | None = None class CapturedResource(BaseModel): @@ -125,6 +127,9 @@ class ServerIdentity(BaseModel): class ToolEntry(BaseModel): """Hashed tool entry, sorted by name (WARDEN_LOCK_SCHEMA.md §5.1, §11). + V4 adds annotations/output-schema hashes and an output skeleton. None hash + defaults belong only to old locks; v4 absent metadata commits hash(JSON null). + The optional ``inspection`` block (§11) is additive: when ``None`` it is excluded from both the serialized lock and the canonicalized entry body, so a tool with no inspection policy hashes BYTE-IDENTICALLY to a v0.1 entry @@ -142,6 +147,9 @@ class ToolEntry(BaseModel): capabilities: list[str] inspection: dict[str, Any] | None = None schema_skeleton: SchemaSkeleton | None = None + annotations_hash: str | None = None # None only for pre-v4 locks + output_schema_hash: str | None = None # absent outputSchema hashes JSON null in v4 + output_schema_skeleton: SchemaSkeleton | None = None entry_digest: str @@ -303,6 +311,15 @@ class WardenLock(BaseModel): overall_digest: str pin: PinMetadata + @model_validator(mode="after") + def _v4_tool_fields_required(self): + if self.schema_version >= 4: + required = {"annotations_hash", "output_schema_hash", "output_schema_skeleton"} + for tool in self.tools: + if not required <= tool.model_fields_set or tool.annotations_hash is None or tool.output_schema_hash is None: + raise ValueError("v4 tool entries require annotations_hash, output_schema_hash, and output_schema_skeleton") + return self + @field_validator("schema_version") @classmethod def _schema_version_supported(cls, v: int) -> int: diff --git a/tests/fixtures/clean.warden.lock b/tests/fixtures/clean.warden.lock index d2bfe9f..73b9a49 100644 --- a/tests/fixtures/clean.warden.lock +++ b/tests/fixtures/clean.warden.lock @@ -1,11 +1,12 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ "tests/fixtures/clean_server.py" ], + "url": null, "command_digest": "sha256:db5131f204944f8a4d13cba802e2a47c576c3932b0eaff0fc0304bba7795904c" }, "tools": [ @@ -40,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -73,7 +77,10 @@ } } }, - "entry_digest": "sha256:54e67f392b48e7adeaee4760d0827cc46e069b852b56d502d4d0e59791da5164" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:06a11d6872a89f3182f92b9fed40f39056b908fe192098199221a3cfbb403bf0" } ], "resources": [ @@ -109,19 +116,19 @@ "snippet": "name token 'read'" } ], - "overall_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "overall_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "pin": { - "created_at": "2026-06-09T21:24:46Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:32:29Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "ci-bot@example.invalid", - "approved_at": "2026-06-09T21:24:46Z", - "approved_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "approved_at": "2026-10-02T02:32:29Z", + "approved_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -130,9 +137,10 @@ "actor": "ci-bot@example.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-09T21:24:46Z", - "bound_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", - "note": null + "created_at": "2026-10-02T02:32:29Z", + "bound_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/alice.lock b/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/alice.lock index d2bfe9f..73b9a49 100644 --- a/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/alice.lock +++ b/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/alice.lock @@ -1,11 +1,12 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ "tests/fixtures/clean_server.py" ], + "url": null, "command_digest": "sha256:db5131f204944f8a4d13cba802e2a47c576c3932b0eaff0fc0304bba7795904c" }, "tools": [ @@ -40,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -73,7 +77,10 @@ } } }, - "entry_digest": "sha256:54e67f392b48e7adeaee4760d0827cc46e069b852b56d502d4d0e59791da5164" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:06a11d6872a89f3182f92b9fed40f39056b908fe192098199221a3cfbb403bf0" } ], "resources": [ @@ -109,19 +116,19 @@ "snippet": "name token 'read'" } ], - "overall_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "overall_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "pin": { - "created_at": "2026-06-09T21:24:46Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:32:29Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "ci-bot@example.invalid", - "approved_at": "2026-06-09T21:24:46Z", - "approved_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "approved_at": "2026-10-02T02:32:29Z", + "approved_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -130,9 +137,10 @@ "actor": "ci-bot@example.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-09T21:24:46Z", - "bound_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", - "note": null + "created_at": "2026-10-02T02:32:29Z", + "bound_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/alice.lock.sigstore b/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/alice.lock.sigstore index 029a769..f648c78 100644 --- a/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/alice.lock.sigstore +++ b/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/alice.lock.sigstore @@ -1 +1 @@ -{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/clean@1.0.0\",\"digest\":\"sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219\"}"} +{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/clean@1.0.0\",\"digest\":\"sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9\"}"} diff --git a/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/bob.lock b/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/bob.lock index d2bfe9f..73b9a49 100644 --- a/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/bob.lock +++ b/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/bob.lock @@ -1,11 +1,12 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ "tests/fixtures/clean_server.py" ], + "url": null, "command_digest": "sha256:db5131f204944f8a4d13cba802e2a47c576c3932b0eaff0fc0304bba7795904c" }, "tools": [ @@ -40,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -73,7 +77,10 @@ } } }, - "entry_digest": "sha256:54e67f392b48e7adeaee4760d0827cc46e069b852b56d502d4d0e59791da5164" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:06a11d6872a89f3182f92b9fed40f39056b908fe192098199221a3cfbb403bf0" } ], "resources": [ @@ -109,19 +116,19 @@ "snippet": "name token 'read'" } ], - "overall_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "overall_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "pin": { - "created_at": "2026-06-09T21:24:46Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:32:29Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "ci-bot@example.invalid", - "approved_at": "2026-06-09T21:24:46Z", - "approved_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "approved_at": "2026-10-02T02:32:29Z", + "approved_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -130,9 +137,10 @@ "actor": "ci-bot@example.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-09T21:24:46Z", - "bound_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", - "note": null + "created_at": "2026-10-02T02:32:29Z", + "bound_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/bob.lock.sigstore b/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/bob.lock.sigstore index 029a769..f648c78 100644 --- a/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/bob.lock.sigstore +++ b/tests/fixtures/corpus/locks/npm/@example__clean/1.0.0/bob.lock.sigstore @@ -1 +1 @@ -{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/clean@1.0.0\",\"digest\":\"sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219\"}"} +{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/clean@1.0.0\",\"digest\":\"sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9\"}"} diff --git a/tests/fixtures/corpus/locks/npm/@example__corrupt/1.0.0/alice.lock b/tests/fixtures/corpus/locks/npm/@example__corrupt/1.0.0/alice.lock index d2bfe9f..73b9a49 100644 --- a/tests/fixtures/corpus/locks/npm/@example__corrupt/1.0.0/alice.lock +++ b/tests/fixtures/corpus/locks/npm/@example__corrupt/1.0.0/alice.lock @@ -1,11 +1,12 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ "tests/fixtures/clean_server.py" ], + "url": null, "command_digest": "sha256:db5131f204944f8a4d13cba802e2a47c576c3932b0eaff0fc0304bba7795904c" }, "tools": [ @@ -40,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -73,7 +77,10 @@ } } }, - "entry_digest": "sha256:54e67f392b48e7adeaee4760d0827cc46e069b852b56d502d4d0e59791da5164" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:06a11d6872a89f3182f92b9fed40f39056b908fe192098199221a3cfbb403bf0" } ], "resources": [ @@ -109,19 +116,19 @@ "snippet": "name token 'read'" } ], - "overall_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "overall_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "pin": { - "created_at": "2026-06-09T21:24:46Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:32:29Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "ci-bot@example.invalid", - "approved_at": "2026-06-09T21:24:46Z", - "approved_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "approved_at": "2026-10-02T02:32:29Z", + "approved_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -130,9 +137,10 @@ "actor": "ci-bot@example.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-09T21:24:46Z", - "bound_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", - "note": null + "created_at": "2026-10-02T02:32:29Z", + "bound_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/tests/fixtures/corpus/locks/npm/@example__nosig/1.0.0/alice.lock b/tests/fixtures/corpus/locks/npm/@example__nosig/1.0.0/alice.lock index d2bfe9f..73b9a49 100644 --- a/tests/fixtures/corpus/locks/npm/@example__nosig/1.0.0/alice.lock +++ b/tests/fixtures/corpus/locks/npm/@example__nosig/1.0.0/alice.lock @@ -1,11 +1,12 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ "tests/fixtures/clean_server.py" ], + "url": null, "command_digest": "sha256:db5131f204944f8a4d13cba802e2a47c576c3932b0eaff0fc0304bba7795904c" }, "tools": [ @@ -40,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -73,7 +77,10 @@ } } }, - "entry_digest": "sha256:54e67f392b48e7adeaee4760d0827cc46e069b852b56d502d4d0e59791da5164" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:06a11d6872a89f3182f92b9fed40f39056b908fe192098199221a3cfbb403bf0" } ], "resources": [ @@ -109,19 +116,19 @@ "snippet": "name token 'read'" } ], - "overall_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "overall_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "pin": { - "created_at": "2026-06-09T21:24:46Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:32:29Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "ci-bot@example.invalid", - "approved_at": "2026-06-09T21:24:46Z", - "approved_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "approved_at": "2026-10-02T02:32:29Z", + "approved_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -130,9 +137,10 @@ "actor": "ci-bot@example.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-09T21:24:46Z", - "bound_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", - "note": null + "created_at": "2026-10-02T02:32:29Z", + "bound_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/alice.lock b/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/alice.lock index 82acfdd..67c7804 100644 --- a/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/alice.lock +++ b/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/alice.lock @@ -1,6 +1,6 @@ { - "schema_version": 3, - "warden_version": "1.1.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ @@ -41,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -84,7 +87,10 @@ } } }, - "entry_digest": "sha256:573998a4573233602259d3bcbccab1f4c9b608d76576f0acd101755584eab4db" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:25276cf592d3579747198a0ef2c441c3eb8f48d4557a96fa914731b71a7ec4eb" }, { "name": "run_command", @@ -117,7 +123,10 @@ } } }, - "entry_digest": "sha256:3f339c568117124db0a47b867008b8f07a13211a04defb5637e9ec0c5a745d58" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:f510b61b54fb50a1534adbcc1cb2845b689c4bf3e599b3e29f7d0670d4bd1643" } ], "resources": [ @@ -160,10 +169,10 @@ "snippet": "property 'command'" } ], - "overall_digest": "sha256:62085ff0d433c540b590407417bed61837258d549512ae94227d599c0e3dbf04", + "overall_digest": "sha256:9192af6e30ab6f3776ee2b9451cb3a0e24ba4552c4a2b1d9c27f5e11b90c6bf4", "pin": { - "created_at": "2026-09-03T10:01:37Z", - "warden_version": "1.1.0", + "created_at": "2026-10-02T02:38:46Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": false, "approver": null, @@ -172,7 +181,7 @@ "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "1.1.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, diff --git a/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/alice.lock.sigstore b/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/alice.lock.sigstore index 69fd563..1f37434 100644 --- a/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/alice.lock.sigstore +++ b/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/alice.lock.sigstore @@ -1 +1 @@ -{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/other@1.0.0\",\"digest\":\"sha256:62085ff0d433c540b590407417bed61837258d549512ae94227d599c0e3dbf04\"}"} +{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/other@1.0.0\",\"digest\":\"sha256:9192af6e30ab6f3776ee2b9451cb3a0e24ba4552c4a2b1d9c27f5e11b90c6bf4\"}"} diff --git a/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/bob.lock b/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/bob.lock index 82acfdd..67c7804 100644 --- a/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/bob.lock +++ b/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/bob.lock @@ -1,6 +1,6 @@ { - "schema_version": 3, - "warden_version": "1.1.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ @@ -41,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -84,7 +87,10 @@ } } }, - "entry_digest": "sha256:573998a4573233602259d3bcbccab1f4c9b608d76576f0acd101755584eab4db" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:25276cf592d3579747198a0ef2c441c3eb8f48d4557a96fa914731b71a7ec4eb" }, { "name": "run_command", @@ -117,7 +123,10 @@ } } }, - "entry_digest": "sha256:3f339c568117124db0a47b867008b8f07a13211a04defb5637e9ec0c5a745d58" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:f510b61b54fb50a1534adbcc1cb2845b689c4bf3e599b3e29f7d0670d4bd1643" } ], "resources": [ @@ -160,10 +169,10 @@ "snippet": "property 'command'" } ], - "overall_digest": "sha256:62085ff0d433c540b590407417bed61837258d549512ae94227d599c0e3dbf04", + "overall_digest": "sha256:9192af6e30ab6f3776ee2b9451cb3a0e24ba4552c4a2b1d9c27f5e11b90c6bf4", "pin": { - "created_at": "2026-09-03T10:01:37Z", - "warden_version": "1.1.0", + "created_at": "2026-10-02T02:38:46Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": false, "approver": null, @@ -172,7 +181,7 @@ "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "1.1.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, diff --git a/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/bob.lock.sigstore b/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/bob.lock.sigstore index 69fd563..1f37434 100644 --- a/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/bob.lock.sigstore +++ b/tests/fixtures/corpus/locks/npm/@example__other/1.0.0/bob.lock.sigstore @@ -1 +1 @@ -{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/other@1.0.0\",\"digest\":\"sha256:62085ff0d433c540b590407417bed61837258d549512ae94227d599c0e3dbf04\"}"} +{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/other@1.0.0\",\"digest\":\"sha256:9192af6e30ab6f3776ee2b9451cb3a0e24ba4552c4a2b1d9c27f5e11b90c6bf4\"}"} diff --git a/tests/fixtures/corpus/locks/npm/@example__solo/1.0.0/alice.lock b/tests/fixtures/corpus/locks/npm/@example__solo/1.0.0/alice.lock index d2bfe9f..73b9a49 100644 --- a/tests/fixtures/corpus/locks/npm/@example__solo/1.0.0/alice.lock +++ b/tests/fixtures/corpus/locks/npm/@example__solo/1.0.0/alice.lock @@ -1,11 +1,12 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ "tests/fixtures/clean_server.py" ], + "url": null, "command_digest": "sha256:db5131f204944f8a4d13cba802e2a47c576c3932b0eaff0fc0304bba7795904c" }, "tools": [ @@ -40,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -73,7 +77,10 @@ } } }, - "entry_digest": "sha256:54e67f392b48e7adeaee4760d0827cc46e069b852b56d502d4d0e59791da5164" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:06a11d6872a89f3182f92b9fed40f39056b908fe192098199221a3cfbb403bf0" } ], "resources": [ @@ -109,19 +116,19 @@ "snippet": "name token 'read'" } ], - "overall_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "overall_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "pin": { - "created_at": "2026-06-09T21:24:46Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:32:29Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "ci-bot@example.invalid", - "approved_at": "2026-06-09T21:24:46Z", - "approved_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "approved_at": "2026-10-02T02:32:29Z", + "approved_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -130,9 +137,10 @@ "actor": "ci-bot@example.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-09T21:24:46Z", - "bound_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", - "note": null + "created_at": "2026-10-02T02:32:29Z", + "bound_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/tests/fixtures/corpus/locks/npm/@example__solo/1.0.0/alice.lock.sigstore b/tests/fixtures/corpus/locks/npm/@example__solo/1.0.0/alice.lock.sigstore index c2b9ee7..45953fc 100644 --- a/tests/fixtures/corpus/locks/npm/@example__solo/1.0.0/alice.lock.sigstore +++ b/tests/fixtures/corpus/locks/npm/@example__solo/1.0.0/alice.lock.sigstore @@ -1 +1 @@ -{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/solo@1.0.0\",\"digest\":\"sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219\"}"} +{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/solo@1.0.0\",\"digest\":\"sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9\"}"} diff --git a/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/alice.lock b/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/alice.lock index d2bfe9f..73b9a49 100644 --- a/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/alice.lock +++ b/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/alice.lock @@ -1,11 +1,12 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ "tests/fixtures/clean_server.py" ], + "url": null, "command_digest": "sha256:db5131f204944f8a4d13cba802e2a47c576c3932b0eaff0fc0304bba7795904c" }, "tools": [ @@ -40,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -73,7 +77,10 @@ } } }, - "entry_digest": "sha256:54e67f392b48e7adeaee4760d0827cc46e069b852b56d502d4d0e59791da5164" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:06a11d6872a89f3182f92b9fed40f39056b908fe192098199221a3cfbb403bf0" } ], "resources": [ @@ -109,19 +116,19 @@ "snippet": "name token 'read'" } ], - "overall_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "overall_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "pin": { - "created_at": "2026-06-09T21:24:46Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:32:29Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "ci-bot@example.invalid", - "approved_at": "2026-06-09T21:24:46Z", - "approved_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "approved_at": "2026-10-02T02:32:29Z", + "approved_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -130,9 +137,10 @@ "actor": "ci-bot@example.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-09T21:24:46Z", - "bound_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", - "note": null + "created_at": "2026-10-02T02:32:29Z", + "bound_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/alice.lock.sigstore b/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/alice.lock.sigstore index 3170d05..6ca5bfb 100644 --- a/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/alice.lock.sigstore +++ b/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/alice.lock.sigstore @@ -1 +1 @@ -{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/split@1.0.0\",\"digest\":\"sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219\"}"} +{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/split@1.0.0\",\"digest\":\"sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9\"}"} diff --git a/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/bob.lock b/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/bob.lock index 82acfdd..67c7804 100644 --- a/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/bob.lock +++ b/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/bob.lock @@ -1,6 +1,6 @@ { - "schema_version": 3, - "warden_version": "1.1.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ @@ -41,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -84,7 +87,10 @@ } } }, - "entry_digest": "sha256:573998a4573233602259d3bcbccab1f4c9b608d76576f0acd101755584eab4db" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:25276cf592d3579747198a0ef2c441c3eb8f48d4557a96fa914731b71a7ec4eb" }, { "name": "run_command", @@ -117,7 +123,10 @@ } } }, - "entry_digest": "sha256:3f339c568117124db0a47b867008b8f07a13211a04defb5637e9ec0c5a745d58" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:f510b61b54fb50a1534adbcc1cb2845b689c4bf3e599b3e29f7d0670d4bd1643" } ], "resources": [ @@ -160,10 +169,10 @@ "snippet": "property 'command'" } ], - "overall_digest": "sha256:62085ff0d433c540b590407417bed61837258d549512ae94227d599c0e3dbf04", + "overall_digest": "sha256:9192af6e30ab6f3776ee2b9451cb3a0e24ba4552c4a2b1d9c27f5e11b90c6bf4", "pin": { - "created_at": "2026-09-03T10:01:37Z", - "warden_version": "1.1.0", + "created_at": "2026-10-02T02:38:46Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": false, "approver": null, @@ -172,7 +181,7 @@ "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "1.1.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, diff --git a/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/bob.lock.sigstore b/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/bob.lock.sigstore index 8742715..0d32303 100644 --- a/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/bob.lock.sigstore +++ b/tests/fixtures/corpus/locks/npm/@example__split/1.0.0/bob.lock.sigstore @@ -1 +1 @@ -{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/split@1.0.0\",\"digest\":\"sha256:62085ff0d433c540b590407417bed61837258d549512ae94227d599c0e3dbf04\"}"} +{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/split@1.0.0\",\"digest\":\"sha256:9192af6e30ab6f3776ee2b9451cb3a0e24ba4552c4a2b1d9c27f5e11b90c6bf4\"}"} diff --git a/tests/fixtures/corpus/locks/npm/@example__stranger/1.0.0/mallory.lock b/tests/fixtures/corpus/locks/npm/@example__stranger/1.0.0/mallory.lock index d2bfe9f..73b9a49 100644 --- a/tests/fixtures/corpus/locks/npm/@example__stranger/1.0.0/mallory.lock +++ b/tests/fixtures/corpus/locks/npm/@example__stranger/1.0.0/mallory.lock @@ -1,11 +1,12 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ "tests/fixtures/clean_server.py" ], + "url": null, "command_digest": "sha256:db5131f204944f8a4d13cba802e2a47c576c3932b0eaff0fc0304bba7795904c" }, "tools": [ @@ -40,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -73,7 +77,10 @@ } } }, - "entry_digest": "sha256:54e67f392b48e7adeaee4760d0827cc46e069b852b56d502d4d0e59791da5164" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:06a11d6872a89f3182f92b9fed40f39056b908fe192098199221a3cfbb403bf0" } ], "resources": [ @@ -109,19 +116,19 @@ "snippet": "name token 'read'" } ], - "overall_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "overall_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "pin": { - "created_at": "2026-06-09T21:24:46Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:32:29Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "ci-bot@example.invalid", - "approved_at": "2026-06-09T21:24:46Z", - "approved_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "approved_at": "2026-10-02T02:32:29Z", + "approved_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -130,9 +137,10 @@ "actor": "ci-bot@example.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-09T21:24:46Z", - "bound_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", - "note": null + "created_at": "2026-10-02T02:32:29Z", + "bound_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/tests/fixtures/corpus/locks/npm/@example__unpinned/1.0.0/carol.lock b/tests/fixtures/corpus/locks/npm/@example__unpinned/1.0.0/carol.lock index d2bfe9f..73b9a49 100644 --- a/tests/fixtures/corpus/locks/npm/@example__unpinned/1.0.0/carol.lock +++ b/tests/fixtures/corpus/locks/npm/@example__unpinned/1.0.0/carol.lock @@ -1,11 +1,12 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ "tests/fixtures/clean_server.py" ], + "url": null, "command_digest": "sha256:db5131f204944f8a4d13cba802e2a47c576c3932b0eaff0fc0304bba7795904c" }, "tools": [ @@ -40,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -73,7 +77,10 @@ } } }, - "entry_digest": "sha256:54e67f392b48e7adeaee4760d0827cc46e069b852b56d502d4d0e59791da5164" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:06a11d6872a89f3182f92b9fed40f39056b908fe192098199221a3cfbb403bf0" } ], "resources": [ @@ -109,19 +116,19 @@ "snippet": "name token 'read'" } ], - "overall_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "overall_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "pin": { - "created_at": "2026-06-09T21:24:46Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:32:29Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "ci-bot@example.invalid", - "approved_at": "2026-06-09T21:24:46Z", - "approved_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "approved_at": "2026-10-02T02:32:29Z", + "approved_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -130,9 +137,10 @@ "actor": "ci-bot@example.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-09T21:24:46Z", - "bound_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", - "note": null + "created_at": "2026-10-02T02:32:29Z", + "bound_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/tests/fixtures/corpus/locks/npm/@example__unpinned/1.0.0/carol.lock.sigstore b/tests/fixtures/corpus/locks/npm/@example__unpinned/1.0.0/carol.lock.sigstore index 60a8db1..7802182 100644 --- a/tests/fixtures/corpus/locks/npm/@example__unpinned/1.0.0/carol.lock.sigstore +++ b/tests/fixtures/corpus/locks/npm/@example__unpinned/1.0.0/carol.lock.sigstore @@ -1 +1 @@ -{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/unpinned@1.0.0\",\"digest\":\"sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219\"}"} +{"_fake": "mcp-warden TEST sidecar, not a sigstore bundle", "over": "{\"_type\":\"mcp-warden-lock-digest/v2\",\"coordinate\":\"npm:@example/unpinned@1.0.0\",\"digest\":\"sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9\"}"} diff --git a/tests/fixtures/corpus/locks/npm/@example__wrongsig/1.0.0/alice.lock b/tests/fixtures/corpus/locks/npm/@example__wrongsig/1.0.0/alice.lock index d2bfe9f..73b9a49 100644 --- a/tests/fixtures/corpus/locks/npm/@example__wrongsig/1.0.0/alice.lock +++ b/tests/fixtures/corpus/locks/npm/@example__wrongsig/1.0.0/alice.lock @@ -1,11 +1,12 @@ { - "schema_version": 3, - "warden_version": "0.3.0", + "schema_version": 4, + "warden_version": "1.2.0", "server": { "command": "python", "args": [ "tests/fixtures/clean_server.py" ], + "url": null, "command_digest": "sha256:db5131f204944f8a4d13cba802e2a47c576c3932b0eaff0fc0304bba7795904c" }, "tools": [ @@ -40,7 +41,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -73,7 +77,10 @@ } } }, - "entry_digest": "sha256:54e67f392b48e7adeaee4760d0827cc46e069b852b56d502d4d0e59791da5164" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:06a11d6872a89f3182f92b9fed40f39056b908fe192098199221a3cfbb403bf0" } ], "resources": [ @@ -109,19 +116,19 @@ "snippet": "name token 'read'" } ], - "overall_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "overall_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "pin": { - "created_at": "2026-06-09T21:24:46Z", - "warden_version": "0.3.0", + "created_at": "2026-10-02T02:32:29Z", + "warden_version": "1.2.0", "mcp_protocol_version": "2025-11-25", "approved": true, "approver": "ci-bot@example.invalid", - "approved_at": "2026-06-09T21:24:46Z", - "approved_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", + "approved_at": "2026-10-02T02:32:29Z", + "approved_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", "provenance_version": 1, "pinner": { "tool": "mcp-warden", - "tool_version": "0.3.0", + "tool_version": "1.2.0", "actor": null, "environment": null }, @@ -130,9 +137,10 @@ "actor": "ci-bot@example.invalid", "role": "approver", "method": "manual", - "created_at": "2026-06-09T21:24:46Z", - "bound_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219", - "note": null + "created_at": "2026-10-02T02:32:29Z", + "bound_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9", + "note": null, + "signature_bundle": null } ], "rotated_at": null, diff --git a/tests/fixtures/legacy-v3.warden.lock b/tests/fixtures/legacy-v3.warden.lock new file mode 100644 index 0000000..734e5ae --- /dev/null +++ b/tests/fixtures/legacy-v3.warden.lock @@ -0,0 +1,63 @@ +{ + "schema_version": 3, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:df0cd751860cebb7dbf04cb311a379d2ffcef96035486aafc13f2b6d5c610077", + "capabilities": [], + "schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "x": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:2bbf6c724227113547e88079aee35fe478aa32f6d35ba6eb70ea8683b7d0b251" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": true, + "approver": "historical-reviewer@example.invalid", + "approved_at": null, + "approved_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46", + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } +} diff --git a/tests/fixtures/tool_integrity_server.py b/tests/fixtures/tool_integrity_server.py new file mode 100644 index 0000000..e47bc66 --- /dev/null +++ b/tests/fixtures/tool_integrity_server.py @@ -0,0 +1,21 @@ +"""Real MCP fixture with a changeable definition and stable launch identity.""" + +from __future__ import annotations + +import asyncio +import json +import sys +from pathlib import Path + +import mcp.types as types +from _sdk_compat import build_server, serve_stdio + + +def list_tools() -> list[types.Tool]: + return [types.Tool.model_validate(json.loads(Path(sys.argv[1]).read_text()))] + + +server = build_server("tool-integrity-fixture", tools=list_tools, resources=lambda: [], prompts=lambda: []) + +if __name__ == "__main__": + asyncio.run(serve_stdio(server)) diff --git a/tests/fixtures/tool_metadata_listchange_server.py b/tests/fixtures/tool_metadata_listchange_server.py new file mode 100644 index 0000000..2610534 --- /dev/null +++ b/tests/fixtures/tool_metadata_listchange_server.py @@ -0,0 +1,46 @@ +"""Protocol fixture: real notification followed by a metadata-only list mutation.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + + +def send(frame): + sys.stdout.write(json.dumps(frame) + "\n") + sys.stdout.flush() + + +def main(): + last = None + for line in sys.stdin: + msg = json.loads(line) + rpc_id, method = msg.get("id"), msg.get("method") + if rpc_id is None: + continue + if method == "initialize": + result = {"protocolVersion": "2025-06-18", "capabilities": {"tools": {"listChanged": True}}, + "serverInfo": {"name": "tool-metadata-listchange", "version": "1"}} + elif method == "tools/list": + definition = Path(sys.argv[1]).read_text() + if last is not None and definition != last: + send({"jsonrpc": "2.0", "method": "notifications/tools/list_changed"}) + last = definition + document = json.loads(definition) + if "pages" in document: + cursor = (msg.get("params") or {}).get("cursor") + result = document["pages"][int(cursor) if cursor else 0] + else: + result = {"tools": [document]} + elif method == "resources/list": + result = {"resources": []} + elif method == "prompts/list": + result = {"prompts": []} + else: + result = {} + send({"jsonrpc": "2.0", "id": rpc_id, "result": result}) + + +if __name__ == "__main__": + main() diff --git a/tests/test_drift.py b/tests/test_drift.py index f6fc614..7048834 100644 --- a/tests/test_drift.py +++ b/tests/test_drift.py @@ -164,11 +164,12 @@ def test_schema_version_migrated_additive_low(monkeypatch): s = _surface([CapturedTool(name="t", input_schema=schema)]) base = _v2_approved_lock(s, "ci-bot@example.invalid", monkeypatch) - monkeypatch.undo() # restore SCHEMA_VERSION=3 for the current build + monkeypatch.undo() # restore the current schema version cur = build_lock(s, []) assert base.schema_version == 2 - assert cur.schema_version == 3 + from mcp_warden import SCHEMA_VERSION + assert cur.schema_version == SCHEMA_VERSION assert base.overall_digest != cur.overall_digest # ref resolution moved the digest drift = compute_drift(base, cur) diff --git a/tests/test_inspection_policy.py b/tests/test_inspection_policy.py index 2643150..2a4183b 100644 --- a/tests/test_inspection_policy.py +++ b/tests/test_inspection_policy.py @@ -27,6 +27,8 @@ def __init__(self, name, schema=None, desc="d"): self.name = name self.description = desc self.input_schema = schema or {"type": "object", "properties": {"q": {"type": "string"}}} + self.annotations = None + self.output_schema = None # --- fail-safe defaults (absent => max protection) --------------------------- @@ -55,20 +57,20 @@ def test_absent_policy_ansi_strict_secret_block_url_note(): assert secret and secret[0].tier == "block" -# --- digest inclusion: absent inspection hashes identically to v0.1 ---------- +# --- digest inclusion: absent inspection does not change a current-format entry ---------- -def test_tool_entry_without_inspection_hashes_identically_to_v01(): - """A tool with no inspection block must hash byte-identically to v0.1.""" +def test_tool_entry_without_inspection_hashes_identically_to_explicit_none(): + """Omitting inspection and explicitly passing None have identical current-format hashes.""" t = _Tool("read_file") - # Build the entry the way a v0.1 build would (no inspection arg at all). - v01_like = _tool_entry(t) + # Build the current-format entry without an inspection arg. + without_inspection = _tool_entry(t) # Build it again with explicit inspection=None. - v02_none = _tool_entry(t, inspection=None) - assert v01_like.entry_digest == v02_none.entry_digest + explicit_none = _tool_entry(t, inspection=None) + assert without_inspection.entry_digest == explicit_none.entry_digest # Adding an inspection block CHANGES the digest. - v02_with = _tool_entry(t, inspection={"expected_output_charset": "text"}) - assert v02_with.entry_digest != v01_like.entry_digest + with_inspection = _tool_entry(t, inspection={"expected_output_charset": "text"}) + assert with_inspection.entry_digest != without_inspection.entry_digest def test_overall_digest_unchanged_when_no_inspection(): diff --git a/tests/test_lockfile.py b/tests/test_lockfile.py index e980788..37bde0a 100644 --- a/tests/test_lockfile.py +++ b/tests/test_lockfile.py @@ -47,6 +47,9 @@ def test_entry_digest_excludes_itself(): "capabilities": tool.capabilities, # SCHEMA_VERSION 2: the serialized skeleton is part of the hashed body. "schema_skeleton": tool.schema_skeleton.model_dump(mode="json"), + "annotations_hash": tool.annotations_hash, + "output_schema_hash": tool.output_schema_hash, + "output_schema_skeleton": None, } assert hash_value(body) == tool.entry_digest diff --git a/tests/test_spec_vectors.py b/tests/test_spec_vectors.py index 34d1fa9..8718542 100644 --- a/tests/test_spec_vectors.py +++ b/tests/test_spec_vectors.py @@ -48,7 +48,8 @@ def _surface(doc: dict[str, Any]) -> CapturedSurface: url=doc.get("url"), protocol_version="2025-06-18", tools=[ - CapturedTool(name=t["name"], description=t.get("description"), input_schema=t.get("inputSchema")) + CapturedTool(name=t["name"], description=t.get("description"), input_schema=t.get("inputSchema"), + annotations=t.get("annotations"), output_schema=t.get("outputSchema")) for t in doc.get("tools", []) ], resources=[ diff --git a/tests/test_tool_integrity.py b/tests/test_tool_integrity.py new file mode 100644 index 0000000..ad33c54 --- /dev/null +++ b/tests/test_tool_integrity.py @@ -0,0 +1,219 @@ +"""Tool metadata must be covered without treating server hints as authority.""" + +from __future__ import annotations + +import json +import sys +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from mcp_warden.capture import CaptureError, capture_surface_sync +from mcp_warden.cli import app +from mcp_warden.drift import compute_drift +from mcp_warden.emitters import build_sarif +from mcp_warden.guard_list_gate import diverges_from_lock +from mcp_warden.hashing import hash_value +from mcp_warden.lockfile import build_lock, lock_is_self_consistent, lock_to_pretty_json, read_lock +from mcp_warden.models import CapturedSurface, CapturedTool + + +def make_lock(*, annotations=None, output_schema=None, description="Read a record."): + return build_lock( + CapturedSurface( + command="fixture", protocol_version="2025-06-18", + tools=[CapturedTool( + name="read_record", description=description, input_schema={"type": "object"}, + annotations=annotations, output_schema=output_schema, + )], + ), [], approve=True, approver="reviewer@example.invalid", + ) + + +def classes(base, current): + return {d.drift_class for d in compute_drift(base, current)} + + +@pytest.mark.parametrize("hint", ["readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"]) +def test_every_hint_flip_changes_digest_and_blocks(hint): + base, cur = make_lock(annotations={hint: False}), make_lock(annotations={hint: True}) + assert base.overall_digest != cur.overall_digest + assert {"tool-annotations-modified", "unapproved-change"} <= classes(base, cur) + assert base.tools[0].capabilities == cur.tools[0].capabilities + + +@pytest.mark.parametrize("base,cur", [(None, {}), ({}, None), ({"title": "a"}, {"title": "b"})]) +def test_complete_annotation_object_is_hashed(base, cur): + assert "tool-annotations-modified" in classes(make_lock(annotations=base), make_lock(annotations=cur)) + + +def test_absent_and_null_metadata_have_same_digest(): + implicit = build_lock(CapturedSurface( + command="fixture", protocol_version="2025-06-18", + tools=[CapturedTool(name="read_record", description="Read a record.", input_schema={"type": "object"})], + ), []) + explicit = make_lock() + assert implicit.overall_digest == explicit.overall_digest + assert explicit.tools[0].annotations_hash == hash_value(None) + assert explicit.tools[0].output_schema_hash == hash_value(None) + assert explicit.tools[0].output_schema_skeleton is None + + +def test_metadata_key_order_is_canonical(): + a = make_lock(annotations={"readOnlyHint": True, "destructiveHint": False}) + b = make_lock(annotations={"destructiveHint": False, "readOnlyHint": True}) + assert a.overall_digest == b.overall_digest + + +def output_schema(**field): + return {"type": "object", "properties": {"value": field}} + + +def test_output_type_broadening_has_distinct_structural_rule(): + base = make_lock(output_schema=output_schema(type="string")) + cur = make_lock(output_schema=output_schema(type=["string", "number"])) + assert "schema-out-type-broadened" in classes(base, cur) + rules = {r["ruleId"] for r in build_sarif([], compute_drift(base, cur))["runs"][0]["results"]} + assert "WRD-DRIFT-SCHEMA-OUT-TYPE-BROADENED" in rules + + +@pytest.mark.parametrize("before,after,expected", [ + (None, {}, "schema-out-added"), + ({}, None, "schema-out-removed"), + ({"type": "object", "title": "A"}, {"type": "object", "title": "B"}, "schema-out-cosmetic-modified"), + (output_schema(type="string", maxLength=8), output_schema(type="string", maxLength=64), "schema-out-constraint-relaxed"), +]) +def test_output_presence_and_structure(before, after, expected): + assert expected in classes(make_lock(output_schema=before), make_lock(output_schema=after)) + + +def test_output_schema_changes_inside_local_ref_are_detected(): + a = {"$defs": {"v": {"type": "string"}}, "properties": {"value": {"$ref": "#/$defs/v"}}} + b = {"$defs": {"v": {"type": ["string", "number"]}}, "properties": {"value": {"$ref": "#/$defs/v"}}} + assert "schema-out-type-broadened" in classes(make_lock(output_schema=a), make_lock(output_schema=b)) + + +def test_annotation_values_are_never_rendered(): + sentinel = "private annotation sentinel value" + drift = compute_drift(make_lock(annotations={"title": "before"}), make_lock(annotations={"title": sentinel})) + assert sentinel not in json.dumps(build_sarif([], drift)) + assert sentinel not in lock_to_pretty_json(make_lock(annotations={"title": sentinel})) + + +def test_combined_metadata_changes_are_all_reported(): + a = make_lock(annotations={"destructiveHint": False}, output_schema=output_schema(type="string")) + b = make_lock(annotations={"destructiveHint": True}, output_schema=output_schema(type="number")) + assert {"tool-annotations-modified", "schema-out-type-changed", "unapproved-change"} <= classes(a, b) + + +@pytest.mark.parametrize("field", ["annotations", "output_schema"]) +@pytest.mark.parametrize("value", [[], "not-an-object", True]) +def test_non_object_metadata_is_rejected(field, value): + with pytest.raises(ValueError): + CapturedTool(name="x", **{field: value}) + + +@pytest.mark.parametrize("field", ["annotations_hash", "output_schema_hash", "output_schema_skeleton"]) +def test_v4_lock_requires_new_fields(field, tmp_path): + doc = json.loads(lock_to_pretty_json(make_lock())) + del doc["tools"][0][field] + path = tmp_path / "lock.json" + path.write_text(json.dumps(doc)) + with pytest.raises(ValueError): + read_lock(path) + + +def test_genuine_v3_lock_remains_readable_and_requires_reapproval(tmp_path): + # Freeze historical bytes/formulas, not a v4 document relabeled as v3. + doc = json.loads(Path("tests/fixtures/legacy-v3.warden.lock").read_text()) + path = tmp_path / "old.lock" + path.write_text(json.dumps(doc)) + old = read_lock(path) + assert old.schema_version == 3 + assert lock_is_self_consistent(old) + current = build_lock(CapturedSurface( + command=old.server.command, args=old.server.args, protocol_version="2025-06-18", + tools=[CapturedTool(name="t", input_schema={"type": "object", "properties": {"x": {"type": "string"}}})], + ), []) + old.pin.approved = True + old.pin.approved_digest = old.overall_digest + assert {"schema-version-migrated", "unapproved-change"} <= classes(old, current) + assert "tool-annotations-modified" not in classes(old, current) + old.pin.approved = False + assert "schema-version-migrated" in classes(old, current) + + +@pytest.mark.parametrize("mutation", [ + {"annotations": {"destructiveHint": True}}, + {"outputSchema": output_schema(type="number")}, +]) +def test_runtime_list_gate_covers_v4_metadata(mutation): + base = make_lock(annotations={"destructiveHint": False}, output_schema=output_schema(type="string")) + tool = {"name": "read_record", "description": "Read a record.", "inputSchema": {"type": "object"}, + "annotations": {"destructiveHint": False}, "outputSchema": output_schema(type="string")} + assert diverges_from_lock({"tools": [tool]}, base) == (False, "") + changed, reason = diverges_from_lock({"tools": [tool | mutation]}, base) + assert changed and "modified" in reason + + +@pytest.mark.parametrize("mutation,rule", [ + ({"annotations": {"destructiveHint": True}}, "WRD-DRIFT-TOOL-ANNOTATIONS-MODIFIED"), + ({"outputSchema": output_schema(type=["string", "number"])}, "WRD-DRIFT-SCHEMA-OUT-TYPE-BROADENED"), +]) +def test_real_cli_roundtrip_rejects_metadata_only_change(tmp_path, mutation, rule): + fixture = str(Path(__file__).parent / "fixtures" / "tool_integrity_server.py") + declaration = tmp_path / "declaration.json" + tool = {"name": "read_record", "inputSchema": {"type": "object"}, + "annotations": {"destructiveHint": False}, "outputSchema": output_schema(type="string")} + declaration.write_text(json.dumps(tool)) + lock_path, sarif_path = tmp_path / "warden.lock", tmp_path / "drift.sarif" + argv = [sys.executable, fixture, str(declaration)] + runner = CliRunner() + pinned = runner.invoke(app, ["pin", *argv, "--approve", "--approver", "reviewer@example.invalid", "--lock", str(lock_path)]) + assert pinned.exit_code == 0, pinned.output + clean = runner.invoke(app, ["check", *argv, "--lock", str(lock_path)]) + assert clean.exit_code == 0, clean.output + declaration.write_text(json.dumps(tool | mutation)) + changed = runner.invoke(app, ["check", *argv, "--lock", str(lock_path), "--sarif", str(sarif_path)]) + assert changed.exit_code == 1, changed.output + rules = {r["ruleId"] for r in json.loads(sarif_path.read_text())["runs"][0]["results"]} + assert rule in rules + assert "WRD-DRIFT-UNAPPROVED-CHANGE" in rules + + +@pytest.mark.parametrize("field,before,after", [ + ("annotations", {"x": "before"}, {"x": "after"}), + ("annotations", {}, {"title": None}), + ("annotations", {"x": {"nested": None}}, {"x": {"nested": "after"}}), + ("outputSchema", {"type": "object", "x": None}, {"type": "object", "x": "after"}), +]) +def test_wire_metadata_survives_sdk_projection(tmp_path, field, before, after): + fixture = Path(__file__).parent / "fixtures" / "tool_metadata_listchange_server.py" + declaration = tmp_path / "definition.json" + tool = {"name": "read_record", "inputSchema": {"type": "object"}, field: before} + declaration.write_text(json.dumps(tool)) + argv = [str(fixture), str(declaration)] + surface = capture_surface_sync(sys.executable, argv) + captured = surface.tools[0] + assert getattr(captured, "annotations" if field == "annotations" else "output_schema") == before + baseline = build_lock(surface, [], approve=True, approver="reviewer@example.invalid") + assert diverges_from_lock({"tools": [tool]}, baseline) == (False, "") + changed = tool | {field: after} + declaration.write_text(json.dumps(changed)) + current = build_lock(capture_surface_sync(sys.executable, argv), []) + assert current.overall_digest != baseline.overall_digest + assert "unapproved-change" in classes(baseline, current) + assert diverges_from_lock({"tools": [changed]}, baseline)[0] + + +@pytest.mark.parametrize("tool", [{}, {"name": "missing_schema"}, {"inputSchema": {}}]) +def test_wire_malformed_later_page_is_rejected(tmp_path, tool): + fixture = Path(__file__).parent / "fixtures" / "tool_metadata_listchange_server.py" + declaration = tmp_path / "definition.json" + declaration.write_text(json.dumps({"pages": [ + {"tools": [{"name": "valid", "inputSchema": {"type": "object"}}], "nextCursor": "1"}, + {"tools": [tool]}, + ]})) + with pytest.raises(CaptureError): + capture_surface_sync(sys.executable, [str(fixture), str(declaration)]) diff --git a/tests/test_tool_integrity_guard.py b/tests/test_tool_integrity_guard.py new file mode 100644 index 0000000..21862ba --- /dev/null +++ b/tests/test_tool_integrity_guard.py @@ -0,0 +1,80 @@ +"""A v4 runtime list gate blocks metadata-only changes before client ingestion.""" + +from __future__ import annotations + +import json +import os +import queue +import subprocess +import sys +import threading +from pathlib import Path + +import pytest + +from mcp_warden.capture import capture_surface_sync +from mcp_warden.lockfile import build_lock, write_lock + + +@pytest.mark.parametrize("mutation", [ + {"annotations": {"destructiveHint": True}}, + {"outputSchema": {"type": "object", "properties": {"value": {"type": "number"}}}}, +]) +def test_real_guard_blocks_metadata_change(tmp_path, mutation): + fixture = Path(__file__).parent / "fixtures" / "tool_metadata_listchange_server.py" + declaration = tmp_path / "definition.json" + tool = {"name": "read_record", "inputSchema": {"type": "object"}, + "annotations": {"destructiveHint": False}, + "outputSchema": {"type": "object", "properties": {"value": {"type": "string"}}}} + declaration.write_text(json.dumps(tool)) + argv = [str(fixture), str(declaration)] + lock = tmp_path / "warden.lock" + write_lock(build_lock(capture_surface_sync(sys.executable, argv), []), lock) + proc = subprocess.Popen( + [sys.executable, "-m", "mcp_warden.cli", "guard", "--lock", str(lock), sys.executable, *argv], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=0, + env={**os.environ, "WARDEN_LOG_LEVEL": "ERROR"}, + ) + frames = queue.Queue() + + def read_frames(): + for line in iter(proc.stdout.readline, b""): + frames.put(line) + frames.put(b"") + + reader = threading.Thread(target=read_frames, daemon=True) + reader.start() + + def send(frame): + proc.stdin.write((json.dumps(frame) + "\n").encode()) + proc.stdin.flush() + + def receive(): + return json.loads(frames.get(timeout=30)) + + try: + send({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { + "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": {"name": "test", "version": "1"}}}) + assert receive()["id"] == 1 + send({"jsonrpc": "2.0", "method": "notifications/initialized"}) + send({"jsonrpc": "2.0", "id": 2, "method": "tools/list"}) + assert "result" in receive() + declaration.write_text(json.dumps(tool | mutation)) + send({"jsonrpc": "2.0", "id": 3, "method": "tools/list"}) + changed = receive() + assert changed["method"] == "notifications/tools/list_changed" + changed = receive() + assert changed["id"] == 3 + assert changed["error"]["data"]["stage"] == "list_changed" + assert "result" not in changed + finally: + proc.stdin.close() + try: + proc.wait(timeout=15) + except subprocess.TimeoutExpired: + proc.kill() + proc.wait() + proc.stdout.close() + proc.stderr.close() + reader.join(timeout=1) + assert proc.returncode == 0 diff --git a/vectors/README.md b/vectors/README.md index 4eaaee7..ffc5710 100644 --- a/vectors/README.md +++ b/vectors/README.md @@ -48,7 +48,7 @@ internal model leaks into the corpus: ```jsonc { "command": "node", "args": ["./server.js"], // OR "url": "https://host/mcp" - "tools": [ { "name", "description"?, "inputSchema"? } ], + "tools": [ { "name", "description"?, "inputSchema"?, "annotations"?, "outputSchema"? } ], "resources": [ { "uri", "name"?, "description"?, "mimeType"? } ], "prompts": [ { "name", "description"?, "arguments"? } ] } diff --git a/vectors/cases/digest-absence-rules.json b/vectors/cases/digest-absence-rules.json index 4e03b16..3895a8f 100644 --- a/vectors/cases/digest-absence-rules.json +++ b/vectors/cases/digest-absence-rules.json @@ -36,7 +36,10 @@ "schema_skeleton": { "props": {} }, - "entry_digest": "sha256:e73d702704600656803bc02f9a9853d5d84846b01f17bb67578f8dfd94d131a9" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:e1ae8556e93f6b121fa9e10e97aa2889898bb19621ada6ebd9ea81d51e8a6e27" } ], "resources": [ @@ -56,6 +59,6 @@ "entry_digest": "sha256:37b54601b7622b3734efb093fa82da7e54ea38c9fd65980b6c403cee90959a0d" } ], - "overall_digest": "sha256:7af941616b0bf5c98755e9a543137cba622d986e86ef8404509132c27f3b38b5" + "overall_digest": "sha256:a9519f44dcd63efa1ad0961f21901d43b476f1fdd6d4e7dc5949d3794a3c30af" } } diff --git a/vectors/cases/digest-capabilities-and-refs.json b/vectors/cases/digest-capabilities-and-refs.json index abf93dd..3b47141 100644 --- a/vectors/cases/digest-capabilities-and-refs.json +++ b/vectors/cases/digest-capabilities-and-refs.json @@ -148,7 +148,10 @@ } } }, - "entry_digest": "sha256:458d491dcd951c14b378f89c3a2a281b9d78172459660eff7296d7f15d837fc6" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:88564b4184dd46b5e24c0aea7f807cf4e38d7c38ac2082f59365ac7c52a4bca3" }, { "name": "read_file", @@ -181,7 +184,10 @@ } } }, - "entry_digest": "sha256:6f11256b978f1aac99fab34e4af12c354c2eda0e993913889adcc34940049bc0" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:1b044dd5f7d4145f0b8d3a4aed57eeaadfb6d6f958adb667f573a64e34546136" }, { "name": "refs", @@ -248,7 +254,10 @@ } } }, - "entry_digest": "sha256:e9f2249d92aad65b0183931ad0622c874af71453f4734e31a2d2a676022d6ed2" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:cd4da72ed4c32d96fa0e48570014814631088d0084252a7c8705fcf2c0323e16" }, { "name": "run_shell_command", @@ -281,7 +290,10 @@ } } }, - "entry_digest": "sha256:64b37b52f763980702dadd1a5ef061d7fc21cc1acc560699c44b9cf2752fa2ec" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:2c3145f167e8b5ad750d78ca43c30df9912ced9546bcff92e8371360a848c4d3" }, { "name": "sqlQuery", @@ -314,7 +326,10 @@ } } }, - "entry_digest": "sha256:d3e40dab0cc6d677d69c5e6dd1dfa543e3afc301655f2347370f1c1a78219462" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:322429ef00cd3d096b83df011870d197e09a8b32bb75a54b0772bdc2cc7601fd" }, { "name": "writeFile", @@ -357,11 +372,14 @@ } } }, - "entry_digest": "sha256:ce5a6ad17a64ac7b67af67b64f1e7c2cd5a6394c9e49db17309a480131c0cd08" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:646785d1700211069c62b6a70c6fa7f6279d6bc67199c71716a6279610fc0449" } ], "resources": [], "prompts": [], - "overall_digest": "sha256:24de0128af7fbee8c0c6052b802b241ca7774fa8e2454246059acc618db7dad0" + "overall_digest": "sha256:2833de0f649330ec0a62383d3d7e76865c14689cf16273555199a1e110401d04" } } diff --git a/vectors/cases/digest-clean-fixture.json b/vectors/cases/digest-clean-fixture.json index a092e23..c9950b2 100644 --- a/vectors/cases/digest-clean-fixture.json +++ b/vectors/cases/digest-clean-fixture.json @@ -98,7 +98,10 @@ } } }, - "entry_digest": "sha256:1a3b7e7ce145db27d63fb3ade3a3d2f6822d1e5b570b62f80a6c2f3de5d17569" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a5878781086596122bad370e739dadc591856844fde379ce4b5081b3623df219" }, { "name": "read_file", @@ -131,7 +134,10 @@ } } }, - "entry_digest": "sha256:54e67f392b48e7adeaee4760d0827cc46e069b852b56d502d4d0e59791da5164" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:06a11d6872a89f3182f92b9fed40f39056b908fe192098199221a3cfbb403bf0" } ], "resources": [ @@ -151,6 +157,6 @@ "entry_digest": "sha256:a3214cdc0ac5cf658451d52b81aa4f23b4a3b038042d5a2d9635e71f35624945" } ], - "overall_digest": "sha256:cb20a16ce572cca8f682672c42036e351359d32ca8265e5af3bfc861ab6f9219" + "overall_digest": "sha256:5d7b40f00837bb8747701e81ac9f17db6ad96a17208c84b018227a7456719be9" } } diff --git a/vectors/cases/digest-entry-sorting-astral.json b/vectors/cases/digest-entry-sorting-astral.json index 241a4e3..7492969 100644 --- a/vectors/cases/digest-entry-sorting-astral.json +++ b/vectors/cases/digest-entry-sorting-astral.json @@ -54,7 +54,10 @@ "schema_skeleton": { "props": {} }, - "entry_digest": "sha256:db2c6ef62d1b7f633147a5ffb9235b2ff4ee345874aaea37b053981c1a63aee7" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:39244621081b457130705520e86b8b257e0341f040254829c1382b57ae9a42d5" }, { "name": "x", @@ -64,7 +67,10 @@ "schema_skeleton": { "props": {} }, - "entry_digest": "sha256:7e04a59e75f10560e0a63af88eb82158e15af6ac7427fa242c64a695fd1a5adc" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:07e961d72acd86e33d4a030576845ff1d6fadbf04a8a18e76ce39051c6cc3124" }, { "name": "😀x", @@ -74,7 +80,10 @@ "schema_skeleton": { "props": {} }, - "entry_digest": "sha256:9d3e100db419d9609cb544be93d8ae762c783a3087464fc81619e74623805407" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:7d30fc78baa5582de9d2698e18406905c002c03cb98cd40ce250f807b467ad50" } ], "resources": [ @@ -120,6 +129,6 @@ "entry_digest": "sha256:66aaf4461fd17d0c88a04e4e46d75d0aaf48d00ed66f977f854ddc720b740335" } ], - "overall_digest": "sha256:c75dd5c3454d83265b7d9f41e73f20015b55e415a6bebb1e321d6a1845970e68" + "overall_digest": "sha256:53479e16bdf394db3a83879851ddda9651bd42fea5ddd3df0d553b9ea2ec6253" } } diff --git a/vectors/cases/digest-entry-sorting.json b/vectors/cases/digest-entry-sorting.json index bf44b23..f5f02e8 100644 --- a/vectors/cases/digest-entry-sorting.json +++ b/vectors/cases/digest-entry-sorting.json @@ -54,7 +54,10 @@ "schema_skeleton": { "props": {} }, - "entry_digest": "sha256:d7cac270d09b3f057806b26812b39649c133dd558c90012d64e56f21af4dd11d" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:1f2123f8094d9289724f60721801a25205f567c7cba7918f0019f008c62e5e3e" }, { "name": "_z", @@ -64,7 +67,10 @@ "schema_skeleton": { "props": {} }, - "entry_digest": "sha256:2614db3b8de6b89f2aa19af8ea7ab33d4e42770560feb83da291a22c502179b1" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:72b2794f0e06f12e1c533077d88b5830fb24f27e3d0d26e1ea6fb34293bdee8f" }, { "name": "a", @@ -74,7 +80,10 @@ "schema_skeleton": { "props": {} }, - "entry_digest": "sha256:ecf928a63639fff4a1c1e93abe9d51865e90c85c477bf4fb19014915159d979d" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:2b4ba951dfb3cc6234880565a0eed6ae48e434b45b2b06a2ef1cac9ae971b3e4" }, { "name": "b", @@ -84,7 +93,10 @@ "schema_skeleton": { "props": {} }, - "entry_digest": "sha256:d5e3d0aabfcd7f6c58533e6a54300042942ea35fccec776117183c6894a6488c" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:063b30065f11c596c6f5a9902f244136caf818ed4a61743023a4d73b87cf5965" } ], "resources": [ @@ -124,6 +136,6 @@ "entry_digest": "sha256:3c92d5d5d219517ad9e4d3a77c2dd30bf7fe33290c7d3980c392e1cd21dc1a8a" } ], - "overall_digest": "sha256:7dae1d01b666375460c723e9aebf1dea99504e145810210234edf587da61a713" + "overall_digest": "sha256:77290ff3d412c2d21f6599e0dc7055c88618eb194116653e940e25b889130de9" } } diff --git a/vectors/cases/digest-enum-constraints-skeleton.json b/vectors/cases/digest-enum-constraints-skeleton.json index 355d286..ebd0688 100644 --- a/vectors/cases/digest-enum-constraints-skeleton.json +++ b/vectors/cases/digest-enum-constraints-skeleton.json @@ -190,11 +190,14 @@ } } }, - "entry_digest": "sha256:0c3a0e1b0f5e9748b614818d5db658ae56af7db418b1472335d55d723ed9cb32" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:f5a94ff8b92fbd0383be5f7d512a89c0a8e5d743219d38d2adc6c2bbe95c89cc" } ], "resources": [], "prompts": [], - "overall_digest": "sha256:945a89abf3aacd908242925ba13f8c4049383d9cfc1c912d85df7ef37644bff1" + "overall_digest": "sha256:8f8887e13ad9762151cc9019a87baf6ab75682e3e38958bd777785b38379de67" } } diff --git a/vectors/cases/digest-http-url-server.json b/vectors/cases/digest-http-url-server.json index 92e3631..9cebaee 100644 --- a/vectors/cases/digest-http-url-server.json +++ b/vectors/cases/digest-http-url-server.json @@ -40,11 +40,14 @@ } } }, - "entry_digest": "sha256:6dbb5f5d14ac69981446cc85148ea4992450871a8466534d0338354ba66b4294" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:d6618ea2ccdf7cfacf5f35419dd399b2b32c3172690175e5a0b056450537e18d" } ], "resources": [], "prompts": [], - "overall_digest": "sha256:fc2e3393237a2fc90b42494848c4497f8a6a77fdd02224a44aba2d623d8d5c24" + "overall_digest": "sha256:a5dd175f90ed35ade0e0407df3bcc7c414c51c3d148cb62625072b432577585a" } } diff --git a/vectors/cases/digest-malformed-input-schema.json b/vectors/cases/digest-malformed-input-schema.json index 0becc19..e4cf776 100644 --- a/vectors/cases/digest-malformed-input-schema.json +++ b/vectors/cases/digest-malformed-input-schema.json @@ -36,7 +36,10 @@ "schema_skeleton": { "props": {} }, - "entry_digest": "sha256:6a765bbdeba20bac21a719f1b0f9693311cedce52642d2ba78a45986532c993b" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:8f8105d41e13fe3fbcd87ee431e04a82e331bf1be321170f8124e816f9ef4886" }, { "name": "weirder", @@ -46,11 +49,14 @@ "schema_skeleton": { "props": {} }, - "entry_digest": "sha256:afbe420baedfb3a914bf738a0a12d888b7e3d5a282a512f9a164f0ed300a58eb" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:5edc64c013bd3e29b994d901b0a241b5b82b6241f06e547d28a144b7d558219e" } ], "resources": [], "prompts": [], - "overall_digest": "sha256:dbb3df574496d257c0951c722f2f2edfde333f37fb3d406fa53daf517e22c6de" + "overall_digest": "sha256:baa2ff8f77e627affdf2d88b00f2b45593d84921eebffbea70b7430e4346ccdc" } } diff --git a/vectors/cases/digest-minimal-single-tool.json b/vectors/cases/digest-minimal-single-tool.json index a3591cf..1abab94 100644 --- a/vectors/cases/digest-minimal-single-tool.json +++ b/vectors/cases/digest-minimal-single-tool.json @@ -63,11 +63,14 @@ } } }, - "entry_digest": "sha256:9512f1b4519e48f35f72935fe094fcd3db104cdadcd91de16da13584067ddb9e" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:f0d5e26ac99d25b74bdf9a1886177adcada8d80489d5875f0f8b86502003c34c" } ], "resources": [], "prompts": [], - "overall_digest": "sha256:93acc5354965a023f5e937d468ea6afaf20498cb0b7077a8e1b4227310b4e406" + "overall_digest": "sha256:8dcd1c14a3f719826ee111f2a14551ee9b2c8e5c4bcc64c04fbfd6bf62450ed9" } } diff --git a/vectors/cases/digest-tool-metadata-empty.json b/vectors/cases/digest-tool-metadata-empty.json new file mode 100644 index 0000000..0508204 --- /dev/null +++ b/vectors/cases/digest-tool-metadata-empty.json @@ -0,0 +1,54 @@ +{ + "id": "digest/tool-metadata-empty", + "kind": "digest", + "description": "Empty objects differ from null; the complete annotation object is committed.", + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "annotations": {}, + "outputSchema": {} + } + ], + "resources": [], + "prompts": [] + }, + "expect": { + "server": { + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "output_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "output_schema_skeleton": { + "props": { + "$root": { + "type": null, + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:c6741e9d0aaaa728e29465530e14af42ab864520947b81c619e43f4c2c5977e7" + } + ], + "resources": [], + "prompts": [], + "overall_digest": "sha256:b636114e293cbac1714efddca46aa36f341d1e0d2038630b18e41f9bbe53377d" + } +} diff --git a/vectors/cases/digest-tool-metadata-full.json b/vectors/cases/digest-tool-metadata-full.json new file mode 100644 index 0000000..d382ae0 --- /dev/null +++ b/vectors/cases/digest-tool-metadata-full.json @@ -0,0 +1,82 @@ +{ + "id": "digest/tool-metadata-full", + "kind": "digest", + "description": "Complete hint objects and output schema use RFC 8785 commitments.", + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "annotations": { + "destructiveHint": false, + "readOnlyHint": true, + "title": "Record" + }, + "outputSchema": { + "type": "object", + "properties": { + "value": { + "type": "string", + "maxLength": 64 + } + }, + "required": [ + "value" + ] + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": { + "server": { + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:9db7194b9c289b89474dc30d2bca33dd28a06af0038321d0e42d8bd5637a31a7", + "output_schema_hash": "sha256:552023159e68e4247320e0219538533048b4fdb5a2d71e968699e9ed6796b697", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "value": { + "type": [ + "string" + ], + "required": true, + "enum": null, + "constraints": { + "additionalProperties": true, + "maxLength": 64 + } + } + } + }, + "entry_digest": "sha256:27412db910bdc3a447af86096dd223de2b56fd36bcfb58660a543f7517b20c13" + } + ], + "resources": [], + "prompts": [], + "overall_digest": "sha256:ca9be7b350cf92b812ff2d80196f1e3bd30e8f7d509ac3018a152fe286265379" + } +} diff --git a/vectors/cases/digest-tool-metadata-null.json b/vectors/cases/digest-tool-metadata-null.json new file mode 100644 index 0000000..3085648 --- /dev/null +++ b/vectors/cases/digest-tool-metadata-null.json @@ -0,0 +1,43 @@ +{ + "id": "digest/tool-metadata-null", + "kind": "digest", + "description": "Explicit null metadata equals omitted metadata.", + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "annotations": null, + "outputSchema": null + } + ], + "resources": [], + "prompts": [] + }, + "expect": { + "server": { + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:321c7e7ad98b9b198097bc65f7d14c097be23e03976359ac1344bba24b879029" + } + ], + "resources": [], + "prompts": [], + "overall_digest": "sha256:934bd842756311f661d17ac2d8bb4d847ba6d90db1633ec17e20077ca299dea7" + } +} diff --git a/vectors/cases/drift-capability-added.json b/vectors/cases/drift-capability-added.json index 6db1a07..dd5244d 100644 --- a/vectors/cases/drift-capability-added.json +++ b/vectors/cases/drift-capability-added.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "A command-like property appears -> capability-added shell-exec (high) plus the schema facts.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -41,13 +41,16 @@ } } }, - "entry_digest": "sha256:0e62b4c38ab51ad9f1465c34d1edc5c35caf390f539f19c02b36094a33bd34bc" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:197a8c26a39b8c15ea3e8ca84b82c6af7367c8039a342e33aaf62d963e97b153" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:030e7da96283225d7dbb2dfce22625de08411b05e4b1386bd27bd6046763d6c1", + "overall_digest": "sha256:53d2b3704434882166b8f7434352cfae246085f38631aefa54effc933eadbe24", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-capability-removed.json b/vectors/cases/drift-capability-removed.json index cb51a5e..f6991d4 100644 --- a/vectors/cases/drift-capability-removed.json +++ b/vectors/cases/drift-capability-removed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "The command-like property disappears -> capability-removed (medium).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -43,13 +43,16 @@ } } }, - "entry_digest": "sha256:a5373703d076b11a609c4d9fc62fe40df5f0bc07ebaa40fb218333a32ce304ab" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:7bad6b1676a763d6e1433853239964d64d19cda73e5729e89db3d5bb07001f1e" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:274ad381bec666cd46401a6175e126f519473846c04bb679ed22451817f73334", + "overall_digest": "sha256:d6063128fcdf744327879b0e4bf35f78250b36d45f5c6d157250d0e55ab48b57", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-description-and-schema.json b/vectors/cases/drift-description-and-schema.json index bfafa92..54e0bd7 100644 --- a/vectors/cases/drift-description-and-schema.json +++ b/vectors/cases/drift-description-and-schema.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Description AND schema changed: description-modified is suppressed in favour of the schema facts.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -41,13 +41,16 @@ } } }, - "entry_digest": "sha256:fc8a90335b37d118bbc2847f468af07d7806b20462e19a2cb53ef48a22084c28" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:5f75357dc16b9fc380fb02af404b4a8cb92f32fbe6d36a3be2e6483b7053b5de" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:2afb0cabd4196a43f990df2a1514dcbde697cc22dc6b0fcaf609fb95a22a60b1", + "overall_digest": "sha256:d8a0c738761dd191bb9db36fdaf3878bad8b7fb7e2365e13f431179585515ef8", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-description-modified.json b/vectors/cases/drift-description-modified.json index 890118b..75a12e5 100644 --- a/vectors/cases/drift-description-modified.json +++ b/vectors/cases/drift-description-modified.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Only the description changed -> description-modified (low).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -41,13 +41,16 @@ } } }, - "entry_digest": "sha256:fc8a90335b37d118bbc2847f468af07d7806b20462e19a2cb53ef48a22084c28" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:5f75357dc16b9fc380fb02af404b4a8cb92f32fbe6d36a3be2e6483b7053b5de" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:2afb0cabd4196a43f990df2a1514dcbde697cc22dc6b0fcaf609fb95a22a60b1", + "overall_digest": "sha256:d8a0c738761dd191bb9db36fdaf3878bad8b7fb7e2365e13f431179585515ef8", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-inspection-policy-modified.json b/vectors/cases/drift-inspection-policy-modified.json index 7659ab7..c09f0d3 100644 --- a/vectors/cases/drift-inspection-policy-modified.json +++ b/vectors/cases/drift-inspection-policy-modified.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Baseline tool carries a §11 inspection block, current does not -> inspection-policy-modified (medium).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -47,13 +47,16 @@ } } }, - "entry_digest": "sha256:1dd9b2b30f9aff6554c482d60aa7f7650a27c4aad2cf425dceff5565b83668b2" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:63d2a39673f1cbbde389ff63ff9e8e2128ab9b8309e6cfeed0b1436fbc6ef07d" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:0cf02cd9c122c7b39df516bab8709aa590fc6fa816013d5fadb38733f20b08d9", + "overall_digest": "sha256:f0cdbd2efad96333d37e037143fe448992da6d7557093c7a85128a0317d7dc11", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-mixed-multi-entry.json b/vectors/cases/drift-mixed-multi-entry.json index 62f1bfd..884da5c 100644 --- a/vectors/cases/drift-mixed-multi-entry.json +++ b/vectors/cases/drift-mixed-multi-entry.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Several entries drift at once; items are ordered by (target, drift_class).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -33,7 +33,10 @@ } } }, - "entry_digest": "sha256:cb207139f51a67842803bf299520d3084f2bb81f7da9167038a0dd320fa80fd0" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:f4e65ef148c0ee9d48615bccb119f10dff98891df7aab40893772792f9b76019" }, { "name": "b", @@ -64,7 +67,10 @@ } } }, - "entry_digest": "sha256:fbc8b8134f69304cc296245f76ddb81836a8f72434e8522eec0b77537a970322" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:690fc397a47ed6afd8cc6bbc36e25932574a77bf9393751f8f3780411284cc6b" } ], "resources": [ @@ -85,7 +91,7 @@ } ], "findings": [], - "overall_digest": "sha256:49126bdbc7b6f9f16affe8bafce27133562d35d82d719e17f2579ebca2cf6877", + "overall_digest": "sha256:e2ee45fbc4f2a5315119986327d5383dfd27a50dbbe48dbbb6bcbe5208f57be4", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-no-drift.json b/vectors/cases/drift-no-drift.json index 291a25e..eb4146d 100644 --- a/vectors/cases/drift-no-drift.json +++ b/vectors/cases/drift-no-drift.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Identical surface: overall_digest matches, the drift set is empty.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -43,13 +43,16 @@ } } }, - "entry_digest": "sha256:2bbf6c724227113547e88079aee35fe478aa32f6d35ba6eb70ea8683b7d0b251" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:7f1d4f53ec8dadad5a57319a7b8426479fbb6383e97cf9b9af004383a97c2cca" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46", + "overall_digest": "sha256:cd39071fa9a1f53a88ab1cacec7912ee9430ff5f37ecedeb49f558c8fbfacee6", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-prompt-added.json b/vectors/cases/drift-prompt-added.json index d8f5b8d..0d6900a 100644 --- a/vectors/cases/drift-prompt-added.json +++ b/vectors/cases/drift-prompt-added.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "prompt-added (medium).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -17,7 +17,7 @@ "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:3372c5e9da87c6d5fa8a4408770abea5376cbc7aa3b7d2ea6447e0d91b5abf66", + "overall_digest": "sha256:344f4d1c9106f8b4b6829e0081c8c47816a2c882881d05cdc5b7c2fd339e6ede", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-prompt-modified.json b/vectors/cases/drift-prompt-modified.json index 9b0198f..5af5ab3 100644 --- a/vectors/cases/drift-prompt-modified.json +++ b/vectors/cases/drift-prompt-modified.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Same name, changed arguments -> prompt-modified (low).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -24,7 +24,7 @@ } ], "findings": [], - "overall_digest": "sha256:518d5ede28e2aff4bd0396c27a8ca577b091dfd185f653f7cd6cc5b791b244f7", + "overall_digest": "sha256:c865c0809827b3f5b682854795983c2714cc24dd455625c4dca9f2eededd73ec", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-prompt-removed.json b/vectors/cases/drift-prompt-removed.json index e511571..f553115 100644 --- a/vectors/cases/drift-prompt-removed.json +++ b/vectors/cases/drift-prompt-removed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "prompt-removed (low).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -24,7 +24,7 @@ } ], "findings": [], - "overall_digest": "sha256:6b0bc437fc7e39f90fe0c85da8ef83676be617ffd9e890bb9c6f0917c3b616b0", + "overall_digest": "sha256:791a1c5dc1c27e4caf941fbd615b4d8f3442fcf180a7a36143e60d69b0d95e87", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-resource-added.json b/vectors/cases/drift-resource-added.json index 2b6d52a..326083b 100644 --- a/vectors/cases/drift-resource-added.json +++ b/vectors/cases/drift-resource-added.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "resource-added (medium).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -17,7 +17,7 @@ "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:3372c5e9da87c6d5fa8a4408770abea5376cbc7aa3b7d2ea6447e0d91b5abf66", + "overall_digest": "sha256:344f4d1c9106f8b4b6829e0081c8c47816a2c882881d05cdc5b7c2fd339e6ede", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-resource-modified.json b/vectors/cases/drift-resource-modified.json index c08e137..82ac269 100644 --- a/vectors/cases/drift-resource-modified.json +++ b/vectors/cases/drift-resource-modified.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Same uri, changed description -> resource-modified (low).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -25,7 +25,7 @@ ], "prompts": [], "findings": [], - "overall_digest": "sha256:8ca58792c6481eb495224f281a3b0757c249efeb6586ecd938db0697e2440ea1", + "overall_digest": "sha256:1914b0f61ac11241cfe6544df774e6bff54cdaff272c65d5326b9f56c9c367ae", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-resource-removed.json b/vectors/cases/drift-resource-removed.json index 22310b7..f133ca8 100644 --- a/vectors/cases/drift-resource-removed.json +++ b/vectors/cases/drift-resource-removed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "resource-removed (low).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -25,7 +25,7 @@ ], "prompts": [], "findings": [], - "overall_digest": "sha256:cb998c8b01d11dcaf97299174547fbb8719ecf5af7e06c024788e981c279e3ea", + "overall_digest": "sha256:41abb311430f544d726b9fbdcd1ae7856ca03cebcc12022cda41b6f3a46b2bbb", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-additional-props-opened-to-object.json b/vectors/cases/drift-schema-additional-props-opened-to-object.json index c34e648..6906913 100644 --- a/vectors/cases/drift-schema-additional-props-opened-to-object.json +++ b/vectors/cases/drift-schema-additional-props-opened-to-object.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "false -> a schema object is still the open-world escalation; detail carries the object.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -33,13 +33,16 @@ } } }, - "entry_digest": "sha256:4f4a3b27d255f826ef07c378e1f29cb81871190b4ed5f6679856ddb33c96a2ac" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:2a0c78ebba7342715610d90e2f6d4167f7f3b586787a5704de7a891e0e260ce4" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:1f6b72edc16d8eca4c3b5ef17b502c73f7974af4d43ded11a4fdfb70809aca6e", + "overall_digest": "sha256:073b93c188b3f5489091d00312d4cb143df6b958b5392a4161146f552a1f1e87", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-additional-props-opened.json b/vectors/cases/drift-schema-additional-props-opened.json index 6cdd2fa..0211cfe 100644 --- a/vectors/cases/drift-schema-additional-props-opened.json +++ b/vectors/cases/drift-schema-additional-props-opened.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-additional-props-opened (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -33,13 +33,16 @@ } } }, - "entry_digest": "sha256:4f4a3b27d255f826ef07c378e1f29cb81871190b4ed5f6679856ddb33c96a2ac" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:2a0c78ebba7342715610d90e2f6d4167f7f3b586787a5704de7a891e0e260ce4" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:1f6b72edc16d8eca4c3b5ef17b502c73f7974af4d43ded11a4fdfb70809aca6e", + "overall_digest": "sha256:073b93c188b3f5489091d00312d4cb143df6b958b5392a4161146f552a1f1e87", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-array-items-recursed.json b/vectors/cases/drift-schema-array-items-recursed.json index 6bccefd..8011c0e 100644 --- a/vectors/cases/drift-schema-array-items-recursed.json +++ b/vectors/cases/drift-schema-array-items-recursed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Array items are diffed at the 'a[]' path.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -54,13 +54,16 @@ } } }, - "entry_digest": "sha256:5abef47d6de08f9af86d069ac0d08991119cadabb29779d3aa315dd77e80cefb" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:0aa03344a30cb19d69451d820f96c64285c10fd090bb81974f3d9b60a56c9851" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:935d5c58d42d923cfc911eb2d63ff8917277ec2ccdb8674c6a3f8219a4a60185", + "overall_digest": "sha256:90694d4cdeed1dc21a990737635d36768cef5a8e3f5097e9a97b922727ba3fd6", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-constraint-relaxed-bound-removed.json b/vectors/cases/drift-schema-constraint-relaxed-bound-removed.json index 11ee0d5..f3414aa 100644 --- a/vectors/cases/drift-schema-constraint-relaxed-bound-removed.json +++ b/vectors/cases/drift-schema-constraint-relaxed-bound-removed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Removing an upper bound relaxes; adding a lower bound tightens.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -44,13 +44,16 @@ } } }, - "entry_digest": "sha256:b168ac2d98713763fc21bb84f2478fd06eb20dcac982d419f2c27d5ffd949283" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:685eab49e7c974532a4f5ff90e5956c301d3eb00285a7e58d3369664f5b872a0" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:9002475f2bac4aa37f7c916e9f8f154ff69eb1cb9369ea07c1b8c2f91c15b6a6", + "overall_digest": "sha256:f3edb104245d78d5ac3c0d2384f7bffa7fbb60bee9cf7c93c81858b3be4e757a", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-constraint-relaxed-maxlength-up.json b/vectors/cases/drift-schema-constraint-relaxed-maxlength-up.json index e0bea0e..63201bf 100644 --- a/vectors/cases/drift-schema-constraint-relaxed-maxlength-up.json +++ b/vectors/cases/drift-schema-constraint-relaxed-maxlength-up.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "detail is 'maxLength 64→4096'.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -44,13 +44,16 @@ } } }, - "entry_digest": "sha256:ccec6779bc8ea57f8e327dd9b774f9c5e491ce071b0e4a88d2b31b51aaa90f3c" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:d46144a16c7a368641a7ed6c7d8bd52252e2bb9e42a839f81c8698dd6c051f24" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:78ea78856226cea5e37ca95e23f121c8344ba75014bd3995ef552c98a1bd56af", + "overall_digest": "sha256:4dc32cf9dc8ee45c2ac91658fb0bc1b0a3073195d0076084d11b50afc20381b5", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-constraint-relaxed-minimum-lowered.json b/vectors/cases/drift-schema-constraint-relaxed-minimum-lowered.json index 7b7d36a..d50ca4c 100644 --- a/vectors/cases/drift-schema-constraint-relaxed-minimum-lowered.json +++ b/vectors/cases/drift-schema-constraint-relaxed-minimum-lowered.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-constraint-relaxed-minimum-lowered (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -44,13 +44,16 @@ } } }, - "entry_digest": "sha256:e382ae6ad223d4da11caf1f9261a5e70ff7bda38dac52507cdd67a68f25bb50b" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:e6e0ecd4249f12259374deedc5bafe4fb576dfc208138bd67658a0be66fa830b" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:9b223a7e66b7444ef0635ac60853d4a6346a8b183d03bc5b337953a724e7f80c", + "overall_digest": "sha256:2d2eb28086344215115e236ebe6849a048fa581412b6c602d8474ca0b3d029d4", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-constraint-relaxed-pattern-removed.json b/vectors/cases/drift-schema-constraint-relaxed-pattern-removed.json index f2d5126..bc407ef 100644 --- a/vectors/cases/drift-schema-constraint-relaxed-pattern-removed.json +++ b/vectors/cases/drift-schema-constraint-relaxed-pattern-removed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-constraint-relaxed-pattern-removed (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -44,13 +44,16 @@ } } }, - "entry_digest": "sha256:0641a9a845b27d5733e0fe080fac0fc2a408ea0398005f106e24ef4172ecaa75" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:380ba511d0862ac0e0a9808c878acc803bcc1f2235b3e1a468fdc56484354eea" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:d53ca1d578c99de4fe7fca3f94bcba87ded7c0e067ed43715bede3715e9e073b", + "overall_digest": "sha256:ac02b8840659bb4d69127ee040355a78e0c320b27a6d2d6decc66a3b4161f02d", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-constraint-relaxed-required-to-optional.json b/vectors/cases/drift-schema-constraint-relaxed-required-to-optional.json index fd81dbf..393100c 100644 --- a/vectors/cases/drift-schema-constraint-relaxed-required-to-optional.json +++ b/vectors/cases/drift-schema-constraint-relaxed-required-to-optional.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-constraint-relaxed-required-to-optional (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -43,13 +43,16 @@ } } }, - "entry_digest": "sha256:2efcca49a622e8a657515555e3881434d6877658e9c6ce6ae4d455d46bf507bb" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:8d6c9a7bf087ac45dcf7542435554c7cf0acc11bb701585b820ba4234353d560" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:10ba24d90fc467b06649e08638cecb98f100768c055565805af3971dfa4b0f59", + "overall_digest": "sha256:a1ba2cf6539ea4235fe00e4f8ed33e2e70a96f673bcdc87af18e2ebd368c6686", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-constraint-tightened.json b/vectors/cases/drift-schema-constraint-tightened.json index fa98cdf..a8a7181 100644 --- a/vectors/cases/drift-schema-constraint-tightened.json +++ b/vectors/cases/drift-schema-constraint-tightened.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "maxLength down + additionalProperties object -> false are both tightenings (low).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -46,13 +46,16 @@ } } }, - "entry_digest": "sha256:8f7ead1c3f00cf5682dec0fd36255f7ce755417db1768f1bf95393e0a61e4ac5" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:e5022b07f23de8bb1f66a8dbdbac44f25a201a5a3f7260d0d7c1c1dad6a580e1" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:abed128e8d40f45fcb2e616a622fdc66cc384f2a172014cf537b8d4a3a280e0e", + "overall_digest": "sha256:0f4d50cf996f012a894c54b935d094079e6ea1de5c5b2c236c6ce60dddb2947c", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-cosmetic-modified.json b/vectors/cases/drift-schema-cosmetic-modified.json index a334351..91a2119 100644 --- a/vectors/cases/drift-schema-cosmetic-modified.json +++ b/vectors/cases/drift-schema-cosmetic-modified.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Only a cosmetic key changed: hash differs, skeleton identical -> schema-cosmetic-modified (low).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -43,13 +43,16 @@ } } }, - "entry_digest": "sha256:c9d9119cbfba8355b8e64790f6977cc8cf84e7a43425c9b432348da5a8e28763" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:0d83270005c61a563c34a9b64deef228afd13802bae0490ceeed80513d1f8af8" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:4f3a703659d5b191e5282020e3ba8ea7b207a31eba1bc05778c3586c4cb6a902", + "overall_digest": "sha256:546905a04f1906035b869fbb27f20d070837ae488139d2905d5b326bd9b554d5", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-enum-added.json b/vectors/cases/drift-schema-enum-added.json index e14914d..e955e04 100644 --- a/vectors/cases/drift-schema-enum-added.json +++ b/vectors/cases/drift-schema-enum-added.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-enum-added (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -41,13 +41,16 @@ } } }, - "entry_digest": "sha256:506f31170b8d8548ab7a8b6ee0d2bcccc9be92838b0ea241960d2a2b3660cdd8" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:e9d77c0011310dadd71b847cbb0525d58aa5f2ee4dc0f0d2a50e486936d82208" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:11706002c93593ab288945f05f4af753b02291df13110f9425e8061792a8799d", + "overall_digest": "sha256:7bf759293168ac48ee4abff882e159480b8081adf693037ebe776ce42f9d17ad", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-enum-disjoint-is-widened.json b/vectors/cases/drift-schema-enum-disjoint-is-widened.json index c7e5aca..728ba72 100644 --- a/vectors/cases/drift-schema-enum-disjoint-is-widened.json +++ b/vectors/cases/drift-schema-enum-disjoint-is-widened.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Same size but different members counts as widening.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -44,13 +44,16 @@ } } }, - "entry_digest": "sha256:c0f73b86d221bddb7998df88692d5bf939926d540d315686cebc1ef9a29eb041" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:30708f3b94f06cc2cc003e6d458f1c5f0de75487ebd4c14c5f6b2b6a36206dbf" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:9372b30a06253525070e39981b1d0429800bc44713d4d161065cefa337aaa1e4", + "overall_digest": "sha256:34139eab0fddee4df789fe216d83bc427aab8abebab97eed114863db2fa927d1", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-enum-narrowed.json b/vectors/cases/drift-schema-enum-narrowed.json index 3e196e7..1022562 100644 --- a/vectors/cases/drift-schema-enum-narrowed.json +++ b/vectors/cases/drift-schema-enum-narrowed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-enum-narrowed (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -44,13 +44,16 @@ } } }, - "entry_digest": "sha256:c0f73b86d221bddb7998df88692d5bf939926d540d315686cebc1ef9a29eb041" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:30708f3b94f06cc2cc003e6d458f1c5f0de75487ebd4c14c5f6b2b6a36206dbf" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:9372b30a06253525070e39981b1d0429800bc44713d4d161065cefa337aaa1e4", + "overall_digest": "sha256:34139eab0fddee4df789fe216d83bc427aab8abebab97eed114863db2fa927d1", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-enum-removed.json b/vectors/cases/drift-schema-enum-removed.json index bce0593..7626fde 100644 --- a/vectors/cases/drift-schema-enum-removed.json +++ b/vectors/cases/drift-schema-enum-removed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-enum-removed (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -44,13 +44,16 @@ } } }, - "entry_digest": "sha256:c0f73b86d221bddb7998df88692d5bf939926d540d315686cebc1ef9a29eb041" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:30708f3b94f06cc2cc003e6d458f1c5f0de75487ebd4c14c5f6b2b6a36206dbf" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:9372b30a06253525070e39981b1d0429800bc44713d4d161065cefa337aaa1e4", + "overall_digest": "sha256:34139eab0fddee4df789fe216d83bc427aab8abebab97eed114863db2fa927d1", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-enum-widened.json b/vectors/cases/drift-schema-enum-widened.json index 1701def..92801df 100644 --- a/vectors/cases/drift-schema-enum-widened.json +++ b/vectors/cases/drift-schema-enum-widened.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-enum-widened (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -43,13 +43,16 @@ } } }, - "entry_digest": "sha256:96d7e748fdd9c40fe49f60985a11ea8ccce1db1d6d7e0635658e8de0d61fdf80" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:f0600829a6cdc03bfb74d7a62cbb5b607f4772522d791f4aa9bf9351046d0d83" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:9de11d226e9ba3eade030e4fea456d3096fd4cc4df55347b543f71637a66146c", + "overall_digest": "sha256:38eadc9652447d7f83004263a0a6f1337a42036ceca97043887608e4ad287b27", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-modified-opaque-ref.json b/vectors/cases/drift-schema-modified-opaque-ref.json index 20272c2..c58ea0f 100644 --- a/vectors/cases/drift-schema-modified-opaque-ref.json +++ b/vectors/cases/drift-schema-modified-opaque-ref.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "A remote $ref target changed -> opaque-leaf schema-modified (high) carrying both literals.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -41,13 +41,16 @@ } } }, - "entry_digest": "sha256:0bf1e1755902e74905d651fc9fc8af39966371065b2d5e8bff34092de00d184d" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:9faa7fbbd54e88c24d753e325789b57c5dd9d70d2bd192e3d40e04567021fa82" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:a0d36e125a6ce600c0a513f76f6ec692cc9763fc13221e6c8a8b7810042a77ce", + "overall_digest": "sha256:2bf1c50e3f9e9d5148298353215de110ba15c7eb25d245abe04a54cfeae082f3", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-modified-ref-redacted.json b/vectors/cases/drift-schema-modified-ref-redacted.json index 6d9d84a..776ea67 100644 --- a/vectors/cases/drift-schema-modified-ref-redacted.json +++ b/vectors/cases/drift-schema-modified-ref-redacted.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "An opaque $ref whose target looks secret is redacted in detail.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -41,13 +41,16 @@ } } }, - "entry_digest": "sha256:129daeeab62bd91cb958d605fcb346f5f46b02c0080e2ec0428407e70eb0fb41" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:22654fd167491ef2fb1d620a38c7641f9add415e435152a5640b0db0fefde010" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:5d4b058d9b9a0e7047c5fd5663ec54d8b09624e12b714b19c8d7992b6cc5ca94", + "overall_digest": "sha256:742600e51a3eb4fb1b03354a202297e81bfe80f2cffefb0ea2cbdf505d349485", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-modified-v1-fallback.json b/vectors/cases/drift-schema-modified-v1-fallback.json index 7f0f70e..0792e29 100644 --- a/vectors/cases/drift-schema-modified-v1-fallback.json +++ b/vectors/cases/drift-schema-modified-v1-fallback.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Baseline without a schema_skeleton (v1 lock) + changed schema -> single schema-modified (high).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -20,13 +20,16 @@ "input_schema_hash": "sha256:a4fa3d2b926b7397a8766e81cfe262ab54af1d77989b8ae754e5b255a4d36485", "capabilities": [], "schema_skeleton": null, - "entry_digest": "sha256:506f31170b8d8548ab7a8b6ee0d2bcccc9be92838b0ea241960d2a2b3660cdd8" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:e9d77c0011310dadd71b847cbb0525d58aa5f2ee4dc0f0d2a50e486936d82208" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:11706002c93593ab288945f05f4af753b02291df13110f9425e8061792a8799d", + "overall_digest": "sha256:7bf759293168ac48ee4abff882e159480b8081adf693037ebe776ce42f9d17ad", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-out-added.json b/vectors/cases/drift-schema-out-added.json new file mode 100644 index 0000000..74983b4 --- /dev/null +++ b/vectors/cases/drift-schema-out-added.json @@ -0,0 +1,72 @@ +{ + "id": "drift/schema-out-added", + "kind": "drift", + "description": "Output-schema commitment with structural classification.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:321c7e7ad98b9b198097bc65f7d14c097be23e03976359ac1344bba24b879029" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:934bd842756311f661d17ac2d8bb4d847ba6d90db1633ec17e20077ca299dea7", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "outputSchema": {} + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "schema-out-added", + "severity": "high", + "target": "tools/t", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-schema-out-constraint-relaxed.json b/vectors/cases/drift-schema-out-constraint-relaxed.json new file mode 100644 index 0000000..16b7df3 --- /dev/null +++ b/vectors/cases/drift-schema-out-constraint-relaxed.json @@ -0,0 +1,104 @@ +{ + "id": "drift/schema-out-constraint-relaxed", + "kind": "drift", + "description": "Output-schema commitment with structural classification.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:65dc503200ebe0947671573d0657544d019d435f9c4cc0cc1c5d7a21ad12d0cc", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "x": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true, + "maxLength": 8 + } + } + } + }, + "entry_digest": "sha256:16e4ae881d150a7d393e24464079e1bda89f03695901811e405d76e48c044946" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:c90d0be80c26732fa5ef238dde55679d94bab5e6f46f9cb7afe581b48c4b4459", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "outputSchema": { + "type": "object", + "properties": { + "x": { + "type": "string", + "maxLength": 64 + } + } + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "schema-out-constraint-relaxed", + "severity": "medium", + "target": "tools/t", + "detail": "maxLength 8→64" + } + ] +} diff --git a/vectors/cases/drift-schema-out-cosmetic-modified.json b/vectors/cases/drift-schema-out-cosmetic-modified.json new file mode 100644 index 0000000..6753757 --- /dev/null +++ b/vectors/cases/drift-schema-out-cosmetic-modified.json @@ -0,0 +1,89 @@ +{ + "id": "drift/schema-out-cosmetic-modified", + "kind": "drift", + "description": "Output-schema commitment with structural classification.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:950f13c15c86fbd44f5c70efbd7cda4185302b74e6f794fe9c1715faaa8b68a6", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:6434c6576e7e297d045625801df25b8d4eefeffe194fdeda5bf155d6d0a40513" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:98015c0d26ea80d4f2445374106e8100ac16b1adaf69b0a8e1b86fb862f8f68d", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "outputSchema": { + "type": "object", + "properties": {}, + "title": "after" + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "schema-out-cosmetic-modified", + "severity": "low", + "target": "tools/t", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-schema-out-extension-null.json b/vectors/cases/drift-schema-out-extension-null.json new file mode 100644 index 0000000..461a8ae --- /dev/null +++ b/vectors/cases/drift-schema-out-extension-null.json @@ -0,0 +1,88 @@ +{ + "id": "drift/schema-out-extension-null", + "kind": "drift", + "description": "Output schemas retain explicit null extension values.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:c7c60ff2c19a0e2b4f8112814482ac98dff4f0327684908f555643f3cd4a68d1", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:f76f7cf35b247d98ebd4fa8257d020c4715e3f72df2ee6f096fc47acb9bf3b1e" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:83d5122c7828b7de2b24fd26cac1482a5f01515f205d3879759ba5c99a0a1f55", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "outputSchema": { + "type": "object", + "x": "after" + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "schema-out-cosmetic-modified", + "severity": "low", + "target": "tools/t", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-schema-out-local-ref.json b/vectors/cases/drift-schema-out-local-ref.json new file mode 100644 index 0000000..c9bb356 --- /dev/null +++ b/vectors/cases/drift-schema-out-local-ref.json @@ -0,0 +1,107 @@ +{ + "id": "drift/schema-out-local-ref", + "kind": "drift", + "description": "Output-schema commitment with structural classification.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:adfb48bdd377a4c99cf74a8bdd03d6efc40e26fb1bb22c49b3f765886a82c5cd", + "output_schema_skeleton": { + "props": { + "$root": { + "type": null, + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "x": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:066dcbae0219b9dc960de22ea5753746221598a1ebf809d51924dd83c205dbbd" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:ad6f418a2f243f84518ef63c5eb1b283d589d6837f66ea57a84f66ca39d6e1b7", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "outputSchema": { + "$defs": { + "x": { + "type": [ + "string", + "number" + ] + } + }, + "properties": { + "x": { + "$ref": "#/$defs/x" + } + } + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "schema-out-type-broadened", + "severity": "high", + "target": "tools/t", + "detail": "type ['string']→['number', 'string']" + } + ] +} diff --git a/vectors/cases/drift-schema-out-ref-redacted.json b/vectors/cases/drift-schema-out-ref-redacted.json new file mode 100644 index 0000000..924c095 --- /dev/null +++ b/vectors/cases/drift-schema-out-ref-redacted.json @@ -0,0 +1,100 @@ +{ + "id": "drift/schema-out-ref-redacted", + "kind": "drift", + "description": "Output-schema commitment with structural classification.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:1c42ec4e183d785fcb205eff3fa6d683582c433bad1d94f51ba32c57ef280236", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "x": { + "type": null, + "required": false, + "enum": null, + "constraints": { + "$ref": "https://example.com/apiKey" + } + } + } + }, + "entry_digest": "sha256:2bef9f81140c4ba5ef0680ec6cf81d8421caf661005407f358f3c604b12af24d" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:8865ec659aebb05ca7f07a076e740c8e65a5dc34c87c7c750528dc7b69e8b870", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "outputSchema": { + "type": "object", + "properties": { + "x": { + "$ref": "https://example.com/token" + } + } + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "schema-out-modified", + "severity": "high", + "target": "tools/t", + "detail": "$ref →" + } + ] +} diff --git a/vectors/cases/drift-schema-out-removed.json b/vectors/cases/drift-schema-out-removed.json new file mode 100644 index 0000000..07ae9dd --- /dev/null +++ b/vectors/cases/drift-schema-out-removed.json @@ -0,0 +1,83 @@ +{ + "id": "drift/schema-out-removed", + "kind": "drift", + "description": "Output-schema commitment with structural classification.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "output_schema_skeleton": { + "props": { + "$root": { + "type": null, + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:eae21e6a44a86b2f68ce0d8f51b42e7efefcda2eb610403b8f3312aead3b859a" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:bc70055828e9dfcb36e22fa5175cb99020b414c62ad6e9880088e6195b1dc14a", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "outputSchema": null + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "schema-out-removed", + "severity": "high", + "target": "tools/t", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-schema-out-type-broadened.json b/vectors/cases/drift-schema-out-type-broadened.json new file mode 100644 index 0000000..3f82e48 --- /dev/null +++ b/vectors/cases/drift-schema-out-type-broadened.json @@ -0,0 +1,105 @@ +{ + "id": "drift/schema-out-type-broadened", + "kind": "drift", + "description": "Output-schema commitment with structural classification.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:df0cd751860cebb7dbf04cb311a379d2ffcef96035486aafc13f2b6d5c610077", + "output_schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "x": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:93013d445afcf1ae9130d266dd337c5679f0140659d7bf5ed8d4c938cd2ba58f" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:c13a0154d5559b90170ea5d5f08f284a012bce15f569c4da2ed2a699d7fd96ba", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "outputSchema": { + "type": "object", + "properties": { + "x": { + "type": [ + "string", + "number" + ] + } + } + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "schema-out-type-broadened", + "severity": "high", + "target": "tools/t", + "detail": "type ['string']→['number', 'string']" + } + ] +} diff --git a/vectors/cases/drift-schema-pattern-changed-tightened.json b/vectors/cases/drift-schema-pattern-changed-tightened.json index 57e457d..810024f 100644 --- a/vectors/cases/drift-schema-pattern-changed-tightened.json +++ b/vectors/cases/drift-schema-pattern-changed-tightened.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "pattern changed (not removed) is a tightening with no value echoed.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -44,13 +44,16 @@ } } }, - "entry_digest": "sha256:e442b6d06a34dee82a9950871d9fda38d677f9c48283e471bf31590325abd02a" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:de346cd36c742e14c3aeb6d2d24ba808d027cbc5eacbabd2ea58a1b96e06cd8a" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:a70cd89675df6528d229d2ff406baa3b865ef440f3b3a447af353382bef4debf", + "overall_digest": "sha256:09e261bcfe6397cd4e3f397bd7288d9b46d060fb00271eaa07a9d5306ff9c4cd", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-per-fact-emission.json b/vectors/cases/drift-schema-per-fact-emission.json index f825ccd..da4e024 100644 --- a/vectors/cases/drift-schema-per-fact-emission.json +++ b/vectors/cases/drift-schema-per-fact-emission.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "required->optional AND string->string|null on one property emit two facts.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -43,13 +43,16 @@ } } }, - "entry_digest": "sha256:2efcca49a622e8a657515555e3881434d6877658e9c6ce6ae4d455d46bf507bb" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:8d6c9a7bf087ac45dcf7542435554c7cf0acc11bb701585b820ba4234353d560" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:10ba24d90fc467b06649e08638cecb98f100768c055565805af3971dfa4b0f59", + "overall_digest": "sha256:a1ba2cf6539ea4235fe00e4f8ed33e2e70a96f673bcdc87af18e2ebd368c6686", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-property-added.json b/vectors/cases/drift-schema-property-added.json index 624276f..82225ff 100644 --- a/vectors/cases/drift-schema-property-added.json +++ b/vectors/cases/drift-schema-property-added.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-property-added (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -33,13 +33,16 @@ } } }, - "entry_digest": "sha256:fc9373d75e8760f06d8500e39c99c4b4ffd2ef8a8ded6a68e0c487596ef0d35a" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:332f5e0442396b518012a0706327224715e0b977bacde45b4cc7eb29eed4882e" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:8d995d941c197d3d0a2783e9eea4ffb6f8fc1961db440037d114354902abbdf5", + "overall_digest": "sha256:2a4419bd93b6a2ccb0d81f121d39a2f00904ce29b13ec9273c43eaff3a250e8c", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-property-removed.json b/vectors/cases/drift-schema-property-removed.json index a64528f..9f3c97a 100644 --- a/vectors/cases/drift-schema-property-removed.json +++ b/vectors/cases/drift-schema-property-removed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-property-removed (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -43,13 +43,16 @@ } } }, - "entry_digest": "sha256:4672284c353d4d695b1c78c6f71d2e0f6d45237e1d3b69af9dbc3d07354283a8" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a6a2979046f5b0a277f63b41b5d34859ac59500540a663d7d4cd661aa18690ef" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:4d13d754e03c0b113251a55555061cfd6dfddb256aea5329010fca1f1414e4c7", + "overall_digest": "sha256:53db718d077896e3c2cc2933d4eba8e9612a2808ba40dca1b9e2c184dd6811e1", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-ref-resolved-granular.json b/vectors/cases/drift-schema-ref-resolved-granular.json index f774e8c..b158d9a 100644 --- a/vectors/cases/drift-schema-ref-resolved-granular.json +++ b/vectors/cases/drift-schema-ref-resolved-granular.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "A constraint relaxed inside an in-document $ref target classifies granularly at the property path (R8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -44,13 +44,16 @@ } } }, - "entry_digest": "sha256:c32f7d42819d9d27fd91a36b1c12ab7f090ff592fc16e987964a8b9c3d7c5c4a" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:2773d1b9a271b3218b08d7c33b16eed4a8ac6734f42cafae1db4ab83be2dcb25" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:a851092eb2278aa6f0f9f22e12a83bf71fd8c480d6b61a45bf067f1f14ff5d6c", + "overall_digest": "sha256:1506b787dded709a95302e649a6b8e1bdb40553375a069d3f5522f3fa30144a1", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-required-added.json b/vectors/cases/drift-schema-required-added.json index 384236b..0f7a7a7 100644 --- a/vectors/cases/drift-schema-required-added.json +++ b/vectors/cases/drift-schema-required-added.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-required-added (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -33,13 +33,16 @@ } } }, - "entry_digest": "sha256:fc9373d75e8760f06d8500e39c99c4b4ffd2ef8a8ded6a68e0c487596ef0d35a" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:332f5e0442396b518012a0706327224715e0b977bacde45b4cc7eb29eed4882e" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:8d995d941c197d3d0a2783e9eea4ffb6f8fc1961db440037d114354902abbdf5", + "overall_digest": "sha256:2a4419bd93b6a2ccb0d81f121d39a2f00904ce29b13ec9273c43eaff3a250e8c", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-required-removed.json b/vectors/cases/drift-schema-required-removed.json index 654d1a0..a04bfe7 100644 --- a/vectors/cases/drift-schema-required-removed.json +++ b/vectors/cases/drift-schema-required-removed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-required-removed (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -43,13 +43,16 @@ } } }, - "entry_digest": "sha256:2efcca49a622e8a657515555e3881434d6877658e9c6ce6ae4d455d46bf507bb" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:8d6c9a7bf087ac45dcf7542435554c7cf0acc11bb701585b820ba4234353d560" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:10ba24d90fc467b06649e08638cecb98f100768c055565805af3971dfa4b0f59", + "overall_digest": "sha256:a1ba2cf6539ea4235fe00e4f8ed33e2e70a96f673bcdc87af18e2ebd368c6686", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-required-unconstrained-added.json b/vectors/cases/drift-schema-required-unconstrained-added.json index c6254ce..f6adae8 100644 --- a/vectors/cases/drift-schema-required-unconstrained-added.json +++ b/vectors/cases/drift-schema-required-unconstrained-added.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-required-unconstrained-added (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -33,13 +33,16 @@ } } }, - "entry_digest": "sha256:fc9373d75e8760f06d8500e39c99c4b4ffd2ef8a8ded6a68e0c487596ef0d35a" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:332f5e0442396b518012a0706327224715e0b977bacde45b4cc7eb29eed4882e" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:8d995d941c197d3d0a2783e9eea4ffb6f8fc1961db440037d114354902abbdf5", + "overall_digest": "sha256:2a4419bd93b6a2ccb0d81f121d39a2f00904ce29b13ec9273c43eaff3a250e8c", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-type-any-to-typed.json b/vectors/cases/drift-schema-type-any-to-typed.json index 2b839d6..110d86d 100644 --- a/vectors/cases/drift-schema-type-any-to-typed.json +++ b/vectors/cases/drift-schema-type-any-to-typed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "any -> typed is a narrowing (low); typed -> any is a broadening (high).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -51,13 +51,16 @@ } } }, - "entry_digest": "sha256:e2235490b06847445b96cdf5ff6ff27c70a8858d12d91b9db3e897c6c1260899" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:6221ebac367e2b387195c0d9b1ecf18e9e85755427434da6ab0f101c9c0a42d2" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:a40c514127abb67c7965523a88f60c55921bf2817554b6126c23b10f231eccd7", + "overall_digest": "sha256:8b97888c706317ccf17f7af214b89e405fc2e44eedaf6f3c9bae4e6083795347", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-type-broadened.json b/vectors/cases/drift-schema-type-broadened.json index e0fb777..54d2a41 100644 --- a/vectors/cases/drift-schema-type-broadened.json +++ b/vectors/cases/drift-schema-type-broadened.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-type-broadened (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -43,13 +43,16 @@ } } }, - "entry_digest": "sha256:4672284c353d4d695b1c78c6f71d2e0f6d45237e1d3b69af9dbc3d07354283a8" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a6a2979046f5b0a277f63b41b5d34859ac59500540a663d7d4cd661aa18690ef" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:4d13d754e03c0b113251a55555061cfd6dfddb256aea5329010fca1f1414e4c7", + "overall_digest": "sha256:53db718d077896e3c2cc2933d4eba8e9612a2808ba40dca1b9e2c184dd6811e1", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-type-changed.json b/vectors/cases/drift-schema-type-changed.json index 7466a7d..7056c4e 100644 --- a/vectors/cases/drift-schema-type-changed.json +++ b/vectors/cases/drift-schema-type-changed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-type-changed (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -43,13 +43,16 @@ } } }, - "entry_digest": "sha256:4672284c353d4d695b1c78c6f71d2e0f6d45237e1d3b69af9dbc3d07354283a8" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a6a2979046f5b0a277f63b41b5d34859ac59500540a663d7d4cd661aa18690ef" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:4d13d754e03c0b113251a55555061cfd6dfddb256aea5329010fca1f1414e4c7", + "overall_digest": "sha256:53db718d077896e3c2cc2933d4eba8e9612a2808ba40dca1b9e2c184dd6811e1", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-type-narrowed.json b/vectors/cases/drift-schema-type-narrowed.json index 292c902..b905de1 100644 --- a/vectors/cases/drift-schema-type-narrowed.json +++ b/vectors/cases/drift-schema-type-narrowed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-type-narrowed (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -44,13 +44,16 @@ } } }, - "entry_digest": "sha256:e24fd3a5d2c2581c52e7f65c08f4f35f5615a2e7c78ac7cf70ddc958df0fc34b" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:3a9a7d3aef416995f83cdd428fad6dd6da2309a18cc86c40551446a5443dc522" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:8e5f029348d4c6f5de071cce406fcd26d0d754c032b2b5c4c4d02545840a3b4b", + "overall_digest": "sha256:5514c09badec6896bfbe5821e648fac084b08b22db5c235ac219abf724b7ba3b", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-schema-unconstrained-added.json b/vectors/cases/drift-schema-unconstrained-added.json index 910591e..f59d354 100644 --- a/vectors/cases/drift-schema-unconstrained-added.json +++ b/vectors/cases/drift-schema-unconstrained-added.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Drift class schema-unconstrained-added (see docs/SPEC.md §8).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -33,13 +33,16 @@ } } }, - "entry_digest": "sha256:fc9373d75e8760f06d8500e39c99c4b4ffd2ef8a8ded6a68e0c487596ef0d35a" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:332f5e0442396b518012a0706327224715e0b977bacde45b4cc7eb29eed4882e" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:8d995d941c197d3d0a2783e9eea4ffb6f8fc1961db440037d114354902abbdf5", + "overall_digest": "sha256:2a4419bd93b6a2ccb0d81f121d39a2f00904ce29b13ec9273c43eaff3a250e8c", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-server-identity-url.json b/vectors/cases/drift-server-identity-url.json index ab2d2c7..74b1f2d 100644 --- a/vectors/cases/drift-server-identity-url.json +++ b/vectors/cases/drift-server-identity-url.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "HTTP endpoint changed -> server-identity (critical).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "", @@ -31,13 +31,16 @@ } } }, - "entry_digest": "sha256:fc9373d75e8760f06d8500e39c99c4b4ffd2ef8a8ded6a68e0c487596ef0d35a" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:332f5e0442396b518012a0706327224715e0b977bacde45b4cc7eb29eed4882e" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:1ddb7a0ce0ce1f08f0dffbeeaea4f0675355299d8c6c27d44e55052c7c0ed71d", + "overall_digest": "sha256:2f5e2abed43353f0c223a46941d81e896977ac9e7859bcdff98f4c4ab815731b", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-server-identity.json b/vectors/cases/drift-server-identity.json index 2fafb27..9e9a069 100644 --- a/vectors/cases/drift-server-identity.json +++ b/vectors/cases/drift-server-identity.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "argv changed -> server-identity (critical).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -33,13 +33,16 @@ } } }, - "entry_digest": "sha256:fc9373d75e8760f06d8500e39c99c4b4ffd2ef8a8ded6a68e0c487596ef0d35a" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:332f5e0442396b518012a0706327224715e0b977bacde45b4cc7eb29eed4882e" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:8d995d941c197d3d0a2783e9eea4ffb6f8fc1961db440037d114354902abbdf5", + "overall_digest": "sha256:2a4419bd93b6a2ccb0d81f121d39a2f00904ce29b13ec9273c43eaff3a250e8c", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-tool-added.json b/vectors/cases/drift-tool-added.json index 1a9dcb4..28af456 100644 --- a/vectors/cases/drift-tool-added.json +++ b/vectors/cases/drift-tool-added.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "A new tool name -> tool-added (high).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -33,13 +33,16 @@ } } }, - "entry_digest": "sha256:cb207139f51a67842803bf299520d3084f2bb81f7da9167038a0dd320fa80fd0" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:f4e65ef148c0ee9d48615bccb119f10dff98891df7aab40893772792f9b76019" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:f229e33df177a19793ec1991b6a7a740a30d963c7e094d122d86fdb6372222e3", + "overall_digest": "sha256:6532a3fbb7a49c68e9bb4e7dd0545b4fc792fce354ed2ceac0de8a4d0b0784c6", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-tool-annotations-destructiveHint.json b/vectors/cases/drift-tool-annotations-destructiveHint.json new file mode 100644 index 0000000..4bddbeb --- /dev/null +++ b/vectors/cases/drift-tool-annotations-destructiveHint.json @@ -0,0 +1,74 @@ +{ + "id": "drift/tool-annotations-destructiveHint", + "kind": "drift", + "description": "Hint changes cannot silently reuse an approved surface.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:2d0431446f6285492add633c6774ae2fa56bd35720e145240dd962bfeeae05a6", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a6e9715e32b26d94f3b347a65b7534588a476f6dd723bb190a85a8d7219adc71" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:eabf4d1acd1aec12ba14905919981f8158628e8261d16d7e134f8edc805fab42", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "annotations": { + "destructiveHint": true + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "tool-annotations-modified", + "severity": "high", + "target": "tools/t", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-tool-annotations-explicit-null.json b/vectors/cases/drift-tool-annotations-explicit-null.json new file mode 100644 index 0000000..46f4b18 --- /dev/null +++ b/vectors/cases/drift-tool-annotations-explicit-null.json @@ -0,0 +1,74 @@ +{ + "id": "drift/tool-annotations-explicit-null", + "kind": "drift", + "description": "Complete wire annotations retain extensions and explicit nulls.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:c1f25e033c8f9e94ab8c792e3fd742d3965279377d61133fec3fbccef9398881" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:e20d4fc9a379fb97ba53021e60daf5d32d8fde1ca58bc44e4479b473ce73d4d5", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "annotations": { + "title": null + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "tool-annotations-modified", + "severity": "high", + "target": "tools/t", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-tool-annotations-extension.json b/vectors/cases/drift-tool-annotations-extension.json new file mode 100644 index 0000000..2f4653e --- /dev/null +++ b/vectors/cases/drift-tool-annotations-extension.json @@ -0,0 +1,74 @@ +{ + "id": "drift/tool-annotations-extension", + "kind": "drift", + "description": "Complete wire annotations retain extensions and explicit nulls.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:f2f0fa02d54a648c26fb829f3625839eca4bc310da1097fb88d8cc280579698e", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:3e3c620ac621e7d445e8ad31d53627ac074e84ad63305ba70564dc8f8e9c59fb" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:a1dce5b1a52a06c84fe2a20df5eb858ddd1cf4b82e137f22ec178e188087bfa4", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "annotations": { + "x": "after" + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "tool-annotations-modified", + "severity": "high", + "target": "tools/t", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-tool-annotations-idempotentHint.json b/vectors/cases/drift-tool-annotations-idempotentHint.json new file mode 100644 index 0000000..af52994 --- /dev/null +++ b/vectors/cases/drift-tool-annotations-idempotentHint.json @@ -0,0 +1,74 @@ +{ + "id": "drift/tool-annotations-idempotentHint", + "kind": "drift", + "description": "Hint changes cannot silently reuse an approved surface.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:a9921e362c39501d7eafce412c9ae8217ad6e8ddeb8755c12619cd66fb618f4f", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:14dbff6c583e29bd9c93be5c8b9fcd5308a03c80f6b5a924f5c7fcf8138355bb" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:61b62e91739d4bf6eb77001bb78ecb0a6a1cb8489e6ab000bb82b47620b183a3", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "annotations": { + "idempotentHint": true + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "tool-annotations-modified", + "severity": "high", + "target": "tools/t", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-tool-annotations-nested-null.json b/vectors/cases/drift-tool-annotations-nested-null.json new file mode 100644 index 0000000..bd164a8 --- /dev/null +++ b/vectors/cases/drift-tool-annotations-nested-null.json @@ -0,0 +1,76 @@ +{ + "id": "drift/tool-annotations-nested-null", + "kind": "drift", + "description": "Complete wire annotations retain extensions and explicit nulls.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:1a14ddd66b5d3f1f45e0d787b3598f181156f4cdbfce6ae99211cd910a36f83e", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:3d5931f1aebee591362030841195395ee208d9974fa20dc1b1bafbe620618c2e" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:ef62b68f7449f58f5b41174b5f01e9c66fb63303192962f056d8ecfa8016c4c7", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "annotations": { + "x": { + "nested": "after" + } + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "tool-annotations-modified", + "severity": "high", + "target": "tools/t", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-tool-annotations-openWorldHint.json b/vectors/cases/drift-tool-annotations-openWorldHint.json new file mode 100644 index 0000000..8dce2b7 --- /dev/null +++ b/vectors/cases/drift-tool-annotations-openWorldHint.json @@ -0,0 +1,74 @@ +{ + "id": "drift/tool-annotations-openWorldHint", + "kind": "drift", + "description": "Hint changes cannot silently reuse an approved surface.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:6d6322dee7c5fce5b9ee774b993219adbd3524885cbb4494c8339fe7cbf49fab", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a35fc17259eb0a5432da0d2a55f8d1f9d2c94711459abf368a21b0949c291423" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:7c4e143965ca7417eefe00fe438466e10501a530825d6a9f6d6be61d0142a50b", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "annotations": { + "openWorldHint": true + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "tool-annotations-modified", + "severity": "high", + "target": "tools/t", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-tool-annotations-readOnlyHint.json b/vectors/cases/drift-tool-annotations-readOnlyHint.json new file mode 100644 index 0000000..070d952 --- /dev/null +++ b/vectors/cases/drift-tool-annotations-readOnlyHint.json @@ -0,0 +1,74 @@ +{ + "id": "drift/tool-annotations-readOnlyHint", + "kind": "drift", + "description": "Hint changes cannot silently reuse an approved surface.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:a928ca8041f158ddef4913627a16acdc097291e12de3b72d9d55d8527802f6df", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:48edf36f19c06f4c73006a1f34e95eb140bc9acbcffdd12e5726c4ef73228867" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:bb3a19dcb8e2976e59c06c785e5ef8a0b6a55a02b221679490613b825265cb94", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "annotations": { + "readOnlyHint": true + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "tool-annotations-modified", + "severity": "high", + "target": "tools/t", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-tool-annotations-removed.json b/vectors/cases/drift-tool-annotations-removed.json new file mode 100644 index 0000000..dc0a68f --- /dev/null +++ b/vectors/cases/drift-tool-annotations-removed.json @@ -0,0 +1,71 @@ +{ + "id": "drift/tool-annotations-removed", + "kind": "drift", + "description": "Removing declarations is drift too.", + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a", + "capabilities": [], + "schema_skeleton": { + "props": {} + }, + "annotations_hash": "sha256:2d0431446f6285492add633c6774ae2fa56bd35720e145240dd962bfeeae05a6", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:a6e9715e32b26d94f3b347a65b7534588a476f6dd723bb190a85a8d7219adc71" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:eabf4d1acd1aec12ba14905919981f8158628e8261d16d7e134f8edc805fab42", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t" + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "tool-annotations-modified", + "severity": "high", + "target": "tools/t", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-tool-removed.json b/vectors/cases/drift-tool-removed.json index 6019cad..4c087bb 100644 --- a/vectors/cases/drift-tool-removed.json +++ b/vectors/cases/drift-tool-removed.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "A tool disappeared -> tool-removed (medium).", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -33,7 +33,10 @@ } } }, - "entry_digest": "sha256:cb207139f51a67842803bf299520d3084f2bb81f7da9167038a0dd320fa80fd0" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:f4e65ef148c0ee9d48615bccb119f10dff98891df7aab40893772792f9b76019" }, { "name": "b", @@ -54,13 +57,16 @@ } } }, - "entry_digest": "sha256:df237aeed1c35f312bd738555a61c9a644f6ceb1a29bafca35893517bdb3a44c" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:b566ecee1095488190937dc9ff9fdc7d911fc595e8df3f017cd8ff5b87c89703" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:2c0c42576172c829bcc7e6189e529b07eae25ae0a732a374af06e7c967c7a7bd", + "overall_digest": "sha256:fec1cd0e34943ea2543c3f9195feb74be8ea6a39a865cc84e269df6073682d3a", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/drift-unapproved-change.json b/vectors/cases/drift-unapproved-change.json index 5645d07..5566b43 100644 --- a/vectors/cases/drift-unapproved-change.json +++ b/vectors/cases/drift-unapproved-change.json @@ -3,7 +3,7 @@ "kind": "drift", "description": "Approved baseline + any surface change -> unapproved-change (high) alongside the entry drift.", "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -41,13 +41,16 @@ } } }, - "entry_digest": "sha256:fc8a90335b37d118bbc2847f468af07d7806b20462e19a2cb53ef48a22084c28" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:5f75357dc16b9fc380fb02af404b4a8cb92f32fbe6d36a3be2e6483b7053b5de" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:2afb0cabd4196a43f990df2a1514dcbde697cc22dc6b0fcaf609fb95a22a60b1", + "overall_digest": "sha256:d8a0c738761dd191bb9db36fdaf3878bad8b7fb7e2365e13f431179585515ef8", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", @@ -55,7 +58,7 @@ "approved": true, "approver": "vectors@example.invalid", "approved_at": "2026-01-01T00:00:00Z", - "approved_digest": "sha256:2afb0cabd4196a43f990df2a1514dcbde697cc22dc6b0fcaf609fb95a22a60b1", + "approved_digest": "sha256:d8a0c738761dd191bb9db36fdaf3878bad8b7fb7e2365e13f431179585515ef8", "provenance_version": 1, "pinner": null, "attestations": [], diff --git a/vectors/cases/drift-v3-to-v4-migration.json b/vectors/cases/drift-v3-to-v4-migration.json new file mode 100644 index 0000000..2aa7c37 --- /dev/null +++ b/vectors/cases/drift-v3-to-v4-migration.json @@ -0,0 +1,103 @@ +{ + "id": "drift/v3-to-v4-migration", + "kind": "drift", + "description": "Genuine approved v3 baseline requires re-attestation; no invented annotation drift.", + "lock": { + "schema_version": 3, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:df0cd751860cebb7dbf04cb311a379d2ffcef96035486aafc13f2b6d5c610077", + "capabilities": [], + "schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "x": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:2bbf6c724227113547e88079aee35fe478aa32f6d35ba6eb70ea8683b7d0b251" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": true, + "approver": "historical-reviewer@example.invalid", + "approved_at": null, + "approved_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46", + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "inputSchema": { + "type": "object", + "properties": { + "x": { + "type": "string" + } + } + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "schema-version-migrated", + "severity": "low", + "target": "pin/approved_digest", + "detail": null + }, + { + "drift_class": "unapproved-change", + "severity": "high", + "target": "pin/approved_digest", + "detail": null + } + ] +} diff --git a/vectors/cases/drift-v3-unapproved-migration.json b/vectors/cases/drift-v3-unapproved-migration.json new file mode 100644 index 0000000..b7e0464 --- /dev/null +++ b/vectors/cases/drift-v3-unapproved-migration.json @@ -0,0 +1,97 @@ +{ + "id": "drift/v3-unapproved-migration", + "kind": "drift", + "description": "Even unapproved v3 locks cannot claim v4 metadata coverage.", + "lock": { + "schema_version": 3, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:df0cd751860cebb7dbf04cb311a379d2ffcef96035486aafc13f2b6d5c610077", + "capabilities": [], + "schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "x": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "entry_digest": "sha256:2bbf6c724227113547e88079aee35fe478aa32f6d35ba6eb70ea8683b7d0b251" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": "historical-reviewer@example.invalid", + "approved_at": null, + "approved_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46", + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + }, + "surface": { + "command": "python", + "args": [ + "server.py" + ], + "tools": [ + { + "name": "t", + "inputSchema": { + "type": "object", + "properties": { + "x": { + "type": "string" + } + } + } + } + ], + "resources": [], + "prompts": [] + }, + "expect": [ + { + "drift_class": "schema-version-migrated", + "severity": "low", + "target": "pin/approved_digest", + "detail": null + } + ] +} diff --git a/vectors/cases/malformed-missing-overall-digest.json b/vectors/cases/malformed-missing-overall-digest.json index 99f59ae..699207d 100644 --- a/vectors/cases/malformed-missing-overall-digest.json +++ b/vectors/cases/malformed-missing-overall-digest.json @@ -6,7 +6,7 @@ "error": true }, "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -46,7 +46,10 @@ } } }, - "entry_digest": "sha256:2bbf6c724227113547e88079aee35fe478aa32f6d35ba6eb70ea8683b7d0b251" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:7f1d4f53ec8dadad5a57319a7b8426479fbb6383e97cf9b9af004383a97c2cca" } ], "resources": [], diff --git a/vectors/cases/malformed-missing-pin.json b/vectors/cases/malformed-missing-pin.json index 0f299ab..ecabda3 100644 --- a/vectors/cases/malformed-missing-pin.json +++ b/vectors/cases/malformed-missing-pin.json @@ -6,7 +6,7 @@ "error": true }, "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -46,12 +46,15 @@ } } }, - "entry_digest": "sha256:2bbf6c724227113547e88079aee35fe478aa32f6d35ba6eb70ea8683b7d0b251" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:7f1d4f53ec8dadad5a57319a7b8426479fbb6383e97cf9b9af004383a97c2cca" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46" + "overall_digest": "sha256:cd39071fa9a1f53a88ab1cacec7912ee9430ff5f37ecedeb49f558c8fbfacee6" } } diff --git a/vectors/cases/malformed-schema-version-above-implemented.json b/vectors/cases/malformed-schema-version-above-implemented.json index 41c170e..d3514ce 100644 --- a/vectors/cases/malformed-schema-version-above-implemented.json +++ b/vectors/cases/malformed-schema-version-above-implemented.json @@ -6,7 +6,7 @@ "error": true }, "lock": { - "schema_version": 4, + "schema_version": 5, "warden_version": "0.0.0", "server": { "command": "python", @@ -46,13 +46,16 @@ } } }, - "entry_digest": "sha256:2bbf6c724227113547e88079aee35fe478aa32f6d35ba6eb70ea8683b7d0b251" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:7f1d4f53ec8dadad5a57319a7b8426479fbb6383e97cf9b9af004383a97c2cca" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46", + "overall_digest": "sha256:cd39071fa9a1f53a88ab1cacec7912ee9430ff5f37ecedeb49f558c8fbfacee6", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/malformed-schema-version-not-integer.json b/vectors/cases/malformed-schema-version-not-integer.json index 39fb01f..168753e 100644 --- a/vectors/cases/malformed-schema-version-not-integer.json +++ b/vectors/cases/malformed-schema-version-not-integer.json @@ -46,13 +46,16 @@ } } }, - "entry_digest": "sha256:2bbf6c724227113547e88079aee35fe478aa32f6d35ba6eb70ea8683b7d0b251" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:7f1d4f53ec8dadad5a57319a7b8426479fbb6383e97cf9b9af004383a97c2cca" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46", + "overall_digest": "sha256:cd39071fa9a1f53a88ab1cacec7912ee9430ff5f37ecedeb49f558c8fbfacee6", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/malformed-server-missing-command-digest.json b/vectors/cases/malformed-server-missing-command-digest.json index 605ece4..6afce63 100644 --- a/vectors/cases/malformed-server-missing-command-digest.json +++ b/vectors/cases/malformed-server-missing-command-digest.json @@ -6,7 +6,7 @@ "error": true }, "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -45,13 +45,16 @@ } } }, - "entry_digest": "sha256:2bbf6c724227113547e88079aee35fe478aa32f6d35ba6eb70ea8683b7d0b251" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:7f1d4f53ec8dadad5a57319a7b8426479fbb6383e97cf9b9af004383a97c2cca" } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46", + "overall_digest": "sha256:cd39071fa9a1f53a88ab1cacec7912ee9430ff5f37ecedeb49f558c8fbfacee6", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/malformed-tool-missing-entry-digest.json b/vectors/cases/malformed-tool-missing-entry-digest.json index fb6ce72..fb0771e 100644 --- a/vectors/cases/malformed-tool-missing-entry-digest.json +++ b/vectors/cases/malformed-tool-missing-entry-digest.json @@ -6,7 +6,7 @@ "error": true }, "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -45,13 +45,16 @@ } } } - } + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null } ], "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46", + "overall_digest": "sha256:cd39071fa9a1f53a88ab1cacec7912ee9430ff5f37ecedeb49f558c8fbfacee6", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/malformed-tools-not-array.json b/vectors/cases/malformed-tools-not-array.json index 705aea9..8dbf636 100644 --- a/vectors/cases/malformed-tools-not-array.json +++ b/vectors/cases/malformed-tools-not-array.json @@ -6,7 +6,7 @@ "error": true }, "lock": { - "schema_version": 3, + "schema_version": 4, "warden_version": "0.0.0", "server": { "command": "python", @@ -46,13 +46,16 @@ } } }, - "entry_digest": "sha256:2bbf6c724227113547e88079aee35fe478aa32f6d35ba6eb70ea8683b7d0b251" + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:7f1d4f53ec8dadad5a57319a7b8426479fbb6383e97cf9b9af004383a97c2cca" } }, "resources": [], "prompts": [], "findings": [], - "overall_digest": "sha256:cc72b68fab56bdd19bf063cb0801c5dd53622169f8bf8d0d7439977641f1ae46", + "overall_digest": "sha256:cd39071fa9a1f53a88ab1cacec7912ee9430ff5f37ecedeb49f558c8fbfacee6", "pin": { "created_at": "2026-01-01T00:00:00Z", "warden_version": "0.0.0", diff --git a/vectors/cases/malformed-v4-missing-annotations_hash.json b/vectors/cases/malformed-v4-missing-annotations_hash.json new file mode 100644 index 0000000..dd94eb2 --- /dev/null +++ b/vectors/cases/malformed-v4-missing-annotations_hash.json @@ -0,0 +1,73 @@ +{ + "id": "malformed/v4-missing-annotations_hash", + "kind": "malformed", + "description": "A v4 entry cannot omit its metadata commitments.", + "expect": { + "error": true + }, + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:df0cd751860cebb7dbf04cb311a379d2ffcef96035486aafc13f2b6d5c610077", + "capabilities": [], + "schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "x": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:7f1d4f53ec8dadad5a57319a7b8426479fbb6383e97cf9b9af004383a97c2cca" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:cd39071fa9a1f53a88ab1cacec7912ee9430ff5f37ecedeb49f558c8fbfacee6", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + } +} diff --git a/vectors/cases/malformed-v4-missing-output_schema_hash.json b/vectors/cases/malformed-v4-missing-output_schema_hash.json new file mode 100644 index 0000000..798fb90 --- /dev/null +++ b/vectors/cases/malformed-v4-missing-output_schema_hash.json @@ -0,0 +1,73 @@ +{ + "id": "malformed/v4-missing-output_schema_hash", + "kind": "malformed", + "description": "A v4 entry cannot omit its metadata commitments.", + "expect": { + "error": true + }, + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:df0cd751860cebb7dbf04cb311a379d2ffcef96035486aafc13f2b6d5c610077", + "capabilities": [], + "schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "x": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_skeleton": null, + "entry_digest": "sha256:7f1d4f53ec8dadad5a57319a7b8426479fbb6383e97cf9b9af004383a97c2cca" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:cd39071fa9a1f53a88ab1cacec7912ee9430ff5f37ecedeb49f558c8fbfacee6", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + } +} diff --git a/vectors/cases/malformed-v4-missing-output_schema_skeleton.json b/vectors/cases/malformed-v4-missing-output_schema_skeleton.json new file mode 100644 index 0000000..fc1de17 --- /dev/null +++ b/vectors/cases/malformed-v4-missing-output_schema_skeleton.json @@ -0,0 +1,73 @@ +{ + "id": "malformed/v4-missing-output_schema_skeleton", + "kind": "malformed", + "description": "A v4 entry cannot omit its metadata commitments.", + "expect": { + "error": true + }, + "lock": { + "schema_version": 4, + "warden_version": "0.0.0", + "server": { + "command": "python", + "args": [ + "server.py" + ], + "url": null, + "command_digest": "sha256:b42e4fe9ab2871e34b4115b0f7a8a762c2e2fd3712805c4e323da999456eeb77" + }, + "tools": [ + { + "name": "t", + "description_hash": "sha256:12ae32cb1ec02d01eda3581b127c1fee3b0dc53572ed6baf239721a03d82e126", + "input_schema_hash": "sha256:df0cd751860cebb7dbf04cb311a379d2ffcef96035486aafc13f2b6d5c610077", + "capabilities": [], + "schema_skeleton": { + "props": { + "$root": { + "type": [ + "object" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + }, + "x": { + "type": [ + "string" + ], + "required": false, + "enum": null, + "constraints": { + "additionalProperties": true + } + } + } + }, + "annotations_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "output_schema_hash": "sha256:74234e98afe7498fb5daf1f36ac2d78acc339464f950703b8c019892f982b90b", + "entry_digest": "sha256:7f1d4f53ec8dadad5a57319a7b8426479fbb6383e97cf9b9af004383a97c2cca" + } + ], + "resources": [], + "prompts": [], + "findings": [], + "overall_digest": "sha256:cd39071fa9a1f53a88ab1cacec7912ee9430ff5f37ecedeb49f558c8fbfacee6", + "pin": { + "created_at": "2026-01-01T00:00:00Z", + "warden_version": "0.0.0", + "mcp_protocol_version": "2025-06-18", + "approved": false, + "approver": null, + "approved_at": null, + "approved_digest": null, + "provenance_version": 1, + "pinner": null, + "attestations": [], + "rotated_at": null, + "rotation_count": 0 + } + } +} diff --git a/vectors/manifest.json b/vectors/manifest.json index 76f0e1b..a54964a 100644 --- a/vectors/manifest.json +++ b/vectors/manifest.json @@ -1,8 +1,8 @@ { "format": "mcp-lock-v1", - "schema_version": 3, + "schema_version": 4, "generator": "vectors/tools/generate.py", - "count": 87, + "count": 111, "vectors": [ { "id": "canonical/sorted-keys-utf16", @@ -119,6 +119,21 @@ "kind": "digest", "file": "cases/digest-entry-sorting.json" }, + { + "id": "digest/tool-metadata-null", + "kind": "digest", + "file": "cases/digest-tool-metadata-null.json" + }, + { + "id": "digest/tool-metadata-empty", + "kind": "digest", + "file": "cases/digest-tool-metadata-empty.json" + }, + { + "id": "digest/tool-metadata-full", + "kind": "digest", + "file": "cases/digest-tool-metadata-full.json" + }, { "id": "drift/no-drift", "kind": "drift", @@ -369,6 +384,96 @@ "kind": "drift", "file": "cases/drift-mixed-multi-entry.json" }, + { + "id": "drift/v3-to-v4-migration", + "kind": "drift", + "file": "cases/drift-v3-to-v4-migration.json" + }, + { + "id": "drift/v3-unapproved-migration", + "kind": "drift", + "file": "cases/drift-v3-unapproved-migration.json" + }, + { + "id": "drift/tool-annotations-readOnlyHint", + "kind": "drift", + "file": "cases/drift-tool-annotations-readOnlyHint.json" + }, + { + "id": "drift/tool-annotations-destructiveHint", + "kind": "drift", + "file": "cases/drift-tool-annotations-destructiveHint.json" + }, + { + "id": "drift/tool-annotations-idempotentHint", + "kind": "drift", + "file": "cases/drift-tool-annotations-idempotentHint.json" + }, + { + "id": "drift/tool-annotations-openWorldHint", + "kind": "drift", + "file": "cases/drift-tool-annotations-openWorldHint.json" + }, + { + "id": "drift/tool-annotations-removed", + "kind": "drift", + "file": "cases/drift-tool-annotations-removed.json" + }, + { + "id": "drift/tool-annotations-extension", + "kind": "drift", + "file": "cases/drift-tool-annotations-extension.json" + }, + { + "id": "drift/tool-annotations-explicit-null", + "kind": "drift", + "file": "cases/drift-tool-annotations-explicit-null.json" + }, + { + "id": "drift/tool-annotations-nested-null", + "kind": "drift", + "file": "cases/drift-tool-annotations-nested-null.json" + }, + { + "id": "drift/schema-out-extension-null", + "kind": "drift", + "file": "cases/drift-schema-out-extension-null.json" + }, + { + "id": "drift/schema-out-added", + "kind": "drift", + "file": "cases/drift-schema-out-added.json" + }, + { + "id": "drift/schema-out-removed", + "kind": "drift", + "file": "cases/drift-schema-out-removed.json" + }, + { + "id": "drift/schema-out-type-broadened", + "kind": "drift", + "file": "cases/drift-schema-out-type-broadened.json" + }, + { + "id": "drift/schema-out-constraint-relaxed", + "kind": "drift", + "file": "cases/drift-schema-out-constraint-relaxed.json" + }, + { + "id": "drift/schema-out-cosmetic-modified", + "kind": "drift", + "file": "cases/drift-schema-out-cosmetic-modified.json" + }, + { + "id": "drift/schema-out-local-ref", + "kind": "drift", + "file": "cases/drift-schema-out-local-ref.json" + }, + { + "id": "drift/schema-out-ref-redacted", + "kind": "drift", + "file": "cases/drift-schema-out-ref-redacted.json" + }, { "id": "malformed/invalid-json", "kind": "malformed", @@ -438,6 +543,21 @@ "id": "malformed/depth-513-rejected", "kind": "malformed", "file": "cases/malformed-depth-513-rejected.json" + }, + { + "id": "malformed/v4-missing-annotations_hash", + "kind": "malformed", + "file": "cases/malformed-v4-missing-annotations_hash.json" + }, + { + "id": "malformed/v4-missing-output_schema_hash", + "kind": "malformed", + "file": "cases/malformed-v4-missing-output_schema_hash.json" + }, + { + "id": "malformed/v4-missing-output_schema_skeleton", + "kind": "malformed", + "file": "cases/malformed-v4-missing-output_schema_skeleton.json" } ] } diff --git a/vectors/tools/generate.py b/vectors/tools/generate.py index 10b3909..0bd90b8 100644 --- a/vectors/tools/generate.py +++ b/vectors/tools/generate.py @@ -64,7 +64,8 @@ def surface_from_json(doc: dict[str, Any]) -> CapturedSurface: """Turn a vendor-neutral surface document (MCP wire naming) into a CapturedSurface.""" tools = [ - CapturedTool(name=t["name"], description=t.get("description"), input_schema=t.get("inputSchema")) + CapturedTool(name=t["name"], description=t.get("description"), input_schema=t.get("inputSchema"), + annotations=t.get("annotations"), output_schema=t.get("outputSchema")) for t in doc.get("tools", []) ] resources = [ @@ -123,6 +124,11 @@ def lock_with_inspection(surface_doc: dict[str, Any], tool_name: str, inspection def lock_at_schema_version(surface_doc: dict[str, Any], schema_version: int) -> dict[str, Any]: """An APPROVED baseline written at an older format level (drives schema-version-migrated).""" doc = make_lock(surface_doc, approved=True) + if schema_version < 4: + for entry in doc["tools"]: + for key in ("annotations_hash", "output_schema_hash", "output_schema_skeleton"): + entry.pop(key, None) + entry["entry_digest"] = hash_value({k: v for k, v in entry.items() if k != "entry_digest"}) payload = { "schema_version": schema_version, "server": {"command_digest": doc["server"]["command_digest"]}, @@ -414,6 +420,36 @@ def schema_pair( ), ] +# v4 commitments. The v3 migration fixture preserves actual historical digests. +DIGEST.extend([ + ("tool-metadata-null", "Explicit null metadata equals omitted metadata.", surface([tool("t") | {"annotations": None, "outputSchema": None}])), + ("tool-metadata-empty", "Empty objects differ from null; the complete annotation object is committed.", surface([tool("t") | {"annotations": {}, "outputSchema": {}}])), + ("tool-metadata-full", "Complete hint objects and output schema use RFC 8785 commitments.", surface([tool("t") | {"annotations": {"destructiveHint": False, "readOnlyHint": True, "title": "Record"}, "outputSchema": obj({"value": {"type": "string", "maxLength": 64}}, ["value"])}])), +]) +DRIFT.append(("v3-to-v4-migration", "Genuine approved v3 baseline requires re-attestation; no invented annotation drift.", {"__lock__": json.loads((ROOT / "tests/fixtures/legacy-v3.warden.lock").read_text())}, _SIMPLE)) +_unapproved_v3 = json.loads((ROOT / "tests/fixtures/legacy-v3.warden.lock").read_text()) +_unapproved_v3["pin"]["approved"] = False +DRIFT.append(("v3-unapproved-migration", "Even unapproved v3 locks cannot claim v4 metadata coverage.", {"__lock__": _unapproved_v3}, _SIMPLE)) +for hint in ("readOnlyHint", "destructiveHint", "idempotentHint", "openWorldHint"): + DRIFT.append((f"tool-annotations-{hint}", "Hint changes cannot silently reuse an approved surface.", surface([tool("t") | {"annotations": {hint: False}}]), surface([tool("t") | {"annotations": {hint: True}}]))) +DRIFT.append(("tool-annotations-removed", "Removing declarations is drift too.", surface([tool("t") | {"annotations": {"destructiveHint": False}}]), surface([tool("t")]))) +for ident, before, after in [ + ("extension", {"x": "before"}, {"x": "after"}), + ("explicit-null", {}, {"title": None}), + ("nested-null", {"x": {"nested": None}}, {"x": {"nested": "after"}}), +]: + DRIFT.append((f"tool-annotations-{ident}", "Complete wire annotations retain extensions and explicit nulls.", surface([tool("t") | {"annotations": before}]), surface([tool("t") | {"annotations": after}]))) +DRIFT.append(("schema-out-extension-null", "Output schemas retain explicit null extension values.", surface([tool("t") | {"outputSchema": {"type": "object", "x": None}}]), surface([tool("t") | {"outputSchema": {"type": "object", "x": "after"}}]))) +for ident, before, after in [ + ("added", None, {}), ("removed", {}, None), + ("type-broadened", obj({"x": {"type": "string"}}), obj({"x": {"type": ["string", "number"]}})), + ("constraint-relaxed", obj({"x": {"type": "string", "maxLength": 8}}), obj({"x": {"type": "string", "maxLength": 64}})), + ("cosmetic-modified", obj({}, title="before"), obj({}, title="after")), + ("local-ref", {"$defs": {"x": {"type": "string"}}, "properties": {"x": {"$ref": "#/$defs/x"}}}, {"$defs": {"x": {"type": ["string", "number"]}}, "properties": {"x": {"$ref": "#/$defs/x"}}}), + ("ref-redacted", obj({"x": {"$ref": "https://example.com/apiKey"}}), obj({"x": {"$ref": "https://example.com/token"}})), +]: + DRIFT.append((f"schema-out-{ident}", "Output-schema commitment with structural classification.", surface([tool("t") | {"outputSchema": before}]), surface([tool("t") | {"outputSchema": after}]))) + MALFORMED: list[tuple[str, str, Any]] = [ ("invalid-json", "Not JSON at all.", "{not json"), ("missing-overall-digest", "Top-level overall_digest is required.", None), @@ -431,6 +467,9 @@ def schema_pair( ("depth-513-rejected", "The deepest element sits at depth 513, one past the normative bound (SPEC.md §4). A conforming canonicalizer MUST refuse it — fail closed — rather than produce a digest; one that accepts it because its host recursion limit is larger is not conformant.", {"input_json": "[" * 514 + "]" * 514}), ] +for key in ("annotations_hash", "output_schema_hash", "output_schema_skeleton"): + MALFORMED.append((f"v4-missing-{key}", "A v4 entry cannot omit its metadata commitments.", None)) + def _malformed_doc(ident: str) -> Any: good = make_lock(one_tool_surface(obj({"x": {"type": "string"}}))) @@ -448,7 +487,9 @@ def _malformed_doc(ident: str) -> Any: elif ident == "tool-missing-entry-digest": del bad["tools"][0]["entry_digest"] elif ident == "schema-version-above-implemented": - bad["schema_version"] = 4 + bad["schema_version"] = 5 + elif ident.startswith("v4-missing-"): + del bad["tools"][0][ident.removeprefix("v4-missing-")] else: raise AssertionError(ident) # Sanity: the reference reader MUST reject it (fail closed). @@ -528,7 +569,7 @@ def add(kind: str, ident: str, description: str, body: dict[str, Any]) -> None: manifest = { "format": "mcp-lock-v1", - "schema_version": 3, + "schema_version": 4, "generator": "vectors/tools/generate.py", "count": len(entries), "vectors": entries,