From 60b3cb724b45910fa62397240c58f851b0b263e4 Mon Sep 17 00:00:00 2001 From: Fabio Marcello Salvadori Date: Mon, 19 Oct 2026 17:13:55 +0200 Subject: [PATCH 1/2] feat(pic-standard-mcp-guard): add PIC Standard MCP Guard TRACE adapter Community TRACE record-producer integration. An outer coordinator requires a PIC Standard proposal and the reserved __pic_request_id, drives the released PIC 0.9.1 on_allow MCP pre-execution hook to build and verify a detached authorization bridge before dispatch, and, after a caller-supplied observation verifier passes, signs a Level 0 Trust Record referencing that bridge. A relying-party verifier checks the bridge and record using only agentrust-trace and rfc8785. Includes the manifest, README, pyproject, adapter and verifier modules, a deterministic example, the test suite, a conformance workflow (floating and hash-locked fixed lanes on Python 3.11 to 3.13), and the regenerated marketplace catalog and README index. Signed-off-by: Fabio Marcello Salvadori --- .../pic-standard-mcp-guard-conformance.yml | 119 ++++ README.md | 1 + integrations/pic-standard-mcp-guard/README.md | 120 ++++ .../examples/emit_record.py | 223 +++++++ .../pic-standard-mcp-guard/integration.yaml | 40 ++ .../pic-standard-mcp-guard/pic_trace_guard.py | 578 ++++++++++++++++++ .../pic_trace_verify.py | 236 +++++++ .../pic-standard-mcp-guard/pyproject.toml | 26 + .../pic-standard-mcp-guard/tests/conftest.py | 172 ++++++ .../tests/test_context_store.py | 196 ++++++ .../tests/test_coordinator.py | 354 +++++++++++ .../tests/test_import_independence.py | 22 + .../tests/test_replay.py | 127 ++++ .../tests/test_stage1_contract.py | 341 +++++++++++ .../tests/test_verifier.py | 209 +++++++ marketplace/catalog.json | 27 +- requirements/pic-standard-mcp-guard-fixed.txt | 547 +++++++++++++++++ 17 files changed, 3337 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/pic-standard-mcp-guard-conformance.yml create mode 100644 integrations/pic-standard-mcp-guard/README.md create mode 100644 integrations/pic-standard-mcp-guard/examples/emit_record.py create mode 100644 integrations/pic-standard-mcp-guard/integration.yaml create mode 100644 integrations/pic-standard-mcp-guard/pic_trace_guard.py create mode 100644 integrations/pic-standard-mcp-guard/pic_trace_verify.py create mode 100644 integrations/pic-standard-mcp-guard/pyproject.toml create mode 100644 integrations/pic-standard-mcp-guard/tests/conftest.py create mode 100644 integrations/pic-standard-mcp-guard/tests/test_context_store.py create mode 100644 integrations/pic-standard-mcp-guard/tests/test_coordinator.py create mode 100644 integrations/pic-standard-mcp-guard/tests/test_import_independence.py create mode 100644 integrations/pic-standard-mcp-guard/tests/test_replay.py create mode 100644 integrations/pic-standard-mcp-guard/tests/test_stage1_contract.py create mode 100644 integrations/pic-standard-mcp-guard/tests/test_verifier.py create mode 100644 requirements/pic-standard-mcp-guard-fixed.txt diff --git a/.github/workflows/pic-standard-mcp-guard-conformance.yml b/.github/workflows/pic-standard-mcp-guard-conformance.yml new file mode 100644 index 00000000..0419373c --- /dev/null +++ b/.github/workflows/pic-standard-mcp-guard-conformance.yml @@ -0,0 +1,119 @@ +# PIC Standard MCP Guard conformance workflow. +# +# Two jobs: +# floating installs the integration against its declared lower bounds, so it +# resolves the latest compatible releases, as drift detection. +# fixed installs the exact hash-locked package set that reproduces the +# tested_against result, then asserts the four load-bearing versions +# and installs the integration with --no-deps. +# Both run the integration tests, emit a Level 0 Trust Record from a live PIC +# allow decision, and verify it with trace-tests. The emitted record is JSON. +# Lives at repo root because GitHub Actions only discovers workflows there. +name: pic-standard-mcp-guard conformance +on: + push: + paths: + - "integrations/pic-standard-mcp-guard/**" + - "requirements/pic-standard-mcp-guard-fixed.*" + - ".github/workflows/pic-standard-mcp-guard-conformance.yml" + pull_request: + paths: + - "integrations/pic-standard-mcp-guard/**" + - "requirements/pic-standard-mcp-guard-fixed.*" + - ".github/workflows/pic-standard-mcp-guard-conformance.yml" + schedule: + - cron: "0 6 * * 1" # weekly: catch drift against the latest released packages + workflow_dispatch: + +permissions: + contents: read + +jobs: + floating: + name: floating (latest releases, drift detection) + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + python: ["3.11", "3.12", "3.13"] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python }} + - name: Install this integration (declared lower bounds) + run: pip install -e "integrations/pic-standard-mcp-guard[test]" + - name: Check dependency consistency + run: python -m pip check + - name: Integration tests + run: pytest integrations/pic-standard-mcp-guard/tests -q + - name: Emit a record citing a live PIC allow decision + run: python integrations/pic-standard-mcp-guard/examples/emit_record.py --out trust-record.json + - name: TRACE conformance + run: trace-tests verify --record trust-record.json --level 0 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: floating-${{ matrix.os }}-py${{ matrix.python }} + path: trust-record.json + + fixed: + name: fixed (tested_against versions) + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + python: ["3.11", "3.12", "3.13"] + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: ${{ matrix.python }} + - name: Install the package set that reproduces the tested_against result + run: pip install --require-hashes -r requirements/pic-standard-mcp-guard-fixed.txt + - name: Pinned versions match the four anchors and integration.yaml + run: | + python - <<'PY' + import re, sys + from importlib.metadata import version + want = { + "pic-standard": "0.9.1", + "agentrust-trace": "0.11.0", + "agentrust-trace-adapters": "0.2.0", + "agentrust-trace-tests": "0.6.2", + } + y = open("integrations/pic-standard-mcp-guard/integration.yaml").read() + declared = dict(re.findall(r'^ (agentrust-trace|agentrust-trace-tests): "([^"]+)"', y, re.M)) + have = {k: version(k) for k in want} + print("want", want) + print("have", have) + print("integration.yaml", declared) + ok = ( + have == want + and declared.get("agentrust-trace") == want["agentrust-trace"] + and declared.get("agentrust-trace-tests") == want["agentrust-trace-tests"] + ) + sys.exit(0 if ok else 1) + PY + - name: Install this integration + # --no-deps: every runtime and test dependency came from the lock above. + run: pip install --no-deps -e integrations/pic-standard-mcp-guard + - name: Check dependency consistency + run: python -m pip check + - name: Integration tests + run: pytest integrations/pic-standard-mcp-guard/tests -q + - name: Emit a record citing a live PIC allow decision + run: python integrations/pic-standard-mcp-guard/examples/emit_record.py --out trust-record.json + - name: TRACE conformance + run: trace-tests verify --record trust-record.json --level 0 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: fixed-${{ matrix.os }}-py${{ matrix.python }} + path: trust-record.json diff --git a/README.md b/README.md index 47f0c87c..008565f5 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ TRACE only works as a standard if it is genuinely neutral. Integrations are list | [OpenShell TRACE Adapter](integrations/openshell/) | agentrust-io | trace | community | | [OpenTelemetry GenAI](integrations/otel-genai/) | agentrust-io | trace | community | | [Parmana](integrations/parmana-server/) | Parmana Systems | trace | verified | +| [PIC Standard MCP Guard](integrations/pic-standard-mcp-guard/) | PIC Standard | trace | community | | [Observed-effect references](integrations/probityai-observed-effect/) | probityai | trace | community | | [ramen-ai cMCP Adapter](integrations/ramen-ai-cmcp/) | ramen-ai | cmcp, trace | verified | | [SAGE AgenTrust Bridge](integrations/sage-agenttrust/) | SAGE | cmcp, trace | community | diff --git a/integrations/pic-standard-mcp-guard/README.md b/integrations/pic-standard-mcp-guard/README.md new file mode 100644 index 00000000..72ea6687 --- /dev/null +++ b/integrations/pic-standard-mcp-guard/README.md @@ -0,0 +1,120 @@ +# PIC Standard MCP Guard integration with TRACE + +[PIC Standard](https://github.com/pic-standard/pic-standard) gates an agent's MCP tool +calls before they run. A tool is wrapped by `guard_mcp_tool`; PIC validates the +supplied Action Proposal, applies the configured trust, policy, and +evidence-verification requirements, and, for proposal-backed MCP calls, enforces exact +tool and argument binding. It either allows or blocks the call. This integration +combines a PIC allow decision with a separately produced, caller-verified +post-dispatch observation to emit a signed TRACE Level 0 Trust Record linked through a +detached bridge. + +The adapter consumes PIC 0.9.1's released `on_allow` pre-execution hook. An outer +coordinator reserves the reserved `__pic_request_id` and owns the lifecycle; the hook +runs before dispatch and builds the bridge from the structured `MCPAllowDecision` and +the sanitized `MCPToolCall`: + +```json +{ + "profile": "tag:agentrust-io.com,2026:pic-trace-bridge-v1", + "authorization": { + "authorization_id": "urn:sha256:", + "decision": "allow", + "authorizer": "urn:pic-standard:integration:pic-trace-stage1-harness", + "scope": {"tools": [""], "impacts": [""]}, + "pic": {"profile": "PIC-CJSON/1.0", "intent_digest": "sha256:", "args_digest": "sha256:"}, + "declaration_digest": "sha256:", + "tool_call_digest": "sha256:", + "transcript_required": false + }, + "signature": "" +} +``` + +`decision.to_audit_bytes()` is the UTF-8 encoding of the exact `LogRecord.getMessage()` +text emitted for the PIC allow event, without a logging formatter prefix or trailing +newline, so the `authorization_id` is the hash of PIC's own decision. The bridge's PIC +digests are PIC's `canonical.py` values, prefixed to TRACE's `sha256:` form and +never reinterpreted. After the tool runs and a caller-supplied observation verifier +passes, the adapter builds the Trust Record with `agentrust_trace_adapters.build_record` +and signs it with `agentrust_trace.sign_record`, referencing the bridge. + +## Run it + +From the repository root: + +```bash +pip install -e "integrations/pic-standard-mcp-guard[test]" +pytest integrations/pic-standard-mcp-guard/tests -q +python integrations/pic-standard-mcp-guard/examples/emit_record.py --out trust-record.json +trace-tests verify --record trust-record.json --level 0 +``` + +`emit_record.py` runs one harmless tool (`delete_demo_marker`, operating only inside a +fresh temporary sandbox, declared `impact: irreversible`) through `guard_mcp_tool` with +`strict_trust=True` and `verify_evidence=True`, builds and verifies the bridge in the +hook, observes that the tool actually ran, signs a Level 0 Trust Record, and +self-verifies it before writing. All keys are deterministic Ed25519 TEST keys. + +## What is verified + +- Missing, empty, or `None` `__pic_request_id` is rejected by the coordinator before + entering the guard; a plain `request_id` cannot substitute for it. +- Missing or `None` `__pic` is rejected before entering the guard. The hook + independently refuses any non-proposal-backed allow, preventing `sha256:None`. +- Duplicate active request IDs fail closed, state transitions are one-time, and the + request context is removed on every success or failure path. +- The hook-driven allowed call produces a signed Level 0 Trust Record whose + `authorization_id`, `origin.source_event_id`, and `references[0].id` all equal + `urn:sha256:`. +- The bridge is signed and `verify_bridge`'d **before** the tool is dispatched + (recorded order `bridge_signed -> bridge_verified -> tool_entered`). +- Digest boundary: `decision.args_digest == tool_call.arguments_digest`, and the + bridge's PIC digests equal `canonical.py`'s intent/args digests prefixed with + `sha256:`. A bare-hex PIC digest is refused by the bridge. +- The Trust Record validates (`agentrust_trace.validate.validate_json`), its + `policy.bundle_hash` is the SHA-256 of the exact policy bytes, `runtime.measurement` + recomputes from `software_measurement`, and the supplied bridge matches the record's + signed `authorized-intent` reference by id and JCS digest (the verifier does not + contact the reference `resolver`). +- The record `subject` `did:key` decodes to `cnf.jwk.x`; the three test keys (PIC + evidence, bridge, record) are pairwise distinct. +- `trace-tests verify --level 0` passes on the emitted signed record (Ed25519 + signature check against the record's own `cnf.jwk`). +- A tampered-evidence call **blocks** (`PIC_EVIDENCE_FAILED`): no bridge, no record, + an absence report instead. A call whose live arguments diverge from `action.args` + blocks with `PIC_ARG_BINDING_MISMATCH` (PIC 0.9.1 binds them; the adapter re-checks + as defense-in-depth). +- An altered observation no longer matches the signed transcript hash, and changing + one policy byte changes `bundle_hash` and `runtime.measurement`. + +## What it does NOT claim + +See rules 2 and 5 in [CONTRIBUTING.md](../../CONTRIBUTING.md). + +- **No hardware evidence.** `runtime.platform` is `software-only`. The deterministic + test keys do not chain to a trusted issuer and must never be used in production. +- **No PIC-native authorization signature.** The `MCPAllowDecision` and its + audit-message bytes are not signed by PIC. The bridge signature authenticates the + adapter's translation of that decision; it is not a PIC-issued authorization + signature. +- **No TRACE Level 1**, and **no independent operator**: the observation is a separate + code path in the same workflow, not a separate trusted operator. +- **A PIC allow does not prove the tool was dispatched; dispatch does not prove the + tool succeeded; a filesystem observation does not prove a general real-world + outcome.** Each is checked separately and only within this synthetic workflow. +- **No released TRACE successor artifact.** The two-stage successor-observation + mechanism post-dates `agentrust-trace==0.11.0`; this adapter does not use it. The + observation is carried in the adapter's signed transcript, not a successor artifact. +- **No cMCP or Agent Manifest integration**, and **no portable bridge-corpus + conformance** (that corpus is still in progress upstream and is not a basis here). +- **The example's `model`, `data_class`, and `build_provenance` are fixed TEST + values**; in a real deployment they come from the governed run, not from PIC. + +## Conformance CI + +[`.github/workflows/pic-standard-mcp-guard-conformance.yml`](../../.github/workflows/pic-standard-mcp-guard-conformance.yml), +scoped to this directory, runs two jobs on Python 3.11 to 3.13. `fixed` pins the +versions in `integration.yaml` plus `pic-standard[crypto]` 0.9.1 and +`agentrust-trace-adapters` 0.2.0. `floating` installs the latest releases as drift +detection. Each runs the tests, emits a record, and runs `trace-tests verify --level 0`. diff --git a/integrations/pic-standard-mcp-guard/examples/emit_record.py b/integrations/pic-standard-mcp-guard/examples/emit_record.py new file mode 100644 index 00000000..7badcecc --- /dev/null +++ b/integrations/pic-standard-mcp-guard/examples/emit_record.py @@ -0,0 +1,223 @@ +"""Emit one signed TRACE Level 0 Trust Record from a PIC allow decision. TEST ONLY. + +Runs one harmless tool (`delete_demo_marker`, operating only inside a fresh temporary +sandbox, declared `impact: irreversible`) through the released PIC MCP guard with +`strict_trust=True` and `verify_evidence=True`, builds and verifies the detached +authorization bridge in the `on_allow` hook, observes that the tool actually ran, signs +a Level 0 Trust Record, and self-verifies it (inside the coordinator) before writing it +as JSON. Then run `trace-tests verify --record --level 0` separately as the +separate external CLI Level-0 check. + +All keys are deterministic Ed25519 TEST keys derived from fixed labels; they +authenticate nothing outside this example and must never be used in production. The +record's model, data_class, and workload_digest are fixed TEST values, labeled as such. +""" + +from __future__ import annotations + +import argparse +import base64 +import hashlib +import json +import logging +import os +import shutil +import sys +import tempfile +import time +from pathlib import Path +from typing import Any + +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +from pic_standard.canonical import canonicalize, intent_digest_hex, sha256_hex + +from pic_trace_guard import Observation, PicTraceConfig, PicTraceCoordinator + +TOOL = "delete_demo_marker" +IMPACT = "irreversible" +MARKER = "marker.txt" + +HARNESS = "urn:pic-standard:integration:pic-trace-example-harness" +ARTIFACT_STORE = "urn:pic-standard:integration:pic-trace-example-artifact-store" +OBSERVER = "pic-trace-example-observer" + +EVIDENCE_KEY_ID = "pic-trace-example-evidence-key-1" +BRIDGE_KEY_ID = "pic-trace-example-bridge-key-1" + +MODEL_PROVIDER = "pic-standard-test" +MODEL_ID = "deterministic-tool-calling-model-v1" +DATA_CLASS = "public" + +PROVENANCE_ID = "pic-trace-approval" +INTENT = "Delete the demo marker file in the example sandbox" +REQUEST_ID = "pic-trace-example-001" + +DECLARATION = {"profile": "pic-standard-action-declaration/v1", "tool": TOOL, "impact": IMPACT} +POLICY = { + "impact_by_tool": {TOOL: IMPACT}, + "require_pic_for_impacts": [IMPACT], + "require_evidence_for_impacts": [IMPACT], +} + + +def _test_key(label: str) -> Ed25519PrivateKey: + seed = hashlib.sha256(f"pic-trace-example/test-only/{label}".encode()).digest() + return Ed25519PrivateKey.from_private_bytes(seed) + + +EVIDENCE_KEY = _test_key("pic-evidence-key") +BRIDGE_KEY = _test_key("bridge-key") +RECORD_KEY = _test_key("trace-record-key") + + +def _raw_public(key: Ed25519PrivateKey) -> bytes: + return key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + + +def _scripted_model() -> dict[str, Any]: + """A scripted test model whose sole output is the frozen tool call. No LLM inference.""" + return {"name": TOOL, "arguments": {"path": MARKER}} + + +def _pic_att_evidence() -> dict[str, Any]: + """Canonical PIC-ATT/1.0 signature evidence bound to the exact call.""" + args = _scripted_model()["arguments"] + claims = [{"text": "The example approver authorized deleting marker.txt in the sandbox", + "evidence": [PROVENANCE_ID]}] + attestation = { + "attestation_version": "PIC-ATT/1.0", + "tool": TOOL, + "impact": IMPACT, + "provenance_ids": [PROVENANCE_ID], + "args_digest": sha256_hex(args), + "claims_digest": sha256_hex(claims), + "intent_digest": intent_digest_hex(INTENT), + } + signature = base64.b64encode(EVIDENCE_KEY.sign(canonicalize(attestation))).decode() + return { + "claims": claims, + "evidence": { + "id": PROVENANCE_ID, "type": "sig", "ref": "inline:pic-att", + "payload": json.dumps(attestation, sort_keys=True, separators=(",", ":")), + "alg": "ed25519", "signature": signature, "key_id": EVIDENCE_KEY_ID, + "signer": "pic-trace-example test approver", + }, + } + + +def _proposal() -> dict[str, Any]: + model_call = _scripted_model() + ev = _pic_att_evidence() + return { + "protocol": "PIC/1.0", + "intent": INTENT, + "impact": IMPACT, + "provenance": [{"id": PROVENANCE_ID, "trust": "untrusted", "source": "pic-trace-example test approver"}], + "claims": ev["claims"], + "action": {"tool": model_call["name"], "args": model_call["arguments"]}, + "evidence": [ev["evidence"]], + } + + +def main() -> int: + parser = argparse.ArgumentParser(description="Emit a TEST TRACE Level 0 Trust Record from a PIC allow.") + parser.add_argument("--out", type=Path, default=Path("trust-record.json"), help="output record path (JSON)") + args = parser.parse_args() + + if len({_raw_public(EVIDENCE_KEY), _raw_public(BRIDGE_KEY), _raw_public(RECORD_KEY)}) != 3: + raise RuntimeError("TEST signing keys must be pairwise distinct") + + logging.getLogger("pic_standard.mcp").setLevel(logging.CRITICAL) + policy_bytes = (json.dumps(POLICY, indent=2) + "\n").encode("utf-8") + + prev_keys_path = os.environ.get("PIC_KEYS_PATH") + fixtures = Path(tempfile.mkdtemp(prefix="pic-trace-example-fx-")) + sandbox = Path(tempfile.mkdtemp(prefix="pic-trace-example-")) + try: + (fixtures / "pic_keys.json").write_text( + json.dumps({"trusted_keys": {EVIDENCE_KEY_ID: base64.b64encode(_raw_public(EVIDENCE_KEY)).decode()}}), + encoding="utf-8", + ) + os.environ["PIC_KEYS_PATH"] = str(fixtures / "pic_keys.json") + + (sandbox / MARKER).write_text("pic-trace-example marker\n", encoding="utf-8") + state = {"count": 0} + + def delete_demo_marker(path: str) -> dict[str, Any]: + state["count"] += 1 + target = (sandbox / path).resolve() + if target.parent != sandbox.resolve(): + raise ValueError("path escapes the example sandbox") + target.unlink() + return {"deleted": True} + + existed_before = (sandbox / MARKER).exists() + + def observe(envelope: Any) -> Observation: + document = { + "observation": { + "tool_call_count": state["count"], + "marker_existed_before": existed_before, + "marker_exists_after": (sandbox / MARKER).exists(), + }, + "observer": OBSERVER, + "observed_at": int(time.time()), + } + established = ( + state["count"] == 1 + and existed_before + and not (sandbox / MARKER).exists() + and envelope == {"isError": False, "result": {"deleted": True}} + ) + return Observation( + verdict="established" if established else "contradicted", + document=document, + response_projection=envelope, + ) + + config = PicTraceConfig( + authorizer=HARNESS, + authorizer_key_id=BRIDGE_KEY_ID, + bridge_key=BRIDGE_KEY, + record_key=RECORD_KEY, + model_provider=MODEL_PROVIDER, + model_id=MODEL_ID, + data_class=DATA_CLASS, + policy_bundle=policy_bytes, + workload_digest="sha256:" + hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + declaration=DECLARATION, + resolver=ARTIFACT_STORE, + ) + coordinator = PicTraceCoordinator(config) # default real-time clock (iat freshness window) + + kwargs = {**_scripted_model()["arguments"], "__pic": _proposal(), "__pic_request_id": REQUEST_ID} + outcome = coordinator.run( + tool_name=TOOL, tool_fn=delete_demo_marker, kwargs=kwargs, observe=observe, + verify_evidence=True, proposal_base_dir=fixtures, strict_trust=True, + ) + + if outcome.status != "record-emitted" or outcome.record_artifacts is None: + print(f"example did not emit a record: status={outcome.status} absence={outcome.absence}", file=sys.stderr) + return 1 + + args.out.parent.mkdir(parents=True, exist_ok=True) + args.out.write_text( + json.dumps(outcome.record_artifacts.record, indent=2, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + print(f"wrote {args.out} (status={outcome.status}, pic_decision={outcome.pic_decision})") + print(f"next: trace-tests verify --record {args.out} --level 0") + return 0 + finally: + if prev_keys_path is None: + os.environ.pop("PIC_KEYS_PATH", None) + else: + os.environ["PIC_KEYS_PATH"] = prev_keys_path + shutil.rmtree(fixtures, ignore_errors=True) + shutil.rmtree(sandbox, ignore_errors=True) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/integrations/pic-standard-mcp-guard/integration.yaml b/integrations/pic-standard-mcp-guard/integration.yaml new file mode 100644 index 00000000..d266892b --- /dev/null +++ b/integrations/pic-standard-mcp-guard/integration.yaml @@ -0,0 +1,40 @@ +name: PIC Standard MCP Guard +vendor: PIC Standard +integrates_with: + - trace +description: >- + Uses a PIC Standard MCP pre-execution allow decision and a caller-verified + post-dispatch observation to emit a signed TRACE Level 0 Trust Record linked + through a detached bridge. +maintainer: + github: madeinplutofabio +repository: https://github.com/agentrust-io/integrations +homepage: https://github.com/pic-standard/pic-standard +license: Apache-2.0 +tier: community +marketplace: + display_name: PIC Standard MCP Guard + category: Security & policy + mark: PIC + keywords: [pic, mcp, intent, policy, provenance, trace] + +# The adapter is a record producer. Its outer coordinator requires a PIC +# proposal and the reserved __pic_request_id. The on_allow hook builds and +# verifies a detached bridge before dispatch. After dispatch, a caller-supplied +# observation verifier must pass before the adapter signs a Level 0 Trust +# Record referencing that bridge. +# +# The record's model, runtime, data_class, and build_provenance describe the +# governed run, not PIC. examples/emit_record.py uses fixed TEST values and +# labels them accordingly. Level 0 is software-only signing, with no hardware +# attestation. +trace_roles: + - record-producer +trace_conformance_level: 0 +tested_against: + agentrust-trace: "0.11.0" + agentrust-trace-tests: "0.6.2" + + # The schema accepts agentrust-io package keys only. The fixed CI job also + # pins pic-standard[crypto] 0.9.1 and agentrust-trace-adapters 0.2.0, which + # are the released packages exercised by the adapter, tests, and example. diff --git a/integrations/pic-standard-mcp-guard/pic_trace_guard.py b/integrations/pic-standard-mcp-guard/pic_trace_guard.py new file mode 100644 index 00000000..12fa6a9a --- /dev/null +++ b/integrations/pic-standard-mcp-guard/pic_trace_guard.py @@ -0,0 +1,578 @@ +"""PIC Standard MCP guard allow decisions as signed TRACE Level 0 Trust Records. + +Producer side (synchronous; wraps the released synchronous `guard_mcp_tool`, not +`guard_mcp_tool_async`). An outer `PicTraceCoordinator` requires a PIC proposal and the +reserved `__pic_request_id`, owns a `reserved -> bridged -> consumed` lifecycle in a +process-local store that tombstones recently used ids, and drives PIC's released +`on_allow` hook. Hook entry is proven by a coordinator-local `threading.Event` set +before any store write, so a store failure cannot be misread as a PIC block. The hook +builds and `verify_bridge`'s a detached authorization bridge before dispatch. After +dispatch a caller-supplied observation verifier returns a three-state verdict; only +`established` yields a record. The coordinator self-verifies with +`pic_trace_verify.verify_adapter_artifacts` before returning detached artifacts: an +`AuthorizationArtifacts` bundle whenever a bridge was produced (including later +failures), and the full `RecordArtifacts` bundle only for `record-emitted`. It does not +persist; the caller writes the artifacts. + +Imports `pic_standard`, `agentrust_trace`, and `agentrust_trace_adapters`. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +import threading +import time +from collections import OrderedDict +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from typing import Any, Literal, Optional, Protocol + +import rfc8785 +from agentrust_trace.intent_bridge import digest_jcs, sign_bridge, verify_bridge +from agentrust_trace.sign import key_to_jwk, sign_record +from agentrust_trace_adapters import build_record +from agentrust_trace_adapters.evidence import PolicyEvidence, SourceSystem +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +from pic_standard.config import policy_from_dict +from pic_standard.integrations import MCPAllowDecision, MCPToolCall +from pic_standard.integrations.mcp_pic_guard import evaluate_pic_for_tool_call, guard_mcp_tool + +from pic_trace_verify import BRIDGE_REFERENCE_REL, build_transcript, verify_adapter_artifacts + +_SAFE_INT_MAX = 2**53 - 1 +_B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +_DEFAULT_MAX_TRANSCRIPT_BYTES = 262144 +_DEFAULT_MAX_REQUEST_ID_LEN = 512 +_RESERVED_KEYS = ("__pic", "__pic_request_id") +_OBSERVATION_VERDICTS = ("established", "contradicted", "not-established") + +AdapterStatus = Literal[ + "precondition-failed", "pic-blocked", "post-allow-error", "observation-failed", + "record-emitted", "adapter-error", +] +PicDecision = Literal["not-evaluated", "blocked", "allowed"] +ObservationVerdict = Literal["established", "contradicted", "not-established"] +Clock = Callable[[], int] + + +class AdapterError(RuntimeError): + """A coordinator/hook fail-closed condition; surfaced as a no-record outcome.""" + + +def _positive_int(value: Any, name: str) -> int: + """Require a strictly positive int (rejecting bool and float).""" + if not isinstance(value, int) or isinstance(value, bool) or value <= 0: + raise AdapterError(f"{name} must be a positive integer") + return value + + +def _snapshot(value: Any) -> Any: + """A JCS-canonical detached copy; requires `value` to be JSON/JCS serializable.""" + return json.loads(rfc8785.dumps(value)) + + +def _reject_constant(token: str) -> Any: + raise AdapterError(f"non-finite JSON constant in policy bundle: {token}") + + +def _strict_json_object(data: bytes) -> dict: + """Parse `data` as a JSON object, rejecting duplicate keys and NaN/Infinity.""" + + def _no_dupes(pairs: list[tuple[str, Any]]) -> dict: + out: dict[str, Any] = {} + for key, val in pairs: + if key in out: + raise AdapterError("duplicate key in policy bundle JSON object") + out[key] = val + return out + + obj = json.loads(data.decode("utf-8"), object_pairs_hook=_no_dupes, parse_constant=_reject_constant) + if not isinstance(obj, dict): + raise AdapterError("policy bundle must be a top-level JSON object") + return obj + + +def _load_policy(policy_bundle: bytes) -> Any: + """Parse a policy bundle to a PICPolicy, normalizing every malformed-input failure + (bad UTF-8, malformed JSON, NaN/Infinity, duplicate keys, non-object, or a + structurally invalid policy that `policy_from_dict` rejects) to AdapterError. Shared + by the coordinator constructor and replay so the contract is identical.""" + if not isinstance(policy_bundle, bytes): + raise AdapterError("policy_bundle must be bytes") + try: + obj = _strict_json_object(policy_bundle) + return policy_from_dict(obj) + except AdapterError: + raise + except (UnicodeDecodeError, json.JSONDecodeError, TypeError, ValueError) as exc: + raise AdapterError("invalid policy bundle") from exc + + +def _error_code(envelope: Any) -> Optional[str]: + if not isinstance(envelope, Mapping): + return None + error = envelope.get("error") + if not isinstance(error, Mapping): + return None + code = error.get("code") + return code if isinstance(code, str) else None + + +@dataclass(frozen=True) +class Observation: + verdict: ObservationVerdict + document: Mapping[str, Any] + response_projection: Any # bound into the transcript instead of the full envelope + + +@dataclass(frozen=True) +class AuthorizationArtifacts: + """The verified pre-dispatch authorization; retained even when no record is emitted.""" + + pic_decision_raw: bytes + bridge: dict + tool_call: dict + pic_intent_digest: str + pic_args_digest: str + declaration: dict + authorizer_jwk: dict + + +@dataclass(frozen=True) +class RecordArtifacts: + authorization: AuthorizationArtifacts + transcript: dict + record: dict + + +@dataclass(frozen=True) +class AdapterOutcome: + status: AdapterStatus + pic_decision: PicDecision + envelope: Any # runtime value from the guard; not signed or guaranteed immutable + authorization: Optional[AuthorizationArtifacts] = None + record_artifacts: Optional[RecordArtifacts] = None + absence: Optional[dict] = None + + +ObserveFn = Callable[[Any], Observation] + + +@dataclass(frozen=True) +class PicTraceConfig: + """Explicit provenance inputs; no value is invented. `subject` is derived from + `record_key`; the enforced PIC policy is parsed strictly from `policy_bundle`; + `declaration` is snapshotted once at construction.""" + + authorizer: str + authorizer_key_id: str + bridge_key: Ed25519PrivateKey + record_key: Ed25519PrivateKey + model_provider: str + model_id: str + data_class: str + policy_bundle: bytes + workload_digest: str + declaration: Mapping[str, Any] + resolver: str + bridge_validity_seconds: int = 300 + max_transcript_bytes: int = _DEFAULT_MAX_TRANSCRIPT_BYTES + max_request_id_len: int = _DEFAULT_MAX_REQUEST_ID_LEN + + +class ContextStore(Protocol): + """Request-context lifecycle store. Implementations MUST make every operation + atomic and thread-safe; `transition`/`consume` enforce the expected prior state. + + Contract: + - `reserve()` is atomic and must leave no context behind when it raises; a rejected + reservation (id already active or recently used) raises `AdapterError`. + - `transition()` / `consume()` enforce the expected prior state and raise + `AdapterError` otherwise. + - `abandon_if_active()` is idempotent and must not raise; it removes only an active + context for exactly this id, never another request's. + - The store owns detached transition data and returns detached snapshots, so callers + cannot mutate stored context and peek/consume results cannot mutate the store. + + The default store is process-local: its tombstones reject only recently used ids + (bounded, so eviction eventually allows reuse). Durable global uniqueness across + workers or restarts requires a shared persistent implementation.""" + + def reserve(self, request_id: str) -> None: ... + def transition(self, request_id: str, expected: str, new: str, **data: Any) -> None: ... + def peek(self, request_id: str) -> Mapping[str, Any]: ... + def consume(self, request_id: str, expected: str = "bridged") -> Mapping[str, Any]: ... + def abandon_if_active(self, request_id: str) -> None: ... + + +class InMemoryContextStore: + """Process-local store with bounded recent tombstones that reject reuse of a + request id (until a tombstone is evicted). Not durable across workers or restarts.""" + + def __init__(self, tombstone_capacity: int = 4096, max_request_id_len: int = _DEFAULT_MAX_REQUEST_ID_LEN) -> None: + self._lock = threading.Lock() + self._active: dict[str, dict[str, Any]] = {} + self._tombstones: "OrderedDict[str, str]" = OrderedDict() + self._cap = _positive_int(tombstone_capacity, "tombstone_capacity") + self._max_rid_len = _positive_int(max_request_id_len, "max_request_id_len") + + def reserve(self, request_id: str) -> None: + if not isinstance(request_id, str) or not (0 < len(request_id) <= self._max_rid_len): + raise AdapterError("request id missing or too long") + with self._lock: + if request_id in self._active or request_id in self._tombstones: + raise AdapterError("request id already used") + self._active[request_id] = {"state": "reserved"} + + def transition(self, request_id: str, expected: str, new: str, **data: Any) -> None: + if "state" in data: + raise AdapterError("transition data cannot override state") + detached = copy.deepcopy(data) + with self._lock: + c = self._active.get(request_id) + if c is None or c["state"] != expected: + raise AdapterError(f"illegal transition {expected} -> {new}") + c["state"] = new + c.update(detached) + + def peek(self, request_id: str) -> Mapping[str, Any]: + with self._lock: + return copy.deepcopy(self._active.get(request_id) or {}) + + def _tombstone(self, request_id: str, reason: str) -> None: + self._tombstones[request_id] = reason + while len(self._tombstones) > self._cap: + self._tombstones.popitem(last=False) + + def consume(self, request_id: str, expected: str = "bridged") -> Mapping[str, Any]: + with self._lock: + c = self._active.get(request_id) + if c is None or c["state"] != expected: + raise AdapterError("cannot consume: not in expected state") + self._active.pop(request_id) + self._tombstone(request_id, "consumed") + return copy.deepcopy(c) + + def abandon_if_active(self, request_id: str) -> None: + with self._lock: + if request_id in self._active: + self._active.pop(request_id) + self._tombstone(request_id, "abandoned") + + +def _raw_public(key: Ed25519PrivateKey) -> bytes: + return key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + + +def _base58btc(data: bytes) -> str: + n = int.from_bytes(data, "big") + out = "" + while n: + n, r = divmod(n, 58) + out = _B58[r] + out + return "1" * (len(data) - len(data.lstrip(b"\0"))) + out + + +def did_key(key: Ed25519PrivateKey) -> str: + """did:key for an Ed25519 key: multicodec 0xed01 + raw key, base58btc, 'z'.""" + return "did:key:z" + _base58btc(b"\xed\x01" + _raw_public(key)) + + +class PicTraceCoordinator: + def __init__( + self, + config: PicTraceConfig, + *, + store: Optional[ContextStore] = None, + clock: Optional[Clock] = None, + ) -> None: + _positive_int(config.bridge_validity_seconds, "bridge_validity_seconds") + _positive_int(config.max_transcript_bytes, "max_transcript_bytes") + _positive_int(config.max_request_id_len, "max_request_id_len") + if not isinstance(config.declaration, Mapping): + raise AdapterError("declaration must be a mapping") + if not isinstance(config.policy_bundle, bytes): + raise AdapterError("policy_bundle must be bytes") + self._cfg = config + self._policy_bundle = config.policy_bundle # immutable; the enforced and recorded bytes + self._declaration = _snapshot(dict(config.declaration)) # frozen JCS copy + self._subject = did_key(config.record_key) + self._policy = _load_policy(self._policy_bundle) + self._authorizer_jwk = {**key_to_jwk(config.bridge_key), "kid": config.authorizer_key_id} + self._store: ContextStore = store or InMemoryContextStore(max_request_id_len=config.max_request_id_len) + self._clock: Clock = clock or (lambda: int(time.time())) + + def _safe_timestamp(self) -> int: + t = self._clock() + if not isinstance(t, int) or isinstance(t, bool) or t < 0 or t > _SAFE_INT_MAX: + raise AdapterError("clock returned an invalid timestamp") + return t + + def _make_hook( + self, rid: str, hook_entered: threading.Event + ) -> Callable[[MCPAllowDecision, MCPToolCall], None]: + cfg = self._cfg + + def on_allow(decision: MCPAllowDecision, tool_call: MCPToolCall) -> None: + hook_entered.set() # prove hook entry before any store write + if not (decision.proposal_present and decision.intent_digest and decision.args_digest): + raise AdapterError("adapter requires a proposal-backed allow") + if decision.request_id != rid: + raise AdapterError("decision.request_id does not match the reserved id") + if not isinstance(decision.impact, str) or not decision.impact: + raise AdapterError("proposal-backed decision has no impact") + if decision.args_digest != tool_call.arguments_digest: + raise AdapterError("args digest mismatch") + call = tool_call.to_dict() + if digest_jcs(call) != "sha256:" + tool_call.tool_call_digest: + raise AdapterError("tool call digest mismatch") + raw = decision.to_audit_bytes() + authorized_at = self._safe_timestamp() + expires_at = authorized_at + cfg.bridge_validity_seconds + if not (authorized_at < expires_at <= _SAFE_INT_MAX): + raise AdapterError("bridge expiry out of range") + authorization = { + "authorization_id": "urn:sha256:" + hashlib.sha256(raw).hexdigest(), + "decision": "allow", + "authorizer": cfg.authorizer, + "authorizer_key_id": cfg.authorizer_key_id, + "authorized_at": authorized_at, + "expires_at": expires_at, + "scope": {"tools": [decision.tool], "impacts": [decision.impact]}, + "pic": { + "profile": "PIC-CJSON/1.0", + "intent_digest": "sha256:" + decision.intent_digest, + "args_digest": "sha256:" + decision.args_digest, + }, + "declaration_digest": digest_jcs(self._declaration), + "tool_call_digest": digest_jcs(call), + "transcript_required": False, + } + bridge = sign_bridge(authorization, cfg.bridge_key) + verify_bridge( + bridge, self._authorizer_jwk, + declaration=self._declaration, + pic_intent_digest="sha256:" + decision.intent_digest, + pic_args_digest="sha256:" + decision.args_digest, + tool_call=call, now=authorized_at, + ) + self._store.transition( + rid, "reserved", "bridged", + raw=raw, bridge=bridge, tool_call=call, + intent_digest=decision.intent_digest, args_digest=decision.args_digest, + ) + + return on_allow + + def _authz_from_ctx(self, ctx: Mapping[str, Any]) -> AuthorizationArtifacts: + return AuthorizationArtifacts( + pic_decision_raw=ctx["raw"], + bridge=_snapshot(ctx["bridge"]), + tool_call=_snapshot(ctx["tool_call"]), + pic_intent_digest=ctx["intent_digest"], + pic_args_digest=ctx["args_digest"], + declaration=_snapshot(self._declaration), + authorizer_jwk=_snapshot(self._authorizer_jwk), + ) + + def _safe_authz(self, rid: str) -> Optional[AuthorizationArtifacts]: + """Best-effort authorization extraction that never escapes the fail-closed path.""" + try: + ctx = self._store.peek(rid) + if ctx.get("state") != "bridged": + return None + return self._authz_from_ctx(ctx) + except Exception: # noqa: BLE001 + return None + + def _validate_observation(self, observation: Any) -> None: + if not isinstance(observation, Observation): + raise AdapterError("observe did not return an Observation") + if observation.verdict not in _OBSERVATION_VERDICTS: + raise AdapterError("invalid observation verdict") + if not isinstance(observation.document, Mapping): + raise AdapterError("observation document must be a mapping") + + def run( + self, + *, + tool_name: str, + tool_fn: Callable[..., Any], + kwargs: Mapping[str, Any], + observe: ObserveFn, + verify_evidence: bool = True, + proposal_base_dir: Any = None, + evidence_root_dir: Any = None, + limits: Any = None, + key_resolver: Any = None, + strict_trust: bool = True, + ) -> AdapterOutcome: + cfg = self._cfg + if kwargs.get("__pic") is None: + return AdapterOutcome("precondition-failed", "not-evaluated", None, absence={"reason": "no_proposal"}) + rid = kwargs.get("__pic_request_id") + if not isinstance(rid, str) or not rid or rid != rid.strip() or len(rid) > cfg.max_request_id_len: + return AdapterOutcome("precondition-failed", "not-evaluated", None, absence={"reason": "bad_request_id"}) + try: + self._store.reserve(rid) + except AdapterError: + return AdapterOutcome("precondition-failed", "not-evaluated", None, absence={"reason": "request_id_unavailable"}) + + hook_entered = threading.Event() + envelope: Any = None + try: + guarded = guard_mcp_tool( + tool_name, tool_fn, policy=self._policy, limits=limits, + verify_evidence=verify_evidence, proposal_base_dir=proposal_base_dir, + evidence_root_dir=evidence_root_dir, strict_trust=strict_trust, + key_resolver=key_resolver, on_allow=self._make_hook(rid, hook_entered), + ) + envelope = guarded(**dict(kwargs)) + state = self._store.peek(rid).get("state") + entered = hook_entered.is_set() + + if state == "bridged": + ctx = self._store.peek(rid) + authz = self._authz_from_ctx(ctx) + if isinstance(envelope, Mapping) and envelope.get("isError"): # tool failed after allow + return AdapterOutcome( + "post-allow-error", "allowed", envelope, authorization=authz, + absence={"reason": "tool_error_after_allow", "error_code": _error_code(envelope)}, + ) + observation = observe(envelope) # trusted callback; raw runtime envelope + self._validate_observation(observation) + if observation.verdict != "established": + return AdapterOutcome( + "observation-failed", "allowed", envelope, authorization=authz, + absence={"reason": "observation_" + observation.verdict}, + ) + try: + record_artifacts = self._finalize(ctx, authz, observation) + except Exception: # noqa: BLE001 - keep the verified authorization bundle + return AdapterOutcome( + "post-allow-error", "allowed", envelope, authorization=authz, + absence={"reason": "finalization_failed"}, + ) + self._store.consume(rid, expected="bridged") + return AdapterOutcome( + "record-emitted", "allowed", envelope, + authorization=record_artifacts.authorization, record_artifacts=record_artifacts, + ) + + if entered: # PIC allowed and the hook ran, but no bridge was produced; no dispatch + return AdapterOutcome( + "post-allow-error", "allowed", envelope, + absence={"reason": "hook_failed_before_bridge", "error_code": _error_code(envelope)}, + ) + + # Hook never entered. + if isinstance(envelope, Mapping) and envelope.get("isError"): # PIC blocked/refused before the hook + return AdapterOutcome( + "pic-blocked", "blocked", envelope, + absence={"reason": "pic_blocked", "error_code": _error_code(envelope)}, + ) + # A non-error envelope with no hook entry is an inconsistent adapter condition. + return AdapterOutcome("adapter-error", "not-evaluated", envelope, absence={"reason": "no_hook_entry"}) + except Exception: # noqa: BLE001 - fail closed; never emit a record on error + authz = self._safe_authz(rid) + if authz is not None: + return AdapterOutcome("post-allow-error", "allowed", envelope, authorization=authz, absence={"reason": "adapter_error"}) + if hook_entered.is_set(): + return AdapterOutcome("post-allow-error", "allowed", envelope, absence={"reason": "adapter_error"}) + return AdapterOutcome("adapter-error", "not-evaluated", envelope, absence={"reason": "adapter_error"}) + finally: + self._store.abandon_if_active(rid) # idempotent; no-op after consume() + + def _finalize( + self, ctx: Mapping[str, Any], authz: AuthorizationArtifacts, observation: Observation + ) -> RecordArtifacts: + cfg = self._cfg + raw, bridge, call = ctx["raw"], ctx["bridge"], ctx["tool_call"] + authorization_id = bridge["authorization"]["authorization_id"] + authorized_at = bridge["authorization"]["authorized_at"] + # The record timestamp comes from the injected clock too, and cannot precede the + # authorization. Bridge validity is still checked at authorized_at; execution may + # legitimately finish after the bridge expires. + record_iat = self._safe_timestamp() + if record_iat < authorized_at: + raise AdapterError("record iat precedes authorization") + + transcript = build_transcript( + pic_decision_raw=raw, bridge=bridge, tool_call=call, + tool_response=_snapshot(observation.response_projection), + observation=_snapshot(dict(observation.document)), + ) + transcript_bytes = rfc8785.dumps(transcript) + if len(transcript_bytes) > cfg.max_transcript_bytes: + raise AdapterError("transcript exceeds max_transcript_bytes") + + record = build_record( + source=SourceSystem(producer=cfg.authorizer, source_event_id=authorization_id), + subject=self._subject, + model_provider=cfg.model_provider, model_id=cfg.model_id, + policy=PolicyEvidence(bundle=self._policy_bundle, enforcement_mode="enforce"), + data_class=cfg.data_class, jwk=key_to_jwk(cfg.record_key), + transcript_bytes=transcript_bytes, tool_call_count=1, + workload_digest=cfg.workload_digest, iat=record_iat, + ) + record["references"] = [{ + "rel": BRIDGE_REFERENCE_REL, "id": authorization_id, + "resolver": cfg.resolver, "digest": digest_jcs(bridge), + }] + signed = sign_record(record, cfg.record_key) + # Self-verify the complete bundle before returning (raises on any failure). + verify_adapter_artifacts( + record=signed, bridge=bridge, pic_decision_raw=raw, + declaration=self._declaration, + pic_intent_digest=ctx["intent_digest"], pic_args_digest=ctx["args_digest"], + tool_call=call, authorizer_jwk=self._authorizer_jwk, transcript=transcript, + ) + return RecordArtifacts( + authorization=authz, + transcript=_snapshot(transcript), + record=_snapshot(signed), + ) + + +def replay_pic_verification( + *, + proposal: Mapping[str, Any], + policy_bundle: bytes, + tool_args: Mapping[str, Any], + limits: Any = None, + verify_evidence: bool = True, + proposal_base_dir: Any = None, + evidence_root_dir: Any = None, + key_resolver: Any = None, + strict_trust: bool = True, + request_id: Optional[str] = None, +) -> Any: + """Re-evaluate `proposal` against the PIC pipeline NOW, from the exact policy bytes. + + This is a CURRENT re-evaluation, not proof of the historical trust state: faithful + historical reproduction requires the original trust and key snapshot, policy bytes, + evidence bytes, `limits`, and verification configuration as they were at decision + time. `tool_args` must be the sanitized business arguments; the reserved `__pic` / + `__pic_request_id` keys are supplied by this function, not by the caller. Returns + `evaluate_pic_for_tool_call`'s 2-tuple or raises `PICError`. + """ + for reserved in _RESERVED_KEYS: + if reserved in tool_args: + raise AdapterError("tool_args must be sanitized business arguments without reserved keys") + if request_id is not None: + if not isinstance(request_id, str) or not request_id or request_id != request_id.strip() \ + or len(request_id) > _DEFAULT_MAX_REQUEST_ID_LEN: + raise AdapterError("invalid request_id") + policy = _load_policy(policy_bundle) + call_args = {**dict(tool_args), "__pic": dict(proposal)} + if request_id is not None: + call_args["__pic_request_id"] = request_id + return evaluate_pic_for_tool_call( + tool_name=proposal["action"]["tool"], tool_args=call_args, policy=policy, limits=limits, + verify_evidence=verify_evidence, proposal_base_dir=proposal_base_dir, + evidence_root_dir=evidence_root_dir, key_resolver=key_resolver, strict_trust=strict_trust, + ) diff --git a/integrations/pic-standard-mcp-guard/pic_trace_verify.py b/integrations/pic-standard-mcp-guard/pic_trace_verify.py new file mode 100644 index 00000000..224dd7ba --- /dev/null +++ b/integrations/pic-standard-mcp-guard/pic_trace_verify.py @@ -0,0 +1,236 @@ +"""Relying-party verification for the PIC Standard MCP guard TRACE adapter. + +This verification module does not import or invoke `pic_standard` or +`agentrust_trace_adapters`; its verification path uses only `agentrust_trace`, +`rfc8785`, and the standard library. (The installed distribution still pulls the +producer dependencies; the guarantee here is code-level, enforced by a test.) The +producer side, bridge construction, record building, and measurement, lives in +`pic_trace_guard`. + +Two public entry points: `verify_authorization_artifacts` verifies the detached +authorization bridge alone (the bridge-only outcome where no Trust Record was emitted), +and `verify_adapter_artifacts` delegates to it and then adds the record-specific checks. +""" + +from __future__ import annotations + +import base64 +import binascii +import hashlib +import re +from collections.abc import Mapping +from typing import Any + +import rfc8785 +from agentrust_trace import verify_record +from agentrust_trace.intent_bridge import verify_bridge +from agentrust_trace.validate import validate_json + +TRANSCRIPT_PROFILE = "pic-trace-stage1-transcript/v1" +BRIDGE_REFERENCE_REL = "authorized-intent" +_B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +_PIC_HEX = re.compile(r"^[0-9a-f]{64}$") +_B64URL_32 = re.compile(r"^[A-Za-z0-9_-]{43}$") + + +class AdapterVerifyError(Exception): + """A relying-party verification check failed (no sensitive detail in the message).""" + + +def sha256_prefixed(data: bytes) -> str: + """SHA-256 of `data` in TRACE's `sha256:` form.""" + return "sha256:" + hashlib.sha256(data).hexdigest() + + +def _digest_jcs(value: Any) -> str: + return sha256_prefixed(rfc8785.dumps(value)) + + +def build_transcript( + *, + pic_decision_raw: bytes, + bridge: Mapping[str, Any], + tool_call: Mapping[str, Any], + tool_response: Any, + observation: Mapping[str, Any], +) -> dict: + """The transcript preimage hashed into `tool_transcript.hash` (JCS-canonical).""" + return { + "profile": TRANSCRIPT_PROFILE, + "pic_decision_digest": sha256_prefixed(pic_decision_raw), + "authorization_bridge_digest": _digest_jcs(bridge), + "tool_call": dict(tool_call), + "tool_response": tool_response, + "observation": dict(observation), + } + + +def _require(cond: bool, message: str) -> None: + if not cond: + raise AdapterVerifyError(message) + + +def _b64url_decode_32(s: str) -> bytes: + _require( + isinstance(s, str) and bool(_B64URL_32.fullmatch(s)), + "invalid unpadded base64url Ed25519 key", + ) + try: + raw = base64.urlsafe_b64decode(s + "=") + except (binascii.Error, ValueError) as exc: + raise AdapterVerifyError("invalid unpadded base64url Ed25519 key") from exc + _require(len(raw) == 32, "expected a 32-byte Ed25519 public key") + # Reject alternate spellings whose unused final bits are non-zero. + canonical = base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") + _require(s == canonical, "non-canonical base64url Ed25519 key") + return raw + + +def _did_key_public_bytes(did: str) -> bytes: + prefix = "did:key:z" + _require(isinstance(did, str) and did.startswith(prefix), "subject is not a base58btc did:key") + n = 0 + for ch in did[len(prefix):]: + idx = _B58.find(ch) + _require(idx >= 0, "invalid base58btc character in did:key") + n = n * 58 + idx + body = n.to_bytes((n.bit_length() + 7) // 8, "big") + _require(body[:2] == b"\xed\x01", "did:key multicodec is not Ed25519 (0xed01)") + _require(len(body) - 2 == 32, "did:key does not hold a 32-byte Ed25519 key") + return body[2:] + + +def verify_authorization_artifacts( + *, + bridge: Mapping[str, Any], + pic_decision_raw: bytes, + declaration: Mapping[str, Any], + pic_intent_digest: str, + pic_args_digest: str, + tool_call: Mapping[str, Any], + authorizer_jwk: Mapping[str, Any], +) -> dict: + """Verify the detached authorization bridge alone, with only agentrust_trace + rfc8785. + + This is the public verifier for the bridge-only outcome (PIC allowed and the bridge + was produced, but no Trust Record was emitted). `pic_decision_raw` is the exact PIC + decision bytes (`decision.to_audit_bytes()`); the `authorization_id` is recomputed + from them. `pic_intent_digest` / `pic_args_digest` must be bare lowercase 64-char + SHA-256 hex (PIC's `canonical.py` form); this function adds the `sha256:` prefix the + bridge requires. Raises `AdapterVerifyError` on any failure. + """ + try: + _require(bool(_PIC_HEX.match(pic_intent_digest)), "pic_intent_digest is not bare 64-char hex") + _require(bool(_PIC_HEX.match(pic_args_digest)), "pic_args_digest is not bare 64-char hex") + authz = bridge["authorization"] + + # 1. authorization_id is the hash of the PIC decision bytes, not an arbitrary id. + expected_authorization_id = "urn:sha256:" + hashlib.sha256(pic_decision_raw).hexdigest() + _require( + authz["authorization_id"] == expected_authorization_id, + "authorization_id does not match the PIC decision bytes", + ) + + # 2. Bridge signature + binding (declaration, tool call, PIC digests), replayed + # at its own authorized_at. + verify_bridge( + bridge, + authorizer_jwk, + declaration=dict(declaration), + pic_intent_digest="sha256:" + pic_intent_digest, + pic_args_digest="sha256:" + pic_args_digest, + tool_call=dict(tool_call), + now=authz["authorized_at"], + ) + _require(authz["pic"]["intent_digest"] == "sha256:" + pic_intent_digest, "bridge intent digest") + _require(authz["pic"]["args_digest"] == "sha256:" + pic_args_digest, "bridge args digest") + + return { + "authorization_id_bound_to_decision": True, + "bridge_signature": True, + "bridge_intent_digest": True, + "bridge_args_digest": True, + } + except AdapterVerifyError: + raise + except Exception as exc: # noqa: BLE001 - normalize to the module's error, keep cause, hide detail + raise AdapterVerifyError("authorization artifact verification failed") from exc + + +def verify_adapter_artifacts( + *, + record: Mapping[str, Any], + bridge: Mapping[str, Any], + pic_decision_raw: bytes, + declaration: Mapping[str, Any], + pic_intent_digest: str, + pic_args_digest: str, + tool_call: Mapping[str, Any], + authorizer_jwk: Mapping[str, Any], + transcript: Mapping[str, Any] | None = None, +) -> dict: + """Verify the adapter's bridge + Trust Record with only agentrust_trace + rfc8785. + + Delegates to `verify_authorization_artifacts` for the bridge checks (no duplicated + logic), then adds: the record validates (`validate_json`) and its signature verifies + against its own embedded confirmation key; the supplied `bridge` is cited by the + record's single signed `authorized-intent` reference by id and JCS digest (the + function does not contact the reference `resolver`); the `subject` `did:key` decodes + to `cnf.jwk.x`; and, if `transcript` (the preimage) is supplied, its bindings and the + signed `tool_transcript.hash`. Raises `AdapterVerifyError` on any failure. + """ + try: + checked = dict( + verify_authorization_artifacts( + bridge=bridge, + pic_decision_raw=pic_decision_raw, + declaration=declaration, + pic_intent_digest=pic_intent_digest, + pic_args_digest=pic_args_digest, + tool_call=tool_call, + authorizer_jwk=authorizer_jwk, + ) + ) + authz = bridge["authorization"] + expected_authorization_id = "urn:sha256:" + hashlib.sha256(pic_decision_raw).hexdigest() + + # Record: schema + signature against its own embedded confirmation key. + validate_json(record) + verify_record(record, record["cnf"]["jwk"]) + checked["record_signature"] = True + + # Record -> bridge linkage: source_event_id + exactly one authorized-intent + # reference citing this bridge by id and JCS digest. Other references compose. + _require(record["origin"]["source_event_id"] == expected_authorization_id, "origin.source_event_id") + refs = record.get("references") or [] + matching = [r for r in refs if r.get("id") == authz["authorization_id"]] + _require(len(matching) == 1, "expected exactly one reference citing the bridge") + bridge_ref = matching[0] + _require(bridge_ref.get("rel") == BRIDGE_REFERENCE_REL, "bridge reference rel is not authorized-intent") + _require(bridge_ref["digest"] == _digest_jcs(bridge), "bridge reference digest mismatch") + checked["reference_matches_supplied_bridge"] = True + + # Subject did:key binds the record's confirmation key (both exactly 32 bytes). + _require( + _did_key_public_bytes(record["subject"]) == _b64url_decode_32(record["cnf"]["jwk"]["x"]), + "subject did:key does not decode to cnf.jwk.x", + ) + checked["subject_binds_cnf_jwk"] = True + + # Optional transcript preimage: bindings first, then the signed hash. + if transcript is not None: + _require(transcript.get("profile") == TRANSCRIPT_PROFILE, "transcript profile") + _require(transcript.get("pic_decision_digest") == sha256_prefixed(pic_decision_raw), "transcript pic_decision_digest") + _require(transcript.get("authorization_bridge_digest") == _digest_jcs(bridge), "transcript authorization_bridge_digest") + _require(transcript.get("tool_call") == dict(tool_call), "transcript tool_call") + _require( + record["tool_transcript"]["hash"] == sha256_prefixed(rfc8785.dumps(dict(transcript))), + "tool_transcript.hash does not match the supplied transcript preimage", + ) + checked["transcript_bound_and_hashed"] = True + + return checked + except AdapterVerifyError: + raise + except Exception as exc: # noqa: BLE001 - normalize to the module's error, keep cause, hide detail + raise AdapterVerifyError("adapter artifact verification failed") from exc diff --git a/integrations/pic-standard-mcp-guard/pyproject.toml b/integrations/pic-standard-mcp-guard/pyproject.toml new file mode 100644 index 00000000..618f9b34 --- /dev/null +++ b/integrations/pic-standard-mcp-guard/pyproject.toml @@ -0,0 +1,26 @@ +[build-system] +requires = ["setuptools>=68"] +build-backend = "setuptools.build_meta" + +[project] +name = "pic-standard-mcp-guard-agentrust-integration" +version = "0.1.0" +description = "PIC MCP allow decisions and verified observations as signed TRACE Level 0 Trust Records" +requires-python = ">=3.11" +license = "Apache-2.0" +dependencies = [ + "pic-standard[crypto]>=0.9.1", + "agentrust-trace>=0.11.0", + "agentrust-trace-adapters>=0.2.0", + "rfc8785", +] + +[project.optional-dependencies] +test = [ + "pytest", + "agentrust-trace-tests>=0.6.2", + "packaging>=24", +] + +[tool.setuptools] +py-modules = ["pic_trace_guard", "pic_trace_verify"] diff --git a/integrations/pic-standard-mcp-guard/tests/conftest.py b/integrations/pic-standard-mcp-guard/tests/conftest.py new file mode 100644 index 00000000..68d7e26d --- /dev/null +++ b/integrations/pic-standard-mcp-guard/tests/conftest.py @@ -0,0 +1,172 @@ +"""Shared fixtures for the PIC Standard MCP Guard adapter tests. TEST keys only.""" + +from __future__ import annotations + +import base64 +import hashlib +import itertools +import json +import logging +import pathlib +import sys +from typing import Any + +import pytest +from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey +from cryptography.hazmat.primitives.serialization import Encoding, PublicFormat + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent)) + +from pic_standard.canonical import canonicalize, intent_digest_hex, sha256_hex # noqa: E402 + +import pic_trace_guard as g # noqa: E402 + +TOOL = "delete_demo_marker" +IMPACT = "irreversible" +MARKER = "marker.txt" +HARNESS = "urn:pic-standard:integration:pic-trace-test-harness" +ARTIFACT_STORE = "urn:pic-standard:integration:pic-trace-test-artifact-store" +OBSERVER = "pic-trace-test-observer" +EVIDENCE_KEY_ID = "pic-trace-test-evidence-key-1" +BRIDGE_KEY_ID = "pic-trace-test-bridge-key-1" +PROVENANCE_ID = "pic-trace-approval" +INTENT = "Delete the demo marker file in the test sandbox" + +DECLARATION = {"profile": "pic-standard-action-declaration/v1", "tool": TOOL, "impact": IMPACT} +POLICY = { + "impact_by_tool": {TOOL: IMPACT}, + "require_pic_for_impacts": [IMPACT], + "require_evidence_for_impacts": [IMPACT], +} +POLICY_BYTES = (json.dumps(POLICY, indent=2) + "\n").encode("utf-8") + +_sandbox_counter = itertools.count() + + +def _test_key(label: str) -> Ed25519PrivateKey: + return Ed25519PrivateKey.from_private_bytes(hashlib.sha256(f"pic-trace-test/{label}".encode()).digest()) + + +EVIDENCE_KEY = _test_key("pic-evidence-key") +BRIDGE_KEY = _test_key("bridge-key") +RECORD_KEY = _test_key("trace-record-key") + + +def raw_public(key: Ed25519PrivateKey) -> bytes: + return key.public_key().public_bytes(Encoding.Raw, PublicFormat.Raw) + + +def make_config(policy_bytes: bytes = POLICY_BYTES) -> g.PicTraceConfig: + return g.PicTraceConfig( + authorizer=HARNESS, authorizer_key_id=BRIDGE_KEY_ID, + bridge_key=BRIDGE_KEY, record_key=RECORD_KEY, + model_provider="pic-standard-test", model_id="deterministic-tool-calling-model-v1", + data_class="public", policy_bundle=policy_bytes, + workload_digest="sha256:" + hashlib.sha256(b"pic-trace-test").hexdigest(), + declaration=DECLARATION, resolver=ARTIFACT_STORE, + ) + + +def build_proposal(*, tamper: bool = False) -> dict[str, Any]: + """Proposal whose action.args is always {path: marker.txt}; `tamper` corrupts the + evidence signature so PIC blocks.""" + args = {"path": MARKER} + claims = [{"text": "The test approver authorized deleting marker.txt", "evidence": [PROVENANCE_ID]}] + attestation = { + "attestation_version": "PIC-ATT/1.0", "tool": TOOL, "impact": IMPACT, + "provenance_ids": [PROVENANCE_ID], "args_digest": sha256_hex(args), + "claims_digest": sha256_hex(claims), "intent_digest": intent_digest_hex(INTENT), + } + signature = base64.b64encode(EVIDENCE_KEY.sign(canonicalize(attestation))).decode() + if tamper: + c = signature[20] + signature = signature[:20] + ("B" if c == "A" else "A") + signature[21:] + return { + "protocol": "PIC/1.0", "intent": INTENT, "impact": IMPACT, + "provenance": [{"id": PROVENANCE_ID, "trust": "untrusted", "source": "pic-trace-test approver"}], + "claims": claims, "action": {"tool": TOOL, "args": args}, + "evidence": [{ + "id": PROVENANCE_ID, "type": "sig", "ref": "inline:pic-att", + "payload": json.dumps(attestation, sort_keys=True, separators=(",", ":")), + "alg": "ed25519", "signature": signature, "key_id": EVIDENCE_KEY_ID, + "signer": "pic-trace-test approver", + }], + } + + +@pytest.fixture +def fixtures_dir(tmp_path, monkeypatch): + d = tmp_path / "fx" + d.mkdir() + (d / "pic_keys.json").write_text( + json.dumps({"trusted_keys": {EVIDENCE_KEY_ID: base64.b64encode(raw_public(EVIDENCE_KEY)).decode()}}), + encoding="utf-8", + ) + monkeypatch.setenv("PIC_KEYS_PATH", str(d / "pic_keys.json")) + pic_log = logging.getLogger("pic_standard.mcp") + prev_level = pic_log.level + pic_log.setLevel(logging.CRITICAL) + try: + yield d + finally: + pic_log.setLevel(prev_level) + + +@pytest.fixture +def coordinator(fixtures_dir): + return g.PicTraceCoordinator(make_config()) # default real-time clock (iat freshness window) + + +@pytest.fixture +def runner(tmp_path, fixtures_dir): + """Run one guarded call in a unique sandbox. + + `tool_behavior(path, sb, info)` overrides what the tool does after instrumentation + (e.g. raise, or return a non-JSON value); the default deletes the marker. The tool is + always wrapped so `count`, `received`, and `bridge_state_at_entry` (the store state at + tool entry, which proves bridge-before-dispatch) are recorded in every case. + `observe_behavior(envelope, sb, info)` overrides the observation callback; the default + establishes iff the marker was deleted exactly once. Returns (outcome, info). + """ + + def _run(coordinator, *, rid, tamper=False, live_path=MARKER, verdict="auto", + tool_behavior=None, observe_behavior=None): + sb = tmp_path / f"sb-{next(_sandbox_counter)}" + sb.mkdir() + (sb / MARKER).write_text("marker\n", encoding="utf-8") + info: dict[str, Any] = {"count": 0, "received": [], "bridge_state_at_entry": None, "sandbox": sb} + + def instrumented_tool(path: str) -> Any: + info["count"] += 1 + info["received"].append({"path": path}) + try: + info["bridge_state_at_entry"] = coordinator._store.peek(rid).get("state") + except Exception: # noqa: BLE001 - instrumentation must never change behaviour + pass + if tool_behavior is not None: + return tool_behavior(path, sb, info) + target = (sb / path).resolve() + if target.parent != sb.resolve(): + raise ValueError("path escapes the sandbox") + target.unlink() + return {"deleted": True} + + def observe(envelope: Any) -> g.Observation: + if observe_behavior is not None: + return observe_behavior(envelope, sb, info) + doc = {"observation": {"tool_call_count": info["count"], + "marker_exists_after": (sb / MARKER).exists()}, "observer": OBSERVER} + if verdict == "auto": + v = "established" if (info["count"] == 1 and not (sb / MARKER).exists()) else "contradicted" + else: + v = verdict + return g.Observation(verdict=v, document=doc, response_projection=envelope) + + kwargs = {"path": live_path, "__pic": build_proposal(tamper=tamper), "__pic_request_id": rid} + outcome = coordinator.run( + tool_name=TOOL, tool_fn=instrumented_tool, kwargs=kwargs, observe=observe, + verify_evidence=True, proposal_base_dir=fixtures_dir, strict_trust=True, + ) + return outcome, info + + return _run diff --git a/integrations/pic-standard-mcp-guard/tests/test_context_store.py b/integrations/pic-standard-mcp-guard/tests/test_context_store.py new file mode 100644 index 00000000..1662e6d9 --- /dev/null +++ b/integrations/pic-standard-mcp-guard/tests/test_context_store.py @@ -0,0 +1,196 @@ +"""Unit tests for InMemoryContextStore lifecycle, tombstones, and validation. + +Two store-related guarantees are exercised at the coordinator level instead of here, +because they are classification/precedence properties of run(), not the store API: +a custom-store transition failure staying classified PIC-allowed, and an overlong +request id being rejected before any store.reserve() call (see test_coordinator.py). +""" + +from __future__ import annotations + +import threading + +import pytest + +import pic_trace_guard as g + + +def test_reserve_transition_consume_happy_path(): + store = g.InMemoryContextStore() + store.reserve("r1") + assert store.peek("r1") == {"state": "reserved"} + store.transition("r1", "reserved", "bridged", raw=b"x", bridge={"a": 1}) + assert store.peek("r1")["state"] == "bridged" + ctx = store.consume("r1", expected="bridged") + assert ctx["state"] == "bridged" and ctx["raw"] == b"x" and ctx["bridge"] == {"a": 1} + assert store.peek("r1") == {} # removed after consume + + +def test_stored_context_is_detached_from_caller_and_snapshots(): + store = g.InMemoryContextStore() + store.reserve("p1") + payload = {"nested": {"value": 1}} + store.transition("p1", "reserved", "bridged", bridge=payload) + payload["nested"]["value"] = 2 # mutating the original input must not reach the store + assert store.peek("p1")["bridge"]["nested"]["value"] == 1 + snapshot = store.peek("p1") + snapshot["bridge"]["nested"]["value"] = 3 # mutating a snapshot must not reach the store + assert store.peek("p1")["bridge"]["nested"]["value"] == 1 + + +def test_transition_rejects_state_override(): + store = g.InMemoryContextStore() + store.reserve("s1") + with pytest.raises(g.AdapterError): + store.transition("s1", "reserved", "bridged", state="hacked") + assert store.peek("s1")["state"] == "reserved" + + +def test_concurrent_reservations_exactly_one_wins(): + for i in range(5): + store = g.InMemoryContextStore() + rid = f"race-{i}" + results: list[str] = [] + lock = threading.Lock() + barrier = threading.Barrier(2) + + def worker(): + barrier.wait(timeout=5) + try: + store.reserve(rid) + outcome = "ok" + except g.AdapterError: + outcome = "rejected" + with lock: + results.append(outcome) + + threads = [threading.Thread(target=worker) for _ in range(2)] + for t in threads: + t.start() + for t in threads: + t.join(timeout=5) + assert not t.is_alive() + assert sorted(results) == ["ok", "rejected"] + + +def test_concurrent_consume_is_one_time(): + for i in range(5): + store = g.InMemoryContextStore() + rid = f"consume-race-{i}" + store.reserve(rid) + store.transition(rid, "reserved", "bridged") + results: list[str] = [] + lock = threading.Lock() + barrier = threading.Barrier(2) + + def worker(): + barrier.wait(timeout=5) + try: + store.consume(rid, expected="bridged") + outcome = "ok" + except g.AdapterError: + outcome = "rejected" + with lock: + results.append(outcome) + + threads = [threading.Thread(target=worker) for _ in range(2)] + for t in threads: + t.start() + for t in threads: + t.join(timeout=5) + assert not t.is_alive() + assert sorted(results) == ["ok", "rejected"] # finalization consumption is one-time + + +def test_recent_tombstone_rejects_reuse(): + store = g.InMemoryContextStore() + store.reserve("t1") + store.consume("t1", expected="reserved") + with pytest.raises(g.AdapterError): + store.reserve("t1") + + +def test_abandoned_id_is_tombstoned(): + store = g.InMemoryContextStore() + store.reserve("a1") + store.abandon_if_active("a1") + with pytest.raises(g.AdapterError): + store.reserve("a1") + + +def test_tombstone_eviction_permits_oldest_reuse(): + store = g.InMemoryContextStore(tombstone_capacity=2) + for i in range(3): + store.reserve(f"id{i}") + store.abandon_if_active(f"id{i}") + # Tombstones are bounded (cap 2): id0 is evicted and may be reused; id1/id2 still rejected. + store.reserve("id0") + assert store.peek("id0") == {"state": "reserved"} + with pytest.raises(g.AdapterError): + store.reserve("id1") + with pytest.raises(g.AdapterError): + store.reserve("id2") + + +def test_consume_rejects_wrong_state_non_destructively(): + store = g.InMemoryContextStore() + store.reserve("c1") + with pytest.raises(g.AdapterError): + store.consume("c1", expected="bridged") # still 'reserved' + assert store.peek("c1")["state"] == "reserved" # the failed consume did not remove it + + +def test_consume_rejects_missing_context(): + store = g.InMemoryContextStore() + with pytest.raises(g.AdapterError): + store.consume("missing") + + +def test_transition_rejects_wrong_expected_state_non_destructively(): + store = g.InMemoryContextStore() + store.reserve("x1") + with pytest.raises(g.AdapterError): + store.transition("x1", "bridged", "consumed") # expected 'bridged' but is 'reserved' + assert store.peek("x1")["state"] == "reserved" + + +def test_transition_rejects_unknown_id(): + store = g.InMemoryContextStore() + with pytest.raises(g.AdapterError): + store.transition("nope", "reserved", "bridged") + + +def test_abandon_if_active_is_idempotent_and_never_raises(): + store = g.InMemoryContextStore() + store.reserve("a1") + store.abandon_if_active("a1") # removes + tombstones + store.abandon_if_active("a1") # no-op, must not raise + store.abandon_if_active("never-reserved") # no-op, must not raise + + +def test_request_id_length_boundary(): + store = g.InMemoryContextStore() + n = g._DEFAULT_MAX_REQUEST_ID_LEN + store.reserve("a" * n) # exactly the maximum is accepted + assert store.peek("a" * n)["state"] == "reserved" + with pytest.raises(g.AdapterError): + store.reserve("b" * (n + 1)) # one over is rejected + + +@pytest.mark.parametrize("bad_id", ["", "a" * (g._DEFAULT_MAX_REQUEST_ID_LEN + 1), 123, None]) +def test_reserve_rejects_bad_request_id(bad_id): + store = g.InMemoryContextStore() + with pytest.raises(g.AdapterError): + store.reserve(bad_id) + + +@pytest.mark.parametrize("bad", [True, 0.5, 0, -1]) +def test_store_rejects_bad_tombstone_capacity(bad): + with pytest.raises(g.AdapterError): + g.InMemoryContextStore(tombstone_capacity=bad) + + +@pytest.mark.parametrize("bad", [True, 0.5, 0, -1]) +def test_store_rejects_bad_max_request_id_len(bad): + with pytest.raises(g.AdapterError): + g.InMemoryContextStore(max_request_id_len=bad) diff --git a/integrations/pic-standard-mcp-guard/tests/test_coordinator.py b/integrations/pic-standard-mcp-guard/tests/test_coordinator.py new file mode 100644 index 00000000..4fb5034d --- /dev/null +++ b/integrations/pic-standard-mcp-guard/tests/test_coordinator.py @@ -0,0 +1,354 @@ +"""Coordinator lifecycle, fail-closed classification, and strict input/clock handling.""" + +from __future__ import annotations + +import dataclasses +import threading +import time + +import pytest + +import pic_trace_guard as g +from conftest import IMPACT, MARKER, TOOL, make_config + + +def _seq_clock(values): + it = iter(values) + last = {"v": values[-1]} + + def _clock(): + try: + last["v"] = next(it) + except StopIteration: + pass + return last["v"] + + return _clock + + +# --- Lifecycle ------------------------------------------------------------- + +def test_hook_failure_before_bridge_is_allowed_no_dispatch(coordinator, runner, monkeypatch): + def boom(*a, **k): + raise RuntimeError("sign failed") + + monkeypatch.setattr(g, "sign_bridge", boom) + outcome, info = runner(coordinator, rid="hook-fail") + assert outcome.status == "post-allow-error" + assert outcome.pic_decision == "allowed" + assert outcome.absence["reason"] == "hook_failed_before_bridge" + assert outcome.record_artifacts is None and outcome.authorization is None + assert info["count"] == 0 # tool never dispatched + assert coordinator._store.peek("hook-fail") == {} + + +def test_store_transition_failure_still_classified_allowed(runner): + class _FailOnTransition(g.InMemoryContextStore): + def transition(self, *a, **k): + raise g.AdapterError("transition unavailable") + + coordinator = g.PicTraceCoordinator(make_config(), store=_FailOnTransition()) + outcome, info = runner(coordinator, rid="store-fail") + # The store write is where hook-entry proof could be lost; the Event must prevent + # this being read as a PIC block. + assert outcome.pic_decision == "allowed" + assert outcome.status == "post-allow-error" + assert outcome.absence["reason"] == "hook_failed_before_bridge" + assert outcome.authorization is None + assert outcome.record_artifacts is None + assert info["count"] == 0 + assert coordinator._store.peek("store-fail") == {} + + +def test_tool_raises_after_bridge_preserves_authorization(coordinator, runner): + def raising_tool(path, sb, info): + raise RuntimeError("tool boom") + + outcome, info = runner(coordinator, rid="tool-raise", tool_behavior=raising_tool) + assert outcome.status == "post-allow-error" + assert outcome.pic_decision == "allowed" + assert outcome.absence["reason"] == "tool_error_after_allow" + assert outcome.authorization is not None + assert outcome.record_artifacts is None + assert info["count"] == 1 and info["bridge_state_at_entry"] == "bridged" + assert coordinator._store.peek("tool-raise") == {} + + +def test_observation_raises_preserves_authorization_and_envelope(coordinator, runner): + def raising_observe(envelope, sb, info): + raise RuntimeError("observe boom") + + outcome, _ = runner(coordinator, rid="obs-raise", observe_behavior=raising_observe) + assert outcome.status == "post-allow-error" + assert outcome.pic_decision == "allowed" + assert outcome.authorization is not None + assert outcome.envelope == {"isError": False, "result": {"deleted": True}} + assert outcome.record_artifacts is None + assert coordinator._store.peek("obs-raise") == {} + + +def test_finalization_failure_preserves_authorization(coordinator, runner, monkeypatch): + def boom(*a, **k): + raise RuntimeError("build_record failed") + + monkeypatch.setattr(g, "build_record", boom) + outcome, _ = runner(coordinator, rid="final-fail") + assert outcome.status == "post-allow-error" + assert outcome.pic_decision == "allowed" + assert outcome.absence["reason"] == "finalization_failed" + assert outcome.authorization is not None + assert outcome.record_artifacts is None + assert outcome.envelope == {"isError": False, "result": {"deleted": True}} + assert coordinator._store.peek("final-fail") == {} + + +def test_non_json_tool_result_with_canonicalizable_projection(coordinator, runner): + class Weird: + pass + + weird = Weird() + + def tool(path, sb, info): + (sb / MARKER).unlink() + return weird + + def observe(envelope, sb, info): + return g.Observation(verdict="established", document={"ok": True}, response_projection={"projection": "redacted"}) + + outcome, _ = runner(coordinator, rid="nonjson", tool_behavior=tool, observe_behavior=observe) + assert outcome.status == "record-emitted" + assert outcome.envelope["result"] is weird # runtime value preserved, never canonicalized + assert outcome.record_artifacts is not None + assert outcome.record_artifacts.transcript["tool_response"] == {"projection": "redacted"} + + +def test_success_envelope_without_hook_entry_is_adapter_error(coordinator, runner, monkeypatch): + def fake_guard(tool_name, tool_fn, **kwargs): + def guarded(**call_kwargs): + return {"isError": False, "result": {"ok": True}} + return guarded + + monkeypatch.setattr(g, "guard_mcp_tool", fake_guard) + outcome, _ = runner(coordinator, rid="no-hook") + assert outcome.status == "adapter-error" + assert outcome.pic_decision == "not-evaluated" + assert outcome.absence["reason"] == "no_hook_entry" + assert coordinator._store.peek("no-hook") == {} + + +def test_keyboardinterrupt_escapes_but_context_is_cleaned(coordinator, runner): + def ki(envelope, sb, info): + raise KeyboardInterrupt + + with pytest.raises(KeyboardInterrupt): + runner(coordinator, rid="ki-1", observe_behavior=ki) + assert coordinator._store.peek("ki-1") == {} # finally ran despite BaseException + + +@pytest.mark.parametrize("rid,kwargs", [ + ("na-block", {"tamper": True}), + ("na-tool", {"tool_behavior": lambda p, sb, i: (_ for _ in ()).throw(RuntimeError("x"))}), + ("na-obs", {"observe_behavior": lambda e, sb, i: (_ for _ in ()).throw(RuntimeError("x"))}), + ("na-done", {}), # success path also leaves no active context +]) +def test_no_active_context_after_any_path(coordinator, runner, rid, kwargs): + runner(coordinator, rid=rid, **kwargs) + assert coordinator._store.peek(rid) == {} + + +# --- Observation verdict + callback contract ------------------------------- + +@pytest.mark.parametrize("verdict", ["contradicted", "not-established"]) +def test_non_established_observation_preserves_authorization(coordinator, runner, verdict): + outcome, _ = runner(coordinator, rid=f"obs-{verdict}", verdict=verdict) + assert outcome.status == "observation-failed" + assert outcome.pic_decision == "allowed" + assert outcome.authorization is not None + assert outcome.record_artifacts is None + assert outcome.absence["reason"] == f"observation_{verdict}" + assert coordinator._store.peek(f"obs-{verdict}") == {} + + +@pytest.mark.parametrize("make_obs,label", [ + (lambda: (lambda e, sb, i: {"not": "observation"}), "not-observation"), + (lambda: (lambda e, sb, i: None), "none"), + (lambda: (lambda e, sb, i: g.Observation("bogus", {"a": 1}, {})), "bad-verdict"), + (lambda: (lambda e, sb, i: g.Observation("established", ["not-a-mapping"], {})), "doc-not-mapping"), +]) +def test_invalid_observation_contract_is_adapter_error(coordinator, runner, make_obs, label): + outcome, _ = runner(coordinator, rid="bad-obs", observe_behavior=make_obs()) + assert outcome.status == "post-allow-error" + assert outcome.pic_decision == "allowed" + assert outcome.authorization is not None + assert outcome.record_artifacts is None + assert outcome.absence["reason"] == "adapter_error" + assert coordinator._store.peek("bad-obs") == {} + + +# --- Preconditions (before any store) -------------------------------------- + +@pytest.mark.parametrize("kwargs,reason", [ + ({"path": MARKER}, "no_proposal"), + ({"path": MARKER, "__pic": None, "__pic_request_id": "x"}, "no_proposal"), + ({"path": MARKER, "__pic": {"x": 1}}, "bad_request_id"), + ({"path": MARKER, "__pic": {"x": 1}, "request_id": "business-only"}, "bad_request_id"), + ({"path": MARKER, "__pic": {"x": 1}, "__pic_request_id": ""}, "bad_request_id"), + ({"path": MARKER, "__pic": {"x": 1}, "__pic_request_id": " x"}, "bad_request_id"), + ({"path": MARKER, "__pic": {"x": 1}, "__pic_request_id": "x "}, "bad_request_id"), +]) +def test_preconditions_fail_before_guard(kwargs, reason): + coordinator = g.PicTraceCoordinator(make_config()) + outcome = coordinator.run( + tool_name=TOOL, tool_fn=lambda *a, **k: {"deleted": True}, kwargs=kwargs, observe=lambda e: None, + ) + assert outcome.status == "precondition-failed" + assert outcome.pic_decision == "not-evaluated" + assert outcome.absence["reason"] == reason + + +def test_overlong_request_id_rejected_before_store(): + class _SpyStore(g.InMemoryContextStore): + def __init__(self): + super().__init__() + self.reserve_calls = 0 + + def reserve(self, request_id): + self.reserve_calls += 1 + super().reserve(request_id) + + spy = _SpyStore() + coordinator = g.PicTraceCoordinator(make_config(), store=spy) + overlong = "a" * (make_config().max_request_id_len + 1) + outcome = coordinator.run( + tool_name=TOOL, tool_fn=lambda *a, **k: {"deleted": True}, + kwargs={"path": MARKER, "__pic": {"x": 1}, "__pic_request_id": overlong}, observe=lambda e: None, + ) + assert outcome.status == "precondition-failed" + assert outcome.absence["reason"] == "bad_request_id" + assert spy.reserve_calls == 0 # length gate precedes every store implementation + + +# --- Argument-binding block (real PIC 0.9.1 behaviour) --------------------- + +def test_argument_binding_mismatch_blocks(coordinator, runner): + outcome, info = runner(coordinator, rid="arg-mismatch", live_path="other.txt") + assert outcome.status == "pic-blocked" + assert outcome.pic_decision == "blocked" + assert outcome.absence["error_code"] == "PIC_ARG_BINDING_MISMATCH" + assert info["count"] == 0 + assert outcome.authorization is None and outcome.record_artifacts is None + + +# --- Defense-in-depth hook invariant (unreachable via run()) --------------- + +def test_hook_refuses_non_proposal_backed_allow_sets_entry(): + class FakeDecision: + proposal_present = False + intent_digest = None + args_digest = None + request_id = "rid" + tool = TOOL + impact = IMPACT + + def to_audit_bytes(self): + return b"{}" + + coordinator = g.PicTraceCoordinator(make_config()) + event = threading.Event() + hook = coordinator._make_hook("rid", event) + with pytest.raises(g.AdapterError): + hook(FakeDecision(), object()) + assert event.is_set() # hook entry recorded before the proposal-backed refusal + + +# --- Strict policy-bundle inputs (construction fails closed) --------------- + +@pytest.mark.parametrize("policy_bytes,label", [ + (b"\xff\xfe", "malformed-utf8"), + (b"{not json", "malformed-json"), + (b'{"x": NaN}', "nan"), + (b'{"x": Infinity}', "infinity"), + (b'{"a": {"b": 1, "b": 2}}', "nested-duplicate-keys"), + (b'{"a": 1, "a": 2}', "top-level-duplicate-keys"), + (b"[1, 2, 3]", "non-object"), + (b'{"require_pic_for_impacts": 123}', "structurally-invalid-policy"), +]) +def test_policy_bundle_strict_parse(policy_bytes, label): + with pytest.raises(g.AdapterError): + g.PicTraceCoordinator(make_config(policy_bytes=policy_bytes)) + + +def test_policy_bundle_must_be_bytes(): + with pytest.raises(g.AdapterError): + g.PicTraceCoordinator(dataclasses.replace(make_config(), policy_bundle="not-bytes")) + + +# --- Integer settings reject bool/float/non-positive ----------------------- + +@pytest.mark.parametrize("field", ["bridge_validity_seconds", "max_transcript_bytes", "max_request_id_len"]) +@pytest.mark.parametrize("bad", [True, 0.5, 0, -1]) +def test_integer_settings_reject_bad_scalars(field, bad): + with pytest.raises(g.AdapterError): + g.PicTraceCoordinator(dataclasses.replace(make_config(), **{field: bad})) + + +# --- Clock validation ------------------------------------------------------ + +@pytest.mark.parametrize("bad", [-1, True, 1.5, 2**53]) +def test_safe_timestamp_rejects_bad_clock(bad): + coordinator = g.PicTraceCoordinator(make_config(), clock=lambda: bad) + with pytest.raises(g.AdapterError): + coordinator._safe_timestamp() + + +def test_expiry_overflow_fails_closed(runner): + coordinator = g.PicTraceCoordinator(make_config(), clock=lambda: g._SAFE_INT_MAX) + outcome, info = runner(coordinator, rid="expiry-overflow") + assert outcome.status == "post-allow-error" + assert outcome.pic_decision == "allowed" + assert outcome.absence["reason"] == "hook_failed_before_bridge" + assert info["count"] == 0 + assert coordinator._store.peek("expiry-overflow") == {} + + +def test_record_iat_preceding_authorization_fails_closed(runner): + now = int(time.time()) + coordinator = g.PicTraceCoordinator(make_config(), clock=_seq_clock([now, now - 100])) + outcome, _ = runner(coordinator, rid="iat-backwards") + assert outcome.status == "post-allow-error" + assert outcome.absence["reason"] == "finalization_failed" + assert outcome.authorization is not None + assert coordinator._store.peek("iat-backwards") == {} + + +# --- Transcript / observation canonicalization ----------------------------- + +def test_oversized_transcript_fails_closed(runner): + coordinator = g.PicTraceCoordinator(dataclasses.replace(make_config(), max_transcript_bytes=1)) + outcome, _ = runner(coordinator, rid="big-transcript") + assert outcome.status == "post-allow-error" + assert outcome.absence["reason"] == "finalization_failed" + assert outcome.authorization is not None + assert coordinator._store.peek("big-transcript") == {} + + +def test_noncanonicalizable_observation_document_fails_closed(coordinator, runner): + def observe(envelope, sb, info): + return g.Observation(verdict="established", document={"bad": object()}, response_projection={"ok": 1}) + + outcome, _ = runner(coordinator, rid="bad-doc", observe_behavior=observe) + assert outcome.status == "post-allow-error" + assert outcome.absence["reason"] == "finalization_failed" + assert outcome.authorization is not None + assert coordinator._store.peek("bad-doc") == {} + + +def test_noncanonicalizable_response_projection_fails_closed(coordinator, runner): + def observe(envelope, sb, info): + return g.Observation(verdict="established", document={"ok": 1}, response_projection=object()) + + outcome, _ = runner(coordinator, rid="bad-proj", observe_behavior=observe) + assert outcome.status == "post-allow-error" + assert outcome.absence["reason"] == "finalization_failed" + assert outcome.authorization is not None + assert coordinator._store.peek("bad-proj") == {} diff --git a/integrations/pic-standard-mcp-guard/tests/test_import_independence.py b/integrations/pic-standard-mcp-guard/tests/test_import_independence.py new file mode 100644 index 00000000..ca3f9854 --- /dev/null +++ b/integrations/pic-standard-mcp-guard/tests/test_import_independence.py @@ -0,0 +1,22 @@ +"""pic_trace_verify must verify with only agentrust_trace + rfc8785 + stdlib.""" + +from __future__ import annotations + +import pathlib +import subprocess +import sys + +MODULE_DIR = str(pathlib.Path(__file__).resolve().parent.parent) + + +def test_pic_trace_verify_does_not_load_producer_dependencies(): + code = ( + "import sys;" + f"sys.path.insert(0, {MODULE_DIR!r});" + "import pic_trace_verify;" + "banned=[m for m in ('pic_standard','agentrust_trace_adapters') if m in sys.modules];" + "print(','.join(banned));" + "sys.exit(1 if banned else 0)" + ) + result = subprocess.run([sys.executable, "-I", "-c", code], capture_output=True, text=True) + assert result.returncode == 0, f"producer deps leaked: {result.stdout}\n{result.stderr}" diff --git a/integrations/pic-standard-mcp-guard/tests/test_replay.py b/integrations/pic-standard-mcp-guard/tests/test_replay.py new file mode 100644 index 00000000..f97046a0 --- /dev/null +++ b/integrations/pic-standard-mcp-guard/tests/test_replay.py @@ -0,0 +1,127 @@ +"""Tests for replay_pic_verification: sanitized args, reserved-key rejection, exact +policy bytes, full control forwarding, request-id validation, allow/block, and +current-re-evaluation semantics.""" + +from __future__ import annotations + +import pytest +from pic_standard.errors import PICError + +import pic_trace_guard as g +from conftest import MARKER, POLICY_BYTES, TOOL, build_proposal + + +def _pic_code(exc): + code = getattr(exc, "code", "") + return str(getattr(code, "value", code)) + "|" + str(exc) + + +def test_replay_valid_current_reevaluation(fixtures_dir): + result = g.replay_pic_verification( + proposal=build_proposal(), policy_bundle=POLICY_BYTES, tool_args={"path": MARKER}, + verify_evidence=True, proposal_base_dir=fixtures_dir, strict_trust=True, + ) + assert isinstance(result, tuple) and len(result) == 2 + action_proposal, _ = result + assert action_proposal is not None # allowed re-evaluation + + +def test_replay_tampered_evidence_raises_evidence_failed(fixtures_dir): + with pytest.raises(PICError) as ei: + g.replay_pic_verification( + proposal=build_proposal(tamper=True), policy_bundle=POLICY_BYTES, tool_args={"path": MARKER}, + verify_evidence=True, proposal_base_dir=fixtures_dir, strict_trust=True, + ) + assert "PIC_EVIDENCE_FAILED" in _pic_code(ei.value) + + +def test_replay_argument_binding_mismatch(fixtures_dir): + with pytest.raises(PICError) as ei: + g.replay_pic_verification( + proposal=build_proposal(), policy_bundle=POLICY_BYTES, tool_args={"path": "other.txt"}, + verify_evidence=True, proposal_base_dir=fixtures_dir, strict_trust=True, + ) + assert "PIC_ARG_BINDING_MISMATCH" in _pic_code(ei.value) + + +@pytest.mark.parametrize("bad_key", ["__pic", "__pic_request_id"]) +def test_replay_rejects_reserved_keys_in_tool_args(bad_key): + with pytest.raises(g.AdapterError): + g.replay_pic_verification( + proposal={"action": {"tool": TOOL}}, policy_bundle=POLICY_BYTES, tool_args={bad_key: "x"}, + ) + + +@pytest.mark.parametrize("bad_rid", ["", " x", "x ", "a" * (g._DEFAULT_MAX_REQUEST_ID_LEN + 1), 123]) +def test_replay_rejects_invalid_request_id(bad_rid): + with pytest.raises(g.AdapterError): + g.replay_pic_verification( + proposal={"action": {"tool": TOOL}}, policy_bundle=POLICY_BYTES, + tool_args={"path": MARKER}, request_id=bad_rid, + ) + + +@pytest.mark.parametrize("policy_bytes", [ + b"\xff\xfe", b"{not json", b'{"x": NaN}', b'{"x": Infinity}', + b'{"a": 1, "a": 2}', b"[1, 2, 3]", b'{"require_pic_for_impacts": 123}', +]) +def test_replay_parses_supplied_policy_strictly(policy_bytes): + # Proves replay parses the EXACT supplied bytes through the shared strict loader. + with pytest.raises(g.AdapterError): + g.replay_pic_verification( + proposal={"action": {"tool": TOOL}}, policy_bundle=policy_bytes, tool_args={"path": MARKER}, + ) + + +def test_replay_forwards_all_controls_and_inserts_request_id(monkeypatch): + captured = {} + + def spy(**kwargs): + captured.update(kwargs) + return ("action_proposal", "tool_args") + + monkeypatch.setattr(g, "evaluate_pic_for_tool_call", spy) + s_limits, s_pbd, s_erd, s_kr = object(), object(), object(), object() + g.replay_pic_verification( + proposal={"action": {"tool": TOOL}}, policy_bundle=POLICY_BYTES, tool_args={"path": MARKER}, + limits=s_limits, verify_evidence=False, proposal_base_dir=s_pbd, evidence_root_dir=s_erd, + key_resolver=s_kr, strict_trust=False, request_id="rid-1", + ) + assert captured["tool_name"] == TOOL + assert captured["limits"] is s_limits + assert captured["verify_evidence"] is False + assert captured["proposal_base_dir"] is s_pbd + assert captured["evidence_root_dir"] is s_erd + assert captured["key_resolver"] is s_kr + assert captured["strict_trust"] is False + assert captured["policy"] is not None + assert captured["tool_args"]["path"] == MARKER + assert captured["tool_args"]["__pic"] == {"action": {"tool": TOOL}} + assert captured["tool_args"]["__pic_request_id"] == "rid-1" + + +def test_replay_omits_request_id_when_not_supplied(monkeypatch): + captured = {} + + def spy(**kwargs): + captured.update(kwargs) + return ("action_proposal", "tool_args") + + monkeypatch.setattr(g, "evaluate_pic_for_tool_call", spy) + g.replay_pic_verification( + proposal={"action": {"tool": TOOL}}, policy_bundle=POLICY_BYTES, tool_args={"path": MARKER}, + ) + assert "__pic_request_id" not in captured["tool_args"] + + +def test_replay_delegates_to_live_pipeline(monkeypatch): + # Current re-evaluation (not a cached/historical artifact): it calls the live pipeline. + calls = {"n": 0} + + def spy(**kwargs): + calls["n"] += 1 + return ("action_proposal", "tool_args") + + monkeypatch.setattr(g, "evaluate_pic_for_tool_call", spy) + g.replay_pic_verification(proposal={"action": {"tool": TOOL}}, policy_bundle=POLICY_BYTES, tool_args={"path": MARKER}) + assert calls["n"] == 1 diff --git a/integrations/pic-standard-mcp-guard/tests/test_stage1_contract.py b/integrations/pic-standard-mcp-guard/tests/test_stage1_contract.py new file mode 100644 index 00000000..8cfd194c --- /dev/null +++ b/integrations/pic-standard-mcp-guard/tests/test_stage1_contract.py @@ -0,0 +1,341 @@ +"""Maintained 22-check successor to the C1 stage-1 verifier. + +Nineteen check names are carried over verbatim from the C1 verifier; three are honestly +renamed because their original infrastructure does not exist in the maintained +integration: 1e (the signed artifact records no package versions -> installed versions +satisfy the declared pyproject requirements), 2a (no trace-spec git checkout -> the +released bridge's exact profile and required fields), and 5c (there is no resolver +resolution -> the supplied in-memory bridge matches the signed reference). Most checks are +re-derived from the coordinator's in-memory artifacts for one allow run and one blocked +run; 1a captures PIC's own audit log around both runs, and 4 runs the real +`trace-tests verify --level 0` CLI once against a temporary record. +""" + +from __future__ import annotations + +import base64 +import copy +import hashlib +import json +import logging +import os +import pathlib +import subprocess +import sys +import tomllib +from importlib.metadata import version +from types import SimpleNamespace + +import pytest +import rfc8785 +from agentrust_trace.intent_bridge import IntentBridgeError, digest_jcs, sign_bridge, verify_bridge +from agentrust_trace.validate import validate_json +from agentrust_trace_adapters import software_measurement +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name + +from pic_standard.canonical import intent_digest_hex, sha256_hex +from pic_trace_verify import AdapterVerifyError, sha256_prefixed, verify_adapter_artifacts + +from conftest import ( + ARTIFACT_STORE, + BRIDGE_KEY, + BRIDGE_KEY_ID, + EVIDENCE_KEY, + HARNESS, + IMPACT, + INTENT, + MARKER, + POLICY_BYTES, + RECORD_KEY, + TOOL, + raw_public, +) + +_B58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" +_REQUIRED_AUTHZ_FIELDS = ( + "authorization_id", "decision", "authorizer", "authorizer_key_id", "authorized_at", + "expires_at", "scope", "pic", "declaration_digest", "tool_call_digest", "transcript_required", +) + + +def _did_public_bytes(did: str) -> bytes: + prefix = "did:key:z" + assert did.startswith(prefix) + n = 0 + for ch in did[len(prefix):]: + n = n * 58 + _B58.index(ch) + body = n.to_bytes((n.bit_length() + 7) // 8, "big") + assert body[:2] == b"\xed\x01" + return body[2:] + + +def _b64url(s: str) -> bytes: + return base64.urlsafe_b64decode(s + "=" * (-len(s) % 4)) + + +def _one_json_object(raw: bytes) -> dict: + text = raw.decode("utf-8") + assert "\n" not in text and "\r" not in text, "raw decision contains a line break" + + def refuse_dupes(pairs): + keys = [k for k, _ in pairs] + assert len(keys) == len(set(keys)), "duplicate member names" + return dict(pairs) + + value, end = json.JSONDecoder(object_pairs_hook=refuse_dupes).raw_decode(text) + assert end == len(text), "bytes follow the first JSON value" + assert isinstance(value, dict) + return value + + +@pytest.fixture +def stage1(coordinator, runner): + allow_outcome, allow_info = runner(coordinator, rid="stage1-allow") + block_outcome, block_info = runner(coordinator, rid="stage1-block", tamper=True) + assert allow_outcome.status == "record-emitted", allow_outcome.absence + ra = allow_outcome.record_artifacts + return SimpleNamespace( + allow_outcome=allow_outcome, allow_info=allow_info, + block_outcome=block_outcome, block_info=block_info, + record=ra.record, bridge=ra.authorization.bridge, transcript=ra.transcript, + authz=ra.authorization, raw=ra.authorization.pic_decision_raw, tool_call=ra.authorization.tool_call, + ) + + +# --- 1. PIC behaviour and raw decisions ------------------------------------ + +def test_1a_pic_raw_decisions_one_event_each(coordinator, runner, caplog): + with caplog.at_level(logging.INFO, logger="pic_standard.mcp"): + allow_outcome, _ = runner(coordinator, rid="1a-allow") + block_outcome, _ = runner(coordinator, rid="1a-block", tamper=True) + + events = [] + for rec in caplog.records: + if rec.name != "pic_standard.mcp": + continue + try: + obj = json.loads(rec.getMessage()) + except json.JSONDecodeError: + continue + if isinstance(obj, dict) and obj.get("event") == "pic_mcp_decision": + events.append((rec.getMessage(), obj)) + + assert len(events) == 2, [o for _, o in events] + assert sorted(obj["decision"] for _, obj in events) == ["allow", "block"] + for msg, obj in events: + parsed = _one_json_object(msg.encode("utf-8")) # duplicate-free, single object, no newline + assert parsed["event"] == "pic_mcp_decision" + + allow_msg = next(msg for msg, obj in events if obj["decision"] == "allow") + assert allow_msg.encode("utf-8") == allow_outcome.record_artifacts.authorization.pic_decision_raw + block_obj = next(obj for _, obj in events if obj["decision"] == "block") + assert block_obj.get("reason_code") == "PIC_EVIDENCE_FAILED" + assert block_outcome.absence["error_code"] == "PIC_EVIDENCE_FAILED" + + +def test_1b_pic_allow_behaviour(stage1): + assert stage1.allow_outcome.envelope == {"isError": False, "result": {"deleted": True}} + assert stage1.allow_info["count"] == 1 + assert not (stage1.allow_info["sandbox"] / MARKER).exists() + + +def test_1c_pic_block_behaviour(stage1): + out = stage1.block_outcome + assert out.status == "pic-blocked" and out.pic_decision == "blocked" + assert isinstance(out.envelope, dict) and out.envelope.get("isError") is True + assert out.absence["error_code"] == "PIC_EVIDENCE_FAILED" + assert out.record_artifacts is None and out.authorization is None + assert stage1.block_info["count"] == 0 + + +def test_1d_raw_decision_digest_bindings(stage1): + expected = "urn:sha256:" + hashlib.sha256(stage1.raw).hexdigest() + assert stage1.bridge["authorization"]["authorization_id"] == expected + assert stage1.record["origin"]["source_event_id"] == expected + assert stage1.record["references"][0]["id"] == expected + + +def test_1e_installed_versions_meet_declared_floors(): + pyproject = pathlib.Path(__file__).resolve().parent.parent / "pyproject.toml" + data = tomllib.loads(pyproject.read_text(encoding="utf-8")) + specs = list(data["project"].get("dependencies", [])) + for extra in data["project"].get("optional-dependencies", {}).values(): + specs.extend(extra) + requirements = {} + for spec in specs: + req = Requirement(spec) + if req.marker is None or req.marker.evaluate(): + requirements[canonicalize_name(req.name)] = req + expected = { + "pic-standard", + "agentrust-trace", + "agentrust-trace-adapters", + "agentrust-trace-tests", + } + assert expected <= requirements.keys() + for package in expected: + req = requirements[package] + assert req.specifier, f"{package} has no declared version floor" + installed = version(req.name) + assert req.specifier.contains(installed, prereleases=True), \ + f"{req.name} {installed} does not satisfy {req.specifier}" + + +# --- 2. Bridge ------------------------------------------------------------- + +def test_2a_bridge_profile_and_required_fields(stage1): + bridge = stage1.bridge + assert bridge["profile"] == "tag:agentrust-io.com,2026:pic-trace-bridge-v1" + assert "signature" in bridge + authz = bridge["authorization"] + for field in _REQUIRED_AUTHZ_FIELDS: + assert field in authz, f"missing authorization field {field}" + for field in ("profile", "intent_digest", "args_digest"): + assert field in authz["pic"] + assert authz["transcript_required"] is False + assert "successor_observation_digest" not in authz + + +def test_2b_verify_bridge(stage1): + a = stage1.authz + verify_bridge( + stage1.bridge, a.authorizer_jwk, declaration=a.declaration, + pic_intent_digest="sha256:" + a.pic_intent_digest, pic_args_digest="sha256:" + a.pic_args_digest, + tool_call=a.tool_call, now=stage1.bridge["authorization"]["authorized_at"], + ) + + +def test_2c_bridge_fields(stage1): + a = stage1.bridge["authorization"] + assert a["decision"] == "allow" + assert a["authorizer"] == HARNESS + assert a["authorizer_key_id"] == BRIDGE_KEY_ID == stage1.authz.authorizer_jwk["kid"] + assert a["scope"] == {"tools": [TOOL], "impacts": [IMPACT]} + assert a["transcript_required"] is False + assert a["pic"]["profile"] == "PIC-CJSON/1.0" + assert a["declaration_digest"] == digest_jcs(stage1.authz.declaration) + assert a["tool_call_digest"] == digest_jcs(stage1.tool_call) + assert a["pic"]["intent_digest"] == "sha256:" + intent_digest_hex(INTENT) + assert a["pic"]["args_digest"] == "sha256:" + sha256_hex(stage1.tool_call["arguments"]) + + +# --- 3-4. Trust Record: schema, fixed values, real CLI --------------------- + +def test_3a_record_schema_validation(stage1): + validate_json(stage1.record) + + +def test_3b_record_fixed_values(stage1): + r = stage1.record + assert r["origin"]["kind"] == "third-party-control-plane" + assert r["origin"]["producer"] == HARNESS + assert r["runtime"]["platform"] == "software-only" + assert r["appraisal"]["status"] == "none" + assert r["policy"]["enforcement_mode"] == "enforce" + assert r["model"] == {"provider": "pic-standard-test", "model_id": "deterministic-tool-calling-model-v1"} + assert r["data_class"] == "public" + assert r["tool_transcript"]["call_count"] == 1 + assert r["build_provenance"] == {"slsa_level": 0, "digest": "sha256:" + hashlib.sha256(b"pic-trace-test").hexdigest()} + + +def test_4_trace_tests_level0(stage1, tmp_path): + record_path = tmp_path / "stage1-record.json" + record_path.write_text(json.dumps(stage1.record, ensure_ascii=False), encoding="utf-8") + exe = pathlib.Path(sys.executable).with_name("trace-tests.exe" if os.name == "nt" else "trace-tests") + proc = subprocess.run( + [str(exe), "verify", "--record", str(record_path), "--level", "0"], + capture_output=True, text=True, + ) + assert proc.returncode == 0, proc.stdout + proc.stderr + + +# --- 5. Harness-side recomputation ----------------------------------------- + +def test_5a_transcript_hash_recomputed(stage1): + assert stage1.record["tool_transcript"]["hash"] == sha256_prefixed(rfc8785.dumps(stage1.transcript)) + + +def test_5b_measurement_recomputed(stage1): + bundle_hash = stage1.record["policy"]["bundle_hash"] + assert bundle_hash == sha256_prefixed(POLICY_BYTES) + expected = software_measurement(HARNESS, stage1.record["subject"], bundle_hash) + assert stage1.record["runtime"]["measurement"] == expected + + +def test_5c_reference_matches_supplied_bridge_and_digest(stage1): + refs = stage1.record["references"] + assert len(refs) == 1 + ref = refs[0] + assert ref["rel"] == "authorized-intent" + assert ref["resolver"] == ARTIFACT_STORE + assert ref["id"] == stage1.bridge["authorization"]["authorization_id"] + assert ref["digest"] == digest_jcs(stage1.bridge) + + +def test_5d_subject_did_key_binding(stage1): + assert _did_public_bytes(stage1.record["subject"]) == _b64url(stage1.record["cnf"]["jwk"]["x"]) + + +def test_5e_three_keys_distinct(): + assert len({raw_public(EVIDENCE_KEY), raw_public(BRIDGE_KEY), raw_public(RECORD_KEY)}) == 3 + + +def test_5f_model_output_consistency(stage1): + assert stage1.allow_info["received"] == [{"path": MARKER}] + assert stage1.tool_call == {"name": TOOL, "arguments": {"path": MARKER}} + assert stage1.bridge["authorization"]["tool_call_digest"] == sha256_prefixed(rfc8785.dumps(stage1.tool_call)) + assert stage1.transcript["tool_call"] == stage1.tool_call + + +def test_5g_bridge_before_tool_lifecycle(stage1): + assert stage1.allow_info["bridge_state_at_entry"] == "bridged" + assert stage1.block_info["bridge_state_at_entry"] is None # tool never dispatched on block + + +# --- 6. Negative checks ---------------------------------------------------- + +def test_6a_neg_altered_observation(stage1): + altered = copy.deepcopy(stage1.transcript) + altered["observation"]["observation"]["marker_exists_after"] = True + assert sha256_prefixed(rfc8785.dumps(altered)) != stage1.record["tool_transcript"]["hash"] + a = stage1.authz + with pytest.raises(AdapterVerifyError): + verify_adapter_artifacts( + record=stage1.record, bridge=a.bridge, pic_decision_raw=a.pic_decision_raw, + declaration=a.declaration, pic_intent_digest=a.pic_intent_digest, + pic_args_digest=a.pic_args_digest, tool_call=a.tool_call, + authorizer_jwk=a.authorizer_jwk, transcript=altered, + ) + + +def test_6b_neg_bare_hex_digest_refused(stage1): + # Construct and SIGN a bridge carrying bare hex where TRACE requires sha256:, + # then prove verification rejects it (the real boundary, not an API input guard). + authz = copy.deepcopy(stage1.bridge["authorization"]) + authz["pic"]["intent_digest"] = stage1.authz.pic_intent_digest # bare hex, no sha256: prefix + malformed = sign_bridge(authz, BRIDGE_KEY) + with pytest.raises(IntentBridgeError): + verify_bridge( + malformed, stage1.authz.authorizer_jwk, declaration=stage1.authz.declaration, + pic_intent_digest="sha256:" + stage1.authz.pic_intent_digest, + pic_args_digest="sha256:" + stage1.authz.pic_args_digest, + tool_call=stage1.tool_call, now=authz["authorized_at"], + ) + + +def test_6c_neg_block_has_no_bridge_or_record(stage1): + out = stage1.block_outcome + assert out.record_artifacts is None + assert out.authorization is None + assert out.absence["reason"] == "pic_blocked" + assert stage1.block_info["count"] == 0 + + +def test_6d_neg_policy_byte_change(stage1): + changed = POLICY_BYTES.replace(b"irreversible", b"irreversiblE", 1) + assert changed != POLICY_BYTES + changed_hash = sha256_prefixed(changed) + assert changed_hash != stage1.record["policy"]["bundle_hash"] + changed_measurement = software_measurement(HARNESS, stage1.record["subject"], changed_hash) + assert changed_measurement != stage1.record["runtime"]["measurement"] diff --git a/integrations/pic-standard-mcp-guard/tests/test_verifier.py b/integrations/pic-standard-mcp-guard/tests/test_verifier.py new file mode 100644 index 00000000..210680a2 --- /dev/null +++ b/integrations/pic-standard-mcp-guard/tests/test_verifier.py @@ -0,0 +1,209 @@ +"""Artifact-tamper matrix for pic_trace_verify (record + bridge-only verification).""" + +from __future__ import annotations + +import copy +from types import SimpleNamespace + +import pytest +from agentrust_trace import verify_record +from agentrust_trace.intent_bridge import sign_bridge +from agentrust_trace.sign import key_to_jwk, sign_record +from agentrust_trace.validate import validate_json + +import pic_trace_guard as g +from pic_trace_verify import ( + AdapterVerifyError, + verify_adapter_artifacts, + verify_authorization_artifacts, +) + +from conftest import BRIDGE_KEY, EVIDENCE_KEY, RECORD_KEY + + +@pytest.fixture +def bundle(coordinator, runner): + outcome, _ = runner(coordinator, rid="verifier-1") + assert outcome.status == "record-emitted", outcome.absence + ra = outcome.record_artifacts + a = ra.authorization + return SimpleNamespace( + record=ra.record, transcript=ra.transcript, bridge=a.bridge, + raw=a.pic_decision_raw, declaration=a.declaration, + intent=a.pic_intent_digest, args=a.pic_args_digest, + tool_call=a.tool_call, jwk=a.authorizer_jwk, + ) + + +def _flip(s: str) -> str: + return ("B" if s[0] != "B" else "A") + s[1:] + + +def _mut(obj, fn): + out = copy.deepcopy(obj) + fn(out) + return out + + +def _resign(record): + r = copy.deepcopy(record) + r.pop("signature", None) + return sign_record(r, RECORD_KEY) + + +def _mut_record_resigned(b, fn): + return _resign(_mut(b.record, fn)) + + +def _record_with_different_cnf_key(record): + # Sign with EVIDENCE_KEY and set cnf.jwk to its public key, leaving subject unchanged: + # schema and signature verification pass, so the subject-binding check is what rejects. + out = copy.deepcopy(record) + out["cnf"]["jwk"] = key_to_jwk(EVIDENCE_KEY) + out.pop("signature", None) + return sign_record(out, EVIDENCE_KEY) + + +def _verify(b, **over): + kw = dict( + record=b.record, bridge=b.bridge, pic_decision_raw=b.raw, declaration=b.declaration, + pic_intent_digest=b.intent, pic_args_digest=b.args, tool_call=b.tool_call, + authorizer_jwk=b.jwk, transcript=b.transcript, + ) + kw.update(over) + return verify_adapter_artifacts(**kw) + + +def _verify_authz(b, **over): + kw = dict( + bridge=b.bridge, pic_decision_raw=b.raw, declaration=b.declaration, + pic_intent_digest=b.intent, pic_args_digest=b.args, tool_call=b.tool_call, + authorizer_jwk=b.jwk, + ) + kw.update(over) + return verify_authorization_artifacts(**kw) + + +# --- Baseline -------------------------------------------------------------- + +def test_valid_bundle_verifies(bundle): + checked = _verify(bundle) + assert checked["authorization_id_bound_to_decision"] is True + assert checked["record_signature"] is True + assert checked["reference_matches_supplied_bridge"] is True + assert checked["subject_binds_cnf_jwk"] is True + assert checked["transcript_bound_and_hashed"] is True + + +def test_valid_bundle_verifies_without_transcript(bundle): + # Transcript-preimage verification is deliberately optional. + checked = _verify(bundle, transcript=None) + assert checked["record_signature"] is True + assert "transcript_bound_and_hashed" not in checked + + +# --- Full record-verification tamper matrix -------------------------------- + +_RECORD_TAMPERS = [ + ("bridge-signature", + lambda b: {"bridge": _mut(b.bridge, lambda x: x.__setitem__("signature", _flip(x["signature"])))}), + ("declaration", + lambda b: {"declaration": {"profile": "pic-standard-action-declaration/v1", "tool": "x", "impact": "y"}}), + ("tool-call", + lambda b: {"tool_call": {"name": "other", "arguments": {}}}), + ("pic-intent-digest", lambda b: {"pic_intent_digest": "0" * 64}), + ("pic-args-digest", lambda b: {"pic_args_digest": "0" * 64}), + ("decision-bytes", lambda b: {"pic_decision_raw": b.raw + b"x"}), + ("origin-source-event-id", + lambda b: {"record": _mut_record_resigned(b, lambda r: r["origin"].__setitem__("source_event_id", "urn:sha256:" + "0" * 64))}), + ("reference-rel", + lambda b: {"record": _mut_record_resigned(b, lambda r: r["references"][0].__setitem__("rel", "unrelated"))}), + ("reference-id", + lambda b: {"record": _mut_record_resigned(b, lambda r: r["references"][0].__setitem__("id", "urn:sha256:" + "0" * 64))}), + ("reference-digest", + lambda b: {"record": _mut_record_resigned(b, lambda r: r["references"][0].__setitem__("digest", "sha256:" + "0" * 64))}), + ("record-signature", + lambda b: {"record": _mut(b.record, lambda r: r.__setitem__("signature", _flip(r["signature"])))}), + ("subject-did-key", + lambda b: {"record": _mut_record_resigned(b, lambda r: r.__setitem__("subject", g.did_key(EVIDENCE_KEY)))}), + ("transcript-profile", + lambda b: {"transcript": _mut(b.transcript, lambda t: t.__setitem__("profile", "wrong"))}), + ("transcript-decision-digest", + lambda b: {"transcript": _mut(b.transcript, lambda t: t.__setitem__("pic_decision_digest", "sha256:" + "0" * 64))}), + ("transcript-bridge-digest", + lambda b: {"transcript": _mut(b.transcript, lambda t: t.__setitem__("authorization_bridge_digest", "sha256:" + "0" * 64))}), + ("transcript-tool-call", + lambda b: {"transcript": _mut(b.transcript, lambda t: t.__setitem__("tool_call", {"name": "x", "arguments": {}}))}), + ("transcript-hash", + lambda b: {"transcript": _mut(b.transcript, lambda t: t["observation"]["observation"].__setitem__("marker_exists_after", True))}), +] + + +@pytest.mark.parametrize("label,factory", _RECORD_TAMPERS, ids=[t[0] for t in _RECORD_TAMPERS]) +def test_adapter_artifact_tampering_detected(bundle, label, factory): + with pytest.raises(AdapterVerifyError): + _verify(bundle, **factory(bundle)) + + +# --- Dedicated semantic cases that must reach their intended check --------- + +def test_changed_cnf_key_reaches_subject_binding(bundle): + # Signed by a different key with matching cnf.jwk: schema and signature pass, so the + # failure is specifically the subject<->confirmation-key binding, not the signature. + record = _record_with_different_cnf_key(bundle.record) + validate_json(record) + verify_record(record, record["cnf"]["jwk"]) + with pytest.raises(AdapterVerifyError, match="subject did:key"): + _verify(bundle, record=record) + + +def test_duplicate_matching_reference_rejected(bundle): + # A second reference with the same bridge id but a different resolver so JSON Schema + # uniqueItems cannot reject it as an identical object; schema+signature pass first. + def add_duplicate(record): + duplicate = copy.deepcopy(record["references"][0]) + duplicate["resolver"] = "urn:pic-standard:test:second-resolver" + record["references"].append(duplicate) + + record = _mut_record_resigned(bundle, add_duplicate) + validate_json(record) + verify_record(record, record["cnf"]["jwk"]) + with pytest.raises(AdapterVerifyError, match="exactly one reference"): + _verify(bundle, record=record) + + +# --- Bridge-only verification (verify_authorization_artifacts) ------------- + +def test_authorization_artifacts_verify(bundle): + checked = _verify_authz(bundle) + assert checked["authorization_id_bound_to_decision"] is True + assert checked["bridge_signature"] is True + assert checked["bridge_intent_digest"] is True + assert checked["bridge_args_digest"] is True + + +_AUTHZ_TAMPERS = [ + ("decision-bytes", lambda b: {"pic_decision_raw": b.raw + b"x"}), + ("declaration", lambda b: {"declaration": {"profile": "x", "tool": "y", "impact": "z"}}), + ("tool-call", lambda b: {"tool_call": {"name": "x", "arguments": {}}}), + ("pic-intent-digest", lambda b: {"pic_intent_digest": "0" * 64}), + ("pic-args-digest", lambda b: {"pic_args_digest": "0" * 64}), + ("prefixed-input-digest", lambda b: {"pic_intent_digest": "sha256:" + b.intent}), # bare hex expected + ("bridge-signature", lambda b: {"bridge": _mut(b.bridge, lambda x: x.__setitem__("signature", _flip(x["signature"])))}), +] + + +@pytest.mark.parametrize("label,factory", _AUTHZ_TAMPERS, ids=[t[0] for t in _AUTHZ_TAMPERS]) +def test_authorization_artifacts_tampering_detected(bundle, label, factory): + with pytest.raises(AdapterVerifyError): + _verify_authz(bundle, **factory(bundle)) + + +def test_signed_bare_hex_bridge_rejected(bundle): + # A genuinely signed bridge whose pic.intent_digest is bare hex (not sha256:) + # must be rejected by verification, not accepted because the signature is valid. + authz = copy.deepcopy(bundle.bridge["authorization"]) + authz["pic"]["intent_digest"] = bundle.intent # bare hex + malformed = sign_bridge(authz, BRIDGE_KEY) + with pytest.raises(AdapterVerifyError): + _verify_authz(bundle, bridge=malformed) diff --git a/marketplace/catalog.json b/marketplace/catalog.json index 07de0e09..08cafb84 100644 --- a/marketplace/catalog.json +++ b/marketplace/catalog.json @@ -1,6 +1,6 @@ { "catalog_version": 1, - "count": 47, + "count": 48, "integrations": [ { "name": "Claude Code", @@ -778,6 +778,31 @@ "evidence" ] }, + { + "name": "PIC Standard MCP Guard", + "package_name": "PIC Standard MCP Guard", + "vendor": "PIC Standard", + "description": "Uses a PIC Standard MCP pre-execution allow decision and a caller-verified post-dispatch observation to emit a signed TRACE Level 0 Trust Record linked through a detached bridge.", + "path": "integrations/pic-standard-mcp-guard", + "url": "https://github.com/agentrust-io/integrations/tree/main/integrations/pic-standard-mcp-guard", + "homepage": "https://github.com/pic-standard/pic-standard", + "repository": "https://github.com/agentrust-io/integrations", + "tier": "community", + "stack": [ + "TRACE" + ], + "category": "Security & policy", + "mark": "PIC", + "featured": null, + "keywords": [ + "pic", + "mcp", + "intent", + "policy", + "provenance", + "trace" + ] + }, { "name": "ramen-ai cMCP Adapter", "package_name": "ramen-ai cMCP Adapter", diff --git a/requirements/pic-standard-mcp-guard-fixed.txt b/requirements/pic-standard-mcp-guard-fixed.txt new file mode 100644 index 00000000..5e26b2d2 --- /dev/null +++ b/requirements/pic-standard-mcp-guard-fixed.txt @@ -0,0 +1,547 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --constraint=C:/Users/Fabio/AppData/Local/Temp/claude/C--projects-pic-standard/e0168d7a-dc7f-49c4-b4cf-72050988ed1a/scratchpad/lockgen/anchors.txt --extra=test --generate-hashes --output-file=C:/projects/integrations/requirements/pic-standard-mcp-guard-fixed.txt C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml +# +agentrust-trace==0.11.0 \ + --hash=sha256:7de621eaf795449e365d04967a82a551a18d75c99d248b4a0923f6d593bdb09e \ + --hash=sha256:8b76de58ec74fad57417093bf7977fe826ce6ade5859b4d5be8ce3938f9fcb35 + # via + # -c C:/Users/Fabio/AppData/Local/Temp/claude/C--projects-pic-standard/e0168d7a-dc7f-49c4-b4cf-72050988ed1a/scratchpad/lockgen/anchors.txt + # agentrust-trace-adapters + # pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) +agentrust-trace-adapters==0.2.0 \ + --hash=sha256:098f06e185b39692960cfb4c7c136dadba25073c05992dbc14457d07b681c4aa \ + --hash=sha256:541ef28e0c1c9bf781b1a90f93d8293bf9ec371b23bb4dbad47809b00a20bbb1 + # via + # -c C:/Users/Fabio/AppData/Local/Temp/claude/C--projects-pic-standard/e0168d7a-dc7f-49c4-b4cf-72050988ed1a/scratchpad/lockgen/anchors.txt + # pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) +agentrust-trace-tests==0.6.2 \ + --hash=sha256:6bb0a86f5514c266223818d0883ea9c7c1a449d87947a0b7bd57dcdf58dbfbea \ + --hash=sha256:bb2021031b0705645a75780ee443e6be16e8758d83ad4486d1cbf0e2f5b59fad + # via + # -c C:/Users/Fabio/AppData/Local/Temp/claude/C--projects-pic-standard/e0168d7a-dc7f-49c4-b4cf-72050988ed1a/scratchpad/lockgen/anchors.txt + # pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 + # via pydantic +attrs==26.1.0 \ + --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ + --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 + # via + # jsonschema + # referencing +cffi==2.1.1 \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:194cffa889098ced9976c3fc6340305e43f6303657d298da55366907c05c22d6 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:1dea0e4d7d4f11f619fe8c1d76caf49e24405b4b5743c0e3be16a500ecd930c9 \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:3d22a20b1fb1632cc72c22f95f7b0d2961c3e1c235f245ba4c606c4771035659 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42e2f76b9455f5a9a844f770bf3e200ed3da0e15f5df3db9c31fe80b04b3d004 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5a59cc1c4442bc3d5c703bf720b51138d0bfc173618807c9ee2490a7541dd3d9 \ + --hash=sha256:5bb4e7ea95dcd6a014a6fef62e62467d67d8e582326443f3d68e71d6320a9fcf \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:75f80557d1389eddbd0de2681f6a390a0c5338c31ddaa821381c203fc3fd50d9 \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7ce713ace7c0e4520535b42b77eaa742c16dab813978064913e5a3cf82973b41 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:9f8d177621de5cb38ee3e731eda45d421db093ec0739f46a5594babda7987a98 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a48d62ab9d6f4f98c983223a547af44be6ca3691074c31cecced6facd3ba2dc1 \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:baed1e86cc735622097354b9d1281406caf42ff42a886d29faa8e8d1630333be \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:ca82be1a1d406ecfe1d25dc16cb33488e5a16bf4438c9fb590484ea29d92478b \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 + # via cryptography +click==8.5.0 \ + --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \ + --hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34 + # via agentrust-trace-tests +colorama==0.4.6 \ + --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ + --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 + # via pytest +cryptography==49.0.0 \ + --hash=sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001 \ + --hash=sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122 \ + --hash=sha256:084ef1af862eb07ec46d25f68689f2102a9fc0e05ce7b80f14f5fe51e4eef0f6 \ + --hash=sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c \ + --hash=sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325 \ + --hash=sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69 \ + --hash=sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d \ + --hash=sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36 \ + --hash=sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc \ + --hash=sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6 \ + --hash=sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b \ + --hash=sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27 \ + --hash=sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61 \ + --hash=sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18 \ + --hash=sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db \ + --hash=sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b \ + --hash=sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb \ + --hash=sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2 \ + --hash=sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459 \ + --hash=sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e \ + --hash=sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21 \ + --hash=sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8 \ + --hash=sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7 \ + --hash=sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa \ + --hash=sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9 \ + --hash=sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db \ + --hash=sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64 \ + --hash=sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505 \ + --hash=sha256:b39efa323140595abd3ecca8529d321ae50f55f3aa3ba9cc81ea56a6011953d5 \ + --hash=sha256:b47db11c2c3525083296069b98ac5221907455e989ae0c2e3008bde851921615 \ + --hash=sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f \ + --hash=sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866 \ + --hash=sha256:be9fcb48a55f023493482827d4f459bd263cc20efde64f204b97c123201850c6 \ + --hash=sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561 \ + --hash=sha256:c83782480a4a9da4d0feb51950131ba32e12e70813848b3343f6e18c28a66838 \ + --hash=sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9 \ + --hash=sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7 \ + --hash=sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68 \ + --hash=sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8 \ + --hash=sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3 \ + --hash=sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e \ + --hash=sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a \ + --hash=sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d \ + --hash=sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4 \ + --hash=sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493 \ + --hash=sha256:fc1e275c2f1d97b1a6450b8b0ea3ebfa6e087a611c2b26cb2404d48588abab7b + # via + # agentrust-trace + # agentrust-trace-adapters + # agentrust-trace-tests + # pic-standard +iniconfig==2.3.1 \ + --hash=sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960 \ + --hash=sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7 + # via pytest +jsonschema==4.26.0 \ + --hash=sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326 \ + --hash=sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce + # via + # agentrust-trace + # agentrust-trace-tests + # pic-standard +jsonschema-specifications==2025.9.1 \ + --hash=sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe \ + --hash=sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d + # via jsonschema +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c + # via + # pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) + # pytest +pic-standard[crypto]==0.9.1 \ + --hash=sha256:befe885d3f1b40c75daf1928a6e669b608cd72d78accbf709180292431893cb1 \ + --hash=sha256:e35a129a1472fd2a101ede63bf1cbb3d991e4117681335230ca213a1ea7b0d4e + # via + # -c C:/Users/Fabio/AppData/Local/Temp/claude/C--projects-pic-standard/e0168d7a-dc7f-49c4-b4cf-72050988ed1a/scratchpad/lockgen/anchors.txt + # pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) +pluggy==1.7.0 \ + --hash=sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec \ + --hash=sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8 + # via pytest +pycparser==3.11 \ + --hash=sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80 \ + --hash=sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc + # via cffi +pydantic==2.14.1 \ + --hash=sha256:9195d967ec791692a04438115466764fb8b9a27b31f14a760437694f40d6b454 \ + --hash=sha256:94f478203dd03404682a1ada216965651dd74b1d2d5ffd62e00e0837caab5c26 + # via + # agentrust-trace + # pic-standard +pydantic-core==2.50.1 \ + --hash=sha256:0036473f5583e6a60e50b8b21651511564277a3f05cc5dab8cf579f552cd5f6c \ + --hash=sha256:0048b6dddc8ef4b64fccaad878bd143b0c3882ea9936279dc11d613f6b7dd1bc \ + --hash=sha256:009634b83993777ddcd69cad0ffcace43dabde692109528e35f0fde91e386a8b \ + --hash=sha256:028e2f212273d4a39b1ec1e0de8166b1165a65fc0f1111452a9d94fc7c625c63 \ + --hash=sha256:062e891facce5ca296a1c37098e5e466780457f86413894b399f0cf22934f769 \ + --hash=sha256:06e01fbbfdb9be777b316a71b6c49efaf4a08b615d0a98d678cda3023f79d019 \ + --hash=sha256:06ead20d39ffd6f2f6f2a8f8a6de67ff8bb1b4f14a8a30e058502514ee2ac685 \ + --hash=sha256:0b3a6f334c6a2345ca15318ff894502a90012536404b37c844a976c76c846e0b \ + --hash=sha256:0c003c3b7f49debb893d2d85ae099ac5959c9839e2f330fadb1fcdf7a6594482 \ + --hash=sha256:131059670f1d2444269b8585cb888963994871932447c08b39ac6a51fcfef658 \ + --hash=sha256:132529c83901437ff642f585216831bf5fd7a91df66829907e155192ead62498 \ + --hash=sha256:17e722e156d0444ecaefbe640bdb60928752bf2013e2b7a11cdb099aaae19bec \ + --hash=sha256:1a9006395dece0e32e704c315eff8a00bede494f6108546cfc5539c89fef4f9a \ + --hash=sha256:1c8632d4ac04e6f91128fca584b3a8a507d81604c24eeaaad00d4be42765c32b \ + --hash=sha256:1c96fd793b73d1b92e65570132505498fe7b21eaef73cdf74e67e5dfba7ac9e4 \ + --hash=sha256:1cf41f1ae3fa155cf167a72689ad044bcc1e3c97e064123677149bdfb5dafc4a \ + --hash=sha256:1deeacb112d14d3f4fcb16b165f7dbaf76c70ba6e82f37ba042bdab51970a0b8 \ + --hash=sha256:1ef800dd7d85bcdadf4c3076e4c94e43939493558a3b69a1ea830c706d4617bb \ + --hash=sha256:1fa4c8bc12c1354c5550c0c35c1852c8c1901e89e06561724e03f8d0342e1f87 \ + --hash=sha256:2005207aafe1231315718bf6ed5d064a7300fb4772754af35ee72fc68159492e \ + --hash=sha256:23923ab9292c40da026330b1ecf4dc2618c8e86e0422e5d1fbf50d94d64ca4f8 \ + --hash=sha256:23edad659e8dbd8ca7e4e877fe6c81573abbdf215bd25a68b53e1272f58b80c7 \ + --hash=sha256:2ab756b72bd5054e4c7ef3ded331b35786cbd3cf931531a508f79a9537517064 \ + --hash=sha256:2cbd1b75b09e976ed0d6b6ca297675632ca35df86130088457cdc60ef36970ae \ + --hash=sha256:2cf91809d0721ab81592ba67bea7694821679c10b1a2e3c3460082b286c1918a \ + --hash=sha256:2df1ff41884de2bc4b307bafd7c40a691094fad2ff8e767e5b45a319257bcf4e \ + --hash=sha256:2eb75304506894a281d346220a4f7481a1b8729577c5ed2a05395991966a8396 \ + --hash=sha256:2eedf82ee4753cdab8e50044c6bd569577eebc3859b11fecf4eb9223761ff966 \ + --hash=sha256:30ddf019d082c117b5d309e5b86710c2a78909907ec1a9381feec3eec02eca0b \ + --hash=sha256:325c23f3e35cfbf0fe3486fa5f7260d1e45885173002d30a28ca019994124255 \ + --hash=sha256:32fad3a91e51b6d2039c572db04a5a873260b399f6bd62c3552671fa7a4a2899 \ + --hash=sha256:36c426eac0af8d1529ff8467e612b933346caec1fdc0d774f78f67a1a11e16c1 \ + --hash=sha256:3a5fce22f1e87d181e924e12da7d81cfe031fb3881a5ddf26ad28f141756ca43 \ + --hash=sha256:3aa9de446b793de2beb6fa2d9d0961803126c4e2a99c2f25ab59b9fd6ea125c0 \ + --hash=sha256:3e46a9eb0a0901dd6275e6b06ac3a464885ef350ec4121fe486869de8053e4bb \ + --hash=sha256:3fde4fdc6487a58d944ca87cf5adc95d5f266e872c19599f5f4c0a8a1b1f9f9f \ + --hash=sha256:409e0ea40ec30d9158f33574fd758e689f6045a0f2596701828c27816ca9687d \ + --hash=sha256:40f523349960fa30f3ea51404308ff50f9997a90df639590f47a057c1f32b415 \ + --hash=sha256:41bc8237121bd8dc8d888dfd6279fc166ffc88c1f1bf3a8bf00869680533ca4c \ + --hash=sha256:42b54c2c90ad348b5e3a85e03e715d572c1fde357ef104cdfe3b03b697a404ea \ + --hash=sha256:455a773617b5913bf5c20d0692e5787b119e52c4d40ea644ca31f5758fd31be2 \ + --hash=sha256:45b11cac094aa25725581d9304eee93c9028516b9ea80dd9e175e13a5a2c840e \ + --hash=sha256:45c6266d071c241f2a168d45bf8c54344f0effce35e7e6b73afdec11f3687568 \ + --hash=sha256:46b3301d3b5c886f77de7546e47274a5842c622ea2020b8c6524c6b66913b4a6 \ + --hash=sha256:476f6ed8e43cd1e0b460920e23571700872b284e77331cb30c4faf459cf48a4b \ + --hash=sha256:48569b0ade9edfbe065cad1d700175546592aebbb42f02adcebcc26e75b896fe \ + --hash=sha256:49c2cbb2397fe4d0987e84606e691af6cb87bc0ee1bd3e7b737f7e10b4c142f9 \ + --hash=sha256:4a53d13cdfbedbfa87f08b83c1a0a5efcc767d785a4b41934fa9cb672670493a \ + --hash=sha256:4be846f55c9477f5f3ddde8f2ce941137e16862a56d018ed885d422bb6ae02f2 \ + --hash=sha256:4df197990c15b5a37c5a277d131d9f2c67de6133f2e5dafd80d9bba4b99f46f9 \ + --hash=sha256:4e834f6a8e4ff772dcc34f58ef5504147a3ea5b0f4eeb13b0f8eb2ca75ac57f1 \ + --hash=sha256:57f51b31ff826e2859120cf4737c5a758a48d96f3e97da40ccee1796d58078ff \ + --hash=sha256:5958c72adb417c39b12ac87525ac60b0d73315fcdc59e21f44ee4a5e2512c9ef \ + --hash=sha256:5dfe41f232befddb9c4377f6cfc702b51595e2d78ed082672adf8758d2c4619f \ + --hash=sha256:5f3cae32fc46121f787cb2486de9cf95a8bf72aec5cc78f64c606fa1735a6ef5 \ + --hash=sha256:64f6047f62a6c5ae08d0a6afb035667aa2d97c3d20d69762e034c5ea144d92a5 \ + --hash=sha256:6a733778df2f7087ec1100ed0b41533e4f3001976e99570fa34f57c66e7f8e3e \ + --hash=sha256:6b20a4bffabdad0db2927ac034ae3b8a681b1f7a0182f3e60b479ad2fde21ebb \ + --hash=sha256:6dbcbee53bf17196a7f745aa9bf5a9603953a1e365b1f020be3207c676a3e7c4 \ + --hash=sha256:6ed4f3cef55164b026fefb41341b7754cc6b624c75dfe7142d2ecceb5ad21c87 \ + --hash=sha256:704075d10b74f2f3c6e15407c696d88701df35fc8953f434a431add0d0074db0 \ + --hash=sha256:739dc730e6be3bd5ec2f4ab5cfc7eb047cc45fc1497b3bafec74ff2ed07df597 \ + --hash=sha256:7456d699b13954e9c0164dcb267250a10ae0dfb03e6e26d6796ab0d46e189c84 \ + --hash=sha256:756d669f04e62ec4148ecfe22be6a4484d9b1181a6ef32e205ebfd200540858b \ + --hash=sha256:7689580e72a642ab5ec64d5f55b2e33636fa43b4ebe63c0c2c965ef307c7d1aa \ + --hash=sha256:76e2e83fa6ec8cdc972d438dafc2522b3a47bee4ec0ae668b29cfb1977ab5242 \ + --hash=sha256:7816e98acc08119dc0f340ab167048ecc54126316330c1f0caf7c6756c88e28f \ + --hash=sha256:79490e33c4c0fcb933bbbcfc3a62184d8803b99f535863dfbb925e1bcb6945ad \ + --hash=sha256:7f476456ac2bb0d937f75191494a09c83a30765fea4f70f3b404942fe25f6cdf \ + --hash=sha256:844b869f118e22a41a091bdcedda8a71bc1b0f62c38d1a0c3211cece47e1d8fc \ + --hash=sha256:84bc765b282a9d5b7fe0348b8648904f25a6a04b2139da52b1dd30c8ac3a2c8f \ + --hash=sha256:84f34323a61a365b4e9295de6028474754829aaddd59c7bf1a040e7487ef8f3c \ + --hash=sha256:8812592c85d0edf423f10eadcef42716d71e8219085ad9e85b775057b7306133 \ + --hash=sha256:88e492e8b9d0312e7dc13667c30222abf284dc3b79b5302b3607b41a5784ce61 \ + --hash=sha256:8a6791afa2245e6c6b180122d105941644f5bd410bb18623b408808cc41a3102 \ + --hash=sha256:8b4c3df25bd323bf1d36a648d563cf1fc69d717451569927151bdad7cad07a77 \ + --hash=sha256:8daa7ee75245d43ad7d747e5c9ecc1b1d06552f72b14887e9276f787d57375f4 \ + --hash=sha256:93ba4e9d8210d941c200431a56b2c0400b131865947903937ed3ec5404307d2e \ + --hash=sha256:94845ff54dc5193f228cab81b2662a04bfbb892e95bdc15edf7399000ce57d54 \ + --hash=sha256:94be440c03fede26969a5ce75468e0e6a9927a1b46d9b679ee8adc1b057b0350 \ + --hash=sha256:9572c1369e9c9da2d64a7b7992c786d90ff295abc93964cfe3125e4290768070 \ + --hash=sha256:983a662de2571cb2502fc8ff47b6770b03d025d2eb314c92f77b3f07c74720ed \ + --hash=sha256:992c3514ec891fa7858099183e4d64e6bd5a5d4ff452fae29df22faa77a006bb \ + --hash=sha256:99ba9bc2b8062ea0c326a990f7f00e6530c23579de66dd246e72c4cafef950a5 \ + --hash=sha256:9d1bed94af6a63835461f3cf7502058eb166c58c4778e11d0f433cfb1bd69e19 \ + --hash=sha256:9e4472072de0137ee0d8e72d6620e85939c271d2f90f6bbb4b15c24638b79f92 \ + --hash=sha256:a27c09d86600f1bf2fe3f37e1ae697faf3143931c09322cd799da94deee923b5 \ + --hash=sha256:a29a061fec0b4e2d714f277e70a3a18125ecff803f2fea6eade2f2e53711d112 \ + --hash=sha256:a3cda0e538208e5d722bbf3698b24f19c0a7d05bc8d5f8a7f9b121ea7fa243d9 \ + --hash=sha256:a44101320cfe99432db74237545a63057dc7a88dfe792cbcad0647f2af56cb81 \ + --hash=sha256:a4aaaa791bdae1c972a7e81765f4f3571c926b8e0b9b6e47346499fb80079665 \ + --hash=sha256:a51eee75939cf811ac09b278745a6cee7dc873ccfbc8b9af3cc88fe4b7ce25b5 \ + --hash=sha256:a7c58106de36ac6a56314182958de20db8d3a29dfd5db527192cc754e4f8e7fb \ + --hash=sha256:aa8224f10880d9bf1b5993988ba153d42a8b4f3f4f511f93b1f09c93ff613c72 \ + --hash=sha256:acbf31f37c53a5ac0c34706c80b4f5107ba20b05fdd3816124bf236ef0c57dd2 \ + --hash=sha256:adc06d218a1cadfd2ec4628424d7d79ce4eba69c2965e7e7b55106f0da5208c8 \ + --hash=sha256:ae28183297fb0d2b8dc46a1f01d51f5e45825fc5afe76a835a6cb7fb34821295 \ + --hash=sha256:b0135bcdcaa0f23573f286e4cb5e0fd2962700964ed13df085b85f2b97aeab9e \ + --hash=sha256:b087b1c5be7ac687cf22eabfe4b6b608d40df23610651e93611e1f49118baf84 \ + --hash=sha256:b0d955195bbbe489ad343fcc956eacea9357b79cb22192c66cacdefcbc14b32f \ + --hash=sha256:b281a3b0f0822618fe5e3e0d8a2048b6356b14388505dc9374ccffeb69989713 \ + --hash=sha256:b6d0c2183008c188e19f4906d426b293bdc4f67ab17df8e180fe16cda208fa71 \ + --hash=sha256:bbce99252ba3167b2b6277f1829d5bf4b43b754524bddf7f944707c3db7d2253 \ + --hash=sha256:bc1f08f68dac9f9e83845a8039880aba2ab553eb9b2259c3243a313182c253fe \ + --hash=sha256:bc94f474417604bd383d2cd445d071b07dd55fedceed3ce33407bf1fcc107290 \ + --hash=sha256:bed5163e03b98bc1fa2eb05d74c63d9c5c95d8ed6254985481640fbf5e237dea \ + --hash=sha256:c17799a62c142d61b8a3c51752a7cbc87fe2ad4ccfab10e628a77b405075c662 \ + --hash=sha256:c18db21573bd2c6489f9a544b7499f0df2853958c568e5e783536ee1f690af41 \ + --hash=sha256:c3ede305158e75510be50869b319550ab072008c13d64d4ab1e094fb286b6f44 \ + --hash=sha256:c516cc5367ca3448995d42cb994bf3f4c9002d2a7c22eac9622551269ad1b807 \ + --hash=sha256:c531166c42ea7bdfecc8c50049581f05dd1993b09cc7c52bb36a14e96deaec7d \ + --hash=sha256:c73622ef819328873b53109ee4f77ceb598bffedd02daf916102be3228866b78 \ + --hash=sha256:c8dce1f1e0e5358b682a6ad3fa5e31b31d4560997b8e61417e9217c8d60f8a0c \ + --hash=sha256:cb57f304525a5e3c13333b772bf9a473f36326e9c821b2e8e1b2fd36f80ae2c3 \ + --hash=sha256:ce8c25ca38cc0e3d7753ba180808de2c0c8cb24eae0df64491e40921454e9831 \ + --hash=sha256:ceff0acc940be2715bd6ad17b24c0e5304abf44f6efd0f81ee8499e640f9dc86 \ + --hash=sha256:cf356f70551d40374eaffb1aa63f1eb6d2006681cbd7a9faea173ce0f4dd7cd2 \ + --hash=sha256:d2d82aa62521c55ddfb000ae70f88cdd8de974078f6024e821dfe5addd0c818f \ + --hash=sha256:d32f3acc081cc3923386d88f422cde8892335e95f034e0104bb4cf9310d9915f \ + --hash=sha256:d4193206b6587047437f6f11d7e776df23e1c1e23af2a54d9347275614791e10 \ + --hash=sha256:d5c0e32fdbce7f1e8ef4d11f655694bf5f4175c757a9f1dc2be09b8864e5bcf5 \ + --hash=sha256:d5e062c01286d861fd6a1c4ff6e063547b3e713067f2df033c0ff97ac2ca006b \ + --hash=sha256:d8f9e8a6c4ab04b78d61f78627370d834eb004b2869dcb28cfffa647b4ea1980 \ + --hash=sha256:d939de9c82e2126f7f48a7e658f8a85ed46d57662d53f44c49b8895fe94a3eb7 \ + --hash=sha256:de531ce1e2a3364e8767878b58f4ff728a434b4fde089781fe30b1e08e2396e0 \ + --hash=sha256:e50d7b94baac6c7d09927fa5ca5800a0c7ee5015c7fcff65beb3a1931b5a6e09 \ + --hash=sha256:e5faeaee74a57d32b3ab3aebad2e348f06d3ba946fc5d28c1728455f00a3d13a \ + --hash=sha256:e6f0cc1bb9900dc558960894adeb30b0c083366fc1d69b856209fb2ca5c36fe5 \ + --hash=sha256:e8e1d6ce820aa23317e8209a86bd65a540973c12dc7552b48a4f6c8e9926815e \ + --hash=sha256:ed1e728b39a383c81035b2459cfcb35d99dfb01f7d6ebe3a913bc1cc5b81e459 \ + --hash=sha256:ee6db2fbed51a7991302e8fac498cd67e336246026d0dfa84cf5166ce1412760 \ + --hash=sha256:efbecf43d321f7b9281441f1f213f7c21c66988b0e06c2730ba13ed47a46bb08 \ + --hash=sha256:f2c634642694e6a0dad2ab1d375589fa671fd442edd5caf7d9737b8f6ca22906 \ + --hash=sha256:f3377c8c2b3ce898423c5e5dd94c7982e30aa7717a7e6ab2470b9de364963709 \ + --hash=sha256:f5187624823423e1d1b82b1072ac41dc837389e18d3d0572cc19bbee46cd550a \ + --hash=sha256:f77ac30b19221cd9bd3fcfa3d4614eff93140d0572ab730cded17b64adca05f3 \ + --hash=sha256:fe90228920fd8ff2be62622b6bb8a2b11acd65046d50c6b130614b5879605a20 + # via pydantic +pygments==2.21.0 \ + --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \ + --hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c + # via pytest +pytest==9.1.1 \ + --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ + --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c + # via pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) +referencing==0.37.0 \ + --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ + --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 + # via + # agentrust-trace + # jsonschema + # jsonschema-specifications +rfc3986-validator==0.1.1 \ + --hash=sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9 \ + --hash=sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055 + # via agentrust-trace +rfc8785==0.1.4 \ + --hash=sha256:520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48 \ + --hash=sha256:e545841329fe0eee4f6a3b44e7034343100c12b4ec566dc06ca9735681deb4da + # via + # agentrust-trace + # agentrust-trace-tests + # pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) +rpds-py==2026.9.1 \ + --hash=sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25 \ + --hash=sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68 \ + --hash=sha256:028ad274ea951dac64491b5d1e65712a4aeabfdbdb9fccf797b57bd899b0c495 \ + --hash=sha256:0483515261947e4e8b8e1375bf7463e7eb6ccfb3d86e7b554d90cd5285f20f32 \ + --hash=sha256:068c37bba854ec2fe42f7365c640af11dd9895890ccbf2df5070d0c059bd7f96 \ + --hash=sha256:074a4d198bc34d9a8ea425114fc3ded6d11ec01f6a314a8db67454a5152d8834 \ + --hash=sha256:07deecbfce94c78473018bc7d10b337cc651d12df87a1eb2cb3e4024bc9c33d0 \ + --hash=sha256:08dae4a4095150a7c4545a1fb40b98e1ab1744fbc2770d92c977b9dadaa49ab6 \ + --hash=sha256:0da298fb372dc192610a4b9ecbc68a0cd8b675bbbd1fc519d01b41cfd658333e \ + --hash=sha256:0f045bb053c9057720d72c56dffe30dffdc05997b2897a827b9325f0ab6623fa \ + --hash=sha256:10e208f2425d973938afcd56e28a7c4be32e27b6a60b5d381f49fb9d8acf9759 \ + --hash=sha256:136a1c3fe4402b7008bc81cb62ee538481795b61a7e83df88dff3b3f02b726ff \ + --hash=sha256:159a7aab5c5e8b112c8830f54717ce56da1252ebdbb526f5be2df2309280b9e7 \ + --hash=sha256:172e47169583f46ce118cbec68e6795d0da0f4606b488b6434f8276bca0a058c \ + --hash=sha256:1c2d1f6da5128eabf34e963d7163a818846075a52568250d006c4c953b40f903 \ + --hash=sha256:1d55198263bb51f557550c6ed2e6d1cb6a6fed6eb5c9120b741c5926bef8a45d \ + --hash=sha256:1d77b649e6f7cdf12ca5c2a98dad0ad37f9ea9b6f960408a92f0cb12bb3d04d9 \ + --hash=sha256:1e8d4d79d828299bf44a55db22a9388ab967b49d17132c88eab0f4360b48da8e \ + --hash=sha256:22ffd29a63d71fb1b81552c21f2c2b734949b7ac751a9be70675a939a900839b \ + --hash=sha256:2693b2728bbcc48d09a981a356954b0c47c53ff25b545856f28a889ea619f69a \ + --hash=sha256:270bdcdaac5d5b6f73c5e22e7e135c7f2a50e789f71d9e241d5be8d90026e19a \ + --hash=sha256:2711d29b653b3bce48a63d18b9c6b53274669e6d6c4094dddeb4d9a0e45128b2 \ + --hash=sha256:2c16ab111bc27c646ba8aa005d0527754edc538ebb636f0b1bf8e244b48d1945 \ + --hash=sha256:306ee1850d8105b5baf977e78d45fcadd12c1a54678d614c9baf217708446e91 \ + --hash=sha256:3231c4c0e521dafa5be0c9f114ee2c2ad46650836f2d72caa86801950c3e7044 \ + --hash=sha256:3890a6aa36e6baa53d5258a2a25d3ef8b37ad165a6ab27a892d7c3e3a432cd69 \ + --hash=sha256:3a72c11530d71abfb66c8d7696a2f86c43e63fca8b948f1a784ac490f4ec688e \ + --hash=sha256:3b5a6f40f0a1486b4b36c888123afc67acdbd9f33235927acf5ff295429a0ba3 \ + --hash=sha256:3c91c210ae7645626c608400e3519b4a642f837cce09ca830db3beb2e9f274d4 \ + --hash=sha256:3cd182d7291d29b92c521a0069d9c01ba6193628a9a105531d11b40a6d731a33 \ + --hash=sha256:3e524c7874ac72884d28e16dd5b8d839fd09e0fe76b020d3fbca23212a7b8c52 \ + --hash=sha256:3e93b2cd69a9830be33e03945cd7cda940a0a8bfcfbff41d6144f0cb0d3d8bd9 \ + --hash=sha256:3edae8c5ddfdb6985d49ae9d150516e5076888879022f91a26c2de9276ce0bdb \ + --hash=sha256:3f0e9ac28fc067d4d34b88ae43c48e9489455c97fee9633d851f7eeed5a05d35 \ + --hash=sha256:42e75466f83cd43f6026c81eab74246efb2bdadafb307b85700632d06c68f299 \ + --hash=sha256:44b32a7c4f0da3d28af31c259e38ddcff096f855e205ed0671d02fcf44f1ea1c \ + --hash=sha256:457866b85daf5034296666168b84a69e0b2e89dc4f1af102b46f6448a60b9063 \ + --hash=sha256:45bc6bccf78b20fd834237d18db64965d7ee68ba7f60440a26c7ab71e7b8d51a \ + --hash=sha256:46d80bc76b51a6c24f9944368c28d38b8bcbcea1da4f2f8d3ebc31a67e8c6ec6 \ + --hash=sha256:4793ef7f78268b124b73fa933440f01d258bbae01de9fa53e9080c9ab0425a12 \ + --hash=sha256:492e5e428cbe126221611f47e068f01660352feec4ad18bc0f5ea9b2ae88fb14 \ + --hash=sha256:4b26b03d9d2658ee2fa234f8f4f19f38a09773fe5261028025032e26d4d35af0 \ + --hash=sha256:4c0d2cb595a420b34d5086db0add011e26e2c09d6a024afbac4228bf8f863a30 \ + --hash=sha256:4cfaf02209061880210819934de2f4f6aa83dc04dafe6770276acc240a56da31 \ + --hash=sha256:501909f2e4a1e2dee528ef766fe3c469060ebc17e54a8383d404ba07a81a6f02 \ + --hash=sha256:50906f5aea24b5a865cbd0a589698288631d9f3a54c3a937c83aefa95a0d14af \ + --hash=sha256:54ac2158a6f96cfbabff0b2eedaf94b90c5ec7ca8317fcadc61e1c2b2e0ff6ef \ + --hash=sha256:56c6952a9b15047466d0c2347c446a761d4527f89976156341e68f0ce5cc08b0 \ + --hash=sha256:56cd8b3f77d7b6812f533b662186a1f28316931166ddc00fb893b1b0db7e9888 \ + --hash=sha256:57492a550a1d88d29d003247e5f78dd8cf04a701fac0e4c8db8745a6d2504e0a \ + --hash=sha256:5943980471829f6de242a20b109de3111ba6b77e3af0ffc587028ac854b05e6c \ + --hash=sha256:5c6ee90dee3e85e055ddfd502d611643d9b0fd94c818220bda84ec3dacd9b27b \ + --hash=sha256:5c90e7fa02e8f5de0d10c17595c568ada48c5302e749462c0ea1a4c362111a86 \ + --hash=sha256:5ce8943f79c2210f7abcc28e86367b03b28d95027fd01c46d2472373ae70c86f \ + --hash=sha256:617f59cde379b4f648a09797b7f683d04b90a46344cddab85639da5aff0f5531 \ + --hash=sha256:6307a0da524939decb8ca4a3933b8ab62525794411d6984fca6726e732804af6 \ + --hash=sha256:684fd492fff4fead00587544e059be2bbcb6f93454f21fa2a91b66fc7508be82 \ + --hash=sha256:6b5b393eda5ea42cca1c1a6665f2a4882b4fd5d1777e41ce0545a107fb008c9d \ + --hash=sha256:6b723eb406dec5bc9ec516c73ab9c3239a3284e017f7eb89ee2b3258bd504fb7 \ + --hash=sha256:6b9bf3135b4ad5981df9a73d71a35272d650a2985ae9c2746357b24d59de2448 \ + --hash=sha256:6beb738155fe8ab8091afdfa5a3226b21c2b1593f1e50ebb90eb25b44dbc0391 \ + --hash=sha256:6c0dbbcc19735fe5f8b0a54c07659d154a9e69f47e15d0a6ab7299215daf62cb \ + --hash=sha256:6cdc537c8633d7fd92a82e2e0d2ab74320a3f63d5e59fb9cf08711e08fe151c4 \ + --hash=sha256:6eae33003518fd4cb4f83a218d5371469dd3001aa3b87128c005b07762f7fe5e \ + --hash=sha256:740d0a99cf9de0b17a3943388e9294a59becf75e7c43421f387bd3c7a9901f7c \ + --hash=sha256:75c38c50ab9aca840225d9a9a3810bf11d04bd5c1f186cabbb8aee56db3e9b15 \ + --hash=sha256:761fdae6728ceb99ab182fad2f0cc1e262f610834dc891aea1d1a2a2e634776f \ + --hash=sha256:7664419f27db41d4f1c43a78dccda7dd6e8ef2428df3ee01d0c2a07a6b071297 \ + --hash=sha256:76d3af9732d2dab69f28179b40ba2d87e2f1d5824b4a694780aa787d685e8f36 \ + --hash=sha256:78326f4cb4427a56ba4996c0762b63be45f06b85f086526420d2b3a66e40f84d \ + --hash=sha256:7868b85224291c6cb6759f9b5adb9745f486d226f62b16a614dd5a2a5ab2b35b \ + --hash=sha256:815d26356930846a40c7bc1366e7b1b0320ab8a063e66c11298a208bed0fd237 \ + --hash=sha256:8171b44a054e5c67fd748ada04187f1250bf35b95f85e52ab64bcf3331a923bb \ + --hash=sha256:821b2755db9194409254012f429c56643416fb96ef9be090be82ec8826b7f477 \ + --hash=sha256:837c6b305e26fe0f75b15c92cf3b2ba29e0ae19dc40b1c557b026cb426347d0c \ + --hash=sha256:839dde845559254f34885267c6878f60d61d5205180226d976fe488d45fa128e \ + --hash=sha256:84a6ecc0c940169190d2c23bd969debd48c94dbc855acd60188a68d71d421608 \ + --hash=sha256:8601470267d938bcb7f3ab1a336100af51a4fd5b6ed030ef52461bb3ef5e7e07 \ + --hash=sha256:88b5268892fde430d5531f95bc560b6efbbd67c929662c586afd729a96e7461c \ + --hash=sha256:8aa5dda18d39b6143eb24809d158f9252c88f402749b6f1b62a506cc7d96cc35 \ + --hash=sha256:926bdd3e3b5998ddf70cc64bc8cf57209571f9044542913afb673799fec77dd0 \ + --hash=sha256:96beca19ec79de272e8668585380ff9092c47077c1d7a1e098e00bbd921f4785 \ + --hash=sha256:9a0460d43603d1fd9ef59c30278531e15d78581721ddb538fa560aa7817ea4ad \ + --hash=sha256:a03d57b86d2a51d0a66c92177e2be154ad015f357791d306e714569999cdb4cc \ + --hash=sha256:a36b70596407634ca82d4b989a3729074a008537a0522e4c8046a67c729103e9 \ + --hash=sha256:a3a52a3ba86436ab3aef510fbe21512abc2ddd1993005dfe50514bd2284ef025 \ + --hash=sha256:a3dbc5ed9514908d5046107d7b1346bde71eea61de6e0e4919c19354f97e769f \ + --hash=sha256:a431156bb41865fc14cd5d79bb9d7bbed83110b0159e34e62ae30951f96c0009 \ + --hash=sha256:a575404ebc9cf2e91edd32eaf570ec1430eb900d4f56724ba7dd4bc1fc9c176d \ + --hash=sha256:a5cf77eb04f20b720be95265a3e00eb2a14814074255cc27069c551b2db53118 \ + --hash=sha256:a8763f20692da7df39b0afdd1ba3042b004c50a45994f76c2d9a25641f7673db \ + --hash=sha256:ab4b2fda7c2b542f7f9d886cc6a838c5079d2b76f72e6081411faba11adde2c9 \ + --hash=sha256:addeda51556dac7c1a2f14cda62db8b621cd12afba3091d03a96c72932387eab \ + --hash=sha256:b242c27c8f836305a4a72df9cdd564386ac57b807bd252a063223331c9316b37 \ + --hash=sha256:b4f062343e7ad3fa94f2c66e5ae667dee47ee74dd41a9057c4fbe163236a123d \ + --hash=sha256:b5b8b0753718d258fd454283fbd57e14545d3b40583fa672e27cb4f987626bcc \ + --hash=sha256:be3e47e2d91aa3942ff9bf4077a505226005abfc39b6f7554a91c1b9393986b9 \ + --hash=sha256:befc2d6a953e563f8a7bfd87a42c22ebf8a3e980dcb7b6a4d17b70b0e914e8a3 \ + --hash=sha256:bf35d0568abda97233239ce32896d3ad53fccc537832c104e30c94aa5fb93569 \ + --hash=sha256:c933c6678c6f116ff8af47a4c6db0868b8ace74af0343016c0ef00f00272ea69 \ + --hash=sha256:c9d1aca01f49170fdcf5c92761b1fafe97f554b721ca4570c5949fff778f0d4b \ + --hash=sha256:cdeaa99ce822dca76cfb1b993e9120c5ea212f2eb66d48950ad63c349668a018 \ + --hash=sha256:ce4d4f52e2a4324396caddbd45a97d8d7be5f42edd25d2355282a9c34f9b2f7f \ + --hash=sha256:d1028417bb44037eb3069c1009bd7b7277212876cda22fbe565b0bca9fab6d2c \ + --hash=sha256:d151e148117294133bf8af7eeace085e7e87432db15ab6adf640330298a47f6f \ + --hash=sha256:d7841166b7fa64c9c56404617ae4341448847482d45933b13135d26c130519e5 \ + --hash=sha256:d7fca4eb6df565e2a928f1c7dad92d27db8f9df0f449e76423ed5d7e713ed445 \ + --hash=sha256:d95a354e02393eada6d7351184671aced9d4cce109dabf927cb7aa99624352a1 \ + --hash=sha256:d9edf30457d74eebfd76b045535e36f1cd89062566a128a0db2145ca042d787e \ + --hash=sha256:dbc2673f9223d420c91145599b3ba45a8a50c207d1976908e5fb5ddb0c9b9429 \ + --hash=sha256:e01b3c878c8641913e688edd1b3f08658c6783d29cf6b826bd3c0d1ae7a1ffaa \ + --hash=sha256:e21c1429e205828ea886a2293a4a2c8e01f4c25d9893ca330e97a6cf73f52e7b \ + --hash=sha256:e43d4a1f673e8a1cbd8533e809e02b4bf9d4f2280269bb640436556312121250 \ + --hash=sha256:e6d198bad4e49dd6732fbd636e2fc5c082f45c8cad0b4acb756b00c82c76072e \ + --hash=sha256:e6ea1cda8d8c688278430e4268a42f5e5da3bdd74578dfadc0820c3f1766ce83 \ + --hash=sha256:ea394a937f17a54c51239348bdbe2e3518124c8d4a8951ba04a311d3095bd18f \ + --hash=sha256:eac2f5dbafd585dfe31f86a23ebf0d3ba480a9d49ebc87947267b5608d4ea0cd \ + --hash=sha256:eb61be926bb81567c1f48bdc8aa22b9855048dc2efd53871f9f7e6e9a5632346 \ + --hash=sha256:eba5d173f7d5708b22a93815017a4611873ed54db9f268077c0dd1ed99cfc858 \ + --hash=sha256:ec450527cbf485e13c8d3602a54f428ab0432fdade0ede75efd74b735421c871 \ + --hash=sha256:eef6a03b0b6d08d0835ccfa8ec8d1bc70525e3801387567137b50c557695e6da \ + --hash=sha256:ef0d8c843e2827d6c120ab4687e9423fb1d893db1df27b7c1506615bcb9734a0 \ + --hash=sha256:ef6b65b03247c54692ad4fd9ee97cb772781927db72e3cb05e70b3db6d1ff14f \ + --hash=sha256:f3d6ed6a98cfd19155996605474982cc470d7601746a6439078f1a5a3fa8b050 \ + --hash=sha256:fce4b85234a0cbad67bf8e6e1201ee815d172c9aebad75f25645bc4d834f8e31 \ + --hash=sha256:fda1d96e542c37b6c804547dbf489c129fe7c97183a76a5ec275909ba1a063df \ + --hash=sha256:fdcd198979b4ecffcc1beba366a7fbcf4eb41243691a82fe52ceb0b902f09c12 \ + --hash=sha256:fe5ad0664ec772b02c45859041aa17655709cced7a31005817fbbbd988c25567 + # via + # jsonschema + # referencing +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 + # via + # pydantic + # pydantic-core + # referencing + # typing-inspection +typing-inspection==0.4.4 \ + --hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \ + --hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147 + # via pydantic From 740d571d435b463d258ba20527dfd2234deb1d58 Mon Sep 17 00:00:00 2001 From: Fabio Marcello Salvadori Date: Tue, 20 Oct 2026 15:46:54 +0200 Subject: [PATCH 2/2] build(pic-standard-mcp-guard): commit the fixed lock input and regenerate with uv Add requirements/pic-standard-mcp-guard-fixed.in and regenerate requirements/pic-standard-mcp-guard-fixed.txt with the documented uv pip compile --generate-hashes --universal --python-version 3.11 command, matching the chap-fixed pattern, so the hash-locked fixed lane is reproducible from the repository. No adapter code or claims change. Signed-off-by: Fabio Marcello Salvadori --- requirements/pic-standard-mcp-guard-fixed.in | 17 +++++++++ requirements/pic-standard-mcp-guard-fixed.txt | 37 +++++++------------ 2 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 requirements/pic-standard-mcp-guard-fixed.in diff --git a/requirements/pic-standard-mcp-guard-fixed.in b/requirements/pic-standard-mcp-guard-fixed.in new file mode 100644 index 00000000..d8aa7d87 --- /dev/null +++ b/requirements/pic-standard-mcp-guard-fixed.in @@ -0,0 +1,17 @@ +# Compile with: +# uv pip compile requirements/pic-standard-mcp-guard-fixed.in --generate-hashes --universal \ +# --python-version 3.11 -o requirements/pic-standard-mcp-guard-fixed.txt +# The fixed job in .github/workflows/pic-standard-mcp-guard-conformance.yml +# installs this exact package set. The four pinned packages are the released +# versions exercised by the maintained adapter. The two agentrust-io pins must +# equal tested_against in integrations/pic-standard-mcp-guard/integration.yaml. +# rfc8785, pytest, and packaging are the integration's direct runtime and test +# dependencies, which the --no-deps editable install does not pull in. +# The floating job remains unpinned on purpose for drift detection. +pic-standard[crypto]==0.9.1 +agentrust-trace==0.11.0 +agentrust-trace-adapters==0.2.0 +agentrust-trace-tests==0.6.2 +rfc8785 +pytest +packaging>=24 diff --git a/requirements/pic-standard-mcp-guard-fixed.txt b/requirements/pic-standard-mcp-guard-fixed.txt index 5e26b2d2..5e3f69b9 100644 --- a/requirements/pic-standard-mcp-guard-fixed.txt +++ b/requirements/pic-standard-mcp-guard-fixed.txt @@ -1,28 +1,19 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --constraint=C:/Users/Fabio/AppData/Local/Temp/claude/C--projects-pic-standard/e0168d7a-dc7f-49c4-b4cf-72050988ed1a/scratchpad/lockgen/anchors.txt --extra=test --generate-hashes --output-file=C:/projects/integrations/requirements/pic-standard-mcp-guard-fixed.txt C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml -# +# This file was autogenerated by uv via the following command: +# uv pip compile requirements/pic-standard-mcp-guard-fixed.in --generate-hashes --universal --python-version 3.11 -o requirements/pic-standard-mcp-guard-fixed.txt agentrust-trace==0.11.0 \ --hash=sha256:7de621eaf795449e365d04967a82a551a18d75c99d248b4a0923f6d593bdb09e \ --hash=sha256:8b76de58ec74fad57417093bf7977fe826ce6ade5859b4d5be8ce3938f9fcb35 # via - # -c C:/Users/Fabio/AppData/Local/Temp/claude/C--projects-pic-standard/e0168d7a-dc7f-49c4-b4cf-72050988ed1a/scratchpad/lockgen/anchors.txt + # -r requirements/pic-standard-mcp-guard-fixed.in # agentrust-trace-adapters - # pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) agentrust-trace-adapters==0.2.0 \ --hash=sha256:098f06e185b39692960cfb4c7c136dadba25073c05992dbc14457d07b681c4aa \ --hash=sha256:541ef28e0c1c9bf781b1a90f93d8293bf9ec371b23bb4dbad47809b00a20bbb1 - # via - # -c C:/Users/Fabio/AppData/Local/Temp/claude/C--projects-pic-standard/e0168d7a-dc7f-49c4-b4cf-72050988ed1a/scratchpad/lockgen/anchors.txt - # pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) + # via -r requirements/pic-standard-mcp-guard-fixed.in agentrust-trace-tests==0.6.2 \ --hash=sha256:6bb0a86f5514c266223818d0883ea9c7c1a449d87947a0b7bd57dcdf58dbfbea \ --hash=sha256:bb2021031b0705645a75780ee443e6be16e8758d83ad4486d1cbf0e2f5b59fad - # via - # -c C:/Users/Fabio/AppData/Local/Temp/claude/C--projects-pic-standard/e0168d7a-dc7f-49c4-b4cf-72050988ed1a/scratchpad/lockgen/anchors.txt - # pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) + # via -r requirements/pic-standard-mcp-guard-fixed.in annotated-types==0.8.0 \ --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 @@ -33,7 +24,7 @@ attrs==26.1.0 \ # via # jsonschema # referencing -cffi==2.1.1 \ +cffi==2.1.1 ; platform_python_implementation != 'PyPy' \ --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ @@ -139,7 +130,7 @@ click==8.5.0 \ --hash=sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360 \ --hash=sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34 # via agentrust-trace-tests -colorama==0.4.6 \ +colorama==0.4.6 ; sys_platform == 'win32' \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via pytest @@ -214,19 +205,17 @@ packaging==26.3 \ --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c # via - # pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) + # -r requirements/pic-standard-mcp-guard-fixed.in # pytest -pic-standard[crypto]==0.9.1 \ +pic-standard==0.9.1 \ --hash=sha256:befe885d3f1b40c75daf1928a6e669b608cd72d78accbf709180292431893cb1 \ --hash=sha256:e35a129a1472fd2a101ede63bf1cbb3d991e4117681335230ca213a1ea7b0d4e - # via - # -c C:/Users/Fabio/AppData/Local/Temp/claude/C--projects-pic-standard/e0168d7a-dc7f-49c4-b4cf-72050988ed1a/scratchpad/lockgen/anchors.txt - # pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) + # via -r requirements/pic-standard-mcp-guard-fixed.in pluggy==1.7.0 \ --hash=sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec \ --hash=sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8 # via pytest -pycparser==3.11 \ +pycparser==3.11 ; implementation_name != 'PyPy' and platform_python_implementation != 'PyPy' \ --hash=sha256:51d5a8ba2be0bbe440b99d2112604c95bbbc3c2748a64260186c541e1729cd80 \ --hash=sha256:d875f09c3507d00e1aba0eecc6dcadc1352f30fff09dc6bff2f1c2935e97c2bc # via cffi @@ -382,7 +371,7 @@ pygments==2.21.0 \ pytest==9.1.1 \ --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c - # via pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) + # via -r requirements/pic-standard-mcp-guard-fixed.in referencing==0.37.0 \ --hash=sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231 \ --hash=sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8 @@ -398,9 +387,9 @@ rfc8785==0.1.4 \ --hash=sha256:520d690b448ecf0703691c76e1a34a24ddcd4fc5bc41d589cb7c58ec651bcd48 \ --hash=sha256:e545841329fe0eee4f6a3b44e7034343100c12b4ec566dc06ca9735681deb4da # via + # -r requirements/pic-standard-mcp-guard-fixed.in # agentrust-trace # agentrust-trace-tests - # pic-standard-mcp-guard-agentrust-integration (C:/projects/integrations/integrations/pic-standard-mcp-guard/pyproject.toml) rpds-py==2026.9.1 \ --hash=sha256:00ba2d8c7dd4ee537978ddf4b3fbd712bef2d8751603f7f3146b3f4287768e25 \ --hash=sha256:01445c8d194aa032a08e944f16567672da1c62dbdbefd8b6d0693032e290cf68 \