magenta-canon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +255 -0
  3. package/bin/magenta-canon.mjs +97 -0
  4. package/docs/MAGENTA_VERIFICATION_SPEC.md +122 -0
  5. package/docs/MCP_GATEWAY.md +97 -0
  6. package/docs/NPM_PACKAGING.md +177 -0
  7. package/docs/SECURITY_MODEL.md +96 -0
  8. package/examples/magenta-gateway.config.json +24 -0
  9. package/examples/magenta-gateway.demo.config.json +20 -0
  10. package/examples/mock-mcp-server.mjs +62 -0
  11. package/package.json +171 -0
  12. package/scripts/agent-demo.ts +164 -0
  13. package/scripts/demo.mjs +281 -0
  14. package/scripts/heartbeat-loop.cjs +86 -0
  15. package/scripts/magenta-cli.ts +322 -0
  16. package/scripts/magenta-verify.ts +235 -0
  17. package/scripts/mcp-demo-drive.mjs +52 -0
  18. package/scripts/mcp-gateway.ts +230 -0
  19. package/scripts/post-merge.sh +6 -0
  20. package/scripts/uci-compiler.cjs +286 -0
  21. package/scripts/uci-inspector.cjs +624 -0
  22. package/scripts/uci-inspector.js +35 -0
  23. package/scripts/uci-snippet-generator.cjs +156 -0
  24. package/scripts/uci-snippet-generator.js +28 -0
  25. package/scripts/uci-witness.cjs +102 -0
  26. package/scripts/uci-witness.js +28 -0
  27. package/server/agent-auth.ts +126 -0
  28. package/server/agent-policy.ts +97 -0
  29. package/server/agent-record.ts +96 -0
  30. package/server/authority-containment.ts +582 -0
  31. package/server/authority-topology.ts +826 -0
  32. package/server/behavioral-vector.ts +575 -0
  33. package/server/canon-self-audit/checks/01-spine-body.ts +165 -0
  34. package/server/canon-self-audit/checks/02-deps-coherence.ts +164 -0
  35. package/server/canon-self-audit/checks/03-headers-posture.ts +133 -0
  36. package/server/canon-self-audit/checks/04-mutation-absence.ts +87 -0
  37. package/server/canon-self-audit/checks/05-language-discipline.ts +182 -0
  38. package/server/canon-self-audit/checks/06-validator-health.ts +132 -0
  39. package/server/canon-self-audit/index.ts +29 -0
  40. package/server/canon-self-audit/loopback.ts +53 -0
  41. package/server/canon-self-audit/provenance.ts +73 -0
  42. package/server/canon-self-audit/runner.ts +119 -0
  43. package/server/canon-self-audit/types.ts +236 -0
  44. package/server/canon-spine-validator.selftest.ts +281 -0
  45. package/server/canon-spine-validator.ts +446 -0
  46. package/server/conformance.ts +317 -0
  47. package/server/corrigibility.ts +603 -0
  48. package/server/crypto.ts +133 -0
  49. package/server/economic-trust.ts +511 -0
  50. package/server/execution-gate.ts +553 -0
  51. package/server/execution-receipts.ts +97 -0
  52. package/server/index.ts +351 -0
  53. package/server/ingestion-generators.ts +140 -0
  54. package/server/opus-bridge.ts +117 -0
  55. package/server/origin-proof.ts +245 -0
  56. package/server/persistence.ts +130 -0
  57. package/server/precedent-memory.ts +705 -0
  58. package/server/proposal-containment.ts +747 -0
  59. package/server/routes.ts +2906 -0
  60. package/server/sovereign-auth.ts +353 -0
  61. package/server/ssr-templates.ts +1292 -0
  62. package/server/static.ts +36 -0
  63. package/server/storage.ts +1758 -0
  64. package/server/transparency-log.ts +218 -0
  65. package/server/trust-bootstrap.ts +197 -0
  66. package/server/types/semver.d.ts +7 -0
  67. package/server/ucik-normalize.ts +40 -0
  68. package/server/verification-harness.ts +107 -0
  69. package/server/verification.ts +137 -0
  70. package/server/vite.ts +58 -0
  71. package/server/witness.ts +64 -0
  72. package/shared/canonical.ts +221 -0
  73. package/shared/certificates.ts +233 -0
  74. package/shared/schema.ts +2563 -0
  75. package/tsconfig.json +23 -0
@@ -0,0 +1,97 @@
1
+ # Magenta MCP Gateway (stdio)
2
+
3
+ A witnessed, capability-gated **stdio proxy** that sits between an MCP host
4
+ (Claude Code / Claude Desktop / Cursor) and a downstream MCP server. Every
5
+ `tools/call` is gated against operator-delegated capabilities and witnessed —
6
+ allowed *and* refused — onto the same transparency/evidence surface proven in
7
+ [`VERIFICATION_RUN.md`](./VERIFICATION_RUN.md). Refused calls never reach the
8
+ downstream server. The receipts are verifiable with `scripts/magenta-verify.ts`
9
+ and zero new trust.
10
+
11
+ ```
12
+ MCP host ──stdio──▶ Magenta Gateway ──gate+witness──▶ control plane (/internal/agent/action)
13
+ (Claude) ◀──────── (this proxy) ──forward if allowed──▶ downstream MCP server
14
+ ```
15
+
16
+ **Scope (this lane):** local stdio only. No Streamable HTTP, no hosted/multi-tenant.
17
+
18
+ ## How gating works
19
+
20
+ - Each tool is mapped to a Magenta action (`toolMap`), or falls back to
21
+ `agent.mcp.call` (the default), gated by the operator's `grantedCapabilities`.
22
+ - `payments.refund:<=10000` — a ceilinged grant: the `refund` tool is allowed only
23
+ up to 10000 cents; over that is refused (and recorded).
24
+ - `mcp.call:search`, `mcp.call:read_file` — an allowlist: those tools pass; any
25
+ tool **not** mapped and **not** allowlisted is **default-denied** (and recorded).
26
+ - The gate **always records before it forwards** — there is no path to the
27
+ downstream server that skips the receipt. A denied call returns an MCP tool
28
+ error (`isError: true`) carrying the reason and the witnessing receipt hash.
29
+
30
+ ## Run the demo
31
+
32
+ **1. Boot the control plane** (the witness + evidence surface):
33
+
34
+ ```bash
35
+ INTERNAL_API_KEY=operator-secret-xyz MAGENTA_STATE_FILE=/tmp/magenta-state.json \
36
+ PORT=5000 npx tsx server/index.ts
37
+ # then, once: bootstrap trust
38
+ curl -s -X POST :5000/internal/founder/ceremony -H 'X-Internal-Key: operator-secret-xyz' -d '{}'
39
+ ```
40
+
41
+ **2. Point your MCP host at the gateway.** Edit `examples/magenta-gateway.config.json`
42
+ to set your downstream server command and your granted capabilities, then add the
43
+ gateway to your MCP client config:
44
+
45
+ ```jsonc
46
+ // Claude Desktop / Claude Code / Cursor MCP servers config
47
+ {
48
+ "mcpServers": {
49
+ "magenta-gated-tools": {
50
+ "command": "npx",
51
+ "args": ["tsx", "/abs/path/to/magenta-canon/scripts/mcp-gateway.ts",
52
+ "/abs/path/to/magenta-canon/examples/magenta-gateway.config.json"]
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ The host now spawns the gateway, which spawns your downstream server. Every tool
59
+ call the agent makes flows through the gate.
60
+
61
+ **3. Trigger one ALLOWED call** — e.g. the agent calls `refund { amount_cents: 8900 }`.
62
+ It is witnessed and forwarded; the agent gets the real downstream result.
63
+
64
+ **4. Trigger one REFUSED call** — e.g. `refund { amount_cents: 25000 }` (over the
65
+ $100 ceiling) or any tool not on the allowlist. The downstream server is never
66
+ contacted; the agent receives an MCP tool error noting the block and the receipt hash.
67
+
68
+ **5. Publish evidence and verify** (the same loop as `VERIFICATION_RUN.md`):
69
+
70
+ ```bash
71
+ curl -s :5000/api/trust/evidence > bundle.json
72
+ npx tsx scripts/magenta-verify.ts bundle.json # → RESULT: VERIFIED
73
+ ```
74
+
75
+ Both the allowed and the refused call appear as receipts in the bundle — the
76
+ refusal is evidence too — and the verifier confirms the log is authentic and
77
+ untampered without trusting the server.
78
+
79
+ ## Tests
80
+
81
+ `scripts/mcp-gateway.test.ts` proves, in-process against the real witness surface:
82
+ - allowed tool call passes through downstream **and** writes a receipt;
83
+ - refused tool call **never** reaches downstream **and** still writes a refusal receipt;
84
+ - the standalone verifier **VERIFIES** the resulting evidence bundle;
85
+ - an unknown tool is default-denied and recorded (no bypass of the trust loop);
86
+ - non-`tools/call` messages (`initialize`, `tools/list`) pass through ungated.
87
+
88
+ ## Notes / honesty
89
+
90
+ - The gate **logic** (`McpGate`) is tested in-process with injected `record` +
91
+ `forward`; the stdio + control-plane wiring at the bottom of `mcp-gateway.ts`
92
+ is thin glue exercised by the manual demo above, not the unit tests.
93
+ - Capabilities here are **operator-configured** (Act-1 style) — the gateway is a
94
+ transparent proxy, so the human who installs it sets the ceilings. Per-call
95
+ agent-signed envelopes (Act 2) are a separate, stronger mode.
96
+ - Streamable HTTP transport and hosted/multi-tenant operation are deliberately
97
+ **not** implemented in this lane.
@@ -0,0 +1,177 @@
1
+ # NPM Packaging
2
+
3
+ How Magenta Canon is packaged as an installable CLI, what ships in the tarball,
4
+ what deliberately does not, and how to verify the package locally.
5
+
6
+ > **Status: packaging readiness — NOT published.** Nothing here publishes to npm.
7
+ > The package has not been pushed to the registry. Publishing is a separate,
8
+ > explicitly-authorized step.
9
+
10
+ ## Intended first release
11
+
12
+ The first npm release is planned as **`magenta-canon@0.1.0`** published under the
13
+ **`next`** dist-tag (not `latest`). This is deliberate: Magenta Canon is a
14
+ **proven reference implementation**, not production-hosted infrastructure yet, so
15
+ `0.1.0` + `next` signals early-OSS posture and lets adopters opt in explicitly
16
+ (`npm i magenta-canon@next`). It can be promoted to `latest` (and a higher
17
+ version) once production durability and an externally-mirrored witness land. The
18
+ intended command — run only after explicit authorization on an authenticated
19
+ machine — is:
20
+
21
+ ```bash
22
+ npm publish --tag next --access public
23
+ ```
24
+
25
+ ## The CLI
26
+
27
+ The package exposes a single bin, `magenta-canon`, with three commands. Until the
28
+ package is promoted to `latest`, install from the `next` dist-tag:
29
+
30
+ ```bash
31
+ npx magenta-canon@next demo # full local proof loop (allow/block/verify/tamper)
32
+ npx magenta-canon@next verify <bundle.json> # independently verify an evidence bundle
33
+ npx magenta-canon@next gateway <config.json># run the stdio MCP capability gateway
34
+ npx magenta-canon@next --help | --version
35
+ ```
36
+
37
+ - `verify` exits `0` on **VERIFIED**, `1` on **VERIFICATION FAILED** (and supports
38
+ `--self-test`). It is the standalone verifier — it imports nothing from the
39
+ server.
40
+ - `gateway` is the transparent stdio proxy; point an MCP host at it (see
41
+ [`MCP_GATEWAY.md`](MCP_GATEWAY.md)).
42
+ - `demo` runs the whole wedge locally against an ephemeral, **headless** control
43
+ plane (see "Headless mode" below).
44
+
45
+ The bin (`bin/magenta-canon.mjs`) is a thin dispatcher. It does not change any
46
+ verifier, witness, evidence, or gate behavior — it locates the package's existing
47
+ entry scripts and runs them, resolving the `tsx` TypeScript runner via
48
+ `createRequire` so it works from a global install, `npx`, or a repo checkout.
49
+
50
+ ## Headless control plane (why `demo` works after install)
51
+
52
+ The control plane (`server/index.ts`) normally serves a frontend: in development
53
+ it imports Vite, and in production it serves a pre-built client. Neither belongs
54
+ in a lean OSS CLI. So the package adds an **additive** flag:
55
+
56
+ ```
57
+ MAGENTA_HEADLESS=1 → skip Vite and static-client serving (API only)
58
+ ```
59
+
60
+ `npx magenta-canon demo` sets `MAGENTA_HEADLESS=1` (with `NODE_ENV=production`),
61
+ so the control plane boots with **no Vite and no built client** and still exposes
62
+ every endpoint the demo needs: `/api/health`, `/internal/founder/ceremony`,
63
+ `/internal/agent/action`, `/api/trust/evidence`. This flag changes only what is
64
+ served for the frontend — it does **not** change gate, witness, evidence, or
65
+ verifier behavior.
66
+
67
+ ## What ships in the tarball
68
+
69
+ Controlled by the `files` allowlist in `package.json`. Current contents
70
+ (`magenta-canon-0.1.0.tgz` — 75 files, ~210 KB packed, ~849 KB unpacked):
71
+
72
+ | Path | Why |
73
+ |---|---|
74
+ | `bin/` | the `magenta-canon` CLI dispatcher |
75
+ | `scripts/` | `demo.mjs`, `magenta-verify.ts`, `mcp-gateway.ts`, `mcp-demo-drive.mjs` (+ repo tooling) |
76
+ | `server/` | the headless control plane (gate, witness, transparency log, evidence) |
77
+ | `shared/` | canonical hashing, certificates, and the Drizzle schema the server imports |
78
+ | `examples/` | the demo gateway config + the minimal downstream MCP server |
79
+ | `tsconfig.json` | required so `tsx` resolves the `@shared/*` path alias at runtime |
80
+ | `docs/MAGENTA_VERIFICATION_SPEC.md`, `MCP_GATEWAY.md`, `SECURITY_MODEL.md`, `NPM_PACKAGING.md` | the spec + the docs a CLI user needs |
81
+ | `README.md`, `LICENSE` | always |
82
+
83
+ Runtime dependency added for packaging: **`tsx`** (moved from devDependencies to
84
+ `dependencies`) so the published package can run the TypeScript entry scripts.
85
+
86
+ ## What is deliberately NOT included
87
+
88
+ - **The React client (`client/`)** and any frontend build — the CLI is headless.
89
+ - **The website (`public/`)** — landing page, sitemap, banner, canon artifacts.
90
+ - **All tests (`**/*.test.ts`)**, `reports/`, `diagnostics/`, `attached_assets/`,
91
+ and other repo-only material (excluded via `!` patterns in `files`).
92
+ - **No hosted/cloud functionality, no HTTP gateway, no dashboard, no multi-tenant,
93
+ no production durability.** The package is the local reference implementation.
94
+
95
+ ### Open/paid boundary (unchanged)
96
+
97
+ The package ships the **open** surface: the verifier, the verification spec, the
98
+ stdio gateway, the evidence format, and a runnable local control plane. It does
99
+ **not** ship — and this lane does not create — the proposed paid surface (hosted
100
+ externally-mirrored witness, multi-tenant policy/control plane, compliance
101
+ reporting). See [`OSS_VS_PAID.md`](OSS_VS_PAID.md). Verification stays free and
102
+ re-implementable; that boundary is not weakened by packaging.
103
+
104
+ ## Verify the package locally
105
+
106
+ Inspect exactly what would publish, without publishing:
107
+
108
+ ```bash
109
+ npm pack --dry-run # lists files; add --json for machine output
110
+ npm pack # writes magenta-canon-<version>.tgz
111
+ ```
112
+
113
+ Smoke-test the packed artifact in a throwaway project:
114
+
115
+ ```bash
116
+ mkdir /tmp/mc-try && cd /tmp/mc-try && npm init -y
117
+ npm install /path/to/magenta-canon-0.1.0.tgz # installs deps incl. tsx
118
+ npx magenta-canon --version
119
+ npx magenta-canon verify --self-test # VERIFIED + VERIFICATION FAILED
120
+ npx magenta-canon demo # full headless proof loop
121
+ ```
122
+
123
+ (Offline alternative used in CI/dev: extract the tarball and symlink an existing
124
+ `node_modules` into the extracted `package/` dir, then run
125
+ `node package/bin/magenta-canon.mjs <cmd>`.)
126
+
127
+ ## Dependency footprint (after slimming)
128
+
129
+ Frontend/UI libraries (React, 29 Radix packages, recharts, framer-motion, …) and
130
+ test/type tooling (jest, ts-jest, supertest, jsdom, `@types/*`) were moved from
131
+ `dependencies` to `devDependencies` — none are imported by shipped code, so the
132
+ repo still builds/tests with them while consumers no longer install them. Two
133
+ packages were corrected: `semver` (imported on the server boot path) is now a
134
+ declared `dependency`; `nanoid` (only in the dev-only `server/vite.ts`) is a
135
+ `devDependency`.
136
+
137
+ | | Before | After |
138
+ |---|---|---|
139
+ | runtime `dependencies` | 76 | **19** |
140
+ | production install (`npm i --omit=dev`) | full frontend stack included | **~88 MB / 121 pkgs** |
141
+
142
+ ## Validated
143
+
144
+ In a **real `npm install --omit=dev`** of the tarball (production deps only):
145
+
146
+ - `--version` / `--help` → ok. ✅
147
+ - `verify --self-test` → `VERIFIED` then `VERIFICATION FAILED` (exit 0). ✅
148
+ - `gateway` with no config → usage message, exit 2. ✅
149
+ - `demo` → the full seven-step proof loop (allow `$89`, block `$250`, downstream
150
+ absence, `VERIFIED`, tamper `VERIFICATION FAILED`). ✅
151
+ - `npm test` (repo, full devDeps) → 18 suites / 331 tests green. ✅
152
+ - `npm run build` (client + server) → green. ✅
153
+ - `npm publish --dry-run` → clean (no errors; see "Dry-run output" below). ✅
154
+
155
+ `demo` now works from a true `npm install` because the 9 server files that used
156
+ the `@shared/*` tsconfig path alias were rewritten to relative imports — `tsx`
157
+ resolves those identically whether the package is nested in `node_modules` or run
158
+ from a repo checkout. (`tsconfig.json` is still shipped; the client retains the
159
+ alias, and it is harmless for the CLI.)
160
+
161
+ ### Dry-run output (current)
162
+
163
+ `npm publish --dry-run` reports: package size **~210 kB**, unpacked **~849 kB**,
164
+ **75 files**, unscoped + default public access. No errors. The only prior warning
165
+ (`repository.url` normalization) was fixed with `npm pkg fix`.
166
+
167
+ ## Known follow-ups (not in this lane)
168
+
169
+ - **Optional compiled build:** shipping pre-compiled JS would drop the `tsx`
170
+ runtime dependency entirely. Deferred — not required now that all three CLI
171
+ commands work via `tsx` from a true install.
172
+ - **`scripts/` hygiene:** the package currently ships a few repo-only helper
173
+ scripts (`post-merge.sh`, `heartbeat-loop.cjs`, the `uci-*` tools,
174
+ `agent-demo.ts`, `magenta-cli.ts`) that the CLI does not invoke. Harmless, but a
175
+ tighter `files` allowlist could trim them before publish. Cosmetic.
176
+ - **Publication:** choosing the dist-tag and running `npm publish` is a separate,
177
+ explicitly-authorized step.
@@ -0,0 +1,96 @@
1
+ # Magenta Canon — Security Model
2
+
3
+ This document states precisely **what Magenta Canon protects, what it does not,
4
+ and under what assumptions.** It is the honest backing for the claims in the
5
+ README. If a claim isn't supported here, it doesn't belong in the README.
6
+
7
+ ## What Magenta is
8
+
9
+ A **verifiable control plane for AI-agent tool calls**. Two layers:
10
+
11
+ 1. **Capability gate** — every consequential agent action is checked against a
12
+ delegated capability before it happens (allow / deny, with a recorded reason).
13
+ 2. **Transparency-log witness** — every decision (allowed *and* denied) is
14
+ recorded as a hash-chained, signed **execution receipt**, anchored in an
15
+ append-only Merkle log whose **Signed Tree Head (STH)** an outside party can
16
+ verify without trusting the operator.
17
+
18
+ The MCP Gateway puts layer 1 in the request path of real MCP tool calls; the
19
+ verifier (`scripts/magenta-verify.ts`) lets anyone check layer 2.
20
+
21
+ ## The core property
22
+
23
+ > **An action's outcome is gated before it happens, and recorded so that
24
+ > tampering with the record is detectable by an independent party.**
25
+
26
+ Concretely, proven in `docs/MCP_GATEWAY_RUN.md` and `docs/VERIFICATION_RUN.md`:
27
+ - a refund over the delegated ceiling was **blocked and never reached** the
28
+ downstream tool (proven by the downstream's own call log);
29
+ - the resulting evidence bundle **verified** with an independent tool;
30
+ - a tampered bundle **failed** verification (exit code 1).
31
+
32
+ ## Threats it addresses
33
+
34
+ | Threat | How Magenta addresses it |
35
+ |---|---|
36
+ | An agent takes an action outside its delegated authority | Capability gate denies it **before** it reaches the tool; the attempt is recorded. |
37
+ | After-the-fact log editing by an outsider | Hash-chained receipts: editing one breaks the chain (detected by the verifier). |
38
+ | **Insider** rewrites the log into a self-consistent forgery | The recomputed Merkle root won't match an STH the auditor **already holds**, signed by the **separate witness key** — so the forgery is exposed *(see the trust assumption below)*. |
39
+ | "We can't prove what the agent did" | Every decision is a signed receipt on an independently verifiable log. |
40
+ | Trusting the operator's word | The verifier re-derives all math and imports nothing from the server. |
41
+
42
+ ## Trust assumptions (read these — the guarantees are conditional)
43
+
44
+ 1. **Insider non-repudiation requires the STH to be externalized.** If the
45
+ operator both runs the witness *and* holds the only copy of the Signed Tree
46
+ Head, the guarantee is only **tamper-evidence across a restart**, not proof
47
+ against the operator. The full insider guarantee holds **only when the STH is
48
+ mirrored to an independent party** (the customer, an auditor, or a public
49
+ log) *before* any disputed action. The code emits and verifies STHs; the
50
+ mirror is an **operational commitment**, not yet an automated feature.
51
+ 2. **The witness key must be independent of the request-signing root.** Magenta
52
+ generates it separately; in production it should live in a separate KMS / host.
53
+ 3. **It is accountability, not prevention-of-everything.** The gate prevents an
54
+ *un-authorized* action; the witness proves *what was decided*. A correctly
55
+ *authorized* but harmful action is still recorded but not blocked — the
56
+ boundary is only as good as the capability policy the operator sets.
57
+ 4. **The verifier and the published spec are the trust root for auditors.**
58
+ `scripts/magenta-verify.ts` imports only `node:crypto`, `node:fs`, `tweetnacl`
59
+ (test-enforced) and follows `docs/MAGENTA_VERIFICATION_SPEC.md`. An auditor
60
+ should read the spec / re-implement the verifier rather than trust ours blindly.
61
+
62
+ ## Non-goals / what it does NOT do
63
+
64
+ - It does **not** sandbox or inspect tool *behavior* — it gates *whether* a call
65
+ is authorized and records it; it does not analyze what the tool does internally.
66
+ - It does **not** make a model safer or aligned; it makes its actions accountable.
67
+ - It is **not** a blockchain / trustless-consensus system — there is an operator;
68
+ the property is "you can *catch* the operator," contingent on STH mirroring.
69
+ - It does **not** yet provide production durability (see Status).
70
+
71
+ ## Capability model
72
+
73
+ - **Act 1 (proven):** capabilities are **operator-configured** — the human who
74
+ installs the gateway sets the ceilings (e.g. `payments.refund:<=10000`, or an
75
+ allowlist `mcp.call:<tool>`). Default is **deny**: a tool with no policy is refused.
76
+ - **Act 2 (proven, stronger, separate mode):** the agent **signs** a sovereign
77
+ envelope per action and its capabilities come from a **verified, chain-anchored
78
+ certificate** (root → intermediate → actor) — so the agent cannot widen its own
79
+ authority, and its signature is non-repudiable. Not exercised by the stdio
80
+ gateway demo, which uses Act 1.
81
+
82
+ ## Current status (honest)
83
+
84
+ - **Proven reference implementation**, validated in a **development sandbox**.
85
+ - Demo persistence is **file-backed**; **production durability (single-writer,
86
+ Postgres) is not yet complete.**
87
+ - The downstream MCP server in the demo is **minimal** (real stdio JSON-RPC,
88
+ sufficient to prove the path; not a production tool server).
89
+ - Transport is **stdio only** — no Streamable HTTP, no hosted/multi-tenant.
90
+ - STH mirroring (trust assumption #1) is an operational step, **not yet automated**.
91
+
92
+ ## Cryptographic primitives
93
+
94
+ SHA-256 (hashing + Merkle), Ed25519 (receipt and STH signatures), RFC 6962-style
95
+ Merkle leaf/node domain separation, canonical JSON (sorted keys). Full wire
96
+ formats: `docs/MAGENTA_VERIFICATION_SPEC.md`.
@@ -0,0 +1,24 @@
1
+ {
2
+ "downstream": {
3
+ "command": "node",
4
+ "args": ["./your-downstream-mcp-server.js"]
5
+ },
6
+ "controlPlane": {
7
+ "url": "http://127.0.0.1:5000",
8
+ "internalKey": "operator-secret-xyz"
9
+ },
10
+ "agent": {
11
+ "agentId": "mcp:claude-code",
12
+ "authorizedBy": "operator:cj@trendinghot",
13
+ "model": "mcp-gateway"
14
+ },
15
+ "grantedCapabilities": [
16
+ "payments.refund:<=10000",
17
+ "mcp.call:search",
18
+ "mcp.call:read_file"
19
+ ],
20
+ "toolMap": {
21
+ "refund": { "action": "agent.payments.refund", "paramsFrom": ["amount_cents", "order_id"] }
22
+ },
23
+ "defaultAction": "agent.mcp.call"
24
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "downstream": {
3
+ "command": "node",
4
+ "args": ["examples/mock-mcp-server.mjs"]
5
+ },
6
+ "controlPlane": {
7
+ "url": "http://127.0.0.1:5000",
8
+ "internalKey": "operator-secret-xyz"
9
+ },
10
+ "agent": {
11
+ "agentId": "mcp:claude-code",
12
+ "authorizedBy": "operator:cj@trendinghot",
13
+ "model": "mcp-gateway"
14
+ },
15
+ "grantedCapabilities": ["payments.refund:<=10000"],
16
+ "toolMap": {
17
+ "refund": { "action": "agent.payments.refund", "paramsFrom": ["amount_cents", "order_id"] }
18
+ },
19
+ "defaultAction": "agent.mcp.call"
20
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Minimal real downstream MCP server (stdio, newline-delimited JSON-RPC 2.0).
4
+ *
5
+ * Used to prove the Magenta MCP Gateway end-to-end: it exposes a `refund` tool,
6
+ * and — critically — appends every `tools/call` it RECEIVES to the file named by
7
+ * $DOWNSTREAM_LOG. That file is the ground truth for "the refused call never
8
+ * reached downstream": a blocked call must NOT appear in it.
9
+ */
10
+ import readline from "node:readline";
11
+ import { appendFileSync } from "node:fs";
12
+
13
+ const LOG = process.env.DOWNSTREAM_LOG;
14
+ const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
15
+ const logCall = (name, args) => {
16
+ process.stderr.write(`[downstream] tools/call RECEIVED: ${name} ${JSON.stringify(args)}\n`);
17
+ if (LOG) appendFileSync(LOG, JSON.stringify({ name, args, at: new Date().toISOString() }) + "\n");
18
+ };
19
+
20
+ const TOOLS = [
21
+ {
22
+ name: "refund",
23
+ description: "Issue a refund for an order.",
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: { amount_cents: { type: "integer" }, order_id: { type: "string" } },
27
+ required: ["amount_cents"],
28
+ },
29
+ },
30
+ ];
31
+
32
+ readline.createInterface({ input: process.stdin }).on("line", (line) => {
33
+ if (!line.trim()) return;
34
+ let msg;
35
+ try { msg = JSON.parse(line); } catch { return; }
36
+
37
+ switch (msg.method) {
38
+ case "initialize":
39
+ send({ jsonrpc: "2.0", id: msg.id, result: {
40
+ protocolVersion: "2025-06-18",
41
+ capabilities: { tools: {} },
42
+ serverInfo: { name: "mock-downstream", version: "1.0.0" },
43
+ } });
44
+ break;
45
+ case "notifications/initialized":
46
+ break; // notification, no response
47
+ case "tools/list":
48
+ send({ jsonrpc: "2.0", id: msg.id, result: { tools: TOOLS } });
49
+ break;
50
+ case "tools/call": {
51
+ const name = msg.params?.name;
52
+ const args = msg.params?.arguments ?? {};
53
+ logCall(name, args); // ← ground truth: this call reached downstream
54
+ send({ jsonrpc: "2.0", id: msg.id, result: {
55
+ content: [{ type: "text", text: `refund executed: ${JSON.stringify(args)}` }],
56
+ } });
57
+ break;
58
+ }
59
+ default:
60
+ if (msg.id != null) send({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `method not found: ${msg.method}` } });
61
+ }
62
+ });
package/package.json ADDED
@@ -0,0 +1,171 @@
1
+ {
2
+ "name": "magenta-canon",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "license": "Apache-2.0",
6
+ "description": "A verifiable MCP accountability gateway for AI-agent tool calls: allows authorized calls, blocks unauthorized calls, records both, and produces cryptographic evidence anyone can verify.",
7
+ "keywords": [
8
+ "mcp",
9
+ "model-context-protocol",
10
+ "ai-agents",
11
+ "tool-calls",
12
+ "gateway",
13
+ "transparency-log",
14
+ "merkle",
15
+ "verifiable",
16
+ "accountability",
17
+ "audit"
18
+ ],
19
+ "homepage": "https://github.com/royal-ohio/magenta-canon#readme",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "git+https://github.com/royal-ohio/magenta-canon.git"
23
+ },
24
+ "bugs": {
25
+ "url": "https://github.com/royal-ohio/magenta-canon/issues"
26
+ },
27
+ "engines": {
28
+ "node": ">=20"
29
+ },
30
+ "bin": {
31
+ "magenta-canon": "bin/magenta-canon.mjs"
32
+ },
33
+ "files": [
34
+ "bin/",
35
+ "scripts/",
36
+ "server/",
37
+ "shared/",
38
+ "examples/",
39
+ "tsconfig.json",
40
+ "docs/MAGENTA_VERIFICATION_SPEC.md",
41
+ "docs/MCP_GATEWAY.md",
42
+ "docs/SECURITY_MODEL.md",
43
+ "docs/NPM_PACKAGING.md",
44
+ "README.md",
45
+ "LICENSE",
46
+ "!**/*.test.ts",
47
+ "!**/*.test.mjs",
48
+ "!**/*.test.js"
49
+ ],
50
+ "scripts": {
51
+ "dev": "NODE_ENV=development tsx server/index.ts",
52
+ "demo": "node scripts/demo.mjs",
53
+ "build": "tsx script/build.ts",
54
+ "start": "NODE_ENV=production node dist/index.cjs",
55
+ "check": "tsc",
56
+ "test": "jest",
57
+ "db:push": "drizzle-kit push"
58
+ },
59
+ "dependencies": {
60
+ "@anthropic-ai/sdk": "^0.100.1",
61
+ "connect-pg-simple": "^10.0.0",
62
+ "cors": "^2.8.6",
63
+ "date-fns": "^3.6.0",
64
+ "drizzle-orm": "^0.39.3",
65
+ "drizzle-zod": "^0.7.0",
66
+ "express": "^4.21.2",
67
+ "express-session": "^1.18.1",
68
+ "memorystore": "^1.6.7",
69
+ "passport": "^0.7.0",
70
+ "passport-local": "^1.0.0",
71
+ "pg": "^8.16.3",
72
+ "semver": "^6.3.1",
73
+ "tsx": "^4.7.0",
74
+ "tweetnacl": "^1.0.3",
75
+ "tweetnacl-util": "^0.15.1",
76
+ "ws": "^8.18.0",
77
+ "zod": "^3.24.2",
78
+ "zod-validation-error": "^3.4.0"
79
+ },
80
+ "devDependencies": {
81
+ "@hookform/resolvers": "^3.10.0",
82
+ "@jridgewell/trace-mapping": "^0.3.25",
83
+ "@radix-ui/react-accordion": "^1.2.4",
84
+ "@radix-ui/react-alert-dialog": "^1.1.7",
85
+ "@radix-ui/react-aspect-ratio": "^1.1.3",
86
+ "@radix-ui/react-avatar": "^1.1.4",
87
+ "@radix-ui/react-checkbox": "^1.1.5",
88
+ "@radix-ui/react-collapsible": "^1.1.4",
89
+ "@radix-ui/react-context-menu": "^2.2.7",
90
+ "@radix-ui/react-dialog": "^1.1.7",
91
+ "@radix-ui/react-dropdown-menu": "^2.1.7",
92
+ "@radix-ui/react-hover-card": "^1.1.7",
93
+ "@radix-ui/react-label": "^2.1.3",
94
+ "@radix-ui/react-menubar": "^1.1.7",
95
+ "@radix-ui/react-navigation-menu": "^1.2.6",
96
+ "@radix-ui/react-popover": "^1.1.7",
97
+ "@radix-ui/react-progress": "^1.1.3",
98
+ "@radix-ui/react-radio-group": "^1.2.4",
99
+ "@radix-ui/react-scroll-area": "^1.2.4",
100
+ "@radix-ui/react-select": "^2.1.7",
101
+ "@radix-ui/react-separator": "^1.1.3",
102
+ "@radix-ui/react-slider": "^1.2.4",
103
+ "@radix-ui/react-slot": "^1.2.0",
104
+ "@radix-ui/react-switch": "^1.1.4",
105
+ "@radix-ui/react-tabs": "^1.1.4",
106
+ "@radix-ui/react-toast": "^1.2.7",
107
+ "@radix-ui/react-toggle": "^1.1.3",
108
+ "@radix-ui/react-toggle-group": "^1.1.3",
109
+ "@radix-ui/react-tooltip": "^1.2.0",
110
+ "@replit/vite-plugin-cartographer": "^0.4.4",
111
+ "@replit/vite-plugin-dev-banner": "^0.1.1",
112
+ "@replit/vite-plugin-runtime-error-modal": "^0.0.3",
113
+ "@tailwindcss/typography": "^0.5.15",
114
+ "@tailwindcss/vite": "^4.1.18",
115
+ "@tanstack/react-query": "^5.60.5",
116
+ "@types/connect-pg-simple": "^7.0.3",
117
+ "@types/cors": "^2.8.19",
118
+ "@types/express": "4.17.21",
119
+ "@types/express-session": "^1.18.0",
120
+ "@types/jest": "^30.0.0",
121
+ "@types/jsdom": "^27.0.0",
122
+ "@types/node": "^20.19.41",
123
+ "@types/passport": "^1.0.16",
124
+ "@types/passport-local": "^1.0.38",
125
+ "@types/react": "^18.3.11",
126
+ "@types/react-dom": "^18.3.1",
127
+ "@types/supertest": "^6.0.3",
128
+ "@types/ws": "^8.5.13",
129
+ "@vitejs/plugin-react": "^4.7.0",
130
+ "autoprefixer": "^10.4.20",
131
+ "class-variance-authority": "^0.7.1",
132
+ "clsx": "^2.1.1",
133
+ "cmdk": "^1.1.1",
134
+ "drizzle-kit": "^0.31.8",
135
+ "embla-carousel-react": "^8.6.0",
136
+ "esbuild": "^0.25.0",
137
+ "framer-motion": "^11.13.1",
138
+ "input-otp": "^1.4.2",
139
+ "jest": "^30.2.0",
140
+ "jsdom": "^27.4.0",
141
+ "lucide-react": "^0.453.0",
142
+ "nanoid": "^3.3.11",
143
+ "next-themes": "^0.4.6",
144
+ "postcss": "^8.4.47",
145
+ "react": "^18.3.1",
146
+ "react-day-picker": "^8.10.1",
147
+ "react-dom": "^18.3.1",
148
+ "react-hook-form": "^7.55.0",
149
+ "react-icons": "^5.4.0",
150
+ "react-resizable-panels": "^2.1.7",
151
+ "recharts": "^2.15.2",
152
+ "supertest": "^7.2.2",
153
+ "tailwind-merge": "^2.6.0",
154
+ "tailwindcss": "^3.4.17",
155
+ "tailwindcss-animate": "^1.0.7",
156
+ "ts-jest": "^29.4.6",
157
+ "tw-animate-css": "^1.2.5",
158
+ "typescript": "^5.3.3",
159
+ "vaul": "^1.1.2",
160
+ "vite": "^7.3.0",
161
+ "wouter": "^3.3.5"
162
+ },
163
+ "overrides": {
164
+ "drizzle-kit": {
165
+ "@esbuild-kit/esm-loader": "npm:tsx@^4.20.4"
166
+ }
167
+ },
168
+ "optionalDependencies": {
169
+ "bufferutil": "^4.0.8"
170
+ }
171
+ }