From b40fa064e74c27ccf1ee453245611a1404108565 Mon Sep 17 00:00:00 2001 From: ditdit Date: Mon, 3 Aug 2026 15:18:50 -0700 Subject: [PATCH] Add agent authorization interoperability crosswalk (#69) --- README.md | 2 + docs/agentic-identity-standards-crosswalk.md | 155 +++++++++++++ docs/standards-alignment.md | 7 + .../README.md | 38 ++++ .../vector.json | 128 +++++++++++ scripts/run_mcp_authorization_vector.py | 210 ++++++++++++++++++ ...test_mcp_authorization_interoperability.py | 45 ++++ 7 files changed, 585 insertions(+) create mode 100644 docs/agentic-identity-standards-crosswalk.md create mode 100644 fixtures/mcp-authorization-interoperability-v1/README.md create mode 100644 fixtures/mcp-authorization-interoperability-v1/vector.json create mode 100644 scripts/run_mcp_authorization_vector.py create mode 100644 tests/test_mcp_authorization_interoperability.py diff --git a/README.md b/README.md index e1d8c37..257089e 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,8 @@ contracts, receipts, or standards alignment: - MCP interceptor/PDP shape: [`docs/mcp-interceptor-pdp-shape.md`](docs/mcp-interceptor-pdp-shape.md) - Provider MCP authorization: [`docs/provider-mcp-authorization.md`](docs/provider-mcp-authorization.md) - Receipt profiles: [`docs/receipt-profiles.md`](docs/receipt-profiles.md) +- Agentic identity standards crosswalk and executable MCP vector: + [`docs/agentic-identity-standards-crosswalk.md`](docs/agentic-identity-standards-crosswalk.md) - Community RFCs: [`docs/proposals/`](docs/proposals/) - Intent observability execution plan: [Intent Observability & Assurance](https://github.com/users/dinpd/projects/2) diff --git a/docs/agentic-identity-standards-crosswalk.md b/docs/agentic-identity-standards-crosswalk.md new file mode 100644 index 0000000..f3e746a --- /dev/null +++ b/docs/agentic-identity-standards-crosswalk.md @@ -0,0 +1,155 @@ +# Agentic Identity Standards Crosswalk + +**Status:** Experimental research and interoperability guidance + +**Last reviewed:** 2026-08-03 + +**Scope:** Agent identity, delegation, runtime authorization, action-bound +evidence, provider enforcement, and execution closure + +This document maps the current AgentPass authorization-evidence fields to +established standards, adopted working-group drafts, and exploratory proposals. +It is not a specification and does not claim that the AgentPass field names are +standardized. + +## Maturity Labels + +- **Established:** published RFC, W3C Recommendation, stable standard, or final + OpenID specification. +- **Adopted draft:** an official working-group draft that remains work in + progress. +- **Exploratory:** an individual Internet-Draft, W3C Community Group proposal, + or young open protocol without formal adoption. +- **Gap / profile work:** no single source defines the required semantics; + interoperable profiling or additional evidence is still needed. + +## Layer Map + +| Trust layer | Primary sources | Maturity | AgentPass use | +|---|---|---|---| +| Responsible principal | OpenID Connect; OAuth 2.x | Established | Establish the human, organization, or system represented by the action | +| Fine-grained and delegated authority | OAuth Token Exchange (RFC 8693); Rich Authorization Requests (RFC 9396) | Established | Carry actor/delegation context, audience/resource, and structured authorization details | +| Cross-domain and transaction context | OAuth Identity and Authorization Chaining; Transaction Tokens | Adopted drafts | Preserve downscoped identity and authorization through call chains | +| Runtime identity | SPIFFE; IETF WIMSE architecture, identifiers, credentials, and proof tokens | SPIFFE established; WIMSE adopted drafts | Identify and authenticate the executing workload rather than only the registered application | +| Policy decision | OpenID AuthZEN Authorization API 1.0 | Established final specification | Interoperate between a policy enforcement point and policy decision point | +| Approval and MCP mapping | AuthZEN AARP and COAZ | Adopted drafts | Represent prerequisites such as approval and map MCP calls into subject/action/resource/context | +| MCP connection authorization | MCP Authorization | Protocol specification | OAuth discovery, token acquisition, resource indicators, and audience binding | +| Request integrity | HTTP Message Signatures (RFC 9421) | Established | Bind method, target, selected headers, and content digest | +| Signed authorization evidence | WIMSE Authorization Evidence; Authorization Receipts for High-Risk Agent Actions | Exploratory individual drafts | Adjacent proposals for exact-action authorization evidence, approval, consumption, and durable verification | +| Dynamic risk and revocation | OpenID Shared Signals Framework, CAEP, and RISC | Established final specifications | Feed current posture or revocation into decisions and lifecycle handling | +| Portable declarations | W3C Verifiable Credentials 2.0; W3C agent Community Groups | VC established; agent groups exploratory | Carry relatively durable claims about an agent, operator, capability, or assurance | +| Execution provenance | W3C PROV-O | Established Recommendation | Exchange generic execution lineage; additional cryptographic and replay semantics remain necessary | + +Primary-source links are collected under [References](#references). + +## Authorization Evidence Field Crosswalk + +The MCP vector in +[`../fixtures/mcp-authorization-interoperability-v1/`](../fixtures/mcp-authorization-interoperability-v1/) +uses these fields as a concrete comparison point. + +| AgentPass field | Closest source concept | Mapping assessment | Remaining interoperability question | +|---|---|---|---| +| `profile`, `schema_version` | OAuth/JOSE `typ`; media/profile identifiers; AuthZEN versioning | Profile work | Registry, discovery, and mandatory verifier behavior are not yet agreed | +| `issuer` | JWT `iss`; authorization server; Permit issuer | Direct concept | Providers still need issuer discovery, trust policy, and rotation rules | +| `decision_id` | JWT `jti`; WIMSE audit correlation; Permit/receipt identifier | Compatible extension | Decide whether identity, decision, and evidence identifiers are separate values | +| `outcome` | AuthZEN decision; AgentPass `ALLOW` / `REFER` / `DENY` | Profile work | AuthZEN core decision semantics and prerequisite states need a closed crosswalk | +| `tenant_id` | SSF subject identifiers; deployment/organization context | Extension | Cross-domain tenant naming and privacy rules are deployment-specific | +| `principal_id`, `user_id` | OIDC/OAuth `sub`; WIMSE delegated subject | Direct concept with privacy profile | Pairwise or pseudonymous identifiers may be required across providers | +| `agent_id` | OAuth `client_id`; WIMSE Agent Identifier | Ambiguous without profile | A registered agent application and an executing workload must not share one overloaded identifier | +| `agent_app_id` | A2A Agent Card identity; W3C agent metadata/declaration proposals | Exploratory mapping | No broadly adopted application-identity schema exists | +| `runtime_id` | SPIFFE ID; WIMSE Workload Identifier | Direct concept | Define how credential or attestation evidence is referenced and refreshed | +| `delegation_id` | RFC 8693 `act`; OAuth identity chaining | Compatible extension | An actor chain identifies hops but does not prove authority attenuation | +| `delegation_attenuation` | Delegation-chain and authorization-lineage proposals | Gap / profile work | Verifiers need explicit comparison rules and failure semantics | +| `task_id`, `job_id`, `case_id` | AuthZEN context; Transaction Token transaction context | Extension | Common names and disclosure rules vary by domain | +| `audience` | JWT `aud`; OAuth Resource Indicators; MCP protected-resource URI | Direct concept | Canonical provider/resource URI comparison must be exact | +| `tool`, `action`, `resource` | AuthZEN subject/action/resource/context; RAR `authorization_details`; COAZ MCP mapping | Strong composition point | Tool schemas need stable identifiers and parameter-sensitive resource mapping | +| `action_digest` | HTTP Message Signatures content digest; WIMSE Permit `binding_request_hash`; high-risk receipt `action_hash` | Strong overlap | Canonical MCP/HTTP action bytes and digest coverage require a shared profile | +| `canonicalization` | JCS-style canonical JSON or signed-message component rules | Gap / profile work | Algorithm/version agility and ambiguous numeric/Unicode inputs need test vectors | +| `policy_id`, `policy_hash` | AuthZEN context/decision metadata; high-risk receipt policy commitment | Compatible extension | Decide what is public, immutable, and safe to disclose to a provider | +| `approval_id`, `approval_evidence_ref` | AuthZEN AARP prerequisite/approval; high-risk action signoff | Strong overlap | Evidence retrieval, approver privacy, key custody, and stale approval behavior remain open | +| `jit_grant_id` | OAuth grant/token identifiers; AARP satisfied prerequisite | Extension | A grant reference alone must not be treated as proof without verification | +| `risk_state_ref` | Shared Signals / CAEP event and posture context | Compatible extension | Define freshness and behavior when state changes before execution | +| `idempotency_key` | Application idempotency; authorization-evidence consumption | Profile work | Replay prevention requires provider-side atomic state, not only a signed value | +| `issued_at`, `expires_at` | JWT `iat` / `exp`; OAuth token lifetime | Direct concept | Maximum lifetime and clock-skew rules should be profile-specific | +| signed envelope | JWS/JWKS; COSE/SCITT; HTTP Message Signatures | Multiple established building blocks | An interoperability profile should choose at least one mandatory envelope | + +## Execution Closure Crosswalk + +Authorization evidence proves what was allowed before dispatch. It does not +prove what the provider executed. AgentPass therefore keeps execution closure +as a separate, linked artifact. + +| Closure field | Closest source concept | Mapping assessment | +|---|---|---| +| `closure_id` | WIMSE Permit Closure Record; provenance entity identifier | Compatible extension | +| `decision_id` | Authorization/audit correlation identifier | Strong composition point | +| `authorization_evidence_digest` | SCITT statement/receipt digest; provenance entity link | Profile work | +| `action_digest` | Dispatched-request digest; HTTP content/signature coverage | Strong overlap | +| `provider_id` | Workload/provider identity; PROV agent | Compatible extension | +| `status` | Execution receipt or closure lifecycle state | Profile work; common handling of partial execution is unresolved | +| `executed_at` | Audit/provenance generation time | Direct concept | +| `idempotency_key` | Provider idempotency and consumption state | Deployment mechanism with interoperable evidence value | +| `result_digest` | Closure/output digest; PROV generated entity | Compatible extension | + +## Required Trust-Boundary Tests + +An interoperable verifier should fail closed for at least: + +1. a changed tool argument after authorization; +2. an evidence audience that differs from the executing provider; +3. reuse of already consumed single-use evidence; +4. missing or unverifiable parent delegation evidence; +5. expired approval, policy, runtime, or risk context; +6. unsupported profile, canonicalization, digest, or signature algorithm; and +7. execution closure whose decision or action digest does not match the + authorization evidence. + +The included v1 vector automates the first three negative cases and a valid +linked closure. Later vectors should add delegation attenuation, revocation, +partial execution, retry, and cross-format JWS/COSE cases. + +## AgentPass Contribution Boundary + +AgentPass should contribute interoperability code, mappings, negative fixtures, +and provider-verifier behavior before proposing a new receipt vocabulary. In +particular, the AgentPass evidence model should be compared with both active +individual IETF drafts that already address authorization evidence and +high-risk action receipts. + +The desired implementation seam is: + +```text +OIDC/OAuth principal and delegation + + SPIFFE/WIMSE runtime identity + + AuthZEN decision, AARP approval, and COAZ MCP mapping + -> canonical action-bound evidence + -> independent provider verification and local business policy + -> single-use execution closure and lifecycle signals +``` + +## References + +- [OAuth 2.0 Token Exchange, RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) +- [OAuth 2.0 Rich Authorization Requests, RFC 9396](https://datatracker.ietf.org/doc/html/rfc9396) +- [HTTP Message Signatures, RFC 9421](https://datatracker.ietf.org/doc/html/rfc9421) +- [OAuth Identity and Authorization Chaining](https://datatracker.ietf.org/doc/draft-ietf-oauth-identity-chaining/) +- [OAuth Transaction Tokens](https://datatracker.ietf.org/doc/draft-ietf-oauth-transaction-tokens/) +- [IETF WIMSE documents](https://datatracker.ietf.org/wg/wimse/documents/) +- [AI Agent Authentication and Authorization](https://datatracker.ietf.org/doc/draft-klrc-aiagent-auth/) +- [Signed Authorization-Evidence Records for WIMSE-Authorized AI Agent Actions](https://datatracker.ietf.org/doc/draft-munoz-wimse-authorization-evidence/) +- [Authorization Receipts for High-Risk Agent Actions](https://datatracker.ietf.org/doc/draft-schrock-ep-authorization-receipts/) +- [OpenID AuthZEN Authorization API 1.0](https://openid.net/specs/authorization-api-1_0.html) +- [AuthZEN AARP and COAZ working-group drafts](https://openid.net/openid-foundation-advances-authorization-for-the-agent-era-with-new-authzen-working-group-drafts/) +- [OpenID Shared Signals Framework 1.0](https://openid.net/specs/openid-sharedsignals-framework-1_0-final.html) +- [SPIFFE standard](https://spiffe.io/docs/latest/spiffe-specs/) +- [MCP Authorization](https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization) +- [A2A specification](https://github.com/a2aproject/A2A/blob/main/docs/specification.md) +- [W3C Verifiable Credentials Data Model 2.0](https://www.w3.org/TR/vc-data-model-2.0/) +- [W3C PROV-O](https://www.w3.org/TR/prov-o/) +- [W3C AI Agent Protocol Community Group](https://www.w3.org/groups/cg/agentprotocol/) +- [W3C Agent Identity Registry Protocol Community Group](https://www.w3.org/community/agent-identity/) +- [W3C Agent Declaration and Assurance Community Group](https://www.w3.org/community/adacg/) +- [W3C Agent Trust Protocol Community Group](https://www.w3.org/community/atp/) +- [NIST AI Agent Standards Initiative](https://www.nist.gov/artificial-intelligence/ai-agent-standards-initiative) +- [ISO/IEC 42001:2023](https://www.iso.org/standard/42001) diff --git a/docs/standards-alignment.md b/docs/standards-alignment.md index 07901f9..a575701 100644 --- a/docs/standards-alignment.md +++ b/docs/standards-alignment.md @@ -3,6 +3,13 @@ AgentPass should align with distributed identity standards without becoming a replacement for them. +For the current field-level mapping across AuthZEN, OAuth, WIMSE, SPIFFE, MCP, +authorization-evidence proposals, and execution provenance, see the +[Agentic Identity Standards Crosswalk](agentic-identity-standards-crosswalk.md). +Its companion +[MCP authorization interoperability vector](../fixtures/mcp-authorization-interoperability-v1/) +provides deterministic positive, mutation, audience, and replay cases. + The core distinction is: ```text diff --git a/fixtures/mcp-authorization-interoperability-v1/README.md b/fixtures/mcp-authorization-interoperability-v1/README.md new file mode 100644 index 0000000..1b165f9 --- /dev/null +++ b/fixtures/mcp-authorization-interoperability-v1/README.md @@ -0,0 +1,38 @@ +# MCP Authorization Interoperability Vector v1 + +This directory contains one experimental, non-normative interoperability +vector for an MCP `tools/call` that changes provider state. + +It demonstrates the composition described in +[`../../docs/agentic-identity-standards-crosswalk.md`](../../docs/agentic-identity-standards-crosswalk.md): + +```text +principal + agent application + runtime + delegation + -> policy decision + approval + -> action-bound signed evidence + -> independent provider verification + -> single-use execution closure +``` + +Run the vector from the repository root: + +```bash +python scripts/run_mcp_authorization_vector.py \ + fixtures/mcp-authorization-interoperability-v1/vector.json +``` + +The command exits with status `0` only when all four cases match their expected +outcomes: + +- a valid action produces a linked execution closure; +- changed MCP arguments fail the signed action-digest binding; +- a different provider audience rejects the evidence; and +- reuse of consumed evidence is rejected. + +The runner derives a public deterministic HMAC fixture key from a fixed vector +label. It is test material, not a credential and not a production signing +profile. Production deployments should use asymmetric JWS/JWKS or another +explicitly profiled envelope with managed issuer trust and key rotation. + +All identifiers use reserved `.test` domains or visibly synthetic values. The +fixture contains no operational credentials or personal data. diff --git a/fixtures/mcp-authorization-interoperability-v1/vector.json b/fixtures/mcp-authorization-interoperability-v1/vector.json new file mode 100644 index 0000000..3bf8079 --- /dev/null +++ b/fixtures/mcp-authorization-interoperability-v1/vector.json @@ -0,0 +1,128 @@ +{ + "schema_version": "agentpass.mcp-authorization-interoperability-vector.v1", + "vector_id": "mcp-update-customer-2026-08-03", + "status": "experimental-non-normative", + "protocol_version": "2025-11-25", + "now": "2026-08-03T18:00:30Z", + "mcp_call": { + "jsonrpc": "2.0", + "id": "call-1042", + "method": "tools/call", + "params": { + "name": "provider.crm.update_customer", + "arguments": { + "customer_id": "cus_example_123", + "email_opt_in": false + } + } + }, + "authorization_evidence": { + "schema_version": "agentpass.action-authorization-evidence.v1", + "profile": "https://profiles.agentpass.example.test/mcp-action-authorization/v1", + "issuer": "https://authz.example.test", + "decision_id": "decision-example-1042", + "outcome": "ALLOW", + "tenant_id": "tenant-example", + "principal_id": "user:support-representative-example", + "user_id": "support-representative-example", + "agent_id": "support-agent", + "agent_app_id": "https://agents.example.test/support-agent/1.4.0", + "runtime_id": "spiffe://example.test/ns/agents/sa/support-agent", + "delegation_id": "delegation-example-1042", + "delegation_attenuation": "verified", + "task_id": "support-case-resolution", + "job_id": "support_case_resolution", + "case_id": "case-example-1042", + "customer_id": "cus_example_123", + "audience": "https://provider.example.test/mcp", + "tool": "provider.crm.update_customer", + "action": "write", + "resource": "provider/customer/cus_example_123", + "action_digest": "computed-by-runner", + "canonicalization": "agentid_canonical_json_v1", + "policy_id": "support-customer-update", + "policy_hash": "sha256:2fd75c9ca973981c62d82dd83f223ddf2fbe14cbf3f6f31ce99d70b6741c8f4e", + "approval_id": "approval-example-1042", + "approval_evidence_ref": "urn:example:approval:1042", + "jit_grant_id": "grant-example-1042", + "risk_state_ref": "urn:example:risk:low:1042", + "idempotency_key": "customer-update-example-1042", + "issued_at": "2026-08-03T18:00:00Z", + "expires_at": "2026-08-03T18:02:00Z" + }, + "provider": { + "id": "provider-crm-example", + "audience": "https://provider.example.test/mcp", + "closure_id": "closure-example-1042", + "executed_at": "2026-08-03T18:00:31Z", + "expected_bindings": { + "tenant_id": "tenant-example", + "principal_id": "user:support-representative-example", + "user_id": "support-representative-example", + "agent_id": "support-agent", + "agent_app_id": "https://agents.example.test/support-agent/1.4.0", + "runtime_id": "spiffe://example.test/ns/agents/sa/support-agent", + "delegation_id": "delegation-example-1042", + "delegation_attenuation": "verified", + "task_id": "support-case-resolution", + "job_id": "support_case_resolution", + "case_id": "case-example-1042", + "customer_id": "cus_example_123", + "tool": "provider.crm.update_customer", + "action": "write", + "resource": "provider/customer/cus_example_123", + "policy_id": "support-customer-update", + "policy_hash": "sha256:2fd75c9ca973981c62d82dd83f223ddf2fbe14cbf3f6f31ce99d70b6741c8f4e", + "approval_id": "approval-example-1042", + "approval_evidence_ref": "urn:example:approval:1042", + "jit_grant_id": "grant-example-1042", + "risk_state_ref": "urn:example:risk:low:1042" + }, + "result": { + "customer_id": "cus_example_123", + "updated": true + } + }, + "cases": [ + { + "id": "valid_single_use_execution", + "expect": { + "ok": true, + "codes": [], + "closure_status": "executed" + } + }, + { + "id": "mutated_action_rejected", + "call_overrides": [ + { + "path": "params.arguments.email_opt_in", + "value": true + } + ], + "expect": { + "ok": false, + "codes": ["out_of_scope"], + "closure_status": null + } + }, + { + "id": "wrong_audience_rejected", + "provider_audience": "https://other-provider.example.test/mcp", + "expect": { + "ok": false, + "codes": ["wrong_audience"], + "closure_status": null + } + }, + { + "id": "replay_rejected", + "preconsume": true, + "expect": { + "ok": false, + "codes": ["already_consumed"], + "closure_status": null + } + } + ] +} diff --git a/scripts/run_mcp_authorization_vector.py b/scripts/run_mcp_authorization_vector.py new file mode 100644 index 0000000..678fe2f --- /dev/null +++ b/scripts/run_mcp_authorization_vector.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +"""Run the non-normative AgentPass MCP authorization interoperability vector.""" + +from __future__ import annotations + +import argparse +import copy +import hashlib +import json +import sys +from datetime import datetime +from pathlib import Path +from typing import Any + +ROOT = Path(__file__).resolve().parents[1] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from agentid.provider import ( + provider_receipt_failure_codes, + sign_provider_receipt, + verify_provider_receipt, +) + + +PUBLIC_TEST_KEY = hashlib.sha256(b"agentpass-public-mcp-interop-vector-v1").hexdigest() + + +def canonical_json(value: Any) -> str: + return json.dumps( + value, + allow_nan=False, + ensure_ascii=False, + separators=(",", ":"), + sort_keys=True, + ) + + +def sha256_json(value: Any) -> str: + return "sha256:" + hashlib.sha256(canonical_json(value).encode("utf-8")).hexdigest() + + +def canonical_action( + call: dict[str, Any], *, audience: str, protocol_version: str +) -> dict[str, Any]: + params = call.get("params") if isinstance(call.get("params"), dict) else {} + arguments = params.get("arguments") if isinstance(params.get("arguments"), dict) else {} + return { + "protocol": "mcp", + "protocol_version": protocol_version, + "audience": audience, + "method": call.get("method"), + "tool": params.get("name"), + "arguments": {key: value for key, value in arguments.items() if key != "_agentid_receipt"}, + } + + +def action_digest(call: dict[str, Any], *, audience: str, protocol_version: str) -> str: + return sha256_json(canonical_action(call, audience=audience, protocol_version=protocol_version)) + + +def _set_path(value: dict[str, Any], path: str, replacement: Any) -> None: + parts = path.split(".") + current: dict[str, Any] = value + for part in parts[:-1]: + child = current.get(part) + if not isinstance(child, dict): + raise ValueError(f"override path does not resolve to an object: {path}") + current = child + current[parts[-1]] = replacement + + +def _execution_closure( + vector: dict[str, Any], + signed_receipt: dict[str, Any], + receipt: dict[str, Any], +) -> dict[str, Any]: + result = vector["provider"]["result"] + closure = { + "schema_version": "agentpass.execution-closure.v1", + "closure_id": vector["provider"]["closure_id"], + "decision_id": receipt["decision_id"], + "authorization_evidence_digest": sha256_json(signed_receipt), + "action_digest": receipt["action_digest"], + "provider_id": vector["provider"]["id"], + "status": "executed", + "executed_at": vector["provider"]["executed_at"], + "idempotency_key": receipt["idempotency_key"], + "result_digest": sha256_json(result), + } + closure["link_valid"] = ( + closure["decision_id"] == receipt["decision_id"] + and closure["action_digest"] == receipt["action_digest"] + ) + return closure + + +def _expected_result(case: dict[str, Any]) -> dict[str, Any]: + expected = case["expect"] + return { + "ok": expected["ok"], + "codes": expected["codes"], + "closure_status": expected.get("closure_status"), + } + + +def _observed_result(result: dict[str, Any]) -> dict[str, Any]: + closure = result.get("closure") + return { + "ok": result["ok"], + "codes": result["codes"], + "closure_status": closure.get("status") if isinstance(closure, dict) else None, + } + + +def run_vector(vector: dict[str, Any]) -> dict[str, Any]: + receipt_template = copy.deepcopy(vector["authorization_evidence"]) + receipt_template["action_digest"] = action_digest( + vector["mcp_call"], + audience=receipt_template["audience"], + protocol_version=vector["protocol_version"], + ) + + results: list[dict[str, Any]] = [] + for case in vector["cases"]: + call = copy.deepcopy(vector["mcp_call"]) + for override in case.get("call_overrides", []): + _set_path(call, override["path"], override["value"]) + + receipt = copy.deepcopy(receipt_template) + signed_receipt = sign_provider_receipt(receipt, PUBLIC_TEST_KEY) + expected = vector["provider"]["expected_bindings"] + verification = verify_provider_receipt( + signed_receipt, + secret=PUBLIC_TEST_KEY, + require_signed=True, + expected_tenant=expected["tenant_id"], + expected_agent=expected["agent_id"], + expected_tool=expected["tool"], + expected_action=expected["action"], + expected_resource=expected["resource"], + expected_job=expected["job_id"], + expected_case=expected["case_id"], + expected_customer=expected["customer_id"], + expected_approval=expected["approval_id"], + expected_jit_grant=expected["jit_grant_id"], + now=datetime.fromisoformat(vector["now"].replace("Z", "+00:00")), + ) + findings = list(verification.findings) + + expected_audience = case.get("provider_audience", vector["provider"]["audience"]) + if receipt["audience"] != expected_audience: + findings.append("receipt audience mismatch") + + actual_action_digest = action_digest( + call, + audience=receipt["audience"], + protocol_version=vector["protocol_version"], + ) + if receipt["action_digest"] != actual_action_digest: + findings.append("receipt action_digest mismatch") + + for field, expected_value in expected.items(): + if receipt.get(field) != expected_value: + findings.append(f"receipt {field} mismatch") + + used_receipts: set[str] = set() + if case.get("preconsume"): + used_receipts.add(receipt["decision_id"]) + if not findings and receipt["decision_id"] in used_receipts: + findings.append("receipt was already used") + + closure = None + if not findings: + used_receipts.add(receipt["decision_id"]) + closure = _execution_closure(vector, signed_receipt, receipt) + + result = { + "id": case["id"], + "ok": not findings, + "codes": provider_receipt_failure_codes(findings), + "findings": findings, + "closure": closure, + } + result["matches_expected"] = _observed_result(result) == _expected_result(case) + results.append(result) + + return { + "schema_version": "agentpass.mcp-authorization-interoperability-result.v1", + "vector_id": vector["vector_id"], + "all_cases_match": all(result["matches_expected"] for result in results), + "cases": results, + } + + +def load_vector(path: str | Path) -> dict[str, Any]: + return json.loads(Path(path).read_text(encoding="utf-8")) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("vector", type=Path, help="Path to an interoperability vector JSON file") + args = parser.parse_args() + result = run_vector(load_vector(args.vector)) + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 if result["all_cases_match"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_mcp_authorization_interoperability.py b/tests/test_mcp_authorization_interoperability.py new file mode 100644 index 0000000..51e21ae --- /dev/null +++ b/tests/test_mcp_authorization_interoperability.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +import json +import subprocess +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parent.parent +VECTOR = ROOT / "fixtures" / "mcp-authorization-interoperability-v1" / "vector.json" +RUNNER = ROOT / "scripts" / "run_mcp_authorization_vector.py" + + +def run_vector() -> dict: + completed = subprocess.run( + [sys.executable, str(RUNNER), str(VECTOR)], + cwd=ROOT, + check=True, + capture_output=True, + text=True, + ) + return json.loads(completed.stdout) + + +def test_mcp_authorization_interoperability_vector_is_deterministic(): + first = run_vector() + second = run_vector() + + assert first == second + assert first["all_cases_match"] is True + + +def test_mcp_authorization_interoperability_vector_covers_required_boundaries(): + result = run_vector() + cases = {case["id"]: case for case in result["cases"]} + + valid = cases["valid_single_use_execution"] + assert valid["ok"] is True + assert valid["closure"]["status"] == "executed" + assert valid["closure"]["link_valid"] is True + + assert cases["mutated_action_rejected"]["codes"] == ["out_of_scope"] + assert cases["mutated_action_rejected"]["findings"] == ["receipt action_digest mismatch"] + assert cases["wrong_audience_rejected"]["codes"] == ["wrong_audience"] + assert cases["replay_rejected"]["codes"] == ["already_consumed"]