protect-mcp 0.5.5 → 0.6.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/README.md +111 -3
- package/dist/{chunk-SPHLVRJ2.mjs → chunk-3YCKR72H.mjs} +223 -4
- package/dist/{ed25519-V7HDL2WC.mjs → chunk-LYKNULYU.mjs} +166 -10
- package/dist/{chunk-BYYWYSHM.mjs → chunk-PLKRTBDR.mjs} +15 -3
- package/dist/{chunk-GQWJCHQV.mjs → chunk-S4ICHNSP.mjs} +2 -2
- package/dist/{chunk-YTBC72JJ.mjs → chunk-UV53U6D4.mjs} +69 -25
- package/dist/cli.js +305 -31
- package/dist/cli.mjs +7 -7
- package/dist/ed25519-DZMMNNVE.mjs +38 -0
- package/dist/hook-server.js +283 -28
- package/dist/hook-server.mjs +2 -2
- package/dist/{http-transport-LNBENGXD.mjs → http-transport-MO32ESHZ.mjs} +2 -2
- package/dist/index.d.mts +223 -4
- package/dist/index.d.ts +223 -4
- package/dist/index.js +3057 -420
- package/dist/index.mjs +250 -11
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
# protect-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/protect-mcp)
|
|
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
|
+
```
|
|
71
|
+
|
|
72
|
+
**One-liner:** `protect-mcp` is the policy hook inside your agent.
|
|
73
|
+
`sb-runtime` is the OS sandbox around it. You want both.
|
|
74
|
+
|
|
75
|
+
---
|
|
4
76
|
|
|
5
77
|
## Quick Start — Claude Code
|
|
6
78
|
|
|
@@ -268,6 +340,42 @@ Free tier: 20,000 receipts/month. No credit card required.
|
|
|
268
340
|
|
|
269
341
|
[scopeblind.com/pricing](https://scopeblind.com/pricing)
|
|
270
342
|
|
|
343
|
+
### ScopeBlind tenant integration (Founding Plan)
|
|
344
|
+
|
|
345
|
+
If you have a ScopeBlind Founding Plan tenant, set your `SCOPEBLIND_TOKEN`
|
|
346
|
+
(from the welcome email) in the environment and protect-mcp forwards every
|
|
347
|
+
signed receipt to your dashboard at `https://scopeblind.com/console/<your-slug>`.
|
|
348
|
+
|
|
349
|
+
```bash
|
|
350
|
+
# Your protect-mcp install with cloud-synced receipts
|
|
351
|
+
SCOPEBLIND_TOKEN=scp_... \
|
|
352
|
+
npx protect-mcp init-hooks
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
How it works:
|
|
356
|
+
|
|
357
|
+
1. On first receipt, the bridge exchanges your token for a short-lived BRASS-v2
|
|
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.
|
|
364
|
+
|
|
365
|
+
Quota: founding tier 10,000 receipts/day, enterprise 100,000/day. Anything
|
|
366
|
+
above quota is rejected with a structured response; receipts stay safe in
|
|
367
|
+
`.receipts/` until you upgrade or until tomorrow.
|
|
368
|
+
|
|
369
|
+
Receipt verification by third parties:
|
|
370
|
+
|
|
371
|
+
```bash
|
|
372
|
+
# Anyone can verify your receipts offline using the public JWKS
|
|
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
|
+
|
|
271
379
|
## Interoperability
|
|
272
380
|
|
|
273
381
|
The receipt format is independently implemented and verified across multiple systems:
|
|
@@ -277,7 +385,7 @@ The receipt format is independently implemented and verified across multiple sys
|
|
|
277
385
|
| **4 independent implementations** | TypeScript (protect-mcp), Python (protect-mcp-adk), Rust (Cedar WASM), APS ProxyGateway |
|
|
278
386
|
| **2 IETF Internet-Drafts** | [draft-farley-acta-signed-receipts-01](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/), [draft-pidlisnyi-aps-00](https://datatracker.ietf.org/doc/draft-pidlisnyi-aps/) |
|
|
279
387
|
| **8 cross-engine receipts** | [Composition test](https://github.com/ScopeBlind/examples/tree/main/interop/composition-test): 2 engines, 1 verifier, all VALID |
|
|
280
|
-
| **
|
|
388
|
+
| **PRs merged into microsoft/agent-governance-toolkit** | [#667](https://github.com/microsoft/agent-governance-toolkit/pull/667), [#1159](https://github.com/microsoft/agent-governance-toolkit/pull/1159), [#1168](https://github.com/microsoft/agent-governance-toolkit/pull/1168), [#1186](https://github.com/microsoft/agent-governance-toolkit/pull/1186), [#1197](https://github.com/microsoft/agent-governance-toolkit/pull/1197), [#1202](https://github.com/microsoft/agent-governance-toolkit/pull/1202), [#1203](https://github.com/microsoft/agent-governance-toolkit/pull/1203), [#1205](https://github.com/microsoft/agent-governance-toolkit/pull/1205) |
|
|
281
389
|
| **1 verifier, zero dependencies** | `npx @veritasacta/verify receipt.json --key <hex>` (Apache-2.0, offline) |
|
|
282
390
|
|
|
283
391
|
Verify any receipt from any implementation:
|
|
@@ -291,7 +399,7 @@ npx @veritasacta/verify receipt.json --key <public-key-hex>
|
|
|
291
399
|
|
|
292
400
|
- **IETF Internet-Draft**: [draft-farley-acta-signed-receipts-01](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/)
|
|
293
401
|
- **Patent Status**: 4 Australian provisional patents pending (2025-2026)
|
|
294
|
-
- **Cedar WASM**: [PR #64](https://github.com/cedar-policy/cedar-for-agents/pull/64)
|
|
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)
|
|
295
403
|
|
|
296
404
|
## What's New in v0.5.3
|
|
297
405
|
|
|
@@ -11,13 +11,166 @@ import {
|
|
|
11
11
|
loadPolicy,
|
|
12
12
|
parseRateLimit,
|
|
13
13
|
signDecision
|
|
14
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-UV53U6D4.mjs";
|
|
15
15
|
|
|
16
16
|
// src/hook-server.ts
|
|
17
17
|
import { createServer } from "http";
|
|
18
18
|
import { createHash, randomUUID, randomBytes } from "crypto";
|
|
19
19
|
import { appendFileSync, readFileSync, existsSync, readdirSync } from "fs";
|
|
20
20
|
import { join } from "path";
|
|
21
|
+
|
|
22
|
+
// src/scopeblind-bridge.ts
|
|
23
|
+
var DEFAULT_BASE = "https://scopeblind.com";
|
|
24
|
+
var FLUSH_INTERVAL_MS = 5e3;
|
|
25
|
+
var BATCH_MAX = 128;
|
|
26
|
+
var BRASS_REFRESH_MARGIN_MS = 5 * 60 * 1e3;
|
|
27
|
+
var ScopeBlindBridge = class {
|
|
28
|
+
token;
|
|
29
|
+
base;
|
|
30
|
+
tenantOverride;
|
|
31
|
+
cachedProof = null;
|
|
32
|
+
queue = [];
|
|
33
|
+
flushTimer = null;
|
|
34
|
+
stats;
|
|
35
|
+
shuttingDown = false;
|
|
36
|
+
constructor(env = process.env) {
|
|
37
|
+
this.token = env.SCOPEBLIND_TOKEN || null;
|
|
38
|
+
this.base = (env.SCOPEBLIND_BASE || DEFAULT_BASE).replace(/\/$/, "");
|
|
39
|
+
this.tenantOverride = env.SCOPEBLIND_TENANT || null;
|
|
40
|
+
this.stats = {
|
|
41
|
+
enabled: Boolean(this.token),
|
|
42
|
+
tenant_slug: this.tenantOverride,
|
|
43
|
+
forwarded_total: 0,
|
|
44
|
+
rejected_total: 0,
|
|
45
|
+
last_flush_at: null,
|
|
46
|
+
last_error: null
|
|
47
|
+
};
|
|
48
|
+
if (this.enabled()) {
|
|
49
|
+
this.flushTimer = setInterval(() => {
|
|
50
|
+
void this.flush();
|
|
51
|
+
}, FLUSH_INTERVAL_MS);
|
|
52
|
+
if (typeof this.flushTimer === "object" && this.flushTimer && "unref" in this.flushTimer) {
|
|
53
|
+
this.flushTimer.unref?.();
|
|
54
|
+
}
|
|
55
|
+
process.on("beforeExit", () => {
|
|
56
|
+
void this.shutdown();
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
enabled() {
|
|
61
|
+
return Boolean(this.token);
|
|
62
|
+
}
|
|
63
|
+
/** Push a signed receipt into the queue. Non-blocking. */
|
|
64
|
+
forward(signedReceipt) {
|
|
65
|
+
if (!this.enabled() || this.shuttingDown) return;
|
|
66
|
+
this.queue.push(signedReceipt);
|
|
67
|
+
if (this.queue.length >= BATCH_MAX) void this.flush();
|
|
68
|
+
}
|
|
69
|
+
/** Flush the queue. Safe to call concurrently. */
|
|
70
|
+
async flush() {
|
|
71
|
+
if (!this.enabled() || this.queue.length === 0) return;
|
|
72
|
+
const batch = this.queue.splice(0, BATCH_MAX);
|
|
73
|
+
try {
|
|
74
|
+
const proof = await this.ensureBrassProof();
|
|
75
|
+
const slug = this.tenantOverride || proof?.tenant_id;
|
|
76
|
+
if (!slug) {
|
|
77
|
+
this.queue.unshift(...batch);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
this.stats.tenant_slug = slug;
|
|
81
|
+
const res = await fetch(`${this.base}/fn/console/${slug}/receipts`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: {
|
|
84
|
+
"content-type": "application/json",
|
|
85
|
+
authorization: `Bearer ${this.token}`,
|
|
86
|
+
"user-agent": "protect-mcp/scopeblind-bridge"
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify({ receipts: batch })
|
|
89
|
+
});
|
|
90
|
+
if (!res.ok) {
|
|
91
|
+
const errBody = await res.text().catch(() => "");
|
|
92
|
+
this.stats.last_error = `HTTP ${res.status} ${errBody.slice(0, 160)}`;
|
|
93
|
+
this.stats.rejected_total += batch.length;
|
|
94
|
+
if (res.status >= 500 && res.status !== 503) {
|
|
95
|
+
this.queue.unshift(...batch);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const body = await res.json().catch(() => ({}));
|
|
100
|
+
this.stats.forwarded_total += body?.accepted ?? batch.length;
|
|
101
|
+
this.stats.rejected_total += body?.rejected ?? 0;
|
|
102
|
+
this.stats.last_flush_at = (/* @__PURE__ */ new Date()).toISOString();
|
|
103
|
+
this.stats.last_error = null;
|
|
104
|
+
} catch (err) {
|
|
105
|
+
this.stats.last_error = String(err?.message || err);
|
|
106
|
+
this.queue.unshift(...batch);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Exchange SCOPEBLIND_TOKEN for a BRASS-v2 proof; refresh near expiry. */
|
|
110
|
+
async ensureBrassProof() {
|
|
111
|
+
if (!this.token) return null;
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
if (this.cachedProof && Date.parse(this.cachedProof.expires_at) - now > BRASS_REFRESH_MARGIN_MS) {
|
|
114
|
+
return this.cachedProof;
|
|
115
|
+
}
|
|
116
|
+
try {
|
|
117
|
+
const res = await fetch(`${this.base}/fn/brass/issue`, {
|
|
118
|
+
method: "POST",
|
|
119
|
+
headers: {
|
|
120
|
+
"content-type": "application/json",
|
|
121
|
+
"user-agent": "protect-mcp/scopeblind-bridge"
|
|
122
|
+
},
|
|
123
|
+
body: JSON.stringify({
|
|
124
|
+
token: this.token,
|
|
125
|
+
scope: "protect-mcp-receipt-emit",
|
|
126
|
+
ttl_seconds: 3600
|
|
127
|
+
})
|
|
128
|
+
});
|
|
129
|
+
if (!res.ok) {
|
|
130
|
+
const text = await res.text().catch(() => "");
|
|
131
|
+
this.stats.last_error = `brass-issue: HTTP ${res.status} ${text.slice(0, 160)}`;
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const body = await res.json();
|
|
135
|
+
if (!body?.auth_proof) {
|
|
136
|
+
this.stats.last_error = "brass-issue: missing auth_proof in response";
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
this.cachedProof = body.auth_proof;
|
|
140
|
+
return this.cachedProof;
|
|
141
|
+
} catch (err) {
|
|
142
|
+
this.stats.last_error = `brass-issue: ${err?.message || err}`;
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Return a snapshot of bridge stats. Useful for `protect-mcp scopeblind status`.
|
|
148
|
+
*/
|
|
149
|
+
getStats() {
|
|
150
|
+
return {
|
|
151
|
+
...this.stats,
|
|
152
|
+
queued: this.queue.length,
|
|
153
|
+
brass_proof_expires_at: this.cachedProof?.expires_at || null
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Flush remaining receipts and stop the interval. Called on process exit. */
|
|
157
|
+
async shutdown() {
|
|
158
|
+
if (this.shuttingDown) return;
|
|
159
|
+
this.shuttingDown = true;
|
|
160
|
+
if (this.flushTimer) clearInterval(this.flushTimer);
|
|
161
|
+
if (this.queue.length > 0) await this.flush();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
var singleton = null;
|
|
165
|
+
function getScopeBlindBridge() {
|
|
166
|
+
if (!singleton) singleton = new ScopeBlindBridge();
|
|
167
|
+
return singleton;
|
|
168
|
+
}
|
|
169
|
+
function forwardReceipt(signedReceipt) {
|
|
170
|
+
getScopeBlindBridge().forward(signedReceipt);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/hook-server.ts
|
|
21
174
|
var DEFAULT_PORT = 9377;
|
|
22
175
|
var LOG_FILE = ".protect-mcp-log.jsonl";
|
|
23
176
|
var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
|
|
@@ -225,7 +378,7 @@ async function handlePreToolUse(input, state) {
|
|
|
225
378
|
const hookLatency = Date.now() - hookStart;
|
|
226
379
|
const denyKey = `${toolName}:${input.sessionId || "default"}`;
|
|
227
380
|
state.denyCounter.delete(denyKey);
|
|
228
|
-
emitDecisionLog(state, {
|
|
381
|
+
const emit = emitDecisionLog(state, {
|
|
229
382
|
tool: toolName,
|
|
230
383
|
decision: "allow",
|
|
231
384
|
reason_code: state.cedarPolicies ? "cedar_allow" : state.jsonPolicy ? "policy_allow" : "observe_mode",
|
|
@@ -237,6 +390,15 @@ async function handlePreToolUse(input, state) {
|
|
|
237
390
|
sandbox_state: detectSandboxState(),
|
|
238
391
|
plan_receipt_id: state.activePlanReceiptId || void 0
|
|
239
392
|
});
|
|
393
|
+
if (state.enforce && emit.signingFailed) {
|
|
394
|
+
return {
|
|
395
|
+
hookSpecificOutput: {
|
|
396
|
+
hookEventName: "PreToolUse",
|
|
397
|
+
permissionDecision: "deny",
|
|
398
|
+
permissionDecisionReason: `[ScopeBlind] "${toolName}" was blocked because its receipt could not be signed. Failing closed: a governed action that cannot be proven is not allowed.`
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
}
|
|
240
402
|
return {};
|
|
241
403
|
}
|
|
242
404
|
async function handlePostToolUse(input, state) {
|
|
@@ -484,11 +646,35 @@ function emitDecisionLog(state, entry) {
|
|
|
484
646
|
} catch {
|
|
485
647
|
}
|
|
486
648
|
state.receiptBuffer.add(log.request_id, signed.signed);
|
|
487
|
-
|
|
488
|
-
|
|
649
|
+
try {
|
|
650
|
+
const bridge = getScopeBlindBridge();
|
|
651
|
+
if (bridge.enabled()) {
|
|
652
|
+
const parsed = typeof signed.signed === "string" ? JSON.parse(signed.signed) : signed.signed;
|
|
653
|
+
bridge.forward(parsed);
|
|
654
|
+
}
|
|
655
|
+
} catch (err) {
|
|
656
|
+
process.stderr.write(`[PROTECT_MCP] ScopeBlind forward error: ${err instanceof Error ? err.message : err}
|
|
657
|
+
`);
|
|
658
|
+
}
|
|
659
|
+
} else if (signed.error) {
|
|
660
|
+
const tombstone = JSON.stringify({
|
|
661
|
+
type: "scopeblind.signing_failure.v1",
|
|
662
|
+
request_id: log.request_id,
|
|
663
|
+
tool: log.tool,
|
|
664
|
+
decision: log.decision,
|
|
665
|
+
error: signed.error,
|
|
666
|
+
at: new Date(log.timestamp).toISOString()
|
|
667
|
+
});
|
|
668
|
+
try {
|
|
669
|
+
appendFileSync(state.receiptFilePath, tombstone + "\n");
|
|
670
|
+
} catch {
|
|
671
|
+
}
|
|
672
|
+
process.stderr.write(`[PROTECT_MCP_SIGNING_FAILURE] ${tombstone}
|
|
489
673
|
`);
|
|
674
|
+
return { signingFailed: true };
|
|
490
675
|
}
|
|
491
676
|
}
|
|
677
|
+
return { signingFailed: false };
|
|
492
678
|
}
|
|
493
679
|
async function routeHookEvent(input, state) {
|
|
494
680
|
switch (input.hookEventName) {
|
|
@@ -828,5 +1014,38 @@ function normalizeHookInput(raw) {
|
|
|
828
1014
|
}
|
|
829
1015
|
|
|
830
1016
|
export {
|
|
1017
|
+
ScopeBlindBridge,
|
|
1018
|
+
getScopeBlindBridge,
|
|
1019
|
+
forwardReceipt,
|
|
831
1020
|
startHookServer
|
|
832
1021
|
};
|
|
1022
|
+
/**
|
|
1023
|
+
* scopeblind-bridge.ts
|
|
1024
|
+
*
|
|
1025
|
+
* Optional bridge between protect-mcp (local, MIT) and a paid ScopeBlind
|
|
1026
|
+
* tenant. When SCOPEBLIND_TOKEN is set in the environment, every signed
|
|
1027
|
+
* receipt that protect-mcp emits also gets forwarded to the tenant's
|
|
1028
|
+
* dashboard at https://scopeblind.com/console/<slug>.
|
|
1029
|
+
*
|
|
1030
|
+
* Lifecycle:
|
|
1031
|
+
* 1. On first use, exchange SCOPEBLIND_TOKEN for a short-lived BRASS-v2
|
|
1032
|
+
* auth proof from /fn/brass/issue. Cache the proof in memory until
|
|
1033
|
+
* ~5 minutes before expiry, then refresh.
|
|
1034
|
+
* 2. As receipts are emitted by hook-server.ts, push them into an
|
|
1035
|
+
* in-memory batch queue.
|
|
1036
|
+
* 3. Flush the queue every 5s (or when it reaches 128 receipts) by POSTing
|
|
1037
|
+
* to /fn/console/<slug>/receipts with Bearer SCOPEBLIND_TOKEN.
|
|
1038
|
+
*
|
|
1039
|
+
* Failure mode: forward errors NEVER throw upstream. protect-mcp continues
|
|
1040
|
+
* to mint and persist receipts locally regardless of dashboard availability.
|
|
1041
|
+
* The bridge logs failures to stderr (best-effort) and retries on the next
|
|
1042
|
+
* flush.
|
|
1043
|
+
*
|
|
1044
|
+
* Configuration:
|
|
1045
|
+
* SCOPEBLIND_TOKEN Tenant bearer token (from welcome email).
|
|
1046
|
+
* SCOPEBLIND_TENANT Optional slug override. By default we discover
|
|
1047
|
+
* the slug from the BRASS proof's tenant_id.
|
|
1048
|
+
* SCOPEBLIND_BASE Defaults to https://scopeblind.com.
|
|
1049
|
+
*
|
|
1050
|
+
* @license MIT
|
|
1051
|
+
*/
|
|
@@ -12,10 +12,10 @@ import {
|
|
|
12
12
|
hexToBytes,
|
|
13
13
|
isBytes,
|
|
14
14
|
randomBytes,
|
|
15
|
+
rotr,
|
|
15
16
|
toBytes,
|
|
16
17
|
utf8ToBytes
|
|
17
18
|
} from "./chunk-D733KAPG.mjs";
|
|
18
|
-
import "./chunk-PQJP2ZCI.mjs";
|
|
19
19
|
|
|
20
20
|
// node_modules/@noble/hashes/esm/_md.js
|
|
21
21
|
function setBigUint64(view, byteOffset, value, isLE) {
|
|
@@ -30,6 +30,12 @@ function setBigUint64(view, byteOffset, value, isLE) {
|
|
|
30
30
|
view.setUint32(byteOffset + h, wh, isLE);
|
|
31
31
|
view.setUint32(byteOffset + l, wl, isLE);
|
|
32
32
|
}
|
|
33
|
+
function Chi(a, b, c) {
|
|
34
|
+
return a & b ^ ~a & c;
|
|
35
|
+
}
|
|
36
|
+
function Maj(a, b, c) {
|
|
37
|
+
return a & b ^ a & c ^ b & c;
|
|
38
|
+
}
|
|
33
39
|
var HashMD = class extends Hash {
|
|
34
40
|
constructor(blockLen, outputLen, padOffset, isLE) {
|
|
35
41
|
super();
|
|
@@ -120,6 +126,16 @@ var HashMD = class extends Hash {
|
|
|
120
126
|
return this._cloneInto();
|
|
121
127
|
}
|
|
122
128
|
};
|
|
129
|
+
var SHA256_IV = /* @__PURE__ */ Uint32Array.from([
|
|
130
|
+
1779033703,
|
|
131
|
+
3144134277,
|
|
132
|
+
1013904242,
|
|
133
|
+
2773480762,
|
|
134
|
+
1359893119,
|
|
135
|
+
2600822924,
|
|
136
|
+
528734635,
|
|
137
|
+
1541459225
|
|
138
|
+
]);
|
|
123
139
|
var SHA512_IV = /* @__PURE__ */ Uint32Array.from([
|
|
124
140
|
1779033703,
|
|
125
141
|
4089235720,
|
|
@@ -175,6 +191,143 @@ var add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >
|
|
|
175
191
|
var add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0;
|
|
176
192
|
|
|
177
193
|
// node_modules/@noble/hashes/esm/sha2.js
|
|
194
|
+
var SHA256_K = /* @__PURE__ */ Uint32Array.from([
|
|
195
|
+
1116352408,
|
|
196
|
+
1899447441,
|
|
197
|
+
3049323471,
|
|
198
|
+
3921009573,
|
|
199
|
+
961987163,
|
|
200
|
+
1508970993,
|
|
201
|
+
2453635748,
|
|
202
|
+
2870763221,
|
|
203
|
+
3624381080,
|
|
204
|
+
310598401,
|
|
205
|
+
607225278,
|
|
206
|
+
1426881987,
|
|
207
|
+
1925078388,
|
|
208
|
+
2162078206,
|
|
209
|
+
2614888103,
|
|
210
|
+
3248222580,
|
|
211
|
+
3835390401,
|
|
212
|
+
4022224774,
|
|
213
|
+
264347078,
|
|
214
|
+
604807628,
|
|
215
|
+
770255983,
|
|
216
|
+
1249150122,
|
|
217
|
+
1555081692,
|
|
218
|
+
1996064986,
|
|
219
|
+
2554220882,
|
|
220
|
+
2821834349,
|
|
221
|
+
2952996808,
|
|
222
|
+
3210313671,
|
|
223
|
+
3336571891,
|
|
224
|
+
3584528711,
|
|
225
|
+
113926993,
|
|
226
|
+
338241895,
|
|
227
|
+
666307205,
|
|
228
|
+
773529912,
|
|
229
|
+
1294757372,
|
|
230
|
+
1396182291,
|
|
231
|
+
1695183700,
|
|
232
|
+
1986661051,
|
|
233
|
+
2177026350,
|
|
234
|
+
2456956037,
|
|
235
|
+
2730485921,
|
|
236
|
+
2820302411,
|
|
237
|
+
3259730800,
|
|
238
|
+
3345764771,
|
|
239
|
+
3516065817,
|
|
240
|
+
3600352804,
|
|
241
|
+
4094571909,
|
|
242
|
+
275423344,
|
|
243
|
+
430227734,
|
|
244
|
+
506948616,
|
|
245
|
+
659060556,
|
|
246
|
+
883997877,
|
|
247
|
+
958139571,
|
|
248
|
+
1322822218,
|
|
249
|
+
1537002063,
|
|
250
|
+
1747873779,
|
|
251
|
+
1955562222,
|
|
252
|
+
2024104815,
|
|
253
|
+
2227730452,
|
|
254
|
+
2361852424,
|
|
255
|
+
2428436474,
|
|
256
|
+
2756734187,
|
|
257
|
+
3204031479,
|
|
258
|
+
3329325298
|
|
259
|
+
]);
|
|
260
|
+
var SHA256_W = /* @__PURE__ */ new Uint32Array(64);
|
|
261
|
+
var SHA256 = class extends HashMD {
|
|
262
|
+
constructor(outputLen = 32) {
|
|
263
|
+
super(64, outputLen, 8, false);
|
|
264
|
+
this.A = SHA256_IV[0] | 0;
|
|
265
|
+
this.B = SHA256_IV[1] | 0;
|
|
266
|
+
this.C = SHA256_IV[2] | 0;
|
|
267
|
+
this.D = SHA256_IV[3] | 0;
|
|
268
|
+
this.E = SHA256_IV[4] | 0;
|
|
269
|
+
this.F = SHA256_IV[5] | 0;
|
|
270
|
+
this.G = SHA256_IV[6] | 0;
|
|
271
|
+
this.H = SHA256_IV[7] | 0;
|
|
272
|
+
}
|
|
273
|
+
get() {
|
|
274
|
+
const { A, B, C, D, E, F, G, H } = this;
|
|
275
|
+
return [A, B, C, D, E, F, G, H];
|
|
276
|
+
}
|
|
277
|
+
// prettier-ignore
|
|
278
|
+
set(A, B, C, D, E, F, G, H) {
|
|
279
|
+
this.A = A | 0;
|
|
280
|
+
this.B = B | 0;
|
|
281
|
+
this.C = C | 0;
|
|
282
|
+
this.D = D | 0;
|
|
283
|
+
this.E = E | 0;
|
|
284
|
+
this.F = F | 0;
|
|
285
|
+
this.G = G | 0;
|
|
286
|
+
this.H = H | 0;
|
|
287
|
+
}
|
|
288
|
+
process(view, offset) {
|
|
289
|
+
for (let i = 0; i < 16; i++, offset += 4)
|
|
290
|
+
SHA256_W[i] = view.getUint32(offset, false);
|
|
291
|
+
for (let i = 16; i < 64; i++) {
|
|
292
|
+
const W15 = SHA256_W[i - 15];
|
|
293
|
+
const W2 = SHA256_W[i - 2];
|
|
294
|
+
const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3;
|
|
295
|
+
const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10;
|
|
296
|
+
SHA256_W[i] = s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16] | 0;
|
|
297
|
+
}
|
|
298
|
+
let { A, B, C, D, E, F, G, H } = this;
|
|
299
|
+
for (let i = 0; i < 64; i++) {
|
|
300
|
+
const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
|
|
301
|
+
const T1 = H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i] | 0;
|
|
302
|
+
const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22);
|
|
303
|
+
const T2 = sigma0 + Maj(A, B, C) | 0;
|
|
304
|
+
H = G;
|
|
305
|
+
G = F;
|
|
306
|
+
F = E;
|
|
307
|
+
E = D + T1 | 0;
|
|
308
|
+
D = C;
|
|
309
|
+
C = B;
|
|
310
|
+
B = A;
|
|
311
|
+
A = T1 + T2 | 0;
|
|
312
|
+
}
|
|
313
|
+
A = A + this.A | 0;
|
|
314
|
+
B = B + this.B | 0;
|
|
315
|
+
C = C + this.C | 0;
|
|
316
|
+
D = D + this.D | 0;
|
|
317
|
+
E = E + this.E | 0;
|
|
318
|
+
F = F + this.F | 0;
|
|
319
|
+
G = G + this.G | 0;
|
|
320
|
+
H = H + this.H | 0;
|
|
321
|
+
this.set(A, B, C, D, E, F, G, H);
|
|
322
|
+
}
|
|
323
|
+
roundClean() {
|
|
324
|
+
clean(SHA256_W);
|
|
325
|
+
}
|
|
326
|
+
destroy() {
|
|
327
|
+
this.set(0, 0, 0, 0, 0, 0, 0, 0);
|
|
328
|
+
clean(this.buffer);
|
|
329
|
+
}
|
|
330
|
+
};
|
|
178
331
|
var K512 = /* @__PURE__ */ (() => split([
|
|
179
332
|
"0x428a2f98d728ae22",
|
|
180
333
|
"0x7137449123ef65cd",
|
|
@@ -372,6 +525,7 @@ var SHA512 = class extends HashMD {
|
|
|
372
525
|
this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
373
526
|
}
|
|
374
527
|
};
|
|
528
|
+
var sha256 = /* @__PURE__ */ createHasher(() => new SHA256());
|
|
375
529
|
var sha512 = /* @__PURE__ */ createHasher(() => new SHA512());
|
|
376
530
|
|
|
377
531
|
// node_modules/@noble/curves/esm/utils.js
|
|
@@ -2260,23 +2414,25 @@ var hashToCurve = /* @__PURE__ */ (() => ed25519_hasher.hashToCurve)();
|
|
|
2260
2414
|
var encodeToCurve = /* @__PURE__ */ (() => ed25519_hasher.encodeToCurve)();
|
|
2261
2415
|
var hashToRistretto255 = /* @__PURE__ */ (() => ristretto255_hasher.hashToCurve)();
|
|
2262
2416
|
var hash_to_ristretto255 = /* @__PURE__ */ (() => ristretto255_hasher.hashToCurve)();
|
|
2417
|
+
|
|
2263
2418
|
export {
|
|
2264
|
-
|
|
2265
|
-
RistrettoPoint,
|
|
2419
|
+
sha256,
|
|
2266
2420
|
ed25519,
|
|
2267
|
-
ed25519_hasher,
|
|
2268
2421
|
ed25519ctx,
|
|
2269
2422
|
ed25519ph,
|
|
2423
|
+
x25519,
|
|
2424
|
+
ed25519_hasher,
|
|
2425
|
+
ristretto255,
|
|
2426
|
+
ristretto255_hasher,
|
|
2427
|
+
ED25519_TORSION_SUBGROUP,
|
|
2428
|
+
edwardsToMontgomeryPub,
|
|
2270
2429
|
edwardsToMontgomery,
|
|
2271
2430
|
edwardsToMontgomeryPriv,
|
|
2272
|
-
|
|
2273
|
-
encodeToCurve,
|
|
2431
|
+
RistrettoPoint,
|
|
2274
2432
|
hashToCurve,
|
|
2433
|
+
encodeToCurve,
|
|
2275
2434
|
hashToRistretto255,
|
|
2276
|
-
hash_to_ristretto255
|
|
2277
|
-
ristretto255,
|
|
2278
|
-
ristretto255_hasher,
|
|
2279
|
-
x25519
|
|
2435
|
+
hash_to_ristretto255
|
|
2280
2436
|
};
|
|
2281
2437
|
/*! Bundled license information:
|
|
2282
2438
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
parseRateLimit,
|
|
8
8
|
signDecision,
|
|
9
9
|
startStatusServer
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-UV53U6D4.mjs";
|
|
11
11
|
|
|
12
12
|
// src/evidence-store.ts
|
|
13
13
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
@@ -974,8 +974,20 @@ var ProtectGateway = class {
|
|
|
974
974
|
this.evidenceStore.save();
|
|
975
975
|
}
|
|
976
976
|
}
|
|
977
|
-
} else if (signed.
|
|
978
|
-
|
|
977
|
+
} else if (signed.error) {
|
|
978
|
+
const tombstone = JSON.stringify({
|
|
979
|
+
type: "scopeblind.signing_failure.v1",
|
|
980
|
+
request_id: log.request_id,
|
|
981
|
+
tool: log.tool,
|
|
982
|
+
decision: log.decision,
|
|
983
|
+
error: signed.error,
|
|
984
|
+
at: new Date(log.timestamp).toISOString()
|
|
985
|
+
});
|
|
986
|
+
try {
|
|
987
|
+
appendFileSync(this.receiptFilePath, tombstone + "\n");
|
|
988
|
+
} catch {
|
|
989
|
+
}
|
|
990
|
+
process.stderr.write(`[PROTECT_MCP_SIGNING_FAILURE] ${tombstone}
|
|
979
991
|
`);
|
|
980
992
|
}
|
|
981
993
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
meetsMinTier
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PLKRTBDR.mjs";
|
|
4
4
|
import {
|
|
5
5
|
checkRateLimit,
|
|
6
6
|
getToolPolicy,
|
|
7
7
|
parseRateLimit
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-UV53U6D4.mjs";
|
|
9
9
|
|
|
10
10
|
// src/simulate.ts
|
|
11
11
|
import { readFileSync } from "fs";
|