@permission-protocol/mcp-guard 0.1.0 → 0.2.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 (88) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/LICENSE +21 -0
  3. package/README.md +101 -59
  4. package/SECURITY.md +40 -0
  5. package/dist/src/approval-server.d.ts +69 -3
  6. package/dist/src/approval-server.js +345 -86
  7. package/dist/src/approval-server.js.map +1 -1
  8. package/dist/src/approval-ui.d.ts +5 -0
  9. package/dist/src/approval-ui.js +120 -81
  10. package/dist/src/approval-ui.js.map +1 -1
  11. package/dist/src/classify.d.ts +29 -0
  12. package/dist/src/classify.js +107 -0
  13. package/dist/src/classify.js.map +1 -0
  14. package/dist/src/cli.js +49 -12
  15. package/dist/src/cli.js.map +1 -1
  16. package/dist/src/config.d.ts +21 -1
  17. package/dist/src/config.js +92 -5
  18. package/dist/src/config.js.map +1 -1
  19. package/dist/src/enforcement/index.d.ts +12 -0
  20. package/dist/src/enforcement/index.js +53 -0
  21. package/dist/src/enforcement/index.js.map +1 -0
  22. package/dist/src/enforcement/policy.d.ts +29 -0
  23. package/dist/src/enforcement/policy.js +60 -0
  24. package/dist/src/enforcement/policy.js.map +1 -0
  25. package/dist/src/enforcement/types.d.ts +19 -0
  26. package/dist/src/enforcement/types.js +3 -0
  27. package/dist/src/enforcement/types.js.map +1 -0
  28. package/dist/src/enforcement/verifier.d.ts +4 -0
  29. package/dist/src/enforcement/verifier.js +110 -0
  30. package/dist/src/enforcement/verifier.js.map +1 -0
  31. package/dist/src/engine.d.ts +11 -2
  32. package/dist/src/engine.js +20 -14
  33. package/dist/src/engine.js.map +1 -1
  34. package/dist/src/frame.d.ts +67 -0
  35. package/dist/src/frame.js +113 -0
  36. package/dist/src/frame.js.map +1 -0
  37. package/dist/src/pending.d.ts +108 -12
  38. package/dist/src/pending.js +174 -24
  39. package/dist/src/pending.js.map +1 -1
  40. package/dist/src/proxy.d.ts +29 -2
  41. package/dist/src/proxy.js +274 -105
  42. package/dist/src/proxy.js.map +1 -1
  43. package/dist/src/receipt-verify.d.ts +36 -0
  44. package/dist/src/receipt-verify.js +88 -0
  45. package/dist/src/receipt-verify.js.map +1 -0
  46. package/dist/src/receipt.d.ts +81 -7
  47. package/dist/src/receipt.js +100 -17
  48. package/dist/src/receipt.js.map +1 -1
  49. package/dist/src/signer.d.ts +38 -0
  50. package/dist/src/signer.js +112 -0
  51. package/dist/src/signer.js.map +1 -0
  52. package/dist/src/verify-cli.d.ts +1 -0
  53. package/dist/src/verify-cli.js +133 -0
  54. package/dist/src/verify-cli.js.map +1 -0
  55. package/dist/src/verify-core.d.ts +22 -0
  56. package/dist/src/verify-core.js +50 -0
  57. package/dist/src/verify-core.js.map +1 -0
  58. package/dist/src/webhook.d.ts +35 -0
  59. package/dist/src/webhook.js +57 -0
  60. package/dist/src/webhook.js.map +1 -0
  61. package/example/test.sh +5 -7
  62. package/package.json +33 -7
  63. package/templates/github-actions/receipt-gate.yml +64 -0
  64. package/assets/demo.gif +0 -0
  65. package/demo/_demo-scenario.sh +0 -43
  66. package/dist/src/tests/config.test.d.ts +0 -1
  67. package/dist/src/tests/config.test.js +0 -63
  68. package/dist/src/tests/config.test.js.map +0 -1
  69. package/dist/src/tests/engine.test.d.ts +0 -1
  70. package/dist/src/tests/engine.test.js +0 -46
  71. package/dist/src/tests/engine.test.js.map +0 -1
  72. package/dist/src/tests/receipt.test.d.ts +0 -1
  73. package/dist/src/tests/receipt.test.js +0 -44
  74. package/dist/src/tests/receipt.test.js.map +0 -1
  75. package/docs/index.html +0 -771
  76. package/permission-protocol-mcp-guard-0.1.0.tgz +0 -0
  77. package/src/approval-server.ts +0 -136
  78. package/src/approval-ui.ts +0 -154
  79. package/src/cli.ts +0 -84
  80. package/src/config.ts +0 -68
  81. package/src/engine.ts +0 -31
  82. package/src/pending.ts +0 -65
  83. package/src/proxy.ts +0 -173
  84. package/src/receipt.ts +0 -52
  85. package/src/tests/config.test.ts +0 -61
  86. package/src/tests/engine.test.ts +0 -47
  87. package/src/tests/receipt.test.ts +0 -44
  88. package/tsconfig.json +0 -16
package/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — security hardening (breaking)
4
+
5
+ Fixes the findings of the October 2026 MCP Guard audit.
6
+
7
+ ### Breaking changes and migration
8
+
9
+ - **Strict config.** Unknown keys, duplicate rules, `countdown_seconds` on a rule that is not `reversibility: reversible`, and `cap_usd` without `amount_arg` now fail at startup. Remove any `tools:` block (it was never read) and add `amount_arg` to capped rules:
10
+ ```yaml
11
+ - { id: spend, tool: spend, action: require_approval, cap_usd: 50, amount_arg: amount_usd }
12
+ ```
13
+ - **Spend caps read only `amount_arg`.** `_pp.amount_usd` and the `amount_usd`/`amount` fallbacks are gone. A missing or non-numeric amount is a hard stop.
14
+ - **All methods are gated.** `resources/read`, `prompts/get`, `resources/subscribe` and unknown methods now follow the rules and `default_action` (a `default_action: block` policy blocks them unless a `method:` rule allows them). Plumbing still passes through.
15
+ - **Malformed input is refused.** Unparseable lines get `-32700`; JSON-RPC batches and malformed frames get `-32600`. Nothing is forwarded raw.
16
+ - **Approval UI needs its token.** It listens on 127.0.0.1 only; open the URL printed at startup (`…/#t=<token>`). API clients send `Authorization: Bearer <token>`; `PP_APPROVAL_TOKEN` (32+ characters) supplies it. Cross-origin access (`?api=` on another origin) no longer works.
17
+ - **Post-release undo is removed** (`/api/undo` returns 404). Use Hold before a countdown releases. Verify-lane items can only be acknowledged.
18
+ - **Receipt v2.** New signed envelope; v1 receipts are rejected by the verifier. Receipts carry `args_sha256` and argument names, never raw arguments.
19
+ - **Keys moved** from `./.keys` to `~/.config/mcp-guard/keys` (or `$PP_KEYS_DIR`). Key ids are now `pp-dev-<fingerprint>`. Re-export and re-pin the public key: `mcp-guard export-key`.
20
+ - **`verify` requires `--pubkey` and `--scope`.** The verifier no longer generates or discovers a key, and empty `--scope-*` values are errors.
21
+ - **GitHub commit-status poster removed** (`PP_GITHUB_TOKEN`). Gate merges with the Permission Protocol GitHub App's `permission-protocol/approval` check.
22
+
23
+ ### Added
24
+
25
+ - `mcp-guard verify` and `mcp-guard export-key` subcommands.
26
+ - `--operator <name>`: approvals record `{kind: human, id, via: local-token}`; countdown and standing-rule releases record `{kind: policy}`.
27
+ - `approval_ttl_seconds` and `receipt_ttl_seconds` config keys (default 900 each). Expired held requests answer the agent with `-32004`; denials with `-32003`.
28
+ - The approval UI and the Permission Deck show every argument the server will receive (`args_full`) and the scope an approval signs for (`scope`).
29
+ - The wrapped server no longer inherits `PP_*` environment variables (approval token, webhook secret, receipt token).
30
+ - `merge_pr` scope ref is derived only from the PR number; `_pp.requires_input` no longer moves an irreversible hold out of the Decide lane.
31
+ - GitHub webhook deduplicates `X-GitHub-Delivery`.
32
+ - `templates/github-actions/receipt-gate.yml`: a deploy gate that takes its verifier, key and receipt only from trusted sources.
33
+ - CI workflow running build, tests and the end-to-end demo drivers.
34
+
35
+ ### Fixed
36
+
37
+ - Approval API: no authentication, all interfaces, `Access-Control-Allow-Origin: *`.
38
+ - Spend-cap bypass through `_pp.amount_usd`.
39
+ - Receipt signatures that covered only part of the receipt and could be re-split across fields.
40
+ - Self-gate CI that trusted the key, receipt and verifier from the pull request (workflow removed).
41
+ - Fail-open forwarding of unparseable lines and batches.
42
+ - Approving a webhook PR after it was denied (and approving repeatedly).
43
+ - Countdown releases producing receipts indistinguishable from human approval.
44
+ - Raw tool arguments in stderr, receipts and the CORS-open receipts API.
45
+ - Enforcement module: a missing or invalid policy meant "nothing protected"; duplicate signer names counted as separate approvals.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Permission Protocol
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -4,13 +4,13 @@
4
4
 
5
5
  [MCP](https://modelcontextprotocol.io) (Model Context Protocol) is how AI agents call tools — like APIs, databases, or deployments. MCP has no built-in access control. MCP Guard adds that layer.
6
6
 
7
- Define what's allowed, blocked, or requires approval — in a single YAML file. MCP Guard sits between your MCP client and server, enforces the rules, and logs every decision as an audit receipt.
7
+ Define what's allowed, blocked, or requires approval — in a single YAML file. MCP Guard sits between your MCP client and server, enforces the rules, and records every decision as a signed receipt.
8
8
 
9
- Without MCP Guard, your agent can call any tool. With it, every action is checked. **MCP Guard does not just log — it blocks execution before it happens.**
9
+ Every tool call, resource read and prompt fetch goes through your policy; only protocol plumbing (`initialize`, `ping`, list calls, notifications) passes untouched. **MCP Guard does not just log — it blocks execution before it happens.**
10
10
 
11
11
  → **Blocks** unsafe tool calls before they execute
12
12
  → **Holds** sensitive actions for human approval
13
- → **Logs** every decision as an immutable receipt
13
+ → **Signs** every decision as an Ed25519 receipt that names who authorized it
14
14
  → **Observe mode** — audit what would be blocked before enforcing
15
15
 
16
16
  <p align="center">
@@ -21,7 +21,7 @@ Without MCP Guard, your agent can call any tool. With it, every action is checke
21
21
 
22
22
  ```bash
23
23
  # Install
24
- npm install @permissionprotocol/mcp-guard
24
+ npm install @permission-protocol/mcp-guard
25
25
 
26
26
  # Create a policy file
27
27
  cat > pp.config.yaml << 'EOF'
@@ -39,19 +39,18 @@ EOF
39
39
  mcp-guard --config pp.config.yaml -- node my-mcp-server.js
40
40
  ```
41
41
 
42
- That's it. Your agent can no longer delete user data. Production deploys require approval. Everything is logged to `pp-receipts.jsonl`.
42
+ That's it. Your agent can no longer delete user data. Production deploys require approval. Every decision is signed and logged to `pp-receipts.jsonl`.
43
+
44
+ `default_action: allow` lets any tool without a rule run, and the guard warns about it at startup. Use `block` or `require_approval` to fail closed.
43
45
 
44
46
  ## 60-Second Demo (no MCP server needed)
45
47
 
48
+ See MCP Guard block dangerous tool calls in real-time, no setup required:
49
+
46
50
  ```bash
47
- git clone https://github.com/permission-protocol/mcp-guard
48
- cd mcp-guard
49
- npm install
50
- npm run demo
51
+ npx @permission-protocol/mcp-guard demo
51
52
  ```
52
53
 
53
- Sends three tool calls through the guard and shows: one allowed, one blocked, one held. No setup required.
54
-
55
54
  ## Architecture
56
55
 
57
56
  ```
@@ -66,29 +65,47 @@ Sends three tool calls through the guard and shows: one allowed, one blocked, on
66
65
  (audit trail)
67
66
  ```
68
67
 
69
- MCP Guard intercepts JSON-RPC messages on stdin/stdout. When it sees a `tools/call` request:
68
+ MCP Guard parses every JSON-RPC message on stdin and forwards only what it parsed and evaluated, re-serialized:
70
69
 
71
- 1. Looks up the tool name in the config rules
72
- 2. If **allowed** → forwards to the real server
73
- 3. If **blocked** → returns a JSON-RPC error (`-32001`) directly
74
- 4. If **held for approval** → returns a JSON-RPC error (`-32002`) directly
75
- 5. Emits a receipt for every decision (stderr + jsonl file)
76
-
77
- All other JSON-RPC methods pass through transparently.
70
+ 1. A line that is not a single JSON-RPC 2.0 object (invalid JSON, a batch array, a malformed frame) gets an error and is never forwarded.
71
+ 2. Plumbing passes through: `initialize`, `ping`, `notifications/*`, `tools/list`, `resources/list`, `resources/templates/list`, `resources/unsubscribe`, `prompts/list`, `logging/setLevel`, `completion/complete`.
72
+ 3. Everything else — `tools/call`, `resources/read`, `prompts/get`, `resources/subscribe` and any method MCP Guard does not know — is decided by the rules, then `default_action`.
73
+ 4. **Allowed** → forwarded (agent metadata under `_pp` is stripped first). **Blocked** → error `-32001`. **Held** → waits for a human (or returns `-32002` without an approval UI).
74
+ 5. Every decision emits a signed receipt (one stderr line + `pp-receipts.jsonl`).
78
75
 
79
76
  ## Config Reference
80
77
 
81
78
  ```yaml
82
79
  # pp.config.yaml
83
- default_action: allow # "allow" or "block" — applies when no rule matches
84
- mode: enforce # "enforce" (default) or "observe" (log only, never block)
80
+ default_action: block # allow | block | require_approval — when no rule matches
81
+ mode: enforce # "enforce" (default) or "observe" (log only, never block)
82
+ approval_ttl_seconds: 900 # held requests expire after this (default 900)
83
+ receipt_ttl_seconds: 900 # signed receipts expire after this (default 900)
85
84
 
86
85
  rules:
87
- - id: unique-rule-id # Human-readable identifier
88
- tool: tool_name # Exact match on MCP tool name
89
- action: allow # allow | block | require_approval
86
+ - id: unique-rule-id # unique identifier
87
+ tool: tool_name # exact tool name (tools/call rules)
88
+ action: allow # allow | block | require_approval
89
+
90
+ - id: hold-reads
91
+ method: resources/read # govern another method (no "tool" field)
92
+ action: require_approval
93
+
94
+ - id: hold-email
95
+ tool: send_email
96
+ action: require_approval
97
+ reversibility: reversible # reversible | irreversible (default: irreversible)
98
+ countdown_seconds: 300 # reversible only: auto-release unless someone holds it
99
+
100
+ - id: spend
101
+ tool: spend
102
+ action: require_approval
103
+ cap_usd: 50 # under the cap: runs now, shows in Verify; at/over: hard stop
104
+ amount_arg: amount_usd # required with cap_usd: the argument the tool actually charges
90
105
  ```
91
106
 
107
+ The loader is strict: an unknown key, a duplicate rule, a countdown on an irreversible rule, or `cap_usd` without `amount_arg` fails at startup instead of being ignored. A missing or non-numeric amount is a hard stop. Agent metadata under `_pp` (`confidence`, `requires_input`) can route a card to a different lane but can never turn a held or blocked call into an allowed one, and `requires_input` never changes how an irreversible hold is presented.
108
+
92
109
  ### Modes
93
110
 
94
111
  | Mode | Behavior |
@@ -104,45 +121,72 @@ Use `observe` mode to audit what *would* be blocked before turning enforcement o
104
121
  |--------|----------|-------------------|
105
122
  | `allow` | Forward request to server | — |
106
123
  | `block` | Reject immediately | `-32001` |
107
- | `require_approval` | Reject with hold status | `-32002` |
124
+ | `require_approval` | Hold for approval (or reject without an approval UI) | `-32002` |
125
+
126
+ Other errors the agent can receive: `-32003` denied by a human, `-32004` approval request expired, `-32005` release refused (receipt failed verification), `-32700` / `-32600` / `-32602` malformed input.
108
127
 
109
128
  ## Receipts
110
129
 
111
- Every `tools/call` decision generates an immutable receipt:
130
+ Every decision produces a signed receipt (v2): a small **envelope** that is signed, plus a **display** block for people that is not.
112
131
 
113
132
  ```json
114
133
  {
115
- "receipt_id": "550e8400-e29b-41d4-a716-446655440000",
116
- "timestamp": "2026-03-20T15:30:00.000Z",
117
- "agent_id": "my-agent",
118
- "tool_name": "delete_user_data",
119
- "decision": "blocked",
120
- "reason": "Matched rule \"block-dangerous-delete\"",
121
- "rule_id": "block-dangerous-delete",
122
- "request_payload_hash": "sha256-hex-string",
123
- "target_server": "node my-mcp-server.js",
124
- "mode": "enforce"
134
+ "envelope": {
135
+ "v": 2,
136
+ "receipt_id": "rcpt_mg_mvtj9td5_63c091bfe0e7",
137
+ "method": "tools/call",
138
+ "tool": "deploy",
139
+ "args_sha256": "<sha256 of the canonical arguments the server received>",
140
+ "scope": { "scope": "deploy:production", "ref": null, "sha": "a1b2c3d4" },
141
+ "decision": "allowed",
142
+ "approval": { "kind": "human", "id": "rod", "via": "local-token" },
143
+ "actor": "my-agent",
144
+ "rule_id": "deploy",
145
+ "policy_sha256": "<sha256 of pp.config.yaml>",
146
+ "mode": "enforce",
147
+ "issued_at": "2026-10-29T15:30:00.000Z",
148
+ "expires_at": "2026-10-29T15:45:00.000Z",
149
+ "nonce": "<random>",
150
+ "key_id": "pp-dev-3c1d2e4f5a6b7c8d"
151
+ },
152
+ "signature": { "alg": "ed25519", "key_id": "pp-dev-3c1d2e4f5a6b7c8d", "value": "<hex>" },
153
+ "display": { "summary": "…", "reason": "…", "risk_tier": "high", "target_server": "node server.js", "arg_keys": ["env", "sha"], "viewer_url": "…" }
125
154
  }
126
155
  ```
127
156
 
128
- Receipts are written to:
129
- - **stderr** — for real-time monitoring
130
- - **pp-receipts.jsonl** — append-only audit file
157
+ - The signature covers `pp-mcp-guard-receipt-v2\n` + canonical JSON of the whole envelope, so no authority field can be edited or re-split.
158
+ - `approval` says who authorized the action: `human` (the approval UI operator) or `policy` (a rule, a standing spend cap, or a countdown).
159
+ - Receipts never contain raw arguments — only `args_sha256` and the argument names, plus the scope identifiers a scoped receipt is bound to (environment, ref, commit SHA). Raw arguments appear only in the authenticated approval UI, which shows every argument and the scope an approval signs for.
160
+ - The signing key lives in `~/.config/mcp-guard/keys` (or `$PP_KEYS_DIR`), never in the working directory. Its id is derived from the public key. MCP Guard refuses to start if the private key is readable by others.
131
161
 
132
- The `request_payload_hash` is a SHA-256 of the full request params, so you can verify what was sent without storing sensitive arguments.
162
+ Receipts are written to stderr (one summary line), `pp-receipts.jsonl` (or `$PP_SHARED_RECEIPTS_PATH`), and optionally POSTed to `$PP_RECEIPT_ENDPOINT` (with `$PP_RECEIPT_TOKEN` as bearer auth).
133
163
 
134
- **[→ Visualize your receipts](https://permission-protocol.github.io/mcp-guard/)** — paste a receipt or upload `pp-receipts.jsonl` to see a visual summary.
164
+ ### Verifying a receipt
165
+
166
+ ```bash
167
+ # Once: export the public key and commit it on your default branch
168
+ mcp-guard export-key .pp/mcp-guard.pub.pem
169
+
170
+ # In CI: verify offline against the pinned key
171
+ mcp-guard verify --receipt-file receipt.json --pubkey .pp/mcp-guard.pub.pem \
172
+ --scope deploy:production --scope-sha "$GITHUB_SHA"
173
+ ```
174
+
175
+ The verifier requires `--scope`, accepts only the keys you pin, requires an unexpired `allowed` receipt, checks scope exactly, and refuses scoped receipts that no human approved (pass `--allow-policy-approval` to accept them). Exit 0 = authorized, 1 = refused, 2 = missing input. See `templates/github-actions/receipt-gate.yml` for a deploy workflow that takes the verifier, key and receipt only from trusted places.
135
176
 
136
177
  ## CLI
137
178
 
138
179
  ```
139
180
  mcp-guard [options] -- <server command>
181
+ mcp-guard verify --receipt-file <path> --pubkey <pem> [--scope s] [--scope-ref r] [--scope-sha h]
182
+ mcp-guard export-key [out.pem]
140
183
 
141
184
  Options:
142
- --config <path> Path to config file (default: ./pp.config.yaml)
143
- --agent-id <id> Agent identifier for receipts (default: "unknown")
185
+ --config <path> Path to config file (default: ./pp.config.yaml)
186
+ --agent-id <id> Agent identifier for receipts (default: "unknown")
187
+ --operator <name> Approver name recorded on receipts (default: OS username)
144
188
  --mode <mode> enforce or observe (overrides config file)
145
- --approval-port <port> Enable approval UI on this port (e.g. 3100)
189
+ --approval-port <port> Enable the approval UI on 127.0.0.1:<port>
146
190
  -h, --help Show help
147
191
  ```
148
192
 
@@ -174,29 +218,27 @@ With:
174
218
  }
175
219
  ```
176
220
 
177
- ## Approval Flow (v0.1)
221
+ ## Approval Flow
178
222
 
179
- When a tool call matches a `require_approval` rule, MCP Guard can hold the request open and wait for human approval via a web UI — instead of rejecting immediately.
223
+ When a call matches a `require_approval` rule, MCP Guard can hold it open and wait for a human in a local web UI instead of rejecting it.
180
224
 
181
225
  ```bash
182
- # Start with approval UI on port 3100
183
226
  mcp-guard --config pp.config.yaml --approval-port 3100 -- node my-mcp-server.js
184
-
185
- # Open http://localhost:3100 to see pending approvals
227
+ # stderr: [mcp-guard] Approval UI: http://127.0.0.1:3100/#t=<token>
186
228
  ```
187
229
 
230
+ Open the exact URL printed at startup. The part after `#t=` is a per-process access token; it never leaves the browser except as the `Authorization: Bearer` header on API calls.
231
+
188
232
  **How it works:**
189
- 1. Agent calls a tool that requires approval
190
- 2. MCP Guard holds the JSON-RPC response open (does NOT return an error)
191
- 3. The tool call appears in the approval UI at `http://localhost:<port>`
192
- 4. Human clicks **Approve** → request is forwarded to the server, response returned to the agent
193
- 5. Human clicks **Deny** → JSON-RPC error (-32002) returned to the agent
233
+ 1. Agent calls a tool that requires approval; MCP Guard holds the response open.
234
+ 2. The call appears in the UI with every argument the server will receive.
235
+ 3. **Approve** → MCP Guard signs a receipt naming you as the approver, verifies it, then forwards the call.
236
+ 4. **Deny** → the agent gets error `-32003`. A decision is final: approving a denied call returns 409.
237
+ 5. No decision within `approval_ttl_seconds` → the agent gets error `-32004`.
194
238
 
195
- When `--approval-port` is **not** set, `require_approval` tools return an error immediately (original behavior, no change).
239
+ **Security of the approval channel:** it listens on 127.0.0.1 only, rejects any other `Host` header, sends no CORS headers, and requires the token on every API route except the HMAC-signed GitHub webhook. `PP_APPROVAL_TOKEN` (32+ characters) supplies the token instead of generating one. The wrapped server is started without any `PP_*` environment variables, so a tool that echoes its environment cannot leak the token.
196
240
 
197
- The approval UI auto-refreshes every 3 seconds and shows:
198
- - Pending tool calls with collapsible arguments
199
- - Recent decision history (last 20)
241
+ When `--approval-port` is **not** set, `require_approval` calls return `-32002` immediately.
200
242
 
201
243
  ## Why not just use X?
202
244
 
@@ -210,7 +252,7 @@ The approval UI auto-refreshes every 3 seconds and shows:
210
252
 
211
253
  - **Not a dashboard.** It's a proxy. It sits in the data path and enforces rules.
212
254
  - **Not a scanner.** It doesn't analyze your code or model outputs. It gates tool calls.
213
- - **Not compliance theater.** Every decision has a cryptographic receipt.
255
+ - **Not compliance theater.** Every decision has an Ed25519-signed receipt that names who authorized it.
214
256
  - **Not a replacement for good architecture.** It's one layer in a defense-in-depth strategy.
215
257
 
216
258
  Agents shouldn't be able to execute arbitrary actions without an explicit decision point.
package/SECURITY.md ADDED
@@ -0,0 +1,40 @@
1
+ # Security Policy
2
+
3
+ This repository contains MCP Guard, the Permission Protocol stdio policy proxy for MCP tool calls.
4
+
5
+ ## Reporting a vulnerability
6
+
7
+ **Do not open public issues for security vulnerabilities.**
8
+
9
+ Email: security@permissionprotocol.com
10
+
11
+ Include a description, steps to reproduce, and your assessment of impact. We acknowledge reports within 48 hours and keep you informed through resolution.
12
+
13
+ Machine-readable contact: https://www.permissionprotocol.com/.well-known/security.txt
14
+
15
+ ## Threat model
16
+
17
+ MCP Guard assumes the agent behind the MCP client is untrusted: it controls every tool argument, can send any bytes on stdin, and may be able to reach local ports. MCP Guard defends against that agent:
18
+
19
+ - **Input:** only single, well-formed JSON-RPC 2.0 objects are evaluated, and only the evaluated object is forwarded (re-serialized). Batches and unparseable lines are refused.
20
+ - **Policy:** every method except protocol plumbing is decided by the rules. Agent metadata (`_pp`) can add human attention but never removes it; spend caps read only the argument the rule names.
21
+ - **Approval channel:** loopback only, Host-checked, no CORS, and a per-process bearer token delivered in the URL fragment. Decisions go through one state machine; final states never change.
22
+ - **Receipts:** Ed25519 over a domain-separated canonical envelope, with expiry, nonce and approver. Verifiers trust only pinned keys and refuse scoped receipts no human approved.
23
+ - **Keys:** kept in the operator's config directory with owner-only permissions; MCP Guard refuses to start if the private key is exposed.
24
+
25
+ Out of scope for the local dev signer: an agent running as the **same OS user** with arbitrary shell access can read that user's files and process memory, including the signing key and the approval token. For that threat, run the agent as a different user or use the managed Permission Protocol signer, where approval and signing happen outside the agent's machine.
26
+
27
+ ## Safe harbor
28
+
29
+ We will not pursue or support legal action against good-faith security research that:
30
+
31
+ - respects user privacy and does not access, modify, or destroy data that is not yours;
32
+ - avoids service disruption (no denial of service, no spam);
33
+ - gives us reasonable time to remediate before public disclosure.
34
+
35
+ If you are unsure whether something is in scope, ask first at the address above.
36
+
37
+ ## Notes
38
+
39
+ - There is no formal bug bounty program today. With your permission, we credit reporters in release notes.
40
+ - Our security and compliance posture is published at https://www.permissionprotocol.com/trust.
@@ -1,4 +1,70 @@
1
- import type { Receipt } from './receipt.js';
1
+ import { type PendingAction } from './pending.js';
2
+ import type { Lane } from './engine.js';
3
+ import type { Reversibility } from './config.js';
4
+ import { type Approval, type Receipt } from './receipt.js';
5
+ /**
6
+ * The local approval channel.
7
+ *
8
+ * - Listens on 127.0.0.1 only and rejects any Host header but its own (DNS rebinding).
9
+ * - Sends no CORS headers: other origins cannot read or drive it.
10
+ * - Every /api route except the GitHub webhook needs `Authorization: Bearer <token>`.
11
+ * The token is generated per process (or supplied via PP_APPROVAL_TOKEN) and reaches
12
+ * the browser only in the URL fragment printed at startup; GET / holds no secret.
13
+ * - Every decision goes through the pending-queue state machine, so final states hold.
14
+ */
15
+ export declare const MIN_TOKEN_LENGTH = 32;
2
16
  export declare function pushReceipt(receipt: Receipt): void;
3
- export declare function setOnApprove(cb: (id: string) => void): void;
4
- export declare function startApprovalServer(port: number): void;
17
+ export interface ApprovalHooks {
18
+ /** Release a held request the human approved: sign, forward, return the receipt. */
19
+ release?: (action: PendingAction, approval: Approval) => Receipt;
20
+ }
21
+ export interface ApprovalServerOptions {
22
+ port: number;
23
+ /** Bearer token; defaults to PP_APPROVAL_TOKEN, else 32 random bytes. */
24
+ token?: string;
25
+ /** Recorded as the approver on human decisions; defaults to the OS username. */
26
+ operator?: string;
27
+ hooks?: ApprovalHooks;
28
+ /** For receipts issued on webhook-sourced decisions. */
29
+ receiptTtlSeconds?: number;
30
+ policySha256?: string;
31
+ }
32
+ export interface ApprovalServerHandle {
33
+ token: string;
34
+ operator: string;
35
+ /** Resolves with the bound port once listening. */
36
+ ready: Promise<number>;
37
+ close: () => Promise<void>;
38
+ }
39
+ /** The QueueItem shape from the API contract (backend ⇄ console). */
40
+ export interface QueueItem {
41
+ id: string;
42
+ tool_name: string;
43
+ lane: Lane;
44
+ reversibility: Reversibility;
45
+ countdown_seconds?: number;
46
+ countdown_remaining?: number;
47
+ confidence?: number;
48
+ summary: string;
49
+ args_preview: string;
50
+ /** Every argument the server will receive, so the approver sees all of it. */
51
+ args_full: string;
52
+ /** What an approval authorizes, when the receipt is scoped (merge, migration, deploy). */
53
+ scope?: {
54
+ scope: string;
55
+ ref: string | null;
56
+ sha: string | null;
57
+ };
58
+ agent_id: string;
59
+ rule_id: string | null;
60
+ status: PendingAction['status'];
61
+ acted: boolean;
62
+ created_at: string;
63
+ expires_at?: string;
64
+ receipt_id?: string;
65
+ }
66
+ export declare function toQueueItem(p: PendingAction): QueueItem;
67
+ export declare function pendingQueue(): QueueItem[];
68
+ /** Call after any queue mutation so SSE subscribers see it immediately. */
69
+ export declare function notifyQueueChange(): void;
70
+ export declare function startApprovalServer(opts: ApprovalServerOptions): ApprovalServerHandle;