Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ Each demo has a `run.py` (works on any OS) and a `run.sh` (bash only). Server an

---

## Browser console

A point-and-click version of the same enforcement, for showing on a screen instead of a terminal.

```
python web-console/run.py
```

Opens `http://localhost:8000`. Send tool calls and watch Cedar allow and deny them, close the session into a signed TRACE record, then verify it offline -- every result comes from the real gateway. See [`web-console/README.md`](web-console/README.md).

---

## Demo 1 -- cMCP in action (~90 seconds)

The agent calls three tools through the cMCP gateway. Cedar policy is enforced for every call. At session close, the gateway produces a signed TRACE claim carrying the policy bundle hash.
Expand Down
62 changes: 62 additions & 0 deletions web-console/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Web console -- cMCP in the browser

The same enforcement the other demos show on the command line, in a small web UI
you can click through in front of an audience. Every number on the page comes
from the real gateway: real tool calls, real Cedar decisions, the real signed
TRACE record, and the real `cmcp verify` output.

```
pip install cmcp-runtime
python web-console/run.py
```

`run.py` starts three local processes and opens the console at
`http://localhost:8000`:

- the shared MCP filesystem server on `:9001` (`server/server.py`)
- the cMCP gateway on `:8443` (`CMCP_DEV_MODE=1`, software-only TEE)
- this demo's web server on `:8000`

Press Ctrl+C in the terminal to stop all three.

## What to click

1. **`write_file`** and **`read_file`** -- Cedar permits them. The call is
forwarded to the tool and you see the real result.
2. **`list_dir`** -- Cedar forbids it. The gateway returns HTTP 403
(`POLICY_DENY`) and the tool is never contacted.
3. **Close session -> TRACE record** -- the gateway signs a `GatewayClaim`:
which tools ran, which were denied, the policy bundle hash, an Ed25519
signature. Saved to `workspace/web-console-claim.json`.
4. **Verify record offline** -- runs `cmcp verify` against the saved record.
In software-only mode the result is `partially_verified`: every
cryptographic field checks out, only the hardware root is absent. On real
TDX / SEV-SNP that last check passes too and it reads `verified`.

The **Policy** tab shows the exact Cedar bundle the gateway loaded. Its hash is
measured into the attestation at startup, so the policy that ran is provably the
one you approved.

## How it fits together

The browser never talks to the gateway directly. `webserver.py` serves the UI
and forwards to the gateway server-side, holding the bearer token so it stays
out of the browser and there is no CORS to configure. It is a thin proxy over
the same HTTP endpoints the CLI demos use (`POST /mcp`,
`GET /audit/export`, `POST /sessions/{id}/close`).

The Cedar policy uses the action dialect the runtime evaluates
(`write_file -> Action::"WriteFile"`), the same as `demo-01`. Nothing here is
mocked.

## Files

```
web-console/
run.py launcher (server + gateway + web server)
webserver.py static UI + /api proxy to the gateway
cmcp-config.yaml gateway config (enforcing, software-only)
catalog.json approved tools
policies/ Cedar bundle
web/ index.html, app.js, styles.css
```
106 changes: 106 additions & 0 deletions web-console/catalog.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
[
{
"tool_name": "write_file",
"server": {
"display_name": "Demo Filesystem MCP Server",
"url": "http://localhost:9001/mcp",
"tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"transport": "http-sse"
},
"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": "public",
"requires_baa": false,
"sensitivity_level": "public",
"added_at": "2026-06-22T00:00:00Z",
"approved_by": "demo-setup",
"definition_hash": "sha256:7b4738feb4a0c18b2870276996a14d8c2241380c063815cb98b54cb200e7e0d2"
},
{
"tool_name": "read_file",
"server": {
"display_name": "Demo Filesystem MCP Server",
"url": "http://localhost:9001/mcp",
"tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"transport": "http-sse"
},
"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": "public",
"requires_baa": false,
"sensitivity_level": "public",
"added_at": "2026-06-22T00:00:00Z",
"approved_by": "demo-setup",
"definition_hash": "sha256:3bdb3d438df2208aa5b293e0d71260f808365d6fcef4a9cf44702ebed1acc6cc"
},
{
"tool_name": "list_dir",
"server": {
"display_name": "Demo Filesystem MCP Server",
"url": "http://localhost:9001/mcp",
"tls_fingerprint": "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"transport": "http-sse"
},
"approved_definition": {
"description": "List files in the demo workspace",
"input_schema": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
}
}
},
"compliance_domain": "public",
"requires_baa": false,
"sensitivity_level": "public",
"added_at": "2026-06-22T00:00:00Z",
"approved_by": "demo-setup",
"definition_hash": "sha256:215e8f06120f876cb19bb766a54042a31d521b54cf731f6893c975e4e0861e78"
}
]
9 changes: 9 additions & 0 deletions web-console/cmcp-config.yaml
Original file line number Diff line number Diff line change
@@ -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
21 changes: 21 additions & 0 deletions web-console/policies/allow-filesystem-tools.cedar
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// cMCP Cedar policy for demo-01
//
// AGT's CedarBackend maps tool_name to a Cedar action (PascalCase):
// write_file -> Action::"WriteFile"
// read_file -> Action::"ReadFile"
// list_dir -> Action::"ListDir"
//
// write_file and read_file: ALLOWED
// list_dir: DENIED (demonstrates Cedar enforcement)

permit (
principal,
action in [Action::"WriteFile", Action::"ReadFile"],
resource
);

forbid (
principal,
action == Action::"ListDir",
resource
);
7 changes: 7 additions & 0 deletions web-console/policies/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"version": "1.0.0",
"authored_at": "2026-06-22T00:00:00Z",
"author_identity": "demo-setup",
"commit_sha": "demo-do-not-use-in-production",
"approval_chain": []
}
67 changes: 67 additions & 0 deletions web-console/policies/schema.cedarschema
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
{
"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": {
"session_max_sensitivity": {
"type": "String",
"required": true
},
"workflow_id": {
"type": "String",
"required": true
}
}
}
}
}
}
}
}
88 changes: 88 additions & 0 deletions web-console/run.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
"""cMCP browser console -- one-command launcher (cross-platform).

Starts three local processes and opens the console in your browser:
- the demo MCP filesystem server on :9001 (shared server/server.py)
- the cMCP gateway on :8443 (CMCP_DEV_MODE=1, software-only TEE)
- this demo's web server on :8000

Everything the browser shows comes from the real gateway. Ctrl+C stops it all.

python web-console/run.py # from repo root
python run.py # from the web-console directory
"""
import os
import pathlib
import shutil
import subprocess
import sys
import time
import webbrowser

sys.stdout.reconfigure(line_buffering=True)

HERE = pathlib.Path(__file__).parent.resolve()
REPO_ROOT = HERE.parent
PORT = os.environ.get("WEB_CONSOLE_PORT", "8000")


def _find_cmcp() -> str:
found = shutil.which("cmcp")
if found:
return found
import sysconfig
for scripts in (pathlib.Path(sys.executable).parent,
pathlib.Path(sysconfig.get_path("scripts"))):
for name in ("cmcp.exe", "cmcp"):
if (scripts / name).exists():
return str(scripts / name)
sys.exit("cmcp not found. Run: pip install cmcp-runtime")


def main():
os.environ.setdefault("CMCP_BEARER_TOKEN", "demo-token")
server_log = open(HERE / "server.log", "w")
cmcp_log = open(HERE / "cmcp.log", "w")
procs = []
try:
print("-- MCP filesystem server on :9001", flush=True)
procs.append(subprocess.Popen(
[sys.executable, str(REPO_ROOT / "server" / "server.py")],
stdout=server_log, stderr=server_log))
time.sleep(1)

print("-- cMCP gateway on :8443 (CMCP_DEV_MODE=1)", flush=True)
env = os.environ.copy()
env["CMCP_DEV_MODE"] = "1"
procs.append(subprocess.Popen(
[_find_cmcp(), "start", "--config", str(HERE / "cmcp-config.yaml")],
stdout=cmcp_log, stderr=cmcp_log, cwd=HERE, env=env))
time.sleep(2)

print(f"-- web console on http://localhost:{PORT}", flush=True)
web = subprocess.Popen([sys.executable, str(HERE / "webserver.py")], env=os.environ.copy())
procs.append(web)
time.sleep(1)

url = f"http://localhost:{PORT}"
print(f"\nOpen {url} (opening it for you now). Ctrl+C to stop.\n", flush=True)
try:
webbrowser.open(url)
except Exception:
pass
web.wait()
except KeyboardInterrupt:
print("\nstopping...", flush=True)
finally:
for p in reversed(procs):
if p and p.poll() is None:
p.terminate()
try:
p.wait(timeout=5)
except subprocess.TimeoutExpired:
p.kill()
server_log.close()
cmcp_log.close()


if __name__ == "__main__":
main()
Loading