protect-mcp 0.9.5 → 0.9.7
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 +21 -0
- package/README.md +33 -0
- package/dist/{chunk-H332ZNJ6.mjs → chunk-AUUAVWQM.mjs} +51 -8
- package/dist/{chunk-XLJUZ4WO.mjs → chunk-G6X763MH.mjs} +7 -215
- package/dist/{chunk-7MHK5RF4.mjs → chunk-MOXINIMB.mjs} +2 -2
- package/dist/chunk-MWXDXYWH.mjs +210 -0
- package/dist/{chunk-PB3TC7E3.mjs → chunk-YM6SOJBR.mjs} +4 -2
- package/dist/cli.js +474 -7
- package/dist/cli.mjs +165 -8
- package/dist/hook-server.js +44 -3
- package/dist/hook-server.mjs +3 -2
- package/dist/{http-transport-HLSMVBI6.mjs → http-transport-VHD3YBT5.mjs} +3 -2
- package/dist/index.js +44 -3
- package/dist/index.mjs +11 -9
- package/dist/mcp-server.d.mts +24 -0
- package/dist/mcp-server.d.ts +24 -0
- package/dist/mcp-server.js +445 -0
- package/dist/mcp-server.mjs +269 -0
- package/package.json +4 -3
|
@@ -2,13 +2,15 @@ import {
|
|
|
2
2
|
ReceiptBuffer,
|
|
3
3
|
buildActionReadback,
|
|
4
4
|
checkRateLimit,
|
|
5
|
-
evaluateCedar,
|
|
6
5
|
getToolPolicy,
|
|
7
6
|
isSigningEnabled,
|
|
8
7
|
parseRateLimit,
|
|
9
8
|
signDecision,
|
|
10
9
|
startStatusServer
|
|
11
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-G6X763MH.mjs";
|
|
11
|
+
import {
|
|
12
|
+
evaluateCedar
|
|
13
|
+
} from "./chunk-MWXDXYWH.mjs";
|
|
12
14
|
|
|
13
15
|
// src/evidence-store.ts
|
|
14
16
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
package/dist/cli.js
CHANGED
|
@@ -6414,6 +6414,277 @@ var init_sample = __esm({
|
|
|
6414
6414
|
}
|
|
6415
6415
|
});
|
|
6416
6416
|
|
|
6417
|
+
// src/mcp-server.ts
|
|
6418
|
+
var mcp_server_exports = {};
|
|
6419
|
+
__export(mcp_server_exports, {
|
|
6420
|
+
runMcpServer: () => runMcpServer
|
|
6421
|
+
});
|
|
6422
|
+
async function loadArtifacts() {
|
|
6423
|
+
if (artifacts) return artifacts;
|
|
6424
|
+
const moduleName = "@veritasacta/artifacts";
|
|
6425
|
+
const mod2 = await import(
|
|
6426
|
+
/* @vite-ignore */
|
|
6427
|
+
moduleName
|
|
6428
|
+
);
|
|
6429
|
+
artifacts = mod2;
|
|
6430
|
+
return mod2;
|
|
6431
|
+
}
|
|
6432
|
+
function buildReceiptPayload(args) {
|
|
6433
|
+
return {
|
|
6434
|
+
tool: args.tool,
|
|
6435
|
+
decision: args.decision,
|
|
6436
|
+
reason_code: args.reason_code ?? (args.decision === "deny" ? "policy_denied" : "post_execution_receipt"),
|
|
6437
|
+
policy_digest: args.policy_digest ?? "none",
|
|
6438
|
+
scope: args.request_id,
|
|
6439
|
+
mode: "enforce",
|
|
6440
|
+
request_id: args.request_id,
|
|
6441
|
+
spec: "draft-farley-acta-signed-receipts-01",
|
|
6442
|
+
issuer_certification: "self-signed",
|
|
6443
|
+
public_key: args.public_key
|
|
6444
|
+
};
|
|
6445
|
+
}
|
|
6446
|
+
function newRequestId() {
|
|
6447
|
+
return `mcp-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
6448
|
+
}
|
|
6449
|
+
async function callEvaluate(args) {
|
|
6450
|
+
const tool = typeof args.tool === "string" ? args.tool : "";
|
|
6451
|
+
const policySource = typeof args.policy === "string" ? args.policy : "";
|
|
6452
|
+
const input = args.input && typeof args.input === "object" ? args.input : {};
|
|
6453
|
+
if (!tool) return { error: "missing_tool" };
|
|
6454
|
+
if (!policySource.trim()) return { allowed: false, decision: "deny", reason: "no policy provided (fail-closed)", policy_digest: "none" };
|
|
6455
|
+
const policySet = policySetFromSource(policySource);
|
|
6456
|
+
const decision = await evaluateCedar(
|
|
6457
|
+
policySet,
|
|
6458
|
+
{ tool, tier: "unknown", context: { ...input, input }, toolInput: input },
|
|
6459
|
+
void 0,
|
|
6460
|
+
{ failClosed: true }
|
|
6461
|
+
);
|
|
6462
|
+
return {
|
|
6463
|
+
allowed: decision.allowed,
|
|
6464
|
+
decision: decision.allowed ? "allow" : "deny",
|
|
6465
|
+
reason: decision.reason || (decision.allowed ? "allowed" : "denied by policy"),
|
|
6466
|
+
policy_digest: policySet.digest
|
|
6467
|
+
};
|
|
6468
|
+
}
|
|
6469
|
+
async function callSign(args) {
|
|
6470
|
+
const tool = typeof args.tool === "string" ? args.tool : "";
|
|
6471
|
+
const decision = args.decision === "deny" ? "deny" : args.decision === "allow" ? "allow" : null;
|
|
6472
|
+
if (!tool) return { error: "missing_tool" };
|
|
6473
|
+
if (!decision) return { error: 'decision must be "allow" or "deny"' };
|
|
6474
|
+
const a = await loadArtifacts();
|
|
6475
|
+
let privateKey = typeof args.private_key_hex === "string" ? args.private_key_hex : "";
|
|
6476
|
+
let publicKey;
|
|
6477
|
+
let ephemeral = false;
|
|
6478
|
+
if (privateKey) {
|
|
6479
|
+
publicKey = a.getPublicKey(privateKey);
|
|
6480
|
+
} else {
|
|
6481
|
+
const kp = a.generateKeypair();
|
|
6482
|
+
privateKey = kp.privateKey;
|
|
6483
|
+
publicKey = kp.publicKey;
|
|
6484
|
+
ephemeral = true;
|
|
6485
|
+
}
|
|
6486
|
+
const requestId = newRequestId();
|
|
6487
|
+
const payload = buildReceiptPayload({
|
|
6488
|
+
tool,
|
|
6489
|
+
decision,
|
|
6490
|
+
reason_code: typeof args.reason_code === "string" ? args.reason_code : void 0,
|
|
6491
|
+
policy_digest: typeof args.policy_digest === "string" ? args.policy_digest : void 0,
|
|
6492
|
+
request_id: requestId,
|
|
6493
|
+
public_key: publicKey
|
|
6494
|
+
});
|
|
6495
|
+
const artifactType = decision === "deny" ? "gateway_restraint" : "decision_receipt";
|
|
6496
|
+
const { artifact } = a.createSignedArtifact(artifactType, payload, privateKey, { kid: "mcp", issuer: "protect-mcp" });
|
|
6497
|
+
return { receipt: artifact, artifact_type: artifactType, public_key: publicKey, ephemeral };
|
|
6498
|
+
}
|
|
6499
|
+
async function callVerify(args) {
|
|
6500
|
+
const receipt = args.receipt;
|
|
6501
|
+
if (!receipt || typeof receipt !== "object") return { valid: false, error: "missing_receipt" };
|
|
6502
|
+
const a = await loadArtifacts();
|
|
6503
|
+
const embedded = receipt.payload?.public_key;
|
|
6504
|
+
const key = typeof args.public_key_hex === "string" && args.public_key_hex ? args.public_key_hex : typeof embedded === "string" ? embedded : null;
|
|
6505
|
+
if (!key) {
|
|
6506
|
+
return { valid: false, error: "no_public_key", type: receipt.type ?? "unknown", kid: null, issuer: null };
|
|
6507
|
+
}
|
|
6508
|
+
const result = a.verifyArtifact(receipt, key);
|
|
6509
|
+
return {
|
|
6510
|
+
valid: !!result.valid,
|
|
6511
|
+
error: result.valid ? null : result.error || "invalid_signature",
|
|
6512
|
+
type: receipt.type ?? "unknown",
|
|
6513
|
+
kid: receipt.kid ?? null,
|
|
6514
|
+
issuer: receipt.issuer ?? null
|
|
6515
|
+
};
|
|
6516
|
+
}
|
|
6517
|
+
async function callSelfTest() {
|
|
6518
|
+
const details = {};
|
|
6519
|
+
const denied = await callEvaluate({ tool: "Bash", input: { command: "rm -rf /" }, policy: SELF_TEST_POLICY });
|
|
6520
|
+
const gateDeniesForbidden = denied.allowed === false;
|
|
6521
|
+
details.forbidden_action = { tool: "Bash", command: "rm -rf /", allowed: denied.allowed, reason: denied.reason };
|
|
6522
|
+
const allowed = await callEvaluate({ tool: "Read", input: { path: "./notes.txt" }, policy: SELF_TEST_POLICY });
|
|
6523
|
+
details.safe_action = { tool: "Read", allowed: allowed.allowed };
|
|
6524
|
+
let signVerifyRoundtrip = false;
|
|
6525
|
+
try {
|
|
6526
|
+
const signed = await callSign({ tool: "Bash", decision: "deny", reason_code: "self_test" });
|
|
6527
|
+
if (signed.receipt && signed.public_key) {
|
|
6528
|
+
const ok = await callVerify({ receipt: signed.receipt, public_key_hex: signed.public_key });
|
|
6529
|
+
const tampered = JSON.parse(JSON.stringify(signed.receipt));
|
|
6530
|
+
if (tampered.payload) tampered.payload.tool = "tampered";
|
|
6531
|
+
const bad = await callVerify({ receipt: tampered, public_key_hex: signed.public_key });
|
|
6532
|
+
signVerifyRoundtrip = ok.valid === true && bad.valid === false;
|
|
6533
|
+
details.sign_verify = { valid_verifies: ok.valid, tampered_rejected: bad.valid === false };
|
|
6534
|
+
}
|
|
6535
|
+
} catch (err) {
|
|
6536
|
+
details.sign_verify_error = err instanceof Error ? err.message : String(err);
|
|
6537
|
+
}
|
|
6538
|
+
return {
|
|
6539
|
+
ok: gateDeniesForbidden && allowed.allowed === true && signVerifyRoundtrip,
|
|
6540
|
+
gate_denies_forbidden: gateDeniesForbidden,
|
|
6541
|
+
sign_verify_roundtrip: signVerifyRoundtrip,
|
|
6542
|
+
details,
|
|
6543
|
+
note: "No network was contacted."
|
|
6544
|
+
};
|
|
6545
|
+
}
|
|
6546
|
+
function textResult(id, value) {
|
|
6547
|
+
return JSON.stringify({ jsonrpc: "2.0", id, result: { content: [{ type: "text", text: JSON.stringify(value, null, 2) }] } });
|
|
6548
|
+
}
|
|
6549
|
+
async function handleRequest(request) {
|
|
6550
|
+
if (request.method === "initialize") {
|
|
6551
|
+
return JSON.stringify({
|
|
6552
|
+
jsonrpc: "2.0",
|
|
6553
|
+
id: request.id,
|
|
6554
|
+
result: {
|
|
6555
|
+
protocolVersion: "2024-11-05",
|
|
6556
|
+
serverInfo: { name: "protect-mcp", version: process.env.PROTECT_MCP_VERSION || "0.9.7" },
|
|
6557
|
+
capabilities: { tools: {} }
|
|
6558
|
+
}
|
|
6559
|
+
});
|
|
6560
|
+
}
|
|
6561
|
+
if (request.method === "notifications/initialized") return "";
|
|
6562
|
+
if (request.method === "tools/list") {
|
|
6563
|
+
return JSON.stringify({ jsonrpc: "2.0", id: request.id, result: { tools: TOOLS } });
|
|
6564
|
+
}
|
|
6565
|
+
if (request.method === "tools/call") {
|
|
6566
|
+
const name = request.params?.name || "";
|
|
6567
|
+
const args = request.params?.arguments || {};
|
|
6568
|
+
try {
|
|
6569
|
+
let value;
|
|
6570
|
+
switch (name) {
|
|
6571
|
+
case "evaluate_action":
|
|
6572
|
+
value = await callEvaluate(args);
|
|
6573
|
+
break;
|
|
6574
|
+
case "sign_decision":
|
|
6575
|
+
value = await callSign(args);
|
|
6576
|
+
break;
|
|
6577
|
+
case "verify_receipt":
|
|
6578
|
+
value = await callVerify(args);
|
|
6579
|
+
break;
|
|
6580
|
+
case "self_test":
|
|
6581
|
+
value = await callSelfTest();
|
|
6582
|
+
break;
|
|
6583
|
+
default:
|
|
6584
|
+
value = { error: `unknown tool: ${name}` };
|
|
6585
|
+
}
|
|
6586
|
+
return textResult(request.id, value);
|
|
6587
|
+
} catch (err) {
|
|
6588
|
+
return textResult(request.id, { error: err instanceof Error ? err.message : String(err) });
|
|
6589
|
+
}
|
|
6590
|
+
}
|
|
6591
|
+
if (request.id !== void 0) {
|
|
6592
|
+
return JSON.stringify({ jsonrpc: "2.0", id: request.id, error: { code: -32601, message: `Method not found: ${request.method}` } });
|
|
6593
|
+
}
|
|
6594
|
+
return "";
|
|
6595
|
+
}
|
|
6596
|
+
async function runMcpServer() {
|
|
6597
|
+
const rl = (0, import_node_readline2.createInterface)({ input: process.stdin, crlfDelay: Infinity });
|
|
6598
|
+
let chain = Promise.resolve();
|
|
6599
|
+
rl.on("line", (line) => {
|
|
6600
|
+
const trimmed = line.trim();
|
|
6601
|
+
if (!trimmed) return;
|
|
6602
|
+
chain = chain.then(async () => {
|
|
6603
|
+
try {
|
|
6604
|
+
const request = JSON.parse(trimmed);
|
|
6605
|
+
const response = await handleRequest(request);
|
|
6606
|
+
if (response) process.stdout.write(response + "\n");
|
|
6607
|
+
} catch {
|
|
6608
|
+
}
|
|
6609
|
+
});
|
|
6610
|
+
});
|
|
6611
|
+
process.stderr.write("[PROTECT_MCP] gate MCP server started \u2014 4 tools: evaluate_action, sign_decision, verify_receipt, self_test\n");
|
|
6612
|
+
await new Promise((resolve) => rl.on("close", () => resolve()));
|
|
6613
|
+
}
|
|
6614
|
+
var import_node_readline2, artifacts, RO, TOOLS, SELF_TEST_POLICY;
|
|
6615
|
+
var init_mcp_server = __esm({
|
|
6616
|
+
"src/mcp-server.ts"() {
|
|
6617
|
+
"use strict";
|
|
6618
|
+
import_node_readline2 = require("readline");
|
|
6619
|
+
init_cedar_evaluator();
|
|
6620
|
+
artifacts = null;
|
|
6621
|
+
RO = { readOnlyHint: true, destructiveHint: false, openWorldHint: false };
|
|
6622
|
+
TOOLS = [
|
|
6623
|
+
{
|
|
6624
|
+
name: "evaluate_action",
|
|
6625
|
+
description: `Decide whether a proposed agent tool call is allowed by a Cedar policy, fail-closed. Evaluates the call against the policy the same way the protect-mcp gate does at runtime, and on any policy error the decision is DENY (never a silent allow). Inputs: tool (the tool name, e.g. "Bash" or "send_email"), input (the tool's arguments object; the policy sees it at context.input.*), and policy (inline Cedar source text). Returns JSON { allowed: boolean, decision: "allow" | "deny", reason: string, policy_digest: string (sha256 prefix of the policy) }. A missing or unparseable policy denies. Use this before an agent acts; pair with sign_decision to make the decision auditable.`,
|
|
6626
|
+
inputSchema: {
|
|
6627
|
+
type: "object",
|
|
6628
|
+
properties: {
|
|
6629
|
+
tool: { type: "string", description: 'The tool name being evaluated, e.g. "Bash", "Write", "send_email".' },
|
|
6630
|
+
input: { type: "object", description: "The tool's arguments. Reachable in the policy at context.input.* (e.g. context.input.command)." },
|
|
6631
|
+
policy: { type: "string", description: 'Inline Cedar policy source. Use MCP::Tool::call as the action and Tool::"<name>" as the resource.' }
|
|
6632
|
+
},
|
|
6633
|
+
required: ["tool", "policy"]
|
|
6634
|
+
},
|
|
6635
|
+
annotations: { title: "Evaluate an action against a Cedar policy", idempotentHint: true, ...RO }
|
|
6636
|
+
},
|
|
6637
|
+
{
|
|
6638
|
+
name: "sign_decision",
|
|
6639
|
+
description: `Turn a gate decision into an Ed25519 signed receipt (Veritas Acta format, JCS-canonical). A denial signs a gateway_restraint artifact; an allow signs a decision_receipt. This is what makes 'what the agent was blocked from doing' provable after the fact. Inputs: tool (required), decision ("allow" | "deny", required), reason_code (optional), policy_digest (optional), and private_key_hex (optional 64-hex Ed25519 secret; if omitted, an ephemeral keypair is generated and its public key is returned so the receipt still verifies). Returns JSON { receipt: object (the signed artifact), artifact_type: "decision_receipt" | "gateway_restraint", public_key: string (hex, pass this to verify_receipt or pin it), ephemeral: boolean }. Writes nothing to disk and contacts no network.`,
|
|
6640
|
+
inputSchema: {
|
|
6641
|
+
type: "object",
|
|
6642
|
+
properties: {
|
|
6643
|
+
tool: { type: "string", description: "The tool the decision is about." },
|
|
6644
|
+
decision: { type: "string", enum: ["allow", "deny"], description: "The gate decision to receipt." },
|
|
6645
|
+
reason_code: { type: "string", description: 'Optional machine reason, e.g. "restricted_list" or "post_execution_receipt".' },
|
|
6646
|
+
policy_digest: { type: "string", description: "Optional digest of the policy that produced the decision (e.g. from evaluate_action)." },
|
|
6647
|
+
private_key_hex: { type: "string", description: "Optional 64-character hex Ed25519 secret key. If omitted, an ephemeral key is generated and its public key is returned." }
|
|
6648
|
+
},
|
|
6649
|
+
required: ["tool", "decision"]
|
|
6650
|
+
},
|
|
6651
|
+
// Not idempotent: an ephemeral key and a fresh request_id are minted per call.
|
|
6652
|
+
annotations: { title: "Sign a decision into a receipt", idempotentHint: false, ...RO }
|
|
6653
|
+
},
|
|
6654
|
+
{
|
|
6655
|
+
name: "verify_receipt",
|
|
6656
|
+
description: 'Verify a signed receipt offline against a public key. No network, no accounts: the Ed25519 signature is checked over the canonical bytes. Inputs: receipt (the signed artifact object, required) and public_key_hex (optional; falls back to a public_key embedded in the receipt payload). Returns JSON { valid: boolean, error: string | null (e.g. "invalid_signature", "no_public_key"), type: string, kid: string | null, issuer: string | null }. For authenticity you should verify against a key you pinned out of band, not only the one carried inside the receipt.',
|
|
6657
|
+
inputSchema: {
|
|
6658
|
+
type: "object",
|
|
6659
|
+
properties: {
|
|
6660
|
+
receipt: { type: "object", description: "The signed receipt/artifact to verify." },
|
|
6661
|
+
public_key_hex: { type: "string", description: "Optional Ed25519 public key hex to verify against. Defaults to a key embedded in the receipt payload." }
|
|
6662
|
+
},
|
|
6663
|
+
required: ["receipt"]
|
|
6664
|
+
},
|
|
6665
|
+
annotations: { title: "Verify a receipt offline", idempotentHint: true, ...RO }
|
|
6666
|
+
},
|
|
6667
|
+
{
|
|
6668
|
+
name: "self_test",
|
|
6669
|
+
description: "Prove the gate works, end to end, with no inputs. Runs a known-forbidden action (rm -rf) against a sample deny policy and asserts it is DENIED, then signs a decision and verifies the receipt round-trips. Returns JSON { ok: boolean, gate_denies_forbidden: boolean, sign_verify_roundtrip: boolean, details: object }. This is the 'a gate that cannot prove it denies does not start' check, exposed as a callable tool. Contacts no network.",
|
|
6670
|
+
inputSchema: { type: "object", properties: {} },
|
|
6671
|
+
annotations: { title: "Prove the gate denies and receipts verify", idempotentHint: true, ...RO }
|
|
6672
|
+
}
|
|
6673
|
+
];
|
|
6674
|
+
SELF_TEST_POLICY = `
|
|
6675
|
+
permit(principal, action == Action::"MCP::Tool::call", resource);
|
|
6676
|
+
forbid(principal, action == Action::"MCP::Tool::call", resource == Tool::"Bash")
|
|
6677
|
+
when {
|
|
6678
|
+
context has input && context.input has command &&
|
|
6679
|
+
(context.input.command like "*rm -rf*" || context.input.command like "*mkfs*")
|
|
6680
|
+
};
|
|
6681
|
+
`;
|
|
6682
|
+
if (process.argv[1] && /mcp-server\.(js|mjs|cjs|ts)$/.test(process.argv[1])) {
|
|
6683
|
+
runMcpServer();
|
|
6684
|
+
}
|
|
6685
|
+
}
|
|
6686
|
+
});
|
|
6687
|
+
|
|
6417
6688
|
// src/scopeblind-bridge.ts
|
|
6418
6689
|
function getScopeBlindBridge() {
|
|
6419
6690
|
if (!singleton) singleton = new ScopeBlindBridge();
|
|
@@ -6650,6 +6921,7 @@ async function handlePreToolUse(input, state) {
|
|
|
6650
6921
|
...input.teamName && { team_name: input.teamName },
|
|
6651
6922
|
...input.agentType && { agent_type: input.agentType }
|
|
6652
6923
|
};
|
|
6924
|
+
maybeReloadCedar(state);
|
|
6653
6925
|
if (state.cedarPolicies) {
|
|
6654
6926
|
try {
|
|
6655
6927
|
const cedarDecision = await evaluateCedar(state.cedarPolicies, {
|
|
@@ -6687,10 +6959,13 @@ async function handlePreToolUse(input, state) {
|
|
|
6687
6959
|
sandbox_state: detectSandboxState(),
|
|
6688
6960
|
plan_receipt_id: state.activePlanReceiptId || void 0
|
|
6689
6961
|
});
|
|
6962
|
+
const isDefaultDeny = !reason || reason === "cedar_deny" || /reason":\[\]/.test(reason);
|
|
6963
|
+
const policyRef = state.cedarDir ? ` Policy: ${state.cedarDir}.` : "";
|
|
6964
|
+
const howTo = isDefaultDeny ? ` No permit matched (default-deny, fail-closed).${policyRef} Allow it: npx protect-mcp policy allow ${toolName}` : ` Blocked by an explicit forbid rule.${policyRef} Review: npx protect-mcp policy show`;
|
|
6690
6965
|
if (denyCount === 1) {
|
|
6691
6966
|
process.stderr.write(
|
|
6692
|
-
`[PROTECT_MCP]
|
|
6693
|
-
${
|
|
6967
|
+
`[PROTECT_MCP] Denied "${toolName}" (${isDefaultDeny ? "default-deny" : "forbid"}).
|
|
6968
|
+
Allow with: npx protect-mcp policy allow ${toolName}
|
|
6694
6969
|
`
|
|
6695
6970
|
);
|
|
6696
6971
|
}
|
|
@@ -6698,7 +6973,7 @@ async function handlePreToolUse(input, state) {
|
|
|
6698
6973
|
hookSpecificOutput: {
|
|
6699
6974
|
hookEventName: "PreToolUse",
|
|
6700
6975
|
permissionDecision: "deny",
|
|
6701
|
-
permissionDecisionReason: `[ScopeBlind] Denied
|
|
6976
|
+
permissionDecisionReason: `[ScopeBlind] Denied "${toolName}".${howTo}` + (denyCount > 1 ? ` (attempt ${denyCount})` : "")
|
|
6702
6977
|
}
|
|
6703
6978
|
};
|
|
6704
6979
|
}
|
|
@@ -7197,6 +7472,9 @@ async function startHookServer(options = {}) {
|
|
|
7197
7472
|
}
|
|
7198
7473
|
const state = {
|
|
7199
7474
|
cedarPolicies,
|
|
7475
|
+
cedarDir: cedarDir || null,
|
|
7476
|
+
cedarMtimeMs: cedarDir ? newestCedarMtime(cedarDir) : 0,
|
|
7477
|
+
cedarCheckedMs: 0,
|
|
7200
7478
|
jsonPolicy,
|
|
7201
7479
|
rateLimitStore: /* @__PURE__ */ new Map(),
|
|
7202
7480
|
receiptBuffer: new ReceiptBuffer(),
|
|
@@ -7387,6 +7665,39 @@ async function startHookServer(options = {}) {
|
|
|
7387
7665
|
process.on("SIGTERM", shutdown);
|
|
7388
7666
|
return server;
|
|
7389
7667
|
}
|
|
7668
|
+
function newestCedarMtime(dir) {
|
|
7669
|
+
try {
|
|
7670
|
+
let newest = 0;
|
|
7671
|
+
for (const f of (0, import_node_fs11.readdirSync)(dir)) {
|
|
7672
|
+
if (!f.endsWith(".cedar")) continue;
|
|
7673
|
+
const m = (0, import_node_fs11.statSync)((0, import_node_path8.join)(dir, f)).mtimeMs;
|
|
7674
|
+
if (m > newest) newest = m;
|
|
7675
|
+
}
|
|
7676
|
+
return newest;
|
|
7677
|
+
} catch {
|
|
7678
|
+
return 0;
|
|
7679
|
+
}
|
|
7680
|
+
}
|
|
7681
|
+
function maybeReloadCedar(state) {
|
|
7682
|
+
if (!state.cedarDir) return;
|
|
7683
|
+
const nowMs = Date.now();
|
|
7684
|
+
if (nowMs - state.cedarCheckedMs < CEDAR_CHECK_THROTTLE_MS) return;
|
|
7685
|
+
state.cedarCheckedMs = nowMs;
|
|
7686
|
+
const m = newestCedarMtime(state.cedarDir);
|
|
7687
|
+
if (m <= state.cedarMtimeMs) return;
|
|
7688
|
+
try {
|
|
7689
|
+
const reloaded = loadCedarPolicies(state.cedarDir);
|
|
7690
|
+
state.cedarPolicies = reloaded;
|
|
7691
|
+
state.policyDigest = reloaded.digest;
|
|
7692
|
+
state.cedarMtimeMs = m;
|
|
7693
|
+
process.stderr.write(`[PROTECT_MCP] Cedar policy reloaded (digest: ${reloaded.digest}) after on-disk change.
|
|
7694
|
+
`);
|
|
7695
|
+
} catch (err) {
|
|
7696
|
+
process.stderr.write(`[PROTECT_MCP] Cedar reload failed, keeping the previous policy: ${err instanceof Error ? err.message : err}
|
|
7697
|
+
`);
|
|
7698
|
+
state.cedarMtimeMs = m;
|
|
7699
|
+
}
|
|
7700
|
+
}
|
|
7390
7701
|
function findCedarDir() {
|
|
7391
7702
|
for (const candidate of ["cedar", "policies", "."]) {
|
|
7392
7703
|
try {
|
|
@@ -7412,7 +7723,7 @@ function normalizeHookInput(raw) {
|
|
|
7412
7723
|
}
|
|
7413
7724
|
return result;
|
|
7414
7725
|
}
|
|
7415
|
-
var import_node_http2, import_node_crypto6, import_node_fs11, import_node_path8, DEFAULT_PORT, LOG_FILE3, RECEIPTS_FILE2, PAYLOAD_HASH_THRESHOLD, SNAKE_TO_CAMEL_MAP;
|
|
7726
|
+
var import_node_http2, import_node_crypto6, import_node_fs11, import_node_path8, DEFAULT_PORT, LOG_FILE3, RECEIPTS_FILE2, PAYLOAD_HASH_THRESHOLD, CEDAR_CHECK_THROTTLE_MS, SNAKE_TO_CAMEL_MAP;
|
|
7416
7727
|
var init_hook_server = __esm({
|
|
7417
7728
|
"src/hook-server.ts"() {
|
|
7418
7729
|
"use strict";
|
|
@@ -7431,6 +7742,7 @@ var init_hook_server = __esm({
|
|
|
7431
7742
|
LOG_FILE3 = ".protect-mcp-log.jsonl";
|
|
7432
7743
|
RECEIPTS_FILE2 = ".protect-mcp-receipts.jsonl";
|
|
7433
7744
|
PAYLOAD_HASH_THRESHOLD = 1024;
|
|
7745
|
+
CEDAR_CHECK_THROTTLE_MS = 2e3;
|
|
7434
7746
|
SNAKE_TO_CAMEL_MAP = {
|
|
7435
7747
|
hook_event_name: "hookEventName",
|
|
7436
7748
|
session_id: "sessionId",
|
|
@@ -8857,6 +9169,7 @@ protect-mcp: Enterprise security gateway for MCP servers & Claude Code hooks
|
|
|
8857
9169
|
Usage:
|
|
8858
9170
|
protect-mcp [options] -- <command> [args...]
|
|
8859
9171
|
protect-mcp serve [--port <port>] [--enforce] [--policy <path>] [--cedar <dir>]
|
|
9172
|
+
protect-mcp mcp # the gate as an MCP server (evaluate/sign/verify/self_test tools)
|
|
8860
9173
|
protect-mcp init-hooks [--dir <path>] [--port <port>]
|
|
8861
9174
|
protect-mcp quickstart [--connect]
|
|
8862
9175
|
protect-mcp wrap [--write] [--claude-desktop] [-- <command>]
|
|
@@ -8871,6 +9184,7 @@ Usage:
|
|
|
8871
9184
|
protect-mcp connect
|
|
8872
9185
|
protect-mcp init [--dir <path>]
|
|
8873
9186
|
protect-mcp sample [--dir <path>] [--force]
|
|
9187
|
+
protect-mcp policy list|show|allow <tool>|deny <tool>|path
|
|
8874
9188
|
protect-mcp demo
|
|
8875
9189
|
protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
|
|
8876
9190
|
protect-mcp status [--dir <path>]
|
|
@@ -11872,7 +12186,7 @@ async function handleKillerDemo(argv) {
|
|
|
11872
12186
|
const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
|
|
11873
12187
|
const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
11874
12188
|
const { randomBytes: randomBytes4 } = await import("crypto");
|
|
11875
|
-
const
|
|
12189
|
+
const artifacts2 = await import("@veritasacta/artifacts");
|
|
11876
12190
|
const {
|
|
11877
12191
|
createSelectiveDisclosurePackage: createSelectiveDisclosurePackage2,
|
|
11878
12192
|
signCommittedDecision: signCommittedDecision2,
|
|
@@ -11999,8 +12313,8 @@ async function handleKillerDemo(argv) {
|
|
|
11999
12313
|
} else {
|
|
12000
12314
|
tamperedArtifact.tool = "send_email";
|
|
12001
12315
|
}
|
|
12002
|
-
const validOriginal =
|
|
12003
|
-
const validTampered =
|
|
12316
|
+
const validOriginal = artifacts2.verifyArtifact(receiptArtifact, keypair.publicKey);
|
|
12317
|
+
const validTampered = artifacts2.verifyArtifact(tamperedArtifact, keypair.publicKey);
|
|
12004
12318
|
(0, import_node_fs13.writeFileSync)((0, import_node_path9.join)(dir, "receipts", "tampered.receipt.json"), JSON.stringify(tamperedArtifact, null, 2) + "\n");
|
|
12005
12319
|
const committed = signCommittedDecision2(
|
|
12006
12320
|
executedEntry,
|
|
@@ -12977,6 +13291,151 @@ ${bold("\u{1F6E1} Sample record seeded")} \xB7 8 decisions (1 blocked, 2 payment
|
|
|
12977
13291
|
`);
|
|
12978
13292
|
process.exit(0);
|
|
12979
13293
|
}
|
|
13294
|
+
async function handlePolicy(argv) {
|
|
13295
|
+
const { readFileSync: rf, writeFileSync: wf, existsSync: ex, readdirSync: rd, appendFileSync: af } = await import("fs");
|
|
13296
|
+
const { join: pj } = await import("path");
|
|
13297
|
+
const { createHash: createHash6 } = await import("crypto");
|
|
13298
|
+
const sub = argv[0] || "list";
|
|
13299
|
+
const findDir = () => {
|
|
13300
|
+
for (const c of ["cedar", "policies", "."]) {
|
|
13301
|
+
try {
|
|
13302
|
+
if (ex(c) && rd(c).some((f) => f.endsWith(".cedar"))) return c;
|
|
13303
|
+
} catch {
|
|
13304
|
+
}
|
|
13305
|
+
}
|
|
13306
|
+
return null;
|
|
13307
|
+
};
|
|
13308
|
+
const dir = findDir();
|
|
13309
|
+
const cedarFiles = dir ? rd(dir).filter((f) => f.endsWith(".cedar")).sort() : [];
|
|
13310
|
+
const readAll = () => cedarFiles.map((f) => rf(pj(dir, f), "utf-8")).join("\n\n");
|
|
13311
|
+
const digestOf = (src) => createHash6("sha256").update(src).digest("hex").slice(0, 16);
|
|
13312
|
+
const stripComments = (src) => src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "");
|
|
13313
|
+
const targetFile = cedarFiles.includes("agent.cedar") ? "agent.cedar" : cedarFiles[0];
|
|
13314
|
+
if (sub === "path") {
|
|
13315
|
+
if (!dir) {
|
|
13316
|
+
process.stdout.write("No Cedar policy directory found (looked in ./cedar, ./policies, .).\n");
|
|
13317
|
+
process.exit(0);
|
|
13318
|
+
}
|
|
13319
|
+
process.stdout.write(`${pj(dir, targetFile)}
|
|
13320
|
+
`);
|
|
13321
|
+
process.exit(0);
|
|
13322
|
+
}
|
|
13323
|
+
if (sub === "show") {
|
|
13324
|
+
if (!dir) {
|
|
13325
|
+
process.stderr.write("No Cedar policy found. Run: npx protect-mcp init-hooks\n");
|
|
13326
|
+
process.exit(1);
|
|
13327
|
+
}
|
|
13328
|
+
const src = readAll();
|
|
13329
|
+
process.stdout.write(`${bold("Cedar policy")} ${dim(`(${cedarFiles.length} file${cedarFiles.length === 1 ? "" : "s"} in ${dir}, digest ${digestOf(src)})`)}
|
|
13330
|
+
|
|
13331
|
+
`);
|
|
13332
|
+
process.stdout.write(src.endsWith("\n") ? src : src + "\n");
|
|
13333
|
+
process.exit(0);
|
|
13334
|
+
}
|
|
13335
|
+
if (sub === "allow" || sub === "deny") {
|
|
13336
|
+
const tool = argv[1];
|
|
13337
|
+
if (!tool) {
|
|
13338
|
+
process.stderr.write(`Usage: npx protect-mcp policy ${sub} <ToolName>
|
|
13339
|
+
`);
|
|
13340
|
+
process.exit(1);
|
|
13341
|
+
}
|
|
13342
|
+
if (!/^[A-Za-z0-9_.:-]+$/.test(tool)) {
|
|
13343
|
+
process.stderr.write(`Refusing: "${tool}" is not a valid tool name.
|
|
13344
|
+
`);
|
|
13345
|
+
process.exit(1);
|
|
13346
|
+
}
|
|
13347
|
+
if (!dir) {
|
|
13348
|
+
process.stderr.write("No Cedar policy found. Run: npx protect-mcp init-hooks\n");
|
|
13349
|
+
process.exit(1);
|
|
13350
|
+
}
|
|
13351
|
+
const effect = sub === "allow" ? "permit" : "forbid";
|
|
13352
|
+
const before = readAll();
|
|
13353
|
+
const ruleRe = new RegExp(`${effect}\\s*\\([^)]*resource\\s*==\\s*Tool::"${tool.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}"`, "s");
|
|
13354
|
+
if (ruleRe.test(stripComments(before))) {
|
|
13355
|
+
process.stdout.write(`${dim("No change:")} a ${effect} rule for ${bold(tool)} already exists in ${targetFile}.
|
|
13356
|
+
`);
|
|
13357
|
+
process.exit(0);
|
|
13358
|
+
}
|
|
13359
|
+
const block = `
|
|
13360
|
+
// ${effect === "permit" ? "Allow" : "Block"} ${tool} (added by \`protect-mcp policy ${sub}\`)
|
|
13361
|
+
${effect}(
|
|
13362
|
+
principal,
|
|
13363
|
+
action == Action::"MCP::Tool::call",
|
|
13364
|
+
resource == Tool::"${tool}"
|
|
13365
|
+
);
|
|
13366
|
+
`;
|
|
13367
|
+
af(pj(dir, targetFile), block);
|
|
13368
|
+
const after = readAll();
|
|
13369
|
+
process.stdout.write(`${bold(sub === "allow" ? "\u2713 Allowed" : "\u2713 Denied")} ${tool}
|
|
13370
|
+
`);
|
|
13371
|
+
process.stdout.write(` ${dim(`appended to ${pj(dir, targetFile)}`)}
|
|
13372
|
+
`);
|
|
13373
|
+
process.stdout.write(` ${dim(`policy digest ${digestOf(before)} \u2192 ${digestOf(after)}`)}
|
|
13374
|
+
`);
|
|
13375
|
+
process.stdout.write(`
|
|
13376
|
+
${dim("A running gate (protect-mcp serve) hot-reloads on this change; no restart needed. The one-shot hook path re-reads per call.")}
|
|
13377
|
+
`);
|
|
13378
|
+
process.exit(0);
|
|
13379
|
+
}
|
|
13380
|
+
if (sub === "list") {
|
|
13381
|
+
if (!dir) {
|
|
13382
|
+
process.stderr.write("No Cedar policy found. Run: npx protect-mcp init-hooks\n");
|
|
13383
|
+
process.exit(1);
|
|
13384
|
+
}
|
|
13385
|
+
const src = readAll();
|
|
13386
|
+
const active = stripComments(src);
|
|
13387
|
+
const named = (effect) => {
|
|
13388
|
+
const set = /* @__PURE__ */ new Set();
|
|
13389
|
+
const re = new RegExp(`${effect}\\s*\\([\\s\\S]*?resource\\s*==\\s*Tool::"([^"]+)"[\\s\\S]*?\\);`, "g");
|
|
13390
|
+
let m;
|
|
13391
|
+
while (m = re.exec(active)) set.add(m[1]);
|
|
13392
|
+
return set;
|
|
13393
|
+
};
|
|
13394
|
+
const permitted = named("permit"), forbidden = named("forbid");
|
|
13395
|
+
const seen = /* @__PURE__ */ new Map();
|
|
13396
|
+
try {
|
|
13397
|
+
const logPath = pj(process.cwd(), ".protect-mcp-log.jsonl");
|
|
13398
|
+
if (ex(logPath)) {
|
|
13399
|
+
for (const line of rf(logPath, "utf-8").split("\n")) {
|
|
13400
|
+
if (!line.trim()) continue;
|
|
13401
|
+
try {
|
|
13402
|
+
const e = JSON.parse(line);
|
|
13403
|
+
if (!e.tool) continue;
|
|
13404
|
+
const rec = seen.get(e.tool) || { allow: 0, deny: 0 };
|
|
13405
|
+
if (e.decision === "deny") rec.deny++;
|
|
13406
|
+
else rec.allow++;
|
|
13407
|
+
seen.set(e.tool, rec);
|
|
13408
|
+
} catch {
|
|
13409
|
+
}
|
|
13410
|
+
}
|
|
13411
|
+
}
|
|
13412
|
+
} catch {
|
|
13413
|
+
}
|
|
13414
|
+
process.stdout.write(`${bold("Cedar policy")} ${dim(`(${dir}, digest ${digestOf(src)}) \xB7 default-deny, fail-closed`)}
|
|
13415
|
+
|
|
13416
|
+
`);
|
|
13417
|
+
const allTools = /* @__PURE__ */ new Set([...permitted, ...forbidden, ...seen.keys()]);
|
|
13418
|
+
if (allTools.size === 0) {
|
|
13419
|
+
process.stdout.write(dim(" No rules and no decisions logged yet.\n"));
|
|
13420
|
+
process.exit(0);
|
|
13421
|
+
}
|
|
13422
|
+
const rows = [...allTools].sort().map((t) => {
|
|
13423
|
+
const label = forbidden.has(t) ? "forbid" : permitted.has(t) ? "permit" : "default-deny";
|
|
13424
|
+
const colored = forbidden.has(t) ? red(label) : permitted.has(t) ? green(label) : yellow(label);
|
|
13425
|
+
const pad = " ".repeat(Math.max(1, 13 - label.length));
|
|
13426
|
+
const s = seen.get(t);
|
|
13427
|
+
const hits = s ? dim(` ${s.allow} allowed, ${s.deny} denied`) : "";
|
|
13428
|
+
return ` ${colored}${pad}${bold(t)}${hits}`;
|
|
13429
|
+
});
|
|
13430
|
+
process.stdout.write(rows.join("\n") + "\n");
|
|
13431
|
+
process.stdout.write(`
|
|
13432
|
+
${dim("Allow a tool: npx protect-mcp policy allow <ToolName> \xB7 Block one: policy deny <ToolName>")}
|
|
13433
|
+
`);
|
|
13434
|
+
process.exit(0);
|
|
13435
|
+
}
|
|
13436
|
+
process.stderr.write("Usage: npx protect-mcp policy <list|show|allow <tool>|deny <tool>|path>\n");
|
|
13437
|
+
process.exit(1);
|
|
13438
|
+
}
|
|
12980
13439
|
async function main() {
|
|
12981
13440
|
sendInstallTelemetry().catch(() => {
|
|
12982
13441
|
});
|
|
@@ -13000,6 +13459,10 @@ async function main() {
|
|
|
13000
13459
|
await handleSign(args.slice(1));
|
|
13001
13460
|
return;
|
|
13002
13461
|
}
|
|
13462
|
+
if (args[0] === "mcp") {
|
|
13463
|
+
await (await Promise.resolve().then(() => (init_mcp_server(), mcp_server_exports))).runMcpServer();
|
|
13464
|
+
return;
|
|
13465
|
+
}
|
|
13003
13466
|
if (args[0] === "serve") {
|
|
13004
13467
|
const { startHookServer: startHookServer2 } = await Promise.resolve().then(() => (init_hook_server(), hook_server_exports));
|
|
13005
13468
|
const portIdx = args.indexOf("--port");
|
|
@@ -13046,6 +13509,10 @@ async function main() {
|
|
|
13046
13509
|
await handleSample(args.slice(1));
|
|
13047
13510
|
return;
|
|
13048
13511
|
}
|
|
13512
|
+
if (args[0] === "policy") {
|
|
13513
|
+
await handlePolicy(args.slice(1));
|
|
13514
|
+
return;
|
|
13515
|
+
}
|
|
13049
13516
|
if (args[0] === "init-hooks") {
|
|
13050
13517
|
await handleInitHooks(args.slice(1));
|
|
13051
13518
|
process.exit(0);
|