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.
- package/LICENSE +201 -0
- package/README.md +255 -0
- package/bin/magenta-canon.mjs +97 -0
- package/docs/MAGENTA_VERIFICATION_SPEC.md +122 -0
- package/docs/MCP_GATEWAY.md +97 -0
- package/docs/NPM_PACKAGING.md +177 -0
- package/docs/SECURITY_MODEL.md +96 -0
- package/examples/magenta-gateway.config.json +24 -0
- package/examples/magenta-gateway.demo.config.json +20 -0
- package/examples/mock-mcp-server.mjs +62 -0
- package/package.json +171 -0
- package/scripts/agent-demo.ts +164 -0
- package/scripts/demo.mjs +281 -0
- package/scripts/heartbeat-loop.cjs +86 -0
- package/scripts/magenta-cli.ts +322 -0
- package/scripts/magenta-verify.ts +235 -0
- package/scripts/mcp-demo-drive.mjs +52 -0
- package/scripts/mcp-gateway.ts +230 -0
- package/scripts/post-merge.sh +6 -0
- package/scripts/uci-compiler.cjs +286 -0
- package/scripts/uci-inspector.cjs +624 -0
- package/scripts/uci-inspector.js +35 -0
- package/scripts/uci-snippet-generator.cjs +156 -0
- package/scripts/uci-snippet-generator.js +28 -0
- package/scripts/uci-witness.cjs +102 -0
- package/scripts/uci-witness.js +28 -0
- package/server/agent-auth.ts +126 -0
- package/server/agent-policy.ts +97 -0
- package/server/agent-record.ts +96 -0
- package/server/authority-containment.ts +582 -0
- package/server/authority-topology.ts +826 -0
- package/server/behavioral-vector.ts +575 -0
- package/server/canon-self-audit/checks/01-spine-body.ts +165 -0
- package/server/canon-self-audit/checks/02-deps-coherence.ts +164 -0
- package/server/canon-self-audit/checks/03-headers-posture.ts +133 -0
- package/server/canon-self-audit/checks/04-mutation-absence.ts +87 -0
- package/server/canon-self-audit/checks/05-language-discipline.ts +182 -0
- package/server/canon-self-audit/checks/06-validator-health.ts +132 -0
- package/server/canon-self-audit/index.ts +29 -0
- package/server/canon-self-audit/loopback.ts +53 -0
- package/server/canon-self-audit/provenance.ts +73 -0
- package/server/canon-self-audit/runner.ts +119 -0
- package/server/canon-self-audit/types.ts +236 -0
- package/server/canon-spine-validator.selftest.ts +281 -0
- package/server/canon-spine-validator.ts +446 -0
- package/server/conformance.ts +317 -0
- package/server/corrigibility.ts +603 -0
- package/server/crypto.ts +133 -0
- package/server/economic-trust.ts +511 -0
- package/server/execution-gate.ts +553 -0
- package/server/execution-receipts.ts +97 -0
- package/server/index.ts +351 -0
- package/server/ingestion-generators.ts +140 -0
- package/server/opus-bridge.ts +117 -0
- package/server/origin-proof.ts +245 -0
- package/server/persistence.ts +130 -0
- package/server/precedent-memory.ts +705 -0
- package/server/proposal-containment.ts +747 -0
- package/server/routes.ts +2906 -0
- package/server/sovereign-auth.ts +353 -0
- package/server/ssr-templates.ts +1292 -0
- package/server/static.ts +36 -0
- package/server/storage.ts +1758 -0
- package/server/transparency-log.ts +218 -0
- package/server/trust-bootstrap.ts +197 -0
- package/server/types/semver.d.ts +7 -0
- package/server/ucik-normalize.ts +40 -0
- package/server/verification-harness.ts +107 -0
- package/server/verification.ts +137 -0
- package/server/vite.ts +58 -0
- package/server/witness.ts +64 -0
- package/shared/canonical.ts +221 -0
- package/shared/certificates.ts +233 -0
- package/shared/schema.ts +2563 -0
- 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
|
+
}
|