Skip to content

Commit a52585a

Browse files
feat(demos): one-command runner (python demo.py) (#19)
* feat(demos): add one-command runner for all three demos `python demo.py` runs demo 1, 2, and 3 in order, pausing before each so it can be narrated live. Flags: --no-pause (straight through) and a positional 1|2|3 (run a single demo). Sets CMCP_BEARER_TOKEN and CMCP_DEV_MODE, prints the detected cmcp-runtime version, and self-heals into a local .venv if the active interpreter cannot resolve the cmcp console script (common on Windows Store Python). Colour output only when attached to a TTY. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Imran Siddique <imran.siddique@opaque.co> * docs(demos): document the one-command runner in README Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Imran Siddique <imran.siddique@opaque.co> --------- Signed-off-by: Imran Siddique <imran.siddique@opaque.co> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6658038 commit a52585a

2 files changed

Lines changed: 176 additions & 0 deletions

File tree

‎README.md‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,16 @@ pip install cmcp-runtime
1212

1313
`cmcp-runtime` includes all dependencies (`starlette`, `uvicorn`, `cmcp-verify`). All demos use `CMCP_DEV_MODE=1` (software-only TEE, no hardware required). The local MCP server performs real filesystem operations on `./workspace/`.
1414

15+
## Quick start: one command
16+
17+
```
18+
python demo.py # run all three demos, pausing before each (good for live talks)
19+
python demo.py --no-pause # run straight through
20+
python demo.py 2 # run only demo 2
21+
```
22+
23+
`demo.py` sets the token and dev mode for you and prints the detected `cmcp-runtime` version. If your active Python cannot find the `cmcp` command, it will use a local `.venv` if one exists (`python -m venv .venv` then install `cmcp-runtime` into it). To run the demos individually instead, use the per-demo commands below.
24+
1525
Set a bearer token (the cMCP Runtime requires one):
1626

1727
```bash

‎demo.py‎

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
#!/usr/bin/env python3
2+
"""One command, all three agentrust-io demos.
3+
4+
python demo.py # run all three, pausing before each (for live talks)
5+
python demo.py --no-pause # run straight through, no prompts
6+
python demo.py 2 # run only demo 2 (1, 2, or 3)
7+
8+
The trust chain, end to end:
9+
Demo 1 cMCP enforces Cedar on every tool call and signs a TRACE claim.
10+
Demo 2 Swap the policy bundle and the claim's hash changes; a pinned verifier rejects it.
11+
Demo 3 Verify that signed claim offline: no server, no gateway, no network.
12+
13+
All demos run in software-only mode (CMCP_DEV_MODE=1). That is deliberate: software
14+
proves the whole chain except the hardware root, so verification reads
15+
'partially_verified'. On real TDX / SEV-SNP the hardware field verifies too and it
16+
becomes 'verified'. That last gap is exactly what the hardware path closes.
17+
"""
18+
import argparse
19+
import os
20+
import pathlib
21+
import shutil
22+
import subprocess
23+
import sys
24+
25+
ROOT = pathlib.Path(__file__).parent.resolve()
26+
27+
DEMOS = [
28+
("1", "cMCP in action",
29+
"demo-01-cmcp-in-action/run.py",
30+
"Three tool calls through cMCP. write_file and read_file are allowed; list_dir is\n"
31+
" denied by Cedar. The session closes into a signed TRACE claim."),
32+
("2", "Policy swap = attestation failure",
33+
"demo-02-policy-swap/run.py",
34+
"Load a different Cedar bundle. The policy hash changes. Watch the\n"
35+
" policy_bundle.hash line flip FAIL -> PASS when the pinned hash matches."),
36+
("3", "Offline TRACE verification",
37+
"demo-03-offline-trace/run.py",
38+
"Verify the demo-1 claim with nothing but the claim and a public key. No network."),
39+
]
40+
41+
GREEN = "\033[92m"; BLUE = "\033[96m"; DIM = "\033[90m"; BOLD = "\033[1m"; RST = "\033[0m"
42+
43+
44+
def _c(s, color):
45+
# colour only when attached to a real terminal
46+
return f"{color}{s}{RST}" if sys.stdout.isatty() else s
47+
48+
49+
def banner(idx, title, blurb):
50+
line = "=" * 70
51+
print()
52+
print(_c(line, DIM))
53+
print(_c(f" DEMO {idx}: {title}", BOLD + BLUE))
54+
print(f" {blurb}")
55+
print(_c(line, DIM))
56+
print()
57+
58+
59+
def _venv_python():
60+
p = ROOT / ".venv" / ("Scripts/python.exe" if os.name == "nt" else "bin/python")
61+
return p if p.exists() else None
62+
63+
64+
def _reexec_into_venv_if_needed():
65+
"""If this interpreter can't resolve cmcp but the demo .venv can, re-run there.
66+
Makes `python demo.py` work regardless of which Python is on PATH."""
67+
if os.environ.get("_DEMO_REEXEC"):
68+
return
69+
if shutil.which("cmcp") or _cmcp_in_scripts():
70+
return
71+
vp = _venv_python()
72+
if not vp:
73+
return
74+
os.environ["_DEMO_REEXEC"] = "1"
75+
rc = subprocess.run([str(vp), str(pathlib.Path(__file__).resolve()), *sys.argv[1:]]).returncode
76+
sys.exit(rc)
77+
78+
79+
def preflight():
80+
if not (shutil.which("cmcp") or _cmcp_in_scripts()):
81+
print(_c("cmcp not found.", BOLD))
82+
print("Set up the demo environment once:")
83+
print(" python -m venv .venv")
84+
print(" .venv\\Scripts\\python -m pip install cmcp-runtime # (Scripts/ -> bin/ on macOS/Linux)")
85+
print("Then just run: python demo.py")
86+
sys.exit(1)
87+
ver = _cmcp_version()
88+
print(_c(f"cmcp-runtime {ver or '(unknown)'} detected.", DIM))
89+
if ver and _older_than(ver, (0, 3, 0)):
90+
print(_c(
91+
" WARNING: the demo narration assumes 0.3.0+. On this older version, demo 2\n"
92+
" step 6 and demo 3 will read 'verified' instead of 'partially_verified'.\n"
93+
" For the talk, upgrade: pip install -U cmcp-runtime", BOLD))
94+
95+
96+
def _cmcp_version():
97+
try:
98+
import importlib.metadata as m
99+
return m.version("cmcp-runtime")
100+
except Exception:
101+
return None
102+
103+
104+
def _older_than(ver, target):
105+
try:
106+
parts = tuple(int(x) for x in ver.split(".")[:3])
107+
return parts < target
108+
except Exception:
109+
return False
110+
111+
112+
def _cmcp_in_scripts():
113+
import sysconfig
114+
for base in (pathlib.Path(sys.executable).parent,
115+
pathlib.Path(sysconfig.get_path("scripts")),
116+
pathlib.Path(sysconfig.get_path("scripts", "nt_user"))):
117+
for name in ("cmcp.exe", "cmcp"):
118+
if (base / name).exists():
119+
return True
120+
return False
121+
122+
123+
def run(idx, title, script, blurb, pause):
124+
if pause:
125+
try:
126+
input(_c(f">>> Press Enter to run Demo {idx}: {title} ", GREEN))
127+
except (EOFError, KeyboardInterrupt):
128+
print("\nStopped."); sys.exit(0)
129+
banner(idx, title, blurb)
130+
rc = subprocess.run([sys.executable, str(ROOT / script)]).returncode
131+
if rc != 0:
132+
print(_c(f"\n[!] Demo {idx} exited with code {rc}. See the *.log files in the demo folder.", BOLD))
133+
return rc
134+
135+
136+
def main():
137+
ap = argparse.ArgumentParser(description="Run the agentrust-io trust-chain demos.")
138+
ap.add_argument("only", nargs="?", choices=["1", "2", "3"], help="run only this demo")
139+
ap.add_argument("--no-pause", action="store_true", help="run straight through, no prompts")
140+
args = ap.parse_args()
141+
142+
os.environ.setdefault("CMCP_BEARER_TOKEN", "demo-token")
143+
os.environ.setdefault("CMCP_DEV_MODE", "1")
144+
_reexec_into_venv_if_needed()
145+
preflight()
146+
147+
print(_c("\nagentrust-io · the trust chain, live", BOLD))
148+
print(_c("Agent Manifest (what the agent is) · cMCP (what it does) · TRACE (the proof)", DIM))
149+
150+
selected = [d for d in DEMOS if (args.only is None or d[0] == args.only)]
151+
pause = not args.no_pause and sys.stdin.isatty()
152+
153+
failures = 0
154+
for idx, title, script, blurb in selected:
155+
if run(idx, title, script, blurb, pause) != 0:
156+
failures += 1
157+
158+
print()
159+
if failures:
160+
print(_c(f"Done with {failures} failure(s).", BOLD))
161+
sys.exit(1)
162+
print(_c("Done. The proof outlives the runtime that made it.", BOLD + GREEN))
163+
164+
165+
if __name__ == "__main__":
166+
main()

0 commit comments

Comments
 (0)