From 451b5b8458e2c23677ae652b10866b0be1ad29db Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Mon, 20 Jul 2026 11:53:11 -0700 Subject: [PATCH 1/2] Add demo-04: context-aware enforcement Demos 1-3 are the trust chain (enforce, tamper, verify). This adds a second axis: the same tool is allowed in one workflow and denied in another, so authorization tracks the declared call context rather than the tool's identity or the model's stated intent. - write_file under workflow_id="invoice-run" is permitted - the identical write_file under workflow_id="chat-freeform" is denied (default-deny; no permit matches WriteFile outside invoice-run) - read_file is permitted in any workflow (only the write capability is scoped) - both the allow and the deny are committed to the signed audit chain Uses the shared filesystem MCP server and runs software-only under CMCP_DEV_MODE=1. Wired into demo.py (option 4) and the root README. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 27 +++- demo-04-context-enforcement/README.md | 38 +++++ demo-04-context-enforcement/call.py | 139 ++++++++++++++++++ demo-04-context-enforcement/catalog.json | 80 ++++++++++ demo-04-context-enforcement/cmcp-config.yaml | 9 ++ .../policies/manifest.json | 7 + .../policies/schema.cedarschema | 75 ++++++++++ .../policies/workflow-scoped.cedar | 33 +++++ demo-04-context-enforcement/run.py | 80 ++++++++++ demo-04-context-enforcement/run.sh | 44 ++++++ demo.py | 16 +- 11 files changed, 542 insertions(+), 6 deletions(-) create mode 100644 demo-04-context-enforcement/README.md create mode 100644 demo-04-context-enforcement/call.py create mode 100644 demo-04-context-enforcement/catalog.json create mode 100644 demo-04-context-enforcement/cmcp-config.yaml create mode 100644 demo-04-context-enforcement/policies/manifest.json create mode 100644 demo-04-context-enforcement/policies/schema.cedarschema create mode 100644 demo-04-context-enforcement/policies/workflow-scoped.cedar create mode 100644 demo-04-context-enforcement/run.py create mode 100644 demo-04-context-enforcement/run.sh diff --git a/README.md b/README.md index ae7e8d5..02c5ec8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # agentrust-io demos -Runnable demos for [cMCP](https://github.com/agentrust-io/cmcp) and [TRACE](https://github.com/agentrust-io/trace-spec). Three demos, ~4 minutes total. +Runnable demos for [cMCP](https://github.com/agentrust-io/cmcp) and [TRACE](https://github.com/agentrust-io/trace-spec). Four demos, ~5 minutes total. --- @@ -96,6 +96,22 @@ What you see: --- +## Demo 4 -- Context-aware enforcement (~90 seconds) + +Demos 1-3 are the trust chain: enforce, tamper, verify. Demo 4 is a second axis. The same tool is allowed in one workflow and denied in another, so authorization tracks the declared call context, not the tool's identity and not the model's stated intent. + +``` +python demo-04-context-enforcement/run.py +``` + +What you see: +- `write_file` under `workflow_id="invoice-run"`: Cedar **allows** it (the `WriteFile` permit is guarded by `context.workflow_id == "invoice-run"`) +- the identical `write_file`, same arguments, under `workflow_id="chat-freeform"`: **denied by Cedar** (HTTP 403, POLICY_DENY) -- no permit matches, default-deny holds +- `read_file` under `workflow_id="chat-freeform"`: **allowed** (reads are permitted in any workflow, so the second workflow is not blocked wholesale; only the write capability is scoped) +- both the allow and the deny are committed to the signed audit chain under one `policy.bundle_hash` + +--- + ## Structure ``` @@ -123,7 +139,14 @@ demos/ | +-- run.py # Cross-platform launcher (use this) | +-- run.sh # bash-only launcher +-- demo-03-offline-trace/ - +-- verify.py # cmcp_verify.verify_trace_claim (no network) +| +-- verify.py # cmcp_verify.verify_trace_claim (no network) +| +-- run.py # Cross-platform launcher (use this) +| +-- run.sh # bash-only launcher ++-- demo-04-context-enforcement/ + +-- cmcp-config.yaml + +-- catalog.json # Approves write_file, read_file (compliance_domain: finance) + +-- policies/ # Cedar: WriteFile permitted only when context.workflow_id == "invoice-run" + +-- call.py # Same write in two workflows: one allowed, one denied +-- run.py # Cross-platform launcher (use this) +-- run.sh # bash-only launcher ``` diff --git a/demo-04-context-enforcement/README.md b/demo-04-context-enforcement/README.md new file mode 100644 index 0000000..fe5fa10 --- /dev/null +++ b/demo-04-context-enforcement/README.md @@ -0,0 +1,38 @@ +# Demo 4: context-aware enforcement + +**Duration:** ~90 seconds + +Demo 1 gates on the tool's identity: which tools may be called at all. This demo gates on the **context of the call**. The same capability is permitted inside one workflow and denied inside another. The agent cannot widen its own authority by restating its intent, because cMCP evaluates the declared workflow context, it does not trust the model. + +## Run + +```bash +python demo-04-context-enforcement/run.py # cross-platform +# or, bash only: +bash demo-04-context-enforcement/run.sh +``` + +## What to show the audience + +Three calls go through the gateway in one session: + +1. `write_file` under `workflow_id="invoice-run"` — **allowed**. The Cedar `permit` for `WriteFile` is guarded by `context.workflow_id == "invoice-run"`. +2. The **identical** `write_file`, same arguments, under `workflow_id="chat-freeform"` — **denied, HTTP 403**. No `permit` matches `WriteFile` outside `invoice-run`, so Cedar's default-deny holds. Same tool, same payload, different context, different decision. +3. `read_file` under `workflow_id="chat-freeform"` — **allowed**. Reads are permitted in any workflow, so the point is not that the second workflow is blocked wholesale; only the write capability is scoped. + +Then the session closes into a signed TRACE claim. Both the allow and the deny are committed to the hash-chained audit log under the same `policy.bundle_hash`, so you can prove which policy decided each call. + +## Why it matters + +"Trust the LLM to only write when it's supposed to" is not a control. Here the write authority is bound to a declared workflow and enforced before the call leaves the environment. A model that changes its stated intent does not change what it is allowed to do. + +## Policy + +`policies/workflow-scoped.cedar`: + +- `ReadFile` — permitted for any principal, any workflow. +- `WriteFile` — permitted only `when { context.workflow_id == "invoice-run" }`. + +The runtime maps `tool_name` to a PascalCase Cedar action (`write_file` → `Action::"WriteFile"`) and places call context (`workflow_id`, `compliance_domain`, `baa_covered`, `session_max_sensitivity`) on `context`. The agent supplies `workflow_id` per call via the `_cmcp` field; see `call.py`. + +Runs in software-only mode with `CMCP_DEV_MODE=1`; no hardware required. On real TDX the policy bundle hash flows into RTMR[2] at startup exactly as in demo 1. diff --git a/demo-04-context-enforcement/call.py b/demo-04-context-enforcement/call.py new file mode 100644 index 0000000..89f0096 --- /dev/null +++ b/demo-04-context-enforcement/call.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Demo 4: context-aware enforcement. + +The same capability (write_file) is permitted inside the approved "invoice-run" +workflow and denied under any other workflow. The agent does not get to widen +its own authority by changing its stated intent: cMCP evaluates the declared +workflow context, it does not trust the model. + +Prerequisites: + server/server.py running on :9001 (plain HTTP JSON-RPC 2.0) + cmcp start --config cmcp-config.yaml running on :8443 + CMCP_BEARER_TOKEN env var set (run.py / run.sh export this) + +Usage: + python call.py (run from repo root, or via run.py) +""" +import json +import os +import pathlib +import sys +import time +import urllib.error +import urllib.request + +GATEWAY_URL = "http://localhost:8443" +TOKEN = os.environ.get("CMCP_BEARER_TOKEN", "demo-token") +WORKSPACE = pathlib.Path(__file__).parent.parent / "workspace" + +INVOICE_TEXT = "Invoice 4471 -- ACME Corp -- $12,400 -- net 30\n" + + +def _headers(): + return {"Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}"} + + +def _post(url, payload): + data = json.dumps(payload).encode() + req = urllib.request.Request(url, data=data, headers=_headers(), method="POST") + try: + with urllib.request.urlopen(req, timeout=15) as resp: + return json.loads(resp.read()), resp.status + except urllib.error.HTTPError as exc: + return json.loads(exc.read()), exc.code + + +def mcp_call(name, arguments, workflow_id): + return _post(f"{GATEWAY_URL}/mcp", { + "jsonrpc": "2.0", + "id": int(time.time() * 1000), + "method": "tools/call", + "params": { + "name": name, + "arguments": arguments, + "_cmcp": {"workflow_id": workflow_id}, + }, + }) + + +def main(): + WORKSPACE.mkdir(exist_ok=True) + session_id = None + + print("=== Demo 4: context-aware enforcement ===") + print("Approved tool: write_file. Approved workflow for writes: 'invoice-run'.\n") + + # 1. write_file inside the approved workflow -- Cedar: permit (workflow matches) + print("[1/3] write_file workflow='invoice-run' [approved workflow]") + body, status = mcp_call("write_file", {"path": "invoice.txt", "content": INVOICE_TEXT}, "invoice-run") + if status == 200: + cmcp = body["result"].get("_cmcp", {}) + session_id = cmcp.get("session_id") + print(f" ALLOWED {body['result']['content'][0]['text']!r}") + print(f" session_id={session_id}") + else: + print(f" ERROR {status}: {body}") + sys.exit(1) + + # 2. the IDENTICAL write, under a different workflow -- Cedar: default-deny (no permit) + print() + print("[2/3] write_file workflow='chat-freeform' [same tool, same args, other workflow]") + body, status = mcp_call("write_file", {"path": "invoice.txt", "content": INVOICE_TEXT}, "chat-freeform") + if status == 403: + error = body.get("error", {}) + code = error.get("data", {}).get("error_code", "?") + print(f" HTTP 403 -- {error.get('message', '?')} [{code}]") + print(" No permit matches WriteFile outside 'invoice-run'. Default-deny holds.") + print(" The model changed its stated intent; its authority did not change.") + else: + print(f" unexpected {status}: {body}") + sys.exit(1) + + # 3. read_file under the same 'chat-freeform' workflow -- Cedar: permit (reads allowed anywhere) + print() + print("[3/3] read_file workflow='chat-freeform' [reads permitted in any workflow]") + body, status = mcp_call("read_file", {"path": "invoice.txt"}, "chat-freeform") + if status == 200: + session_id = body["result"].get("_cmcp", {}).get("session_id") or session_id + print(f" ALLOWED content={body['result']['content'][0]['text'].strip()!r}") + print(" The workflow is not blanket-blocked. Only the write capability is scoped.") + else: + print(f" ERROR {status}: {body}") + + # close session -> signed TRACE claim + print() + print("Closing session -> TRACE claim...") + if not session_id: + print("ERROR: no session_id. Is cMCP returning _cmcp metadata?") + sys.exit(1) + + body, status = _post(f"{GATEWAY_URL}/sessions/{session_id}/close", {}) + if status != 200: + print(f"ERROR {status}: {body}") + sys.exit(1) + claim = body + + trace = claim.get("trace", {}) + gw = claim.get("gateway", {}) + chain = gw.get("audit_chain", {}) + + print() + print("=== TRACE claim ===") + print(f" runtime.platform: {trace.get('runtime', {}).get('platform')}") + print(f" policy.bundle_hash: {trace.get('policy', {}).get('bundle_hash')}") + print(f" policy.mode: {trace.get('policy', {}).get('enforcement_mode')}") + print(f" audit_chain.length: {chain.get('length')} (session lifecycle + every allow/deny, hash-chained)") + sig = claim.get("signature", "") + print(f" signature: {sig[:40]}...") + print() + print(" The allow AND the deny are both in the signed audit chain.") + print(" You can prove which policy was in force when each call was decided.") + + claim_path = WORKSPACE / "trace-claim-demo04.json" + claim_path.write_text(json.dumps(claim, indent=2)) + print() + print(f" Claim saved to {claim_path}") + + +if __name__ == "__main__": + main() diff --git a/demo-04-context-enforcement/catalog.json b/demo-04-context-enforcement/catalog.json new file mode 100644 index 0000000..b4a371c --- /dev/null +++ b/demo-04-context-enforcement/catalog.json @@ -0,0 +1,80 @@ +[ + { + "tool_name": "write_file", + "server": { + "display_name": "Demo Filesystem MCP Server", + "url": "http://localhost:9001/mcp", + "tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "transport": "http-sse", + "rotation_mode": "key-pinned" + }, + "approved_definition": { + "description": "Write content to a file in the demo workspace", + "input_schema": { + "type": "object", + "required": [ + "path", + "content" + ], + "properties": { + "path": { + "type": "string" + }, + "content": { + "type": "string" + } + } + }, + "output_schema": { + "type": "object", + "properties": { + "result": { + "type": "string" + } + } + } + }, + "compliance_domain": "finance", + "requires_baa": false, + "sensitivity_level": "confidential", + "added_at": "2026-07-20T00:00:00Z", + "approved_by": "demo-setup" + }, + { + "tool_name": "read_file", + "server": { + "display_name": "Demo Filesystem MCP Server", + "url": "http://localhost:9001/mcp", + "tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "transport": "http-sse", + "rotation_mode": "key-pinned" + }, + "approved_definition": { + "description": "Read a file from the demo workspace", + "input_schema": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string" + } + } + }, + "output_schema": { + "type": "object", + "properties": { + "content": { + "type": "string" + } + } + } + }, + "compliance_domain": "finance", + "requires_baa": false, + "sensitivity_level": "confidential", + "added_at": "2026-07-20T00:00:00Z", + "approved_by": "demo-setup" + } +] diff --git a/demo-04-context-enforcement/cmcp-config.yaml b/demo-04-context-enforcement/cmcp-config.yaml new file mode 100644 index 0000000..7590dd0 --- /dev/null +++ b/demo-04-context-enforcement/cmcp-config.yaml @@ -0,0 +1,9 @@ +attestation: + provider: auto # falls back to software-only when CMCP_DEV_MODE=1 + enforcement_mode: enforcing + validity_seconds: 3600 + +policy_bundle_path: ./policies/ +catalog_path: ./catalog.json +listen_addr: "127.0.0.1:8443" +audit_db_path: ./audit.db diff --git a/demo-04-context-enforcement/policies/manifest.json b/demo-04-context-enforcement/policies/manifest.json new file mode 100644 index 0000000..06e4de9 --- /dev/null +++ b/demo-04-context-enforcement/policies/manifest.json @@ -0,0 +1,7 @@ +{ + "version": "1.0.0", + "authored_at": "2026-07-20T00:00:00Z", + "author_identity": "demo-setup", + "commit_sha": "demo-do-not-use-in-production", + "approval_chain": [] +} diff --git a/demo-04-context-enforcement/policies/schema.cedarschema b/demo-04-context-enforcement/policies/schema.cedarschema new file mode 100644 index 0000000..55e24a6 --- /dev/null +++ b/demo-04-context-enforcement/policies/schema.cedarschema @@ -0,0 +1,75 @@ +{ + "cMCP": { + "entityTypes": { + "Principal": { + "memberOfTypes": [], + "shape": { + "type": "Record", + "attributes": { + "session_id": { + "type": "String", + "required": true + }, + "workflow_id": { + "type": "String", + "required": true + } + } + } + }, + "Resource": { + "memberOfTypes": [], + "shape": { + "type": "Record", + "attributes": { + "tool_name": { + "type": "String", + "required": true + }, + "compliance_domain": { + "type": "String", + "required": true + }, + "baa_covered": { + "type": "Bool", + "required": true + } + } + } + } + }, + "actions": { + "call_tool": { + "appliesTo": { + "principalTypes": [ + "cMCP::Principal" + ], + "resourceTypes": [ + "cMCP::Resource" + ], + "context": { + "type": "Record", + "attributes": { + "workflow_id": { + "type": "String", + "required": true + }, + "compliance_domain": { + "type": "String", + "required": true + }, + "baa_covered": { + "type": "Bool", + "required": true + }, + "session_max_sensitivity": { + "type": "String", + "required": true + } + } + } + } + } + } + } +} diff --git a/demo-04-context-enforcement/policies/workflow-scoped.cedar b/demo-04-context-enforcement/policies/workflow-scoped.cedar new file mode 100644 index 0000000..796d6a7 --- /dev/null +++ b/demo-04-context-enforcement/policies/workflow-scoped.cedar @@ -0,0 +1,33 @@ +// cMCP Cedar policy for demo-04: context-aware enforcement +// +// demo-01 gates on the tool's identity (which tool may be called at all). +// This demo gates on the *context of the call*: the same capability is +// permitted inside one workflow and denied inside another. The agent cannot +// widen its own authority by choosing a different intent -- the policy checks +// the declared workflow, it does not trust the model. +// +// AGT's CedarBackend maps tool_name to a PascalCase action: +// read_file -> Action::"ReadFile" +// write_file -> Action::"WriteFile" +// +// The runtime places call context on `context` (workflow_id, compliance_domain, +// baa_covered, session_max_sensitivity). Cedar is default-deny: any call with no +// matching permit is refused. + +// Reads are permitted in any approved workflow. Low-risk, read-only. +permit ( + principal, + action == Action::"ReadFile", + resource +); + +// Writes are permitted ONLY inside the approved "invoice-run" workflow. +// The identical write attempted under any other workflow has no matching +// permit and is denied by default. Same tool, same arguments, different +// context -> different decision. +permit ( + principal, + action == Action::"WriteFile", + resource +) +when { context.workflow_id == "invoice-run" }; diff --git a/demo-04-context-enforcement/run.py b/demo-04-context-enforcement/run.py new file mode 100644 index 0000000..0b483dd --- /dev/null +++ b/demo-04-context-enforcement/run.py @@ -0,0 +1,80 @@ +"""Demo 4: context-aware enforcement — cross-platform launcher (replaces run.sh). + +Usage: + python demo-04-context-enforcement/run.py # from repo root + python run.py # from demo-04 directory +""" +import os +import pathlib +import shutil +import subprocess +import sys +import time + +sys.stdout.reconfigure(line_buffering=True) + +SCRIPT_DIR = pathlib.Path(__file__).parent.resolve() +REPO_ROOT = SCRIPT_DIR.parent + + +def _find_cmcp() -> str: + found = shutil.which("cmcp") + if found: + return found + import sysconfig + candidates = [ + pathlib.Path(sys.executable).parent, + pathlib.Path(sysconfig.get_path("scripts")), + pathlib.Path(sysconfig.get_path("scripts", "nt_user")), + ] + for scripts in candidates: + for name in ("cmcp.exe", "cmcp"): + p = scripts / name + if p.exists(): + return str(p) + sys.exit("cmcp not found. Run: pip install cmcp-runtime") + + +def main() -> None: + os.environ.setdefault("CMCP_BEARER_TOKEN", "demo-token") + + log_dir = SCRIPT_DIR + server_log = open(log_dir / "server.log", "w") + cmcp_log = open(log_dir / "cmcp.log", "w") + + server = cmcp_proc = None + try: + print("-- Starting MCP filesystem server on :9001 --", flush=True) + server = subprocess.Popen( + [sys.executable, str(REPO_ROOT / "server" / "server.py")], + stdout=server_log, stderr=server_log, + ) + time.sleep(1) + + print("-- Starting cMCP Runtime (CMCP_DEV_MODE=1) on :8443 --", flush=True) + env = os.environ.copy() + env["CMCP_DEV_MODE"] = "1" + cmcp_proc = subprocess.Popen( + [_find_cmcp(), "start", "--config", str(SCRIPT_DIR / "cmcp-config.yaml")], + stdout=cmcp_log, stderr=cmcp_log, + cwd=SCRIPT_DIR, env=env, + ) + time.sleep(2) + + print() + subprocess.run([sys.executable, str(SCRIPT_DIR / "call.py")], check=True) + + finally: + for proc in (cmcp_proc, server): + if proc and proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + server_log.close() + cmcp_log.close() + + +if __name__ == "__main__": + main() diff --git a/demo-04-context-enforcement/run.sh b/demo-04-context-enforcement/run.sh new file mode 100644 index 0000000..9472713 --- /dev/null +++ b/demo-04-context-enforcement/run.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Demo 4: context-aware enforcement +# +# Starts the local MCP filesystem server and the cMCP Runtime (CMCP_DEV_MODE=1), +# then makes three calls through the Runtime: +# write_file workflow=invoice-run -> Cedar permits (approved workflow) +# write_file workflow=chat-freeform -> Cedar DENIES (same tool + args, HTTP 403) +# read_file workflow=chat-freeform -> Cedar permits (reads allowed anywhere) +# +# The point: the same capability is scoped by the call's declared workflow, not +# by the tool's identity. The agent cannot widen its authority by restating intent. +# +# Usage: bash demo-04-context-enforcement/run.sh (from repo root) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +CMCP_BEARER_TOKEN="${CMCP_BEARER_TOKEN:-demo-token}" +export CMCP_BEARER_TOKEN + +cleanup() { + kill "${CMCP_PID:-}" "${SERVER_PID:-}" 2>/dev/null || true + wait "${CMCP_PID:-}" "${SERVER_PID:-}" 2>/dev/null || true +} +trap cleanup EXIT + +echo "" +echo "=== Demo 4: context-aware enforcement ===" +echo "" + +echo "-- Starting MCP filesystem server on :9001 --" +python "$REPO_ROOT/server/server.py" & +SERVER_PID=$! +sleep 1 + +echo "-- Starting cMCP Runtime (CMCP_DEV_MODE=1) on :8443 --" +cd "$SCRIPT_DIR" +CMCP_DEV_MODE=1 cmcp start --config cmcp-config.yaml & +CMCP_PID=$! +sleep 2 + +echo "" +python "$SCRIPT_DIR/call.py" diff --git a/demo.py b/demo.py index 4acb438..044b375 100644 --- a/demo.py +++ b/demo.py @@ -1,15 +1,19 @@ #!/usr/bin/env python3 -"""One command, all three agentrust-io demos. +"""One command, the agentrust-io demos. - python demo.py # run all three, pausing before each (for live talks) + python demo.py # run all, pausing before each (for live talks) python demo.py --no-pause # run straight through, no prompts - python demo.py 2 # run only demo 2 (1, 2, or 3) + python demo.py 2 # run only demo 2 (1, 2, 3, or 4) The trust chain, end to end: Demo 1 cMCP enforces Cedar on every tool call and signs a TRACE claim. Demo 2 Swap the policy bundle and the claim's hash changes; a pinned verifier rejects it. Demo 3 Verify that signed claim offline: no server, no gateway, no network. +Then, on a second axis: + Demo 4 The same tool is allowed in one workflow and denied in another. Enforcement + is on the declared call context, not on the model's stated intent. + All demos run in software-only mode (CMCP_DEV_MODE=1). That is deliberate: software proves the whole chain except the hardware root, so verification reads 'partially_verified'. On real TDX / SEV-SNP the hardware field verifies too and it @@ -36,6 +40,10 @@ ("3", "Offline TRACE verification", "demo-03-offline-trace/run.py", "Verify the demo-1 claim with nothing but the claim and a public key. No network."), + ("4", "Context-aware enforcement", + "demo-04-context-enforcement/run.py", + "The same tool is allowed in one workflow and denied in another. Enforcement is\n" + " on the declared call context, not on the model's stated intent."), ] GREEN = "\033[92m"; BLUE = "\033[96m"; DIM = "\033[90m"; BOLD = "\033[1m"; RST = "\033[0m" @@ -135,7 +143,7 @@ def run(idx, title, script, blurb, pause): def main(): ap = argparse.ArgumentParser(description="Run the agentrust-io trust-chain demos.") - ap.add_argument("only", nargs="?", choices=["1", "2", "3"], help="run only this demo") + ap.add_argument("only", nargs="?", choices=["1", "2", "3", "4"], help="run only this demo") ap.add_argument("--no-pause", action="store_true", help="run straight through, no prompts") args = ap.parse_args() From 60919a87afcc4c052dcc8dd40db044b3568f72de Mon Sep 17 00:00:00 2001 From: Imran Siddique Date: Mon, 20 Jul 2026 12:02:13 -0700 Subject: [PATCH 2/2] Add demo-05: attribute-based enforcement (compliance domain / BAA) A third axis of enforcement alongside tool identity (demo-01) and call context (demo-04): the policy decides on the tool's compliance attributes. - write_file, read_file (compliance_domain=clinical, baa_covered=true) allowed - list_dir (compliance_domain=external-analytics, baa_covered=false) denied - the deny comes from a single guardrail: forbid ... when context.baa_covered == false, which overrides the baseline permit. One rule covers every non-BAA-covered tool in the catalog, present and future. Runtime derives compliance_domain and baa_covered from the catalog entry. Software-only under CMCP_DEV_MODE=1. Wired into demo.py (option 5) and README. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 31 +++- demo-05-compliance-domain/README.md | 46 ++++++ demo-05-compliance-domain/call.py | 140 ++++++++++++++++++ demo-05-compliance-domain/catalog.json | 81 ++++++++++ demo-05-compliance-domain/cmcp-config.yaml | 9 ++ .../policies/baa-guardrail.cedar | 32 ++++ .../policies/manifest.json | 7 + .../policies/schema.cedarschema | 75 ++++++++++ demo-05-compliance-domain/run.py | 80 ++++++++++ demo-05-compliance-domain/run.sh | 44 ++++++ demo.py | 14 +- 11 files changed, 550 insertions(+), 9 deletions(-) create mode 100644 demo-05-compliance-domain/README.md create mode 100644 demo-05-compliance-domain/call.py create mode 100644 demo-05-compliance-domain/catalog.json create mode 100644 demo-05-compliance-domain/cmcp-config.yaml create mode 100644 demo-05-compliance-domain/policies/baa-guardrail.cedar create mode 100644 demo-05-compliance-domain/policies/manifest.json create mode 100644 demo-05-compliance-domain/policies/schema.cedarschema create mode 100644 demo-05-compliance-domain/run.py create mode 100644 demo-05-compliance-domain/run.sh diff --git a/README.md b/README.md index 02c5ec8..673f242 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # agentrust-io demos -Runnable demos for [cMCP](https://github.com/agentrust-io/cmcp) and [TRACE](https://github.com/agentrust-io/trace-spec). Four demos, ~5 minutes total. +Runnable demos for [cMCP](https://github.com/agentrust-io/cmcp) and [TRACE](https://github.com/agentrust-io/trace-spec). Five demos, ~6 minutes total. --- @@ -112,6 +112,22 @@ What you see: --- +## Demo 5 -- Attribute-based enforcement (~90 seconds) + +Demo 1 denies by tool name, demo 4 by call context. Demo 5 denies by the tool's compliance attributes. A tool that is not BAA-covered is refused by a single guardrail rule, whatever it is named. + +``` +python demo-05-compliance-domain/run.py +``` + +What you see: +- `write_file` and `read_file` (`compliance_domain=clinical`, `baa_covered=true`): **allowed** +- `list_dir` (`compliance_domain=external-analytics`, `baa_covered=false`): **denied by Cedar** (HTTP 403, POLICY_DENY) +- the deny comes from `forbid ... when { context.baa_covered == false }`, which overrides the baseline permit; the call is refused on its attribute, not its name +- one rule covers every non-BAA-covered tool in the catalog, present and future + +--- + ## Structure ``` @@ -143,10 +159,17 @@ demos/ | +-- run.py # Cross-platform launcher (use this) | +-- run.sh # bash-only launcher +-- demo-04-context-enforcement/ +| +-- cmcp-config.yaml +| +-- catalog.json # Approves write_file, read_file (compliance_domain: finance) +| +-- policies/ # Cedar: WriteFile permitted only when context.workflow_id == "invoice-run" +| +-- call.py # Same write in two workflows: one allowed, one denied +| +-- run.py # Cross-platform launcher (use this) +| +-- run.sh # bash-only launcher ++-- demo-05-compliance-domain/ +-- cmcp-config.yaml - +-- catalog.json # Approves write_file, read_file (compliance_domain: finance) - +-- policies/ # Cedar: WriteFile permitted only when context.workflow_id == "invoice-run" - +-- call.py # Same write in two workflows: one allowed, one denied + +-- catalog.json # Tags tools with compliance_domain + BAA status + +-- policies/ # Cedar: forbid any tool when context.baa_covered == false + +-- call.py # Two BAA-covered tools allowed, one non-covered tool denied +-- run.py # Cross-platform launcher (use this) +-- run.sh # bash-only launcher ``` diff --git a/demo-05-compliance-domain/README.md b/demo-05-compliance-domain/README.md new file mode 100644 index 0000000..565e63a --- /dev/null +++ b/demo-05-compliance-domain/README.md @@ -0,0 +1,46 @@ +# Demo 5: attribute-based enforcement (compliance domain / BAA coverage) + +**Duration:** ~90 seconds + +Three ways to deny a tool call, one per demo: + +- **demo-01** denies by the tool's **identity** (which tool, by name). +- **demo-04** denies by the **call context** (which workflow). +- **demo-05** (this one) denies by the tool's **compliance attributes**: a tool that is not BAA-covered is refused by a single guardrail rule, whatever it is named. + +## Run + +```bash +python demo-05-compliance-domain/run.py # cross-platform +# or, bash only: +bash demo-05-compliance-domain/run.sh +``` + +## What to show the audience + +The catalog tags each tool with a compliance domain and BAA status: + +| tool | compliance_domain | baa_covered | +|------|-------------------|-------------| +| `write_file` | clinical | true | +| `read_file` | clinical | true | +| `list_dir` | external-analytics | false | + +Three calls in one session: + +1. `write_file` — **allowed** (`baa_covered=true`). +2. `read_file` — **allowed** (`baa_covered=true`). +3. `list_dir` — **denied, HTTP 403**. A `forbid` rule matches `context.baa_covered == false` and overrides the baseline permit. The call is refused on its attribute, not on its name. + +## Why it matters + +The guardrail is one rule: `forbid ... when { context.baa_covered == false }`. It covers every non-BAA-covered tool in the catalog, present and future. Add a tool tomorrow with `requires_baa: true` and it is refused on arrival, with no policy edit. That is the difference between allowlisting tool names one by one and enforcing a compliance property across the whole catalog. + +## Policy + +`policies/baa-guardrail.cedar`: + +- baseline `permit` for `ReadFile`, `WriteFile`, `ListDir`. +- `forbid ... when { context.baa_covered == false }` — a Cedar `forbid` overrides any `permit`, so the guardrail holds regardless of the tool name or the baseline rule. + +The runtime derives `context.compliance_domain` and `context.baa_covered` from the catalog entry (`baa_covered` is true when the entry does not set `requires_baa`). Runs software-only with `CMCP_DEV_MODE=1`; no hardware required. diff --git a/demo-05-compliance-domain/call.py b/demo-05-compliance-domain/call.py new file mode 100644 index 0000000..80a2252 --- /dev/null +++ b/demo-05-compliance-domain/call.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python3 +"""Demo 5: attribute-based enforcement (compliance domain / BAA coverage). + +demo-01 denies by tool name. demo-04 denies by workflow context. This demo +denies by the tool's compliance attributes: a tool that is not BAA-covered is +refused by a single guardrail rule, whatever the tool is named. The decision is +made on `context.baa_covered`, derived by the runtime from the catalog. + +Prerequisites: + server/server.py running on :9001 (plain HTTP JSON-RPC 2.0) + cmcp start --config cmcp-config.yaml running on :8443 + CMCP_BEARER_TOKEN env var set (run.py / run.sh export this) + +Usage: + python call.py (run from repo root, or via run.py) +""" +import json +import os +import pathlib +import sys +import time +import urllib.error +import urllib.request + +GATEWAY_URL = "http://localhost:8443" +TOKEN = os.environ.get("CMCP_BEARER_TOKEN", "demo-token") +WORKSPACE = pathlib.Path(__file__).parent.parent / "workspace" + +CHART_TEXT = "Patient 88213 -- visit note -- BP 128/82 -- follow-up 2 weeks\n" + + +def _headers(): + return {"Content-Type": "application/json", "Authorization": f"Bearer {TOKEN}"} + + +def _post(url, payload): + data = json.dumps(payload).encode() + req = urllib.request.Request(url, data=data, headers=_headers(), method="POST") + try: + with urllib.request.urlopen(req, timeout=15) as resp: + return json.loads(resp.read()), resp.status + except urllib.error.HTTPError as exc: + return json.loads(exc.read()), exc.code + + +def mcp_call(name, arguments): + return _post(f"{GATEWAY_URL}/mcp", { + "jsonrpc": "2.0", + "id": int(time.time() * 1000), + "method": "tools/call", + "params": { + "name": name, + "arguments": arguments, + "_cmcp": {"workflow_id": "clinical-intake"}, + }, + }) + + +def main(): + WORKSPACE.mkdir(exist_ok=True) + session_id = None + + print("=== Demo 5: attribute-based enforcement (BAA coverage) ===") + print("Guardrail: forbid any tool where context.baa_covered == false.\n") + print("Catalog:") + print(" write_file domain=clinical baa_covered=true") + print(" read_file domain=clinical baa_covered=true") + print(" list_dir domain=external-analytics baa_covered=false\n") + + # 1. write_file -- BAA-covered -> permitted + print("[1/3] write_file [clinical, baa_covered=true]") + body, status = mcp_call("write_file", {"path": "chart.txt", "content": CHART_TEXT}) + if status == 200: + session_id = body["result"].get("_cmcp", {}).get("session_id") + print(f" ALLOWED {body['result']['content'][0]['text']!r}") + else: + print(f" ERROR {status}: {body}") + sys.exit(1) + + # 2. read_file -- BAA-covered -> permitted + print() + print("[2/3] read_file [clinical, baa_covered=true]") + body, status = mcp_call("read_file", {"path": "chart.txt"}) + if status == 200: + session_id = body["result"].get("_cmcp", {}).get("session_id") or session_id + print(f" ALLOWED content={body['result']['content'][0]['text'].strip()!r}") + else: + print(f" ERROR {status}: {body}") + + # 3. list_dir -- NOT BAA-covered -> denied by the guardrail, not by its name + print() + print("[3/3] list_dir [external-analytics, baa_covered=false]") + body, status = mcp_call("list_dir", {}) + if status == 403: + error = body.get("error", {}) + code = error.get("data", {}).get("error_code", "?") + print(f" HTTP 403 -- {error.get('message', '?')} [{code}]") + print(" Denied by the BAA guardrail: context.baa_covered == false.") + print(" Not blocked by naming list_dir -- blocked by its compliance attribute.") + else: + print(f" unexpected {status}: {body}") + sys.exit(1) + + # close session -> signed TRACE claim + print() + print("Closing session -> TRACE claim...") + if not session_id: + print("ERROR: no session_id. Is cMCP returning _cmcp metadata?") + sys.exit(1) + + body, status = _post(f"{GATEWAY_URL}/sessions/{session_id}/close", {}) + if status != 200: + print(f"ERROR {status}: {body}") + sys.exit(1) + claim = body + + trace = claim.get("trace", {}) + gw = claim.get("gateway", {}) + chain = gw.get("audit_chain", {}) + + print() + print("=== TRACE claim ===") + print(f" runtime.platform: {trace.get('runtime', {}).get('platform')}") + print(f" policy.bundle_hash: {trace.get('policy', {}).get('bundle_hash')}") + print(f" policy.mode: {trace.get('policy', {}).get('enforcement_mode')}") + print(f" audit_chain.length: {chain.get('length')} (session lifecycle + every allow/deny, hash-chained)") + sig = claim.get("signature", "") + print(f" signature: {sig[:40]}...") + print() + print(" One guardrail rule covers every non-BAA-covered tool, current and future.") + print(" Add a tool to the catalog with requires_baa=true and it is refused on arrival.") + + claim_path = WORKSPACE / "trace-claim-demo05.json" + claim_path.write_text(json.dumps(claim, indent=2)) + print() + print(f" Claim saved to {claim_path}") + + +if __name__ == "__main__": + main() diff --git a/demo-05-compliance-domain/catalog.json b/demo-05-compliance-domain/catalog.json new file mode 100644 index 0000000..f4ed3dc --- /dev/null +++ b/demo-05-compliance-domain/catalog.json @@ -0,0 +1,81 @@ +[ + { + "tool_name": "write_file", + "server": { + "display_name": "Demo Filesystem MCP Server", + "url": "http://localhost:9001/mcp", + "tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "transport": "http-sse", + "rotation_mode": "key-pinned" + }, + "approved_definition": { + "description": "Write content to a file in the demo workspace", + "input_schema": { + "type": "object", + "required": ["path", "content"], + "properties": { + "path": {"type": "string"}, + "content": {"type": "string"} + } + }, + "output_schema": { + "type": "object", + "properties": {"result": {"type": "string"}} + } + }, + "compliance_domain": "clinical", + "requires_baa": false, + "sensitivity_level": "hipaa_phi", + "added_at": "2026-07-20T00:00:00Z", + "approved_by": "demo-setup" + }, + { + "tool_name": "read_file", + "server": { + "display_name": "Demo Filesystem MCP Server", + "url": "http://localhost:9001/mcp", + "tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "transport": "http-sse", + "rotation_mode": "key-pinned" + }, + "approved_definition": { + "description": "Read a file from the demo workspace", + "input_schema": { + "type": "object", + "required": ["path"], + "properties": {"path": {"type": "string"}} + }, + "output_schema": { + "type": "object", + "properties": {"content": {"type": "string"}} + } + }, + "compliance_domain": "clinical", + "requires_baa": false, + "sensitivity_level": "hipaa_phi", + "added_at": "2026-07-20T00:00:00Z", + "approved_by": "demo-setup" + }, + { + "tool_name": "list_dir", + "server": { + "display_name": "Demo Filesystem MCP Server", + "url": "http://localhost:9001/mcp", + "tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "transport": "http-sse", + "rotation_mode": "key-pinned" + }, + "approved_definition": { + "description": "List files in the demo workspace", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}} + } + }, + "compliance_domain": "external-analytics", + "requires_baa": true, + "sensitivity_level": "public", + "added_at": "2026-07-20T00:00:00Z", + "approved_by": "demo-setup" + } +] diff --git a/demo-05-compliance-domain/cmcp-config.yaml b/demo-05-compliance-domain/cmcp-config.yaml new file mode 100644 index 0000000..7590dd0 --- /dev/null +++ b/demo-05-compliance-domain/cmcp-config.yaml @@ -0,0 +1,9 @@ +attestation: + provider: auto # falls back to software-only when CMCP_DEV_MODE=1 + enforcement_mode: enforcing + validity_seconds: 3600 + +policy_bundle_path: ./policies/ +catalog_path: ./catalog.json +listen_addr: "127.0.0.1:8443" +audit_db_path: ./audit.db diff --git a/demo-05-compliance-domain/policies/baa-guardrail.cedar b/demo-05-compliance-domain/policies/baa-guardrail.cedar new file mode 100644 index 0000000..9bc9be4 --- /dev/null +++ b/demo-05-compliance-domain/policies/baa-guardrail.cedar @@ -0,0 +1,32 @@ +// cMCP Cedar policy for demo-05: attribute-based enforcement +// +// demo-01 gates on the tool's identity (which tool by name). +// demo-04 gates on the call context (which workflow). +// This demo gates on the tool's *compliance attributes*: a tool that is not +// BAA-covered is refused, whatever it is called and whoever calls it. +// +// The runtime derives these from the catalog and places them on `context`: +// context.compliance_domain <- catalog entry's compliance_domain +// context.baa_covered <- true when the catalog entry does NOT require a BAA +// +// AGT maps tool_name -> PascalCase action: +// write_file -> Action::"WriteFile" +// read_file -> Action::"ReadFile" +// list_dir -> Action::"ListDir" + +// Baseline: the approved catalog tools may be called. +permit ( + principal, + action in [Action::"WriteFile", Action::"ReadFile", Action::"ListDir"], + resource +); + +// Guardrail: never call a tool that is not BAA-covered. +// A Cedar forbid overrides any permit, so this holds regardless of the tool +// name or the permit above. The decision is made on the attribute, not the name. +forbid ( + principal, + action, + resource +) +when { context.baa_covered == false }; diff --git a/demo-05-compliance-domain/policies/manifest.json b/demo-05-compliance-domain/policies/manifest.json new file mode 100644 index 0000000..06e4de9 --- /dev/null +++ b/demo-05-compliance-domain/policies/manifest.json @@ -0,0 +1,7 @@ +{ + "version": "1.0.0", + "authored_at": "2026-07-20T00:00:00Z", + "author_identity": "demo-setup", + "commit_sha": "demo-do-not-use-in-production", + "approval_chain": [] +} diff --git a/demo-05-compliance-domain/policies/schema.cedarschema b/demo-05-compliance-domain/policies/schema.cedarschema new file mode 100644 index 0000000..55e24a6 --- /dev/null +++ b/demo-05-compliance-domain/policies/schema.cedarschema @@ -0,0 +1,75 @@ +{ + "cMCP": { + "entityTypes": { + "Principal": { + "memberOfTypes": [], + "shape": { + "type": "Record", + "attributes": { + "session_id": { + "type": "String", + "required": true + }, + "workflow_id": { + "type": "String", + "required": true + } + } + } + }, + "Resource": { + "memberOfTypes": [], + "shape": { + "type": "Record", + "attributes": { + "tool_name": { + "type": "String", + "required": true + }, + "compliance_domain": { + "type": "String", + "required": true + }, + "baa_covered": { + "type": "Bool", + "required": true + } + } + } + } + }, + "actions": { + "call_tool": { + "appliesTo": { + "principalTypes": [ + "cMCP::Principal" + ], + "resourceTypes": [ + "cMCP::Resource" + ], + "context": { + "type": "Record", + "attributes": { + "workflow_id": { + "type": "String", + "required": true + }, + "compliance_domain": { + "type": "String", + "required": true + }, + "baa_covered": { + "type": "Bool", + "required": true + }, + "session_max_sensitivity": { + "type": "String", + "required": true + } + } + } + } + } + } + } +} diff --git a/demo-05-compliance-domain/run.py b/demo-05-compliance-domain/run.py new file mode 100644 index 0000000..004ec2f --- /dev/null +++ b/demo-05-compliance-domain/run.py @@ -0,0 +1,80 @@ +"""Demo 4: context-aware enforcement — cross-platform launcher (replaces run.sh). + +Usage: + python demo-05-compliance-domain/run.py # from repo root + python run.py # from demo-05 directory +""" +import os +import pathlib +import shutil +import subprocess +import sys +import time + +sys.stdout.reconfigure(line_buffering=True) + +SCRIPT_DIR = pathlib.Path(__file__).parent.resolve() +REPO_ROOT = SCRIPT_DIR.parent + + +def _find_cmcp() -> str: + found = shutil.which("cmcp") + if found: + return found + import sysconfig + candidates = [ + pathlib.Path(sys.executable).parent, + pathlib.Path(sysconfig.get_path("scripts")), + pathlib.Path(sysconfig.get_path("scripts", "nt_user")), + ] + for scripts in candidates: + for name in ("cmcp.exe", "cmcp"): + p = scripts / name + if p.exists(): + return str(p) + sys.exit("cmcp not found. Run: pip install cmcp-runtime") + + +def main() -> None: + os.environ.setdefault("CMCP_BEARER_TOKEN", "demo-token") + + log_dir = SCRIPT_DIR + server_log = open(log_dir / "server.log", "w") + cmcp_log = open(log_dir / "cmcp.log", "w") + + server = cmcp_proc = None + try: + print("-- Starting MCP filesystem server on :9001 --", flush=True) + server = subprocess.Popen( + [sys.executable, str(REPO_ROOT / "server" / "server.py")], + stdout=server_log, stderr=server_log, + ) + time.sleep(1) + + print("-- Starting cMCP Runtime (CMCP_DEV_MODE=1) on :8443 --", flush=True) + env = os.environ.copy() + env["CMCP_DEV_MODE"] = "1" + cmcp_proc = subprocess.Popen( + [_find_cmcp(), "start", "--config", str(SCRIPT_DIR / "cmcp-config.yaml")], + stdout=cmcp_log, stderr=cmcp_log, + cwd=SCRIPT_DIR, env=env, + ) + time.sleep(2) + + print() + subprocess.run([sys.executable, str(SCRIPT_DIR / "call.py")], check=True) + + finally: + for proc in (cmcp_proc, server): + if proc and proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + proc.kill() + server_log.close() + cmcp_log.close() + + +if __name__ == "__main__": + main() diff --git a/demo-05-compliance-domain/run.sh b/demo-05-compliance-domain/run.sh new file mode 100644 index 0000000..89bdeb4 --- /dev/null +++ b/demo-05-compliance-domain/run.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +# Demo 5: attribute-based enforcement (compliance domain / BAA coverage) +# +# Starts the local MCP filesystem server and the cMCP Runtime (CMCP_DEV_MODE=1), +# then makes three calls through the Runtime: +# write_file clinical, baa_covered=true -> Cedar permits +# read_file clinical, baa_covered=true -> Cedar permits +# list_dir external-analytics, baa_covered=false -> Cedar DENIES (HTTP 403) +# +# The deny is decided on the tool's compliance attribute (context.baa_covered), +# not on its name. One guardrail rule covers every non-BAA-covered tool. +# +# Usage: bash demo-05-compliance-domain/run.sh (from repo root) +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +CMCP_BEARER_TOKEN="${CMCP_BEARER_TOKEN:-demo-token}" +export CMCP_BEARER_TOKEN + +cleanup() { + kill "${CMCP_PID:-}" "${SERVER_PID:-}" 2>/dev/null || true + wait "${CMCP_PID:-}" "${SERVER_PID:-}" 2>/dev/null || true +} +trap cleanup EXIT + +echo "" +echo "=== Demo 5: attribute-based enforcement (BAA coverage) ===" +echo "" + +echo "-- Starting MCP filesystem server on :9001 --" +python "$REPO_ROOT/server/server.py" & +SERVER_PID=$! +sleep 1 + +echo "-- Starting cMCP Runtime (CMCP_DEV_MODE=1) on :8443 --" +cd "$SCRIPT_DIR" +CMCP_DEV_MODE=1 cmcp start --config cmcp-config.yaml & +CMCP_PID=$! +sleep 2 + +echo "" +python "$SCRIPT_DIR/call.py" diff --git a/demo.py b/demo.py index 044b375..cac716a 100644 --- a/demo.py +++ b/demo.py @@ -3,16 +3,16 @@ python demo.py # run all, pausing before each (for live talks) python demo.py --no-pause # run straight through, no prompts - python demo.py 2 # run only demo 2 (1, 2, 3, or 4) + python demo.py 2 # run only demo 2 (1 through 5) The trust chain, end to end: Demo 1 cMCP enforces Cedar on every tool call and signs a TRACE claim. Demo 2 Swap the policy bundle and the claim's hash changes; a pinned verifier rejects it. Demo 3 Verify that signed claim offline: no server, no gateway, no network. -Then, on a second axis: - Demo 4 The same tool is allowed in one workflow and denied in another. Enforcement - is on the declared call context, not on the model's stated intent. +Then, on a second axis, three ways the policy decides a call: + Demo 4 by call context -- the same tool, allowed in one workflow, denied in another. + Demo 5 by tool attribute -- a non-BAA-covered tool refused by one guardrail rule. All demos run in software-only mode (CMCP_DEV_MODE=1). That is deliberate: software proves the whole chain except the hardware root, so verification reads @@ -44,6 +44,10 @@ "demo-04-context-enforcement/run.py", "The same tool is allowed in one workflow and denied in another. Enforcement is\n" " on the declared call context, not on the model's stated intent."), + ("5", "Attribute-based enforcement", + "demo-05-compliance-domain/run.py", + "A tool that is not BAA-covered is refused by one guardrail rule, whatever it is\n" + " named. The decision is on the tool's compliance attribute, not its identity."), ] GREEN = "\033[92m"; BLUE = "\033[96m"; DIM = "\033[90m"; BOLD = "\033[1m"; RST = "\033[0m" @@ -143,7 +147,7 @@ def run(idx, title, script, blurb, pause): def main(): ap = argparse.ArgumentParser(description="Run the agentrust-io trust-chain demos.") - ap.add_argument("only", nargs="?", choices=["1", "2", "3", "4"], help="run only this demo") + ap.add_argument("only", nargs="?", choices=["1", "2", "3", "4", "5"], help="run only this demo") ap.add_argument("--no-pause", action="store_true", help="run straight through, no prompts") args = ap.parse_args()