protect-mcp 0.7.0 → 0.7.2
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/CHANGELOG.md +22 -0
- package/README.md +153 -423
- package/SECURITY.md +48 -0
- package/dist/cli.js +64 -3
- package/dist/cli.mjs +64 -3
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.2: run the gate in other agents (Codex, Cursor, Gemini, Hermes)
|
|
4
|
+
|
|
5
|
+
The `evaluate` and `sign` verbs now accept `--format <host>`
|
|
6
|
+
(claude | codex | gemini | cursor | hermes | grok). With it, the verb reads the
|
|
7
|
+
host's hook payload from stdin (tool name and input) and emits the deny verdict
|
|
8
|
+
in that host's hook contract, so the same fail-closed Cedar gate works as a
|
|
9
|
+
PreToolUse/PostToolUse hook outside Claude Code.
|
|
10
|
+
|
|
11
|
+
The load-bearing detail: **Hermes ignores hook exit codes** and reads the verdict
|
|
12
|
+
from stdout, so `--format hermes` denies via `{"decision":"block"}` on stdout. A
|
|
13
|
+
raw exit-2 (which every other host honors) would have silently failed open there.
|
|
14
|
+
Cursor and Gemini receive their structured stdout deny verdict in addition to
|
|
15
|
+
exit 2. Without `--format`, the verbs behave exactly as before (the
|
|
16
|
+
`--tool`/`--input` flag mode is unchanged).
|
|
17
|
+
|
|
18
|
+
## 0.7.1: documentation and security policy
|
|
19
|
+
|
|
20
|
+
No code change from 0.7.0. Rewrote the README to lead with the fail-closed and
|
|
21
|
+
self-test guarantees, and added a `SECURITY.md` disclosure policy (supported
|
|
22
|
+
versions, the affected 0.5.x/0.6.x range, the published advisory, and the
|
|
23
|
+
coordinated-disclosure process). The package now ships `SECURITY.md`.
|
|
24
|
+
|
|
3
25
|
## 0.7.0 (security release): the gate now fails closed and actually evaluates
|
|
4
26
|
|
|
5
27
|
This release fixes the way the Cedar policy gate behaves when anything goes
|
package/README.md
CHANGED
|
@@ -1,240 +1,142 @@
|
|
|
1
1
|
# protect-mcp
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://www.npmjs.com/package/protect-mcp)
|
|
5
|
-
[](https://github.com/wshobson/agents/tree/main/plugins/protect-mcp)
|
|
6
|
-
[](./LICENSE)
|
|
7
|
-
|
|
8
|
-
> A policy check that sits between your AI agent and the tools it calls.
|
|
9
|
-
> Every tool call is evaluated against a rule you wrote. Every decision is signed.
|
|
10
|
-
|
|
11
|
-
> **Receipt format:** `protect-mcp` emits Veritas Acta receipts. Legacy
|
|
12
|
-
> ScopeBlind receipts remain verifiable, but Acta v0.1 is the canonical
|
|
13
|
-
> format going forward. Spec: [`@veritasacta/protocol`](https://www.npmjs.com/package/@veritasacta/protocol)
|
|
14
|
-
> · IETF: [draft-farley-acta-signed-receipts](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/).
|
|
15
|
-
|
|
16
|
-
## What it does, in plain English
|
|
17
|
-
|
|
18
|
-
When an AI agent (Claude Code, Cursor, a custom LangChain app, anything that
|
|
19
|
-
uses the Model Context Protocol) wants to run a command, edit a file, or call
|
|
20
|
-
an API, `protect-mcp` intercepts that request before it executes:
|
|
21
|
-
|
|
22
|
-
1. **Checks a policy.** You write rules in [Cedar](https://www.cedarpolicy.com/)
|
|
23
|
-
— the same policy language AWS uses for IAM. Rules like *"never allow
|
|
24
|
-
`rm -rf /`"*, *"only allow `Bash` during working hours"*, or *"require
|
|
25
|
-
human approval for anything touching production."*
|
|
26
|
-
2. **Returns a decision.** `allow`, `deny`, or `request_approval`. The agent
|
|
27
|
-
framework respects the decision — if it's `deny`, the tool call doesn't run.
|
|
28
|
-
3. **Signs a receipt.** An Ed25519-signed, hash-chained record of the decision,
|
|
29
|
-
written to `.receipts/`. Verifiable offline by anyone with the public key.
|
|
30
|
-
When your auditor asks *"what did that agent do on 2026-03-14?"* you have
|
|
31
|
-
cryptographic proof — not a log file you might have tampered with.
|
|
32
|
-
|
|
33
|
-
You install it once. Your agent keeps working the same way. The difference:
|
|
34
|
-
every action it takes is now policy-checked and audit-evidenced.
|
|
35
|
-
|
|
36
|
-
## Who this is for
|
|
37
|
-
|
|
38
|
-
- **Developers using Claude Code / Cursor / Cline** who want *"don't let the
|
|
39
|
-
agent delete my repo"* enforced rather than hoped for.
|
|
40
|
-
- **Security teams** shipping agents to engineering who need a portable
|
|
41
|
-
policy layer that travels across frameworks.
|
|
42
|
-
- **Compliance teams** who need tamper-evident evidence of what agents did,
|
|
43
|
-
verifiable without trusting the vendor.
|
|
44
|
-
|
|
45
|
-
## How it relates to `sb-runtime`
|
|
46
|
-
|
|
47
|
-
`protect-mcp` is a **library** — it sits inside your agent framework and
|
|
48
|
-
gates tool calls cooperatively. Right tool when you own the agent's code and
|
|
49
|
-
trust its framework to honour decisions.
|
|
50
|
-
|
|
51
|
-
[`sb-runtime`](https://github.com/ScopeBlind/sb-runtime) is the companion
|
|
52
|
-
**binary** that wraps the whole agent *process* in an OS-level sandbox
|
|
53
|
-
(Landlock + seccomp on Linux). It enforces decisions at the kernel layer —
|
|
54
|
-
the agent can't ignore them even if it tried. Use `sb-runtime` when you're
|
|
55
|
-
running an agent you didn't write, or when you want belt-and-braces defence
|
|
56
|
-
in depth:
|
|
57
|
-
|
|
58
|
-
```
|
|
59
|
-
┌─────────────────────────────────────────────────┐
|
|
60
|
-
│ sb-runtime ← OS refuses forbidden syscalls │
|
|
61
|
-
│ ┌───────────────────────────────────────────┐ │
|
|
62
|
-
│ │ agent process (Claude Code, Python, …) │ │
|
|
63
|
-
│ │ ┌─────────────────────────────────────┐ │ │
|
|
64
|
-
│ │ │ protect-mcp │ │ │
|
|
65
|
-
│ │ │ ← Cedar decides per tool call, │ │ │
|
|
66
|
-
│ │ │ receipts every decision │ │ │
|
|
67
|
-
│ │ └─────────────────────────────────────┘ │ │
|
|
68
|
-
│ └───────────────────────────────────────────┘ │
|
|
69
|
-
└─────────────────────────────────────────────────┘
|
|
70
|
-
```
|
|
3
|
+
Fail-closed Cedar policy gate plus signed receipts for AI agent tool calls.
|
|
71
4
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/protect-mcp)
|
|
6
|
+
[](https://www.npmjs.com/package/protect-mcp)
|
|
7
|
+
[](https://www.npmjs.com/package/protect-mcp)
|
|
8
|
+
[](https://www.npmjs.com/package/protect-mcp)
|
|
9
|
+
|
|
10
|
+
`protect-mcp` is a gate that sits in front of an AI agent's tool calls. It evaluates
|
|
11
|
+
each call against a [Cedar](https://www.cedarpolicy.com/) policy (the same language
|
|
12
|
+
AWS uses for IAM), blocks what breaks the rules before it runs, and signs an
|
|
13
|
+
offline-verifiable Ed25519 receipt of every decision. It runs locally, sends no
|
|
14
|
+
telemetry of your decisions anywhere, and is MIT licensed.
|
|
15
|
+
|
|
16
|
+
## Why it is different
|
|
17
|
+
|
|
18
|
+
- **Fail-closed by default.** On any policy error, a missing engine, or an
|
|
19
|
+
evaluation failure, the decision is DENY. The gate never silently allows. An
|
|
20
|
+
observe mode exists for shadow rollout, but even there a call that would be
|
|
21
|
+
blocked is flagged `would_deny: true`, so a failure is never silent.
|
|
22
|
+
- **It proves its own restraint.** `serve --enforce` and `doctor` run a startup
|
|
23
|
+
self-test and refuse to arm the gate unless they can show that a known-forbidden
|
|
24
|
+
action is actually denied. A gate that cannot prove it denies does not start.
|
|
25
|
+
- **Every decision is a receipt anyone can verify.** Decisions are Ed25519-signed
|
|
26
|
+
and verifiable offline with [`@veritasacta/verify`](https://www.npmjs.com/package/@veritasacta/verify).
|
|
27
|
+
No vendor trust required: the math does not care who runs it.
|
|
28
|
+
|
|
29
|
+
## Quickstart (30 seconds)
|
|
80
30
|
|
|
81
31
|
```bash
|
|
82
|
-
# 1. Generate
|
|
83
|
-
npx protect-mcp init
|
|
32
|
+
# 1. Generate an Ed25519 keypair, a config template, and a sample policy.
|
|
33
|
+
npx protect-mcp init
|
|
84
34
|
|
|
85
|
-
# 2.
|
|
86
|
-
|
|
35
|
+
# 2. Put a Cedar policy in ./cedar (see "Write a policy" below), then serve
|
|
36
|
+
# the Claude Code hook gate in enforce mode. It runs a restraint self-test
|
|
37
|
+
# first and refuses to start if it cannot prove it denies a forbidden vector.
|
|
38
|
+
npx protect-mcp serve --enforce --cedar ./cedar
|
|
87
39
|
```
|
|
88
40
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
To also connect to the ScopeBlind dashboard in one step:
|
|
41
|
+
One-shot evaluation, the way a PreToolUse hook calls it. Exit code 2 means deny
|
|
42
|
+
(the tool is blocked); exit 0 means allow:
|
|
92
43
|
|
|
93
44
|
```bash
|
|
94
|
-
npx protect-mcp
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
Creates a free ScopeBlind dashboard and configures receipt upload automatically. No signup required. Free up to 20,000 receipts/month.
|
|
98
|
-
|
|
99
|
-
### What `init-hooks` creates
|
|
100
|
-
|
|
101
|
-
| File | Purpose |
|
|
102
|
-
|------|---------|
|
|
103
|
-
| `.claude/settings.json` | Hook config (PreToolUse, PostToolUse, + 9 lifecycle events) |
|
|
104
|
-
| `keys/gateway.json` | Ed25519 signing keypair (auto-gitignored) |
|
|
105
|
-
| `policies/agent.cedar` | Starter Cedar policy — customize to your needs |
|
|
106
|
-
| `protect-mcp.json` | JSON policy with signing + rate limits |
|
|
107
|
-
| `.claude/skills/verify-receipt/SKILL.md` | `/verify-receipt` skill for Claude Code |
|
|
108
|
-
|
|
109
|
-
### Architecture
|
|
45
|
+
npx protect-mcp evaluate --cedar ./cedar --tool Bash --input '{"command":"rm"}'
|
|
46
|
+
echo $? # 2 -> denied, fail-closed
|
|
110
47
|
|
|
48
|
+
npx protect-mcp evaluate --cedar ./cedar --tool Read --input '{"path":"README.md"}'
|
|
49
|
+
echo $? # 0 -> allowed
|
|
111
50
|
```
|
|
112
|
-
Claude Code → POST /hook → protect-mcp (Cedar + sign) → response
|
|
113
|
-
↓
|
|
114
|
-
.protect-mcp-log.jsonl
|
|
115
|
-
.protect-mcp-receipts.jsonl
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
- **PreToolUse**: synchronous Cedar policy check → deny blocks the tool
|
|
119
|
-
- **PostToolUse**: async receipt signing → zero latency impact
|
|
120
|
-
- **deny is architecturally final** — it cannot be overridden by the model or other hooks
|
|
121
|
-
|
|
122
|
-
### Endpoints
|
|
123
|
-
|
|
124
|
-
| Method | Path | Description |
|
|
125
|
-
|--------|------|-------------|
|
|
126
|
-
| POST | `/hook` | Claude Code hook endpoint |
|
|
127
|
-
| GET | `/health` | Server status, policy info, signer info |
|
|
128
|
-
| GET | `/receipts` | Recent signed receipts |
|
|
129
|
-
| GET | `/receipts/latest` | Most recent receipt |
|
|
130
|
-
| GET | `/suggestions` | Auto-generated Cedar policy fix suggestions |
|
|
131
|
-
| GET | `/alerts` | Config tamper detection alerts |
|
|
132
51
|
|
|
133
|
-
|
|
52
|
+
A missing or unloadable policy denies (exit 2) unless you explicitly pass
|
|
53
|
+
`--fail-on-missing-policy false`.
|
|
134
54
|
|
|
135
|
-
|
|
136
|
-
# Inside Claude Code:
|
|
137
|
-
/verify-receipt
|
|
55
|
+
## Claude Code hooks
|
|
138
56
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
57
|
+
`protect-mcp init-hooks` writes a `.claude/settings.json` for you. To wire the
|
|
58
|
+
gate by hand, the two verbs you need are `evaluate` (PreToolUse, blocks on exit 2)
|
|
59
|
+
and `sign` (PostToolUse, records a receipt). Pin the version so a Claude Code
|
|
60
|
+
session always runs the gate you tested:
|
|
142
61
|
|
|
143
|
-
|
|
144
|
-
|
|
62
|
+
```json
|
|
63
|
+
{
|
|
64
|
+
"hooks": {
|
|
65
|
+
"PreToolUse": [
|
|
66
|
+
{
|
|
67
|
+
"matcher": "",
|
|
68
|
+
"hooks": [
|
|
69
|
+
{
|
|
70
|
+
"type": "command",
|
|
71
|
+
"command": "npx protect-mcp@0.7.0 evaluate --cedar ./cedar --tool \"$TOOL_NAME\" --input \"$TOOL_INPUT\""
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
],
|
|
76
|
+
"PostToolUse": [
|
|
77
|
+
{
|
|
78
|
+
"matcher": "",
|
|
79
|
+
"hooks": [
|
|
80
|
+
{
|
|
81
|
+
"type": "command",
|
|
82
|
+
"command": "npx protect-mcp@0.7.0 sign --tool \"$TOOL_NAME\" --receipts ./receipts --key ./keys/gateway.json"
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]
|
|
87
|
+
}
|
|
88
|
+
}
|
|
145
89
|
```
|
|
146
90
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
91
|
+
`evaluate` exits 2 on deny so Claude Code blocks the tool call, and 0 on allow.
|
|
92
|
+
`sign` is best-effort: it appends an Ed25519-signed receipt when a key is
|
|
93
|
+
configured, and if no signer is available it records an honest unsigned line
|
|
94
|
+
(`"signed": false`) rather than failing the tool.
|
|
150
95
|
|
|
151
|
-
|
|
152
|
-
# Shadow mode — log every tool call, enforce nothing
|
|
153
|
-
npx protect-mcp -- node my-server.js
|
|
96
|
+
## Use it in other agents (Codex, Cursor, Gemini, Hermes)
|
|
154
97
|
|
|
155
|
-
|
|
156
|
-
|
|
98
|
+
The same fail-closed gate runs as a tool hook in any agent that supports them. Add
|
|
99
|
+
`--format <host>` so the verb reads that host's hook payload from stdin and denies
|
|
100
|
+
in its contract:
|
|
157
101
|
|
|
158
|
-
|
|
159
|
-
|
|
102
|
+
```bash
|
|
103
|
+
# the PreToolUse / before-tool command for each host
|
|
104
|
+
npx -y protect-mcp@latest evaluate --format codex --cedar ./cedar # OpenAI Codex
|
|
105
|
+
npx -y protect-mcp@latest evaluate --format gemini --cedar ./cedar # Gemini CLI BeforeTool
|
|
106
|
+
npx -y protect-mcp@latest evaluate --format cursor --cedar ./cedar # Cursor beforeShellExecution
|
|
107
|
+
npx -y protect-mcp@latest evaluate --format hermes --cedar ./cedar # Hermes pre_tool_call
|
|
160
108
|
```
|
|
161
109
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
110
|
+
Pair each with `sign --format <host>` on the post-tool event for receipts. The
|
|
111
|
+
important case is **Hermes**, which ignores hook exit codes and reads the verdict
|
|
112
|
+
from stdout, so `--format hermes` denies via `{"decision":"block"}` rather than
|
|
113
|
+
exit 2 (a raw exit-2 would silently fail open there). Without `--format`, the
|
|
114
|
+
verbs read `--tool`/`--input` flags exactly as in the Claude Code section above.
|
|
167
115
|
|
|
168
|
-
|
|
169
|
-
|------|-----------|----------|
|
|
170
|
-
| Hook Server | HTTP (`npx protect-mcp serve`) | Claude Code, agent swarms |
|
|
171
|
-
| Stdio Proxy | stdin/stdout (`npx protect-mcp -- ...`) | Claude Desktop, Cursor, any MCP client |
|
|
116
|
+
## Write a policy
|
|
172
117
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|--------|--------|-------|
|
|
177
|
-
| JSON | `--policy policy.json` | Simple per-tool rules |
|
|
178
|
-
| Cedar | `--cedar ./policies/` | Local WASM evaluation via `@cedar-policy/cedar-wasm` |
|
|
179
|
-
| External PDP | `policy_engine: "external"` | OPA, Cerbos, or any HTTP PDP |
|
|
180
|
-
|
|
181
|
-
## Swarm Tracking
|
|
182
|
-
|
|
183
|
-
In multi-agent sessions, protect-mcp automatically tracks the swarm topology.
|
|
184
|
-
|
|
185
|
-
**11 hook events handled:**
|
|
186
|
-
|
|
187
|
-
| Event | Type | Description |
|
|
188
|
-
|-------|------|-------------|
|
|
189
|
-
| `PreToolUse` | Sync | Cedar/policy evaluation before tool execution |
|
|
190
|
-
| `PostToolUse` | Async | Receipt signing after tool execution |
|
|
191
|
-
| `SubagentStart` / `SubagentStop` | Lifecycle | Worker agent spawn/completion |
|
|
192
|
-
| `TaskCreated` / `TaskCompleted` | Lifecycle | Coordinator task assignment |
|
|
193
|
-
| `SessionStart` / `SessionEnd` | Lifecycle | Session lifecycle with sandbox detection |
|
|
194
|
-
| `TeammateIdle` | Lifecycle | Agent utilization monitoring |
|
|
195
|
-
| `ConfigChange` | Security | Tamper detection for `.claude/settings.json` |
|
|
196
|
-
| `Stop` | Lifecycle | Finalization + policy suggestion summary |
|
|
197
|
-
|
|
198
|
-
Each receipt includes:
|
|
199
|
-
- `swarm.agent_id`, `swarm.agent_type`, `swarm.team_name`
|
|
200
|
-
- `timing.tool_duration_ms`, `timing.hook_latency_ms`
|
|
201
|
-
- `payload_digest` (SHA-256 hash for payloads >1KB)
|
|
202
|
-
- `deny_iteration` (retry count after denial)
|
|
203
|
-
- `sandbox_state` (enabled/disabled/unavailable)
|
|
204
|
-
- OpenTelemetry `otel_trace_id` and `otel_span_id`
|
|
205
|
-
|
|
206
|
-
## Policy File
|
|
207
|
-
|
|
208
|
-
```json
|
|
209
|
-
{
|
|
210
|
-
"default_tier": "unknown",
|
|
211
|
-
"tools": {
|
|
212
|
-
"dangerous_tool": { "block": true },
|
|
213
|
-
"admin_tool": { "min_tier": "signed-known", "rate_limit": "5/hour" },
|
|
214
|
-
"read_tool": { "require": "any", "rate_limit": "100/hour" },
|
|
215
|
-
"*": { "rate_limit": "500/hour" }
|
|
216
|
-
},
|
|
217
|
-
"signing": {
|
|
218
|
-
"key_path": "./keys/gateway.json",
|
|
219
|
-
"issuer": "protect-mcp",
|
|
220
|
-
"enabled": true
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
### Cedar Policies
|
|
226
|
-
|
|
227
|
-
Cedar deny decisions are **authoritative** — they cannot be overridden.
|
|
118
|
+
Cedar policies live in a directory you point at with `--cedar`. A `forbid` rule
|
|
119
|
+
denies, a `permit` rule allows. To match against a value in the tool input, use
|
|
120
|
+
the `.contains()` idiom:
|
|
228
121
|
|
|
229
122
|
```cedar
|
|
230
|
-
// Allow read-only tools
|
|
123
|
+
// Allow read-only tools.
|
|
231
124
|
permit(
|
|
232
125
|
principal,
|
|
233
126
|
action == Action::"MCP::Tool::call",
|
|
234
127
|
resource == Tool::"Read"
|
|
235
128
|
);
|
|
236
129
|
|
|
237
|
-
//
|
|
130
|
+
// Deny dangerous shell commands by matching the command against a list.
|
|
131
|
+
forbid(
|
|
132
|
+
principal,
|
|
133
|
+
action == Action::"MCP::Tool::call",
|
|
134
|
+
resource == Tool::"Bash"
|
|
135
|
+
) when {
|
|
136
|
+
["rm", "dd", "mkfs"].contains(context.command)
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// Block destructive tools outright.
|
|
238
140
|
forbid(
|
|
239
141
|
principal,
|
|
240
142
|
action == Action::"MCP::Tool::call",
|
|
@@ -242,239 +144,67 @@ forbid(
|
|
|
242
144
|
);
|
|
243
145
|
```
|
|
244
146
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
| `clinejection.json` | CVE-2025-6514: MCP OAuth proxy hijack (437K environments) | A01, A03 |
|
|
254
|
-
| `terraform-destroy.json` | Autonomous Terraform agent destroys production | A05, A06 |
|
|
255
|
-
| `github-mcp-hijack.json` | Prompt injection via crafted GitHub issue | A01, A02, A03 |
|
|
256
|
-
| `data-exfiltration.json` | Agent data theft via outbound tool abuse | A02, A04 |
|
|
257
|
-
| `financial-safe.json` | Unauthorized financial transaction | A05, A06 |
|
|
258
|
-
|
|
259
|
-
Cedar equivalents available in `policies/cedar/`.
|
|
260
|
-
|
|
261
|
-
## MCP Client Configuration
|
|
262
|
-
|
|
263
|
-
### Claude Desktop
|
|
264
|
-
|
|
265
|
-
```json
|
|
266
|
-
{
|
|
267
|
-
"mcpServers": {
|
|
268
|
-
"my-protected-server": {
|
|
269
|
-
"command": "npx",
|
|
270
|
-
"args": [
|
|
271
|
-
"-y", "protect-mcp",
|
|
272
|
-
"--policy", "/path/to/protect-mcp.json",
|
|
273
|
-
"--enforce",
|
|
274
|
-
"--", "node", "my-server.js"
|
|
275
|
-
]
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
### Cursor / VS Code
|
|
282
|
-
|
|
283
|
-
Same pattern — replace the server command with `protect-mcp` wrapping it.
|
|
284
|
-
|
|
285
|
-
## CLI Commands
|
|
286
|
-
|
|
287
|
-
```
|
|
288
|
-
Commands:
|
|
289
|
-
serve Start HTTP hook server for Claude Code (port 9377)
|
|
290
|
-
init-hooks Generate Claude Code hook config + skill + sample Cedar policy
|
|
291
|
-
quickstart Zero-config onboarding: init + demo + show receipts
|
|
292
|
-
connect Link to ScopeBlind dashboard (creates sandbox if needed)
|
|
293
|
-
init Generate Ed25519 keypair + config template
|
|
294
|
-
demo Start a demo server wrapped with protect-mcp
|
|
295
|
-
doctor Check your setup: keys, policies, verifier, connectivity
|
|
296
|
-
trace <id> Visualize the receipt DAG from a given receipt_id
|
|
297
|
-
status Show tool call statistics from the decision log
|
|
298
|
-
digest Generate a human-readable summary of agent activity
|
|
299
|
-
receipts Show recent persisted signed receipts
|
|
300
|
-
bundle Export an offline-verifiable audit bundle
|
|
301
|
-
simulate Dry-run a policy against recorded tool calls
|
|
302
|
-
report Generate a compliance report from an audit bundle
|
|
303
|
-
|
|
304
|
-
Options:
|
|
305
|
-
--policy <path> Policy/config JSON file
|
|
306
|
-
--cedar <dir> Cedar policy directory
|
|
307
|
-
--enforce Enable enforcement mode (default: shadow)
|
|
308
|
-
--port <port> HTTP server port (default: 9377 for serve)
|
|
309
|
-
--verbose Enable debug logging
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
## Decision Logs
|
|
313
|
-
|
|
314
|
-
Every tool call emits structured JSON to `stderr`:
|
|
315
|
-
|
|
316
|
-
```json
|
|
317
|
-
[PROTECT_MCP] {"v":2,"tool":"read_file","decision":"allow","reason_code":"cedar_allow","policy_digest":"a1b2c3...","mode":"enforce","hook_event":"PreToolUse","timing":{"hook_latency_ms":1},"otel_trace_id":"..."}
|
|
318
|
-
```
|
|
319
|
-
|
|
320
|
-
When signing is configured, a signed receipt is persisted to `.protect-mcp-receipts.jsonl`.
|
|
321
|
-
|
|
322
|
-
## Audit Bundles
|
|
323
|
-
|
|
324
|
-
```bash
|
|
325
|
-
npx protect-mcp bundle --output audit.json
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
Self-contained offline-verifiable bundle with receipts + signing keys. Verify with `npx @veritasacta/verify`.
|
|
329
|
-
|
|
330
|
-
## Dashboard
|
|
331
|
-
|
|
332
|
-
protect-mcp works fully offline. Optionally connect to the ScopeBlind dashboard for:
|
|
333
|
-
|
|
334
|
-
- Real-time receipt visualization
|
|
335
|
-
- Abuse alerts and anomaly detection
|
|
336
|
-
- Compliance export and audit bundles
|
|
337
|
-
- Usage analytics
|
|
338
|
-
|
|
339
|
-
Free tier: 20,000 receipts/month. No credit card required.
|
|
147
|
+
> **Hazard:** do NOT write `context.command in ["rm", "dd"]` to match a string
|
|
148
|
+
> against a list. `in` is for entity hierarchies, not string membership. Cedar
|
|
149
|
+
> treats the expression as a type error and silently discards the whole `forbid`
|
|
150
|
+
> rule, which (under a fail-open gate) leaves a residual `permit` standing. This
|
|
151
|
+
> is the exact defect behind the advisory below. Use `[...].contains(context.command)`
|
|
152
|
+
> instead. From 0.7.0 the gate denies on that error rather than permitting, and a
|
|
153
|
+
> CI tripwire test fails the build if the pattern is reintroduced into a shipped
|
|
154
|
+
> policy. See [GHSA-hm46-7j72-rpv9](https://github.com/ScopeBlind/scopeblind-gateway/security/advisories/GHSA-hm46-7j72-rpv9).
|
|
340
155
|
|
|
341
|
-
|
|
156
|
+
Ready-to-use Cedar packs ship in `policies/cedar/` (Clinejection / CVE-2025-6514,
|
|
157
|
+
Terraform destroy, secret-file exfiltration, spending authority).
|
|
342
158
|
|
|
343
|
-
|
|
159
|
+
## Verify a receipt
|
|
344
160
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
signed receipt to your dashboard at `https://scopeblind.com/console/<your-slug>`.
|
|
161
|
+
Receipts are signed and verifiable offline by anyone with the public key. No
|
|
162
|
+
network, no vendor, no trust in ScopeBlind:
|
|
348
163
|
|
|
349
164
|
```bash
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
npx protect-mcp init-hooks
|
|
165
|
+
npx @veritasacta/verify ./receipts/receipts.jsonl --format jsonl
|
|
166
|
+
# Exit 0 = valid, non-zero = tampered or malformed
|
|
353
167
|
```
|
|
354
168
|
|
|
355
|
-
|
|
169
|
+
`npx protect-mcp bundle --output audit.json` exports a self-contained,
|
|
170
|
+
offline-verifiable audit bundle of your receipts plus the public signing key.
|
|
356
171
|
|
|
357
|
-
|
|
358
|
-
auth proof at `/fn/brass/issue` (ECDSA P-256 signed, hourly expiry).
|
|
359
|
-
2. Receipts are batched and POSTed to `/fn/console/<slug>/receipts` every 5
|
|
360
|
-
seconds (up to 128 per batch).
|
|
361
|
-
3. The local `.receipts/` chain remains authoritative regardless of forward
|
|
362
|
-
status. Network failures are retried; quota exhaustion is reported. No
|
|
363
|
-
crash, no blocking.
|
|
172
|
+
## Security
|
|
364
173
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
174
|
+
`protect-mcp` 0.7.0 fails closed by design. On any policy-evaluation error, a
|
|
175
|
+
missing engine, or a policy that errored at evaluation, the decision is DENY,
|
|
176
|
+
not allow. `serve --enforce` and `doctor` run a boot self-test that proves the
|
|
177
|
+
gate denies a known-forbidden vector before it is trusted, and refuse to arm if
|
|
178
|
+
it cannot.
|
|
368
179
|
|
|
369
|
-
|
|
180
|
+
**Affected versions: 0.5.x and 0.6.x.** Those lines fail open (they return ALLOW
|
|
181
|
+
on evaluation error) and do not evaluate Cedar correctly against the pinned
|
|
182
|
+
engine, so a `forbid` rule could fail to block. **Upgrade to >= 0.7.0.**
|
|
370
183
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
curl https://scopeblind.com/.well-known/jwks.json
|
|
374
|
-
npx @veritasacta/verify .receipts/0001.json
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
Verification is independent of ScopeBlind — the math doesn't care who runs it.
|
|
378
|
-
|
|
379
|
-
## Interoperability
|
|
184
|
+
Details and remediation: [GHSA-hm46-7j72-rpv9](https://github.com/ScopeBlind/scopeblind-gateway/security/advisories/GHSA-hm46-7j72-rpv9).
|
|
185
|
+
To report a vulnerability, see [SECURITY.md](./SECURITY.md).
|
|
380
186
|
|
|
381
|
-
|
|
187
|
+
## Commands
|
|
382
188
|
|
|
383
|
-
|
|
|
384
|
-
|
|
385
|
-
|
|
|
386
|
-
|
|
|
387
|
-
|
|
|
388
|
-
|
|
|
389
|
-
|
|
|
189
|
+
| Command | Description |
|
|
190
|
+
|---------|-------------|
|
|
191
|
+
| `serve` | Start the HTTP hook server for Claude Code (port 9377). `--enforce` runs the restraint self-test first; `--cedar <dir>` and `--policy <path>` select the policy. |
|
|
192
|
+
| `init` | Generate an Ed25519 keypair (`keys/gateway.json`), a config template, and a sample policy. |
|
|
193
|
+
| `evaluate` | Evaluate one tool call against a Cedar policy (PreToolUse gate). Exit 2 = deny (fail-closed), exit 0 = allow. |
|
|
194
|
+
| `sign` | Sign one tool call into a receipt (PostToolUse). Best-effort: records an honest unsigned line if no key. |
|
|
195
|
+
| `simulate` | Dry-run a policy against a recorded decision log to see what it would have blocked. |
|
|
196
|
+
| `demo` | Start a built-in demo server wrapped with the gate, to see receipts instantly. |
|
|
197
|
+
| `doctor` | Check your setup (keys, policies, Cedar engine, verifier) and run the restraint self-test. |
|
|
198
|
+
| `bundle` | Export an offline-verifiable audit bundle of receipts plus the public key. |
|
|
199
|
+
| `report` | Generate a compliance report (Markdown or JSON) from the decision log and receipts. |
|
|
390
200
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
```bash
|
|
394
|
-
npx @veritasacta/verify receipt.json --key <public-key-hex>
|
|
395
|
-
# Exit 0 = valid, 1 = tampered, 2 = malformed
|
|
396
|
-
```
|
|
397
|
-
|
|
398
|
-
## Standards & IP
|
|
399
|
-
|
|
400
|
-
- **IETF Internet-Draft**: [draft-farley-acta-signed-receipts-01](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)
|
|
401
|
-
- **Patent Status**: 4 Australian provisional patents pending (2025-2026)
|
|
402
|
-
- **Cedar WASM**: [PR #64](https://github.com/cedar-policy/cedar-for-agents/pull/64) merged + [PR #73](https://github.com/cedar-policy/cedar-for-agents/pull/73) (RequestGenerator, pending review)
|
|
403
|
-
|
|
404
|
-
## What's New in v0.7.0 (security release)
|
|
405
|
-
|
|
406
|
-
The gate now fails closed. On any policy-evaluation error, a missing engine, or a
|
|
407
|
-
policy that errored, the decision is DENY, not allow. Cedar is also evaluated
|
|
408
|
-
correctly against cedar-wasm 4.x (earlier versions passed the policy in a shape the
|
|
409
|
-
engine rejected, so evaluation errored and the old fail-open default allowed
|
|
410
|
-
everything). `serve --enforce` now runs a restraint self-test before arming and
|
|
411
|
-
refuses to start if it cannot prove it denies a forbidden vector, `doctor` reports
|
|
412
|
-
that self-test, and one-shot `evaluate` and `sign` verbs are available for hooks.
|
|
413
|
-
Upgrade from any 0.5.x or 0.6.x. See CHANGELOG.md.
|
|
414
|
-
|
|
415
|
-
## What's New in v0.5.3
|
|
416
|
-
|
|
417
|
-
- `quickstart --connect`: Auto-create dashboard sandbox and configure receipt upload
|
|
418
|
-
- `connect` subcommand: Link an existing setup to the ScopeBlind dashboard
|
|
419
|
-
- Anonymous install telemetry (opt-out: `PROTECT_MCP_TELEMETRY=off`)
|
|
420
|
-
- Improved Cedar WASM detection
|
|
421
|
-
|
|
422
|
-
## Cybersecurity: Vulnerability Disclosure Receipts
|
|
423
|
-
|
|
424
|
-
protect-mcp provides the infrastructure for receipt-signed vulnerability disclosure workflows. When AI security agents (Claude Code Security, Mythos, or similar) discover vulnerabilities, every step of the disclosure lifecycle can produce a signed, chain-linked receipt:
|
|
425
|
-
|
|
426
|
-
```
|
|
427
|
-
DISCOVER → DISCLOSE → PATCH → DEPLOY
|
|
428
|
-
(Each step: Ed25519-signed, chain-linked, Cedar policy-bound)
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
Cedar policies govern what the scanning agent is allowed to do:
|
|
432
|
-
- **CAN**: scan code, report findings internally
|
|
433
|
-
- **CANNOT**: disclose externally or deploy patches without human approval
|
|
434
|
-
- **MUST**: escalate critical findings to humans
|
|
435
|
-
|
|
436
|
-
See the [security vulnerability disclosure example](https://github.com/ScopeBlind/examples/tree/main/security-vulnerability-disclosure) for a complete working implementation with Cedar policies and example receipt chains.
|
|
437
|
-
|
|
438
|
-
Related: [Vulnerability Disclosure Receipt Design](https://github.com/scopeblind/scopeblind-gateway/issues/2)
|
|
439
|
-
|
|
440
|
-
## Examples
|
|
441
|
-
|
|
442
|
-
See complete working examples at [github.com/ScopeBlind/examples](https://github.com/ScopeBlind/examples):
|
|
443
|
-
- [Claude Code hooks](https://github.com/ScopeBlind/examples/tree/main/claude-code-hooks) — receipt signing for every tool call
|
|
444
|
-
- [Security vulnerability disclosure](https://github.com/ScopeBlind/examples/tree/main/security-vulnerability-disclosure) — receipt-signed disclosure lifecycle with Cedar governance
|
|
445
|
-
- [MCP server signing](https://github.com/ScopeBlind/examples/tree/main/mcp-server-signing) — Cedar WASM policy engine with audit bundles
|
|
446
|
-
|
|
447
|
-
## ScopeBlind Dashboard
|
|
448
|
-
|
|
449
|
-
protect-mcp works fully offline, forever, for free. For teams that want visibility across agents, ScopeBlind offers a hosted dashboard:
|
|
450
|
-
|
|
451
|
-
```bash
|
|
452
|
-
npx protect-mcp connect
|
|
453
|
-
```
|
|
454
|
-
|
|
455
|
-
| | Free | Pro | Enterprise |
|
|
456
|
-
|---|---|---|---|
|
|
457
|
-
| Receipts/month | 20,000 | Pay-as-you-go | Annual commit |
|
|
458
|
-
| Price | $0 | $0.50 / 1K | $0.40 / 1K |
|
|
459
|
-
| Receipt explorer | Yes | Yes | Yes |
|
|
460
|
-
| Compliance reports | Yes | Yes | Yes |
|
|
461
|
-
| SSO / SAML | - | - | Yes |
|
|
462
|
-
| SLA | - | - | 99.9% |
|
|
463
|
-
|
|
464
|
-
No signup required for free tier. No card upfront.
|
|
465
|
-
|
|
466
|
-
[Dashboard](https://scopeblind.com) | [Docs](https://scopeblind.com/docs/protect-mcp) | [Pricing](https://scopeblind.com/pricing)
|
|
467
|
-
|
|
468
|
-
## Telemetry
|
|
469
|
-
|
|
470
|
-
protect-mcp sends a single anonymous install beacon on first run (package name, version, OS, Node version). No PII. Disable with:
|
|
471
|
-
|
|
472
|
-
```bash
|
|
473
|
-
PROTECT_MCP_TELEMETRY=off
|
|
474
|
-
```
|
|
201
|
+
Run `npx protect-mcp --help` for the full flag reference.
|
|
475
202
|
|
|
476
|
-
##
|
|
203
|
+
## Links
|
|
477
204
|
|
|
478
|
-
|
|
205
|
+
- Protocol (IETF): [draft-farley-acta-signed-receipts](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)
|
|
206
|
+
- [CHANGELOG](./CHANGELOG.md)
|
|
207
|
+
- [npm](https://www.npmjs.com/package/protect-mcp)
|
|
208
|
+
- [scopeblind.com](https://scopeblind.com)
|
|
479
209
|
|
|
480
|
-
Built by [ScopeBlind](https://scopeblind.com)
|
|
210
|
+
MIT licensed. Built by [ScopeBlind](https://scopeblind.com).
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
This policy covers the `protect-mcp` npm package.
|
|
4
|
+
|
|
5
|
+
## Supported versions
|
|
6
|
+
|
|
7
|
+
| Version | Supported |
|
|
8
|
+
|---------|-----------|
|
|
9
|
+
| >= 0.7.0 | Yes |
|
|
10
|
+
| 0.6.x | No, upgrade |
|
|
11
|
+
| 0.5.x | No, upgrade |
|
|
12
|
+
|
|
13
|
+
## Affected versions
|
|
14
|
+
|
|
15
|
+
The 0.5.x and 0.6.x lines have a fail-open gate: the Cedar policy was not
|
|
16
|
+
evaluated correctly against the pinned engine, and the evaluator returned ALLOW
|
|
17
|
+
on evaluation error. A `forbid` rule could therefore fail to block. This is fixed
|
|
18
|
+
in 0.7.0, which fails closed (denies) on any evaluation error, missing engine, or
|
|
19
|
+
errored policy. If you are on 0.5.x or 0.6.x, upgrade to >= 0.7.0.
|
|
20
|
+
|
|
21
|
+
Advisory: [GHSA-hm46-7j72-rpv9](https://github.com/ScopeBlind/scopeblind-gateway/security/advisories/GHSA-hm46-7j72-rpv9).
|
|
22
|
+
|
|
23
|
+
## Design posture
|
|
24
|
+
|
|
25
|
+
The gate fails closed by default. On any policy error, a missing engine, or an
|
|
26
|
+
evaluation failure, the decision is DENY, never a silent ALLOW. Before arming an
|
|
27
|
+
enforcing gate, `serve --enforce` and `doctor` run a boot self-test that proves
|
|
28
|
+
the gate denies a known-forbidden vector, and refuse to start if it cannot. The
|
|
29
|
+
observe mode that allows on error is opt-in, and even then flags any call that
|
|
30
|
+
would be blocked as `would_deny: true`.
|
|
31
|
+
|
|
32
|
+
## Reporting a vulnerability
|
|
33
|
+
|
|
34
|
+
Please report security issues privately. Do not open a public issue for an
|
|
35
|
+
unpatched vulnerability.
|
|
36
|
+
|
|
37
|
+
- Email: security@scopeblind.com
|
|
38
|
+
- Or open a private advisory via [GitHub Security Advisories](https://github.com/ScopeBlind/scopeblind-gateway/security/advisories/new).
|
|
39
|
+
|
|
40
|
+
Include the affected version, a description, and (if possible) a minimal
|
|
41
|
+
reproduction. We aim to acknowledge reports within 3 business days and to ship a
|
|
42
|
+
fix or a clear remediation plan as quickly as the severity warrants.
|
|
43
|
+
|
|
44
|
+
## Disclosure and credit
|
|
45
|
+
|
|
46
|
+
We follow coordinated disclosure: we work with you on a fix and a timeline before
|
|
47
|
+
any public detail is released. Reporters who disclose responsibly are credited in
|
|
48
|
+
the advisory and the CHANGELOG unless they ask to remain anonymous.
|
package/dist/cli.js
CHANGED
|
@@ -7622,17 +7622,65 @@ function loadPolicyArg(argv) {
|
|
|
7622
7622
|
}
|
|
7623
7623
|
return null;
|
|
7624
7624
|
}
|
|
7625
|
+
async function readHookStdin() {
|
|
7626
|
+
if (process.stdin.isTTY) return null;
|
|
7627
|
+
try {
|
|
7628
|
+
const chunks = [];
|
|
7629
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
7630
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
7631
|
+
return raw ? JSON.parse(raw) : null;
|
|
7632
|
+
} catch {
|
|
7633
|
+
return null;
|
|
7634
|
+
}
|
|
7635
|
+
}
|
|
7636
|
+
function mapHookPayload(j) {
|
|
7637
|
+
const tool = j.tool_name ?? j.toolName;
|
|
7638
|
+
const input = j.tool_input ?? j.toolInput;
|
|
7639
|
+
if (input === void 0 && j.command !== void 0) {
|
|
7640
|
+
return { tool: tool ?? "Bash", input: { command: j.command } };
|
|
7641
|
+
}
|
|
7642
|
+
return { tool, input };
|
|
7643
|
+
}
|
|
7644
|
+
function emitDecision(format, allowed, reason) {
|
|
7645
|
+
if (format === "hermes") {
|
|
7646
|
+
process.stdout.write(JSON.stringify(allowed ? {} : { decision: "block", reason }) + "\n");
|
|
7647
|
+
process.exit(0);
|
|
7648
|
+
}
|
|
7649
|
+
if (allowed) {
|
|
7650
|
+
process.stdout.write(JSON.stringify({ allowed: true, reason }) + "\n");
|
|
7651
|
+
process.exit(0);
|
|
7652
|
+
}
|
|
7653
|
+
if (format === "cursor") {
|
|
7654
|
+
process.stdout.write(JSON.stringify({ permission: "deny", userMessage: reason }) + "\n");
|
|
7655
|
+
} else if (format === "gemini") {
|
|
7656
|
+
process.stdout.write(JSON.stringify({ decision: "deny", reason }) + "\n");
|
|
7657
|
+
}
|
|
7658
|
+
process.stderr.write(`protect-mcp denied: ${reason}
|
|
7659
|
+
`);
|
|
7660
|
+
process.exit(2);
|
|
7661
|
+
}
|
|
7625
7662
|
async function handleEvaluate(argv) {
|
|
7626
|
-
const
|
|
7627
|
-
|
|
7663
|
+
const format = flagValue(argv, "--format");
|
|
7664
|
+
let tool = flagValue(argv, "--tool") || "";
|
|
7665
|
+
let inputRaw = flagValue(argv, "--input") || "{}";
|
|
7628
7666
|
const contextRaw = flagValue(argv, "--context");
|
|
7629
7667
|
const failOnMissing = flagValue(argv, "--fail-on-missing-policy") !== "false";
|
|
7668
|
+
if (format) {
|
|
7669
|
+
const j = await readHookStdin();
|
|
7670
|
+
if (j) {
|
|
7671
|
+
const m = mapHookPayload(j);
|
|
7672
|
+
if (m.tool) tool = m.tool;
|
|
7673
|
+
if (m.input !== void 0) inputRaw = JSON.stringify(m.input);
|
|
7674
|
+
}
|
|
7675
|
+
}
|
|
7630
7676
|
const policySet = loadPolicyArg(argv);
|
|
7631
7677
|
if (!policySet) {
|
|
7632
7678
|
if (failOnMissing) {
|
|
7679
|
+
if (format) emitDecision(format, false, "policy not found (fail-closed)");
|
|
7633
7680
|
process.stderr.write("protect-mcp evaluate: policy not found; denying (fail-closed). Pass --fail-on-missing-policy false to allow.\n");
|
|
7634
7681
|
process.exit(2);
|
|
7635
7682
|
}
|
|
7683
|
+
if (format) emitDecision(format, true, "no_policy_configured");
|
|
7636
7684
|
process.stdout.write(JSON.stringify({ allowed: true, reason: "no_policy_configured" }) + "\n");
|
|
7637
7685
|
process.exit(0);
|
|
7638
7686
|
}
|
|
@@ -7653,13 +7701,22 @@ async function handleEvaluate(argv) {
|
|
|
7653
7701
|
context.command_pattern = input.command;
|
|
7654
7702
|
}
|
|
7655
7703
|
const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context }, void 0, { failClosed: true });
|
|
7704
|
+
if (format) emitDecision(format, decision.allowed, decision.reason || (decision.allowed ? "allowed" : "denied by policy"));
|
|
7656
7705
|
process.stdout.write(JSON.stringify({ allowed: decision.allowed, reason: decision.reason, policy_digest: policySet.digest }) + "\n");
|
|
7657
7706
|
process.exit(decision.allowed ? 0 : 2);
|
|
7658
7707
|
}
|
|
7659
7708
|
async function handleSign(argv) {
|
|
7660
|
-
const
|
|
7709
|
+
const format = flagValue(argv, "--format");
|
|
7710
|
+
let tool = flagValue(argv, "--tool") || "";
|
|
7661
7711
|
const receiptsDir = flagValue(argv, "--receipts") || "./receipts/";
|
|
7662
7712
|
const keyPath = flagValue(argv, "--key");
|
|
7713
|
+
if (format) {
|
|
7714
|
+
const j = await readHookStdin();
|
|
7715
|
+
if (j) {
|
|
7716
|
+
const m = mapHookPayload(j);
|
|
7717
|
+
if (m.tool) tool = m.tool;
|
|
7718
|
+
}
|
|
7719
|
+
}
|
|
7663
7720
|
if (keyPath && (0, import_node_fs10.existsSync)(keyPath)) {
|
|
7664
7721
|
try {
|
|
7665
7722
|
await initSigning({ enabled: true, key_path: keyPath });
|
|
@@ -7685,6 +7742,10 @@ async function handleSign(argv) {
|
|
|
7685
7742
|
(0, import_node_fs10.appendFileSync)((0, import_node_path6.join)(receiptsDir, "receipts.jsonl"), line + "\n");
|
|
7686
7743
|
} catch {
|
|
7687
7744
|
}
|
|
7745
|
+
if (format === "hermes") {
|
|
7746
|
+
process.stdout.write("{}\n");
|
|
7747
|
+
process.exit(0);
|
|
7748
|
+
}
|
|
7688
7749
|
process.stdout.write(JSON.stringify({ signed: Boolean(signed.signed), artifact_type: signed.artifact_type, request_id: requestId }) + "\n");
|
|
7689
7750
|
process.exit(0);
|
|
7690
7751
|
}
|
package/dist/cli.mjs
CHANGED
|
@@ -1297,17 +1297,65 @@ function loadPolicyArg(argv) {
|
|
|
1297
1297
|
}
|
|
1298
1298
|
return null;
|
|
1299
1299
|
}
|
|
1300
|
+
async function readHookStdin() {
|
|
1301
|
+
if (process.stdin.isTTY) return null;
|
|
1302
|
+
try {
|
|
1303
|
+
const chunks = [];
|
|
1304
|
+
for await (const chunk of process.stdin) chunks.push(chunk);
|
|
1305
|
+
const raw = Buffer.concat(chunks).toString("utf-8").trim();
|
|
1306
|
+
return raw ? JSON.parse(raw) : null;
|
|
1307
|
+
} catch {
|
|
1308
|
+
return null;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
function mapHookPayload(j) {
|
|
1312
|
+
const tool = j.tool_name ?? j.toolName;
|
|
1313
|
+
const input = j.tool_input ?? j.toolInput;
|
|
1314
|
+
if (input === void 0 && j.command !== void 0) {
|
|
1315
|
+
return { tool: tool ?? "Bash", input: { command: j.command } };
|
|
1316
|
+
}
|
|
1317
|
+
return { tool, input };
|
|
1318
|
+
}
|
|
1319
|
+
function emitDecision(format, allowed, reason) {
|
|
1320
|
+
if (format === "hermes") {
|
|
1321
|
+
process.stdout.write(JSON.stringify(allowed ? {} : { decision: "block", reason }) + "\n");
|
|
1322
|
+
process.exit(0);
|
|
1323
|
+
}
|
|
1324
|
+
if (allowed) {
|
|
1325
|
+
process.stdout.write(JSON.stringify({ allowed: true, reason }) + "\n");
|
|
1326
|
+
process.exit(0);
|
|
1327
|
+
}
|
|
1328
|
+
if (format === "cursor") {
|
|
1329
|
+
process.stdout.write(JSON.stringify({ permission: "deny", userMessage: reason }) + "\n");
|
|
1330
|
+
} else if (format === "gemini") {
|
|
1331
|
+
process.stdout.write(JSON.stringify({ decision: "deny", reason }) + "\n");
|
|
1332
|
+
}
|
|
1333
|
+
process.stderr.write(`protect-mcp denied: ${reason}
|
|
1334
|
+
`);
|
|
1335
|
+
process.exit(2);
|
|
1336
|
+
}
|
|
1300
1337
|
async function handleEvaluate(argv) {
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1338
|
+
const format = flagValue(argv, "--format");
|
|
1339
|
+
let tool = flagValue(argv, "--tool") || "";
|
|
1340
|
+
let inputRaw = flagValue(argv, "--input") || "{}";
|
|
1303
1341
|
const contextRaw = flagValue(argv, "--context");
|
|
1304
1342
|
const failOnMissing = flagValue(argv, "--fail-on-missing-policy") !== "false";
|
|
1343
|
+
if (format) {
|
|
1344
|
+
const j = await readHookStdin();
|
|
1345
|
+
if (j) {
|
|
1346
|
+
const m = mapHookPayload(j);
|
|
1347
|
+
if (m.tool) tool = m.tool;
|
|
1348
|
+
if (m.input !== void 0) inputRaw = JSON.stringify(m.input);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1305
1351
|
const policySet = loadPolicyArg(argv);
|
|
1306
1352
|
if (!policySet) {
|
|
1307
1353
|
if (failOnMissing) {
|
|
1354
|
+
if (format) emitDecision(format, false, "policy not found (fail-closed)");
|
|
1308
1355
|
process.stderr.write("protect-mcp evaluate: policy not found; denying (fail-closed). Pass --fail-on-missing-policy false to allow.\n");
|
|
1309
1356
|
process.exit(2);
|
|
1310
1357
|
}
|
|
1358
|
+
if (format) emitDecision(format, true, "no_policy_configured");
|
|
1311
1359
|
process.stdout.write(JSON.stringify({ allowed: true, reason: "no_policy_configured" }) + "\n");
|
|
1312
1360
|
process.exit(0);
|
|
1313
1361
|
}
|
|
@@ -1328,13 +1376,22 @@ async function handleEvaluate(argv) {
|
|
|
1328
1376
|
context.command_pattern = input.command;
|
|
1329
1377
|
}
|
|
1330
1378
|
const decision = await evaluateCedar(policySet, { tool, tier: "unknown", context }, void 0, { failClosed: true });
|
|
1379
|
+
if (format) emitDecision(format, decision.allowed, decision.reason || (decision.allowed ? "allowed" : "denied by policy"));
|
|
1331
1380
|
process.stdout.write(JSON.stringify({ allowed: decision.allowed, reason: decision.reason, policy_digest: policySet.digest }) + "\n");
|
|
1332
1381
|
process.exit(decision.allowed ? 0 : 2);
|
|
1333
1382
|
}
|
|
1334
1383
|
async function handleSign(argv) {
|
|
1335
|
-
const
|
|
1384
|
+
const format = flagValue(argv, "--format");
|
|
1385
|
+
let tool = flagValue(argv, "--tool") || "";
|
|
1336
1386
|
const receiptsDir = flagValue(argv, "--receipts") || "./receipts/";
|
|
1337
1387
|
const keyPath = flagValue(argv, "--key");
|
|
1388
|
+
if (format) {
|
|
1389
|
+
const j = await readHookStdin();
|
|
1390
|
+
if (j) {
|
|
1391
|
+
const m = mapHookPayload(j);
|
|
1392
|
+
if (m.tool) tool = m.tool;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1338
1395
|
if (keyPath && existsSyncCli(keyPath)) {
|
|
1339
1396
|
try {
|
|
1340
1397
|
await initSigning({ enabled: true, key_path: keyPath });
|
|
@@ -1360,6 +1417,10 @@ async function handleSign(argv) {
|
|
|
1360
1417
|
appendFileSyncCli(joinCli(receiptsDir, "receipts.jsonl"), line + "\n");
|
|
1361
1418
|
} catch {
|
|
1362
1419
|
}
|
|
1420
|
+
if (format === "hermes") {
|
|
1421
|
+
process.stdout.write("{}\n");
|
|
1422
|
+
process.exit(0);
|
|
1423
|
+
}
|
|
1363
1424
|
process.stdout.write(JSON.stringify({ signed: Boolean(signed.signed), artifact_type: signed.artifact_type, request_id: requestId }) + "\n");
|
|
1364
1425
|
process.exit(0);
|
|
1365
1426
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "protect-mcp",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"mcpName": "com.scopeblind/protect-mcp",
|
|
5
5
|
"description": "Fail-closed Cedar policy gate + signed receipts for AI agent tool calls. Blocks what breaks the rules before it runs, denies on any policy error, and proves the gate is live with a startup self-test.",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"dist",
|
|
26
26
|
"policies",
|
|
27
27
|
"README.md",
|
|
28
|
-
"CHANGELOG.md"
|
|
28
|
+
"CHANGELOG.md",
|
|
29
|
+
"SECURITY.md"
|
|
29
30
|
],
|
|
30
31
|
"keywords": [
|
|
31
32
|
"scopeblind",
|