protect-mcp 0.6.0 → 0.6.3
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 +42 -1
- package/dist/{chunk-SPHLVRJ2.mjs → chunk-3YCKR72H.mjs} +223 -4
- 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 +329 -2676
- package/dist/cli.mjs +10 -10
- 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 +112 -13
- package/dist/index.d.ts +112 -13
- package/dist/index.js +783 -2582
- package/dist/index.mjs +119 -66
- package/package.json +4 -2
- package/dist/chunk-D733KAPG.mjs +0 -252
- package/dist/chunk-LYKNULYU.mjs +0 -2446
- package/dist/ed25519-DZMMNNVE.mjs +0 -38
- package/dist/utils-6AYZFE5A.mjs +0 -77
package/README.md
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
> A policy check that sits between your AI agent and the tools it calls.
|
|
9
9
|
> Every tool call is evaluated against a rule you wrote. Every decision is signed.
|
|
10
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
|
+
|
|
11
16
|
## What it does, in plain English
|
|
12
17
|
|
|
13
18
|
When an AI agent (Claude Code, Cursor, a custom LangChain app, anything that
|
|
@@ -335,6 +340,42 @@ Free tier: 20,000 receipts/month. No credit card required.
|
|
|
335
340
|
|
|
336
341
|
[scopeblind.com/pricing](https://scopeblind.com/pricing)
|
|
337
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
|
+
|
|
338
379
|
## Interoperability
|
|
339
380
|
|
|
340
381
|
The receipt format is independently implemented and verified across multiple systems:
|
|
@@ -344,7 +385,7 @@ The receipt format is independently implemented and verified across multiple sys
|
|
|
344
385
|
| **4 independent implementations** | TypeScript (protect-mcp), Python (protect-mcp-adk), Rust (Cedar WASM), APS ProxyGateway |
|
|
345
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/) |
|
|
346
387
|
| **8 cross-engine receipts** | [Composition test](https://github.com/ScopeBlind/examples/tree/main/interop/composition-test): 2 engines, 1 verifier, all VALID |
|
|
347
|
-
| **
|
|
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) |
|
|
348
389
|
| **1 verifier, zero dependencies** | `npx @veritasacta/verify receipt.json --key <hex>` (Apache-2.0, offline) |
|
|
349
390
|
|
|
350
391
|
Verify any receipt from any implementation:
|
|
@@ -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
|
+
*/
|
|
@@ -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";
|
|
@@ -78,11 +78,40 @@ function checkRateLimit(key, limit, store) {
|
|
|
78
78
|
import { readFileSync as readFileSync2, existsSync } from "fs";
|
|
79
79
|
var signerState = null;
|
|
80
80
|
var artifactsModule = null;
|
|
81
|
+
var signingConfigured = false;
|
|
82
|
+
var signingInitError = null;
|
|
81
83
|
async function initSigning(config) {
|
|
82
84
|
const warnings = [];
|
|
85
|
+
signerState = null;
|
|
86
|
+
artifactsModule = null;
|
|
87
|
+
signingConfigured = Boolean(config && config.enabled !== false);
|
|
88
|
+
signingInitError = null;
|
|
83
89
|
if (!config || config.enabled === false) {
|
|
84
90
|
return warnings;
|
|
85
91
|
}
|
|
92
|
+
if (!config.key_path) {
|
|
93
|
+
signingInitError = "signing enabled but key_path is not configured";
|
|
94
|
+
warnings.push(`signing: ${signingInitError}`);
|
|
95
|
+
return warnings;
|
|
96
|
+
}
|
|
97
|
+
if (!existsSync(config.key_path)) {
|
|
98
|
+
signingInitError = `key file not found at ${config.key_path}`;
|
|
99
|
+
warnings.push(`signing: ${signingInitError} \u2014 run "protect-mcp init" to generate`);
|
|
100
|
+
return warnings;
|
|
101
|
+
}
|
|
102
|
+
let keyData;
|
|
103
|
+
try {
|
|
104
|
+
keyData = JSON.parse(readFileSync2(config.key_path, "utf-8"));
|
|
105
|
+
if (!keyData.privateKey || !keyData.publicKey) {
|
|
106
|
+
signingInitError = "key file missing privateKey or publicKey fields";
|
|
107
|
+
warnings.push(`signing: ${signingInitError}`);
|
|
108
|
+
return warnings;
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
signingInitError = `failed to load key file: ${err instanceof Error ? err.message : err}`;
|
|
112
|
+
warnings.push(`signing: ${signingInitError}`);
|
|
113
|
+
return warnings;
|
|
114
|
+
}
|
|
86
115
|
try {
|
|
87
116
|
const moduleName = "@veritasacta/artifacts";
|
|
88
117
|
artifactsModule = await import(
|
|
@@ -90,37 +119,48 @@ async function initSigning(config) {
|
|
|
90
119
|
moduleName
|
|
91
120
|
);
|
|
92
121
|
} catch {
|
|
93
|
-
|
|
122
|
+
signingInitError = "@veritasacta/artifacts not available";
|
|
123
|
+
warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
|
|
94
124
|
return warnings;
|
|
95
125
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
signerState = {
|
|
108
|
-
privateKey: keyData.privateKey,
|
|
109
|
-
publicKey: keyData.publicKey,
|
|
110
|
-
kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
|
|
111
|
-
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
112
|
-
};
|
|
113
|
-
} catch (err) {
|
|
114
|
-
warnings.push(`signing: failed to load key file: ${err instanceof Error ? err.message : err}`);
|
|
115
|
-
}
|
|
126
|
+
try {
|
|
127
|
+
signerState = {
|
|
128
|
+
privateKey: keyData.privateKey,
|
|
129
|
+
publicKey: keyData.publicKey,
|
|
130
|
+
kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
|
|
131
|
+
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
132
|
+
};
|
|
133
|
+
} catch (err) {
|
|
134
|
+
signingInitError = `failed to initialize signer: ${err instanceof Error ? err.message : err}`;
|
|
135
|
+
artifactsModule = null;
|
|
136
|
+
warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
|
|
116
137
|
}
|
|
117
138
|
return warnings;
|
|
118
139
|
}
|
|
119
140
|
function signDecision(entry) {
|
|
141
|
+
const artifactType = entry.decision === "deny" ? "gateway_restraint" : "decision_receipt";
|
|
142
|
+
if (signingConfigured && signingInitError) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
signed: null,
|
|
146
|
+
artifact_type: artifactType,
|
|
147
|
+
warning: `signing initialization failed: ${signingInitError}`,
|
|
148
|
+
error: signingInitError
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (signingConfigured && (!signerState || !artifactsModule)) {
|
|
152
|
+
const error = "signing was configured but no signer is ready";
|
|
153
|
+
return {
|
|
154
|
+
ok: false,
|
|
155
|
+
signed: null,
|
|
156
|
+
artifact_type: artifactType,
|
|
157
|
+
warning: error,
|
|
158
|
+
error
|
|
159
|
+
};
|
|
160
|
+
}
|
|
120
161
|
if (!signerState || !artifactsModule) {
|
|
121
|
-
return { signed: null, artifact_type: "none" };
|
|
162
|
+
return { ok: false, signed: null, artifact_type: "none" };
|
|
122
163
|
}
|
|
123
|
-
const artifactType = entry.decision === "deny" ? "gateway_restraint" : "decision_receipt";
|
|
124
164
|
try {
|
|
125
165
|
const payload = {
|
|
126
166
|
tool: entry.tool,
|
|
@@ -161,14 +201,18 @@ function signDecision(entry) {
|
|
|
161
201
|
}
|
|
162
202
|
);
|
|
163
203
|
return {
|
|
204
|
+
ok: true,
|
|
164
205
|
signed: JSON.stringify(result.artifact),
|
|
165
206
|
artifact_type: artifactType
|
|
166
207
|
};
|
|
167
208
|
} catch (err) {
|
|
209
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
168
210
|
return {
|
|
211
|
+
ok: false,
|
|
169
212
|
signed: null,
|
|
170
213
|
artifact_type: artifactType,
|
|
171
|
-
warning: `signing failed: ${
|
|
214
|
+
warning: `signing failed: ${message}`,
|
|
215
|
+
error: message
|
|
172
216
|
};
|
|
173
217
|
}
|
|
174
218
|
}
|
|
@@ -181,7 +225,7 @@ function getSignerInfo() {
|
|
|
181
225
|
};
|
|
182
226
|
}
|
|
183
227
|
function isSigningEnabled() {
|
|
184
|
-
return signerState !== null && artifactsModule !== null;
|
|
228
|
+
return signingConfigured && signingInitError === null && signerState !== null && artifactsModule !== null;
|
|
185
229
|
}
|
|
186
230
|
|
|
187
231
|
// src/cedar-evaluator.ts
|