protect-mcp 0.3.0 → 0.3.1
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 +11 -1
- package/dist/bundle-TXOTFJIJ.mjs +8 -0
- package/dist/chunk-5JXFV37Y.mjs +53 -0
- package/dist/{chunk-3WCA7O4D.mjs → chunk-U7TMVD3E.mjs} +136 -8
- package/dist/cli.js +427 -20
- package/dist/cli.mjs +227 -6
- package/dist/index.d.mts +8 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +135 -7
- package/dist/index.mjs +5 -50
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -67,7 +67,7 @@ __export(utils_exports, {
|
|
|
67
67
|
isLE: () => isLE,
|
|
68
68
|
kdfInputToBytes: () => kdfInputToBytes,
|
|
69
69
|
nextTick: () => nextTick,
|
|
70
|
-
randomBytes: () =>
|
|
70
|
+
randomBytes: () => randomBytes2,
|
|
71
71
|
rotl: () => rotl,
|
|
72
72
|
rotr: () => rotr,
|
|
73
73
|
swap32IfBE: () => swap32IfBE,
|
|
@@ -257,7 +257,7 @@ function createXOFer(hashCons) {
|
|
|
257
257
|
hashC.create = (opts) => hashCons(opts);
|
|
258
258
|
return hashC;
|
|
259
259
|
}
|
|
260
|
-
function
|
|
260
|
+
function randomBytes2(bytesLength = 32) {
|
|
261
261
|
if (crypto && typeof crypto.getRandomValues === "function") {
|
|
262
262
|
return crypto.getRandomValues(new Uint8Array(bytesLength));
|
|
263
263
|
}
|
|
@@ -1700,7 +1700,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
1700
1700
|
});
|
|
1701
1701
|
const { prehash } = eddsaOpts;
|
|
1702
1702
|
const { BASE, Fp: Fp2, Fn: Fn2 } = Point;
|
|
1703
|
-
const
|
|
1703
|
+
const randomBytes3 = eddsaOpts.randomBytes || randomBytes2;
|
|
1704
1704
|
const adjustScalarBytes2 = eddsaOpts.adjustScalarBytes || ((bytes) => bytes);
|
|
1705
1705
|
const domain = eddsaOpts.domain || ((data, ctx, phflag) => {
|
|
1706
1706
|
_abool2(phflag, "phflag");
|
|
@@ -1782,7 +1782,7 @@ function eddsa(Point, cHash, eddsaOpts = {}) {
|
|
|
1782
1782
|
signature: 2 * _size,
|
|
1783
1783
|
seed: _size
|
|
1784
1784
|
};
|
|
1785
|
-
function randomSecretKey(seed =
|
|
1785
|
+
function randomSecretKey(seed = randomBytes3(lengths.seed)) {
|
|
1786
1786
|
return _abytes2(seed, lengths.seed, "seed");
|
|
1787
1787
|
}
|
|
1788
1788
|
function keygen(seed) {
|
|
@@ -2137,7 +2137,7 @@ function montgomery(curveDef) {
|
|
|
2137
2137
|
const is25519 = type === "x25519";
|
|
2138
2138
|
if (!is25519 && type !== "x448")
|
|
2139
2139
|
throw new Error("invalid type");
|
|
2140
|
-
const randomBytes_ = rand ||
|
|
2140
|
+
const randomBytes_ = rand || randomBytes2;
|
|
2141
2141
|
const montgomeryBits = is25519 ? 255 : 448;
|
|
2142
2142
|
const fieldLen = is25519 ? 32 : 56;
|
|
2143
2143
|
const Gu = is25519 ? BigInt(9) : BigInt(5);
|
|
@@ -2638,6 +2638,65 @@ var init_ed25519 = __esm({
|
|
|
2638
2638
|
}
|
|
2639
2639
|
});
|
|
2640
2640
|
|
|
2641
|
+
// src/bundle.ts
|
|
2642
|
+
var bundle_exports = {};
|
|
2643
|
+
__export(bundle_exports, {
|
|
2644
|
+
collectSignedReceipts: () => collectSignedReceipts,
|
|
2645
|
+
createAuditBundle: () => createAuditBundle
|
|
2646
|
+
});
|
|
2647
|
+
function createAuditBundle(opts) {
|
|
2648
|
+
const receipts = opts.receipts.filter(
|
|
2649
|
+
(r) => r && typeof r === "object" && typeof r.signature === "string"
|
|
2650
|
+
);
|
|
2651
|
+
if (receipts.length === 0) {
|
|
2652
|
+
throw new Error("Audit bundle requires at least one signed receipt");
|
|
2653
|
+
}
|
|
2654
|
+
const keyMap = /* @__PURE__ */ new Map();
|
|
2655
|
+
for (const key of opts.signingKeys) {
|
|
2656
|
+
if (!keyMap.has(key.kid)) {
|
|
2657
|
+
keyMap.set(key.kid, key);
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
let timeRange = opts.timeRange || null;
|
|
2661
|
+
if (!timeRange) {
|
|
2662
|
+
const timestamps = receipts.map((r) => r.issued_at || r.timestamp).filter(Boolean).sort();
|
|
2663
|
+
if (timestamps.length > 0) {
|
|
2664
|
+
timeRange = {
|
|
2665
|
+
from: timestamps[0],
|
|
2666
|
+
to: timestamps[timestamps.length - 1]
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
return {
|
|
2671
|
+
format: "scopeblind:audit-bundle",
|
|
2672
|
+
version: 1,
|
|
2673
|
+
exported_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2674
|
+
tenant: opts.tenant,
|
|
2675
|
+
time_range: timeRange,
|
|
2676
|
+
receipts,
|
|
2677
|
+
anchors: opts.anchors || [],
|
|
2678
|
+
verification: {
|
|
2679
|
+
algorithm: "ed25519",
|
|
2680
|
+
signing_keys: Array.from(keyMap.values()),
|
|
2681
|
+
instructions: `Verify each receipt by: (1) remove the "signature" field, (2) canonicalize the remaining object with JCS (sorted keys at every level), (3) encode as UTF-8 bytes, (4) verify the Ed25519 signature using the signing key matching the receipt's "kid" field. CLI: npx @veritasacta/verify bundle.json --bundle`
|
|
2682
|
+
}
|
|
2683
|
+
};
|
|
2684
|
+
}
|
|
2685
|
+
function collectSignedReceipts(logs) {
|
|
2686
|
+
return logs.filter((log) => log.v === 2).map((log) => {
|
|
2687
|
+
const logRecord = log;
|
|
2688
|
+
if (logRecord.receipt) {
|
|
2689
|
+
return logRecord.receipt;
|
|
2690
|
+
}
|
|
2691
|
+
return logRecord;
|
|
2692
|
+
}).filter((r) => typeof r.signature === "string");
|
|
2693
|
+
}
|
|
2694
|
+
var init_bundle = __esm({
|
|
2695
|
+
"src/bundle.ts"() {
|
|
2696
|
+
"use strict";
|
|
2697
|
+
}
|
|
2698
|
+
});
|
|
2699
|
+
|
|
2641
2700
|
// src/gateway.ts
|
|
2642
2701
|
var import_node_child_process = require("child_process");
|
|
2643
2702
|
var import_node_crypto2 = require("crypto");
|
|
@@ -2979,8 +3038,8 @@ async function initSigning(config) {
|
|
|
2979
3038
|
signerState = {
|
|
2980
3039
|
privateKey: keyData.privateKey,
|
|
2981
3040
|
publicKey: keyData.publicKey,
|
|
2982
|
-
kid: artifactsModule.computeKid(keyData.publicKey),
|
|
2983
|
-
issuer: config.issuer || "protect-mcp"
|
|
3041
|
+
kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
|
|
3042
|
+
issuer: config.issuer || keyData.issuer || "protect-mcp"
|
|
2984
3043
|
};
|
|
2985
3044
|
} catch (err) {
|
|
2986
3045
|
warnings.push(`signing: failed to load key file: ${err instanceof Error ? err.message : err}`);
|
|
@@ -3184,13 +3243,16 @@ var ReceiptBuffer = class {
|
|
|
3184
3243
|
count() {
|
|
3185
3244
|
return this.receipts.length;
|
|
3186
3245
|
}
|
|
3246
|
+
getLatest() {
|
|
3247
|
+
return this.receipts.length > 0 ? this.receipts[this.receipts.length - 1] : void 0;
|
|
3248
|
+
}
|
|
3187
3249
|
};
|
|
3188
|
-
function startStatusServer(config, receiptBuffer) {
|
|
3250
|
+
function startStatusServer(config, receiptBuffer, approvalStore, approvalNonce) {
|
|
3189
3251
|
const startTime = Date.now();
|
|
3190
3252
|
const logDir = process.cwd();
|
|
3191
3253
|
const server = (0, import_node_http.createServer)((req, res) => {
|
|
3192
3254
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
3193
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
|
|
3255
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
3194
3256
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
3195
3257
|
res.setHeader("Content-Type", "application/json");
|
|
3196
3258
|
if (req.method === "OPTIONS") {
|
|
@@ -3207,18 +3269,30 @@ function startStatusServer(config, receiptBuffer) {
|
|
|
3207
3269
|
handleStatus(res, logDir);
|
|
3208
3270
|
} else if (path === "/receipts") {
|
|
3209
3271
|
handleReceipts(res, receiptBuffer, url);
|
|
3272
|
+
} else if (path === "/receipts/latest") {
|
|
3273
|
+
handleReceiptLatest(res, receiptBuffer);
|
|
3210
3274
|
} else if (path.startsWith("/receipts/")) {
|
|
3211
3275
|
const id = path.slice("/receipts/".length);
|
|
3212
3276
|
handleReceiptById(res, receiptBuffer, id);
|
|
3277
|
+
} else if (path === "/approve" && req.method === "POST") {
|
|
3278
|
+
handleApprove(req, res, approvalStore, approvalNonce);
|
|
3279
|
+
} else if (path === "/approvals" && req.method === "GET") {
|
|
3280
|
+
handleListApprovals(res, approvalStore);
|
|
3213
3281
|
} else {
|
|
3214
3282
|
res.writeHead(404);
|
|
3215
|
-
res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/:id"] }));
|
|
3283
|
+
res.end(JSON.stringify({ error: "not_found", endpoints: ["/health", "/status", "/receipts", "/receipts/latest", "/receipts/:id", "/approve", "/approvals"] }));
|
|
3216
3284
|
}
|
|
3217
3285
|
} catch (err) {
|
|
3218
3286
|
res.writeHead(500);
|
|
3219
3287
|
res.end(JSON.stringify({ error: "internal_error" }));
|
|
3220
3288
|
}
|
|
3221
3289
|
});
|
|
3290
|
+
server.on("error", (err) => {
|
|
3291
|
+
if (config.verbose) {
|
|
3292
|
+
process.stderr.write(`[PROTECT_MCP] HTTP status server error: ${err.message}
|
|
3293
|
+
`);
|
|
3294
|
+
}
|
|
3295
|
+
});
|
|
3222
3296
|
server.listen(config.port, "127.0.0.1", () => {
|
|
3223
3297
|
if (config.verbose) {
|
|
3224
3298
|
process.stderr.write(`[PROTECT_MCP] HTTP status server listening on http://127.0.0.1:${config.port}
|
|
@@ -3234,7 +3308,7 @@ function handleHealth(res, startTime, config) {
|
|
|
3234
3308
|
status: "ok",
|
|
3235
3309
|
uptime_ms: Date.now() - startTime,
|
|
3236
3310
|
mode: config.mode,
|
|
3237
|
-
version: "0.3.
|
|
3311
|
+
version: "0.3.1"
|
|
3238
3312
|
}));
|
|
3239
3313
|
}
|
|
3240
3314
|
function handleStatus(res, logDir) {
|
|
@@ -3283,6 +3357,16 @@ function handleReceipts(res, buffer, url) {
|
|
|
3283
3357
|
receipts
|
|
3284
3358
|
}));
|
|
3285
3359
|
}
|
|
3360
|
+
function handleReceiptLatest(res, buffer) {
|
|
3361
|
+
const latest = buffer.getLatest();
|
|
3362
|
+
if (!latest) {
|
|
3363
|
+
res.writeHead(404);
|
|
3364
|
+
res.end(JSON.stringify({ error: "no_receipts", message: "No receipts yet. Make a tool call through protect-mcp first." }));
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
res.writeHead(200);
|
|
3368
|
+
res.end(JSON.stringify(latest));
|
|
3369
|
+
}
|
|
3286
3370
|
function handleReceiptById(res, buffer, id) {
|
|
3287
3371
|
const receipt = buffer.getById(id);
|
|
3288
3372
|
if (!receipt) {
|
|
@@ -3293,22 +3377,92 @@ function handleReceiptById(res, buffer, id) {
|
|
|
3293
3377
|
res.writeHead(200);
|
|
3294
3378
|
res.end(JSON.stringify(receipt));
|
|
3295
3379
|
}
|
|
3380
|
+
function handleApprove(req, res, approvalStore, expectedNonce) {
|
|
3381
|
+
if (!approvalStore) {
|
|
3382
|
+
res.writeHead(503);
|
|
3383
|
+
res.end(JSON.stringify({ error: "approval_store_not_available" }));
|
|
3384
|
+
return;
|
|
3385
|
+
}
|
|
3386
|
+
let body = "";
|
|
3387
|
+
req.on("data", (chunk) => {
|
|
3388
|
+
body += chunk.toString();
|
|
3389
|
+
});
|
|
3390
|
+
req.on("end", () => {
|
|
3391
|
+
try {
|
|
3392
|
+
const { request_id, tool, mode, nonce } = JSON.parse(body);
|
|
3393
|
+
if (expectedNonce && nonce !== expectedNonce) {
|
|
3394
|
+
res.writeHead(403);
|
|
3395
|
+
res.end(JSON.stringify({ error: "invalid_nonce", message: "Approval nonce does not match. Check stderr output for the correct nonce." }));
|
|
3396
|
+
return;
|
|
3397
|
+
}
|
|
3398
|
+
if (!tool || typeof tool !== "string") {
|
|
3399
|
+
res.writeHead(400);
|
|
3400
|
+
res.end(JSON.stringify({ error: "missing_tool", usage: '{"request_id":"abc123","tool":"send_email","mode":"once|always","nonce":"..."}' }));
|
|
3401
|
+
return;
|
|
3402
|
+
}
|
|
3403
|
+
const grantMode = mode === "always" ? "always" : "once";
|
|
3404
|
+
const ttlMs = grantMode === "once" ? 5 * 60 * 1e3 : 24 * 60 * 60 * 1e3;
|
|
3405
|
+
const grantEntry = { tool, mode: grantMode, expires_at: Date.now() + ttlMs };
|
|
3406
|
+
if (grantMode === "always") {
|
|
3407
|
+
approvalStore.set(`always:${tool}`, grantEntry);
|
|
3408
|
+
} else if (request_id) {
|
|
3409
|
+
approvalStore.set(request_id, grantEntry);
|
|
3410
|
+
} else {
|
|
3411
|
+
approvalStore.set(tool, grantEntry);
|
|
3412
|
+
}
|
|
3413
|
+
res.writeHead(200);
|
|
3414
|
+
res.end(JSON.stringify({
|
|
3415
|
+
approved: true,
|
|
3416
|
+
request_id: request_id || null,
|
|
3417
|
+
tool,
|
|
3418
|
+
mode: grantMode,
|
|
3419
|
+
expires_in_seconds: ttlMs / 1e3
|
|
3420
|
+
}));
|
|
3421
|
+
} catch {
|
|
3422
|
+
res.writeHead(400);
|
|
3423
|
+
res.end(JSON.stringify({ error: "invalid_json", usage: '{"request_id":"abc123","tool":"send_email","mode":"once","nonce":"..."}' }));
|
|
3424
|
+
}
|
|
3425
|
+
});
|
|
3426
|
+
}
|
|
3427
|
+
function handleListApprovals(res, approvalStore) {
|
|
3428
|
+
if (!approvalStore) {
|
|
3429
|
+
res.writeHead(200);
|
|
3430
|
+
res.end(JSON.stringify({ grants: [] }));
|
|
3431
|
+
return;
|
|
3432
|
+
}
|
|
3433
|
+
const now = Date.now();
|
|
3434
|
+
const grants = [];
|
|
3435
|
+
for (const [key, grant] of approvalStore) {
|
|
3436
|
+
if (now < grant.expires_at) {
|
|
3437
|
+
grants.push({ key, tool: grant.tool, mode: grant.mode, expires_in_seconds: Math.round((grant.expires_at - now) / 1e3) });
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
res.writeHead(200);
|
|
3441
|
+
res.end(JSON.stringify({ grants }));
|
|
3442
|
+
}
|
|
3296
3443
|
|
|
3297
3444
|
// src/gateway.ts
|
|
3298
3445
|
var LOG_FILE2 = ".protect-mcp-log.jsonl";
|
|
3446
|
+
var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
|
|
3299
3447
|
var ProtectGateway = class {
|
|
3300
3448
|
child = null;
|
|
3301
3449
|
config;
|
|
3302
3450
|
rateLimitStore = /* @__PURE__ */ new Map();
|
|
3303
3451
|
clientReader = null;
|
|
3304
3452
|
logFilePath;
|
|
3453
|
+
receiptFilePath;
|
|
3305
3454
|
evidenceStore;
|
|
3306
3455
|
receiptBuffer;
|
|
3456
|
+
/** Approval grants keyed by request_id (scoped to the specific action that was requested) */
|
|
3457
|
+
approvalStore = /* @__PURE__ */ new Map();
|
|
3458
|
+
/** Random nonce generated at startup — required for approval endpoint authentication */
|
|
3459
|
+
approvalNonce = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
3307
3460
|
currentTier = "unknown";
|
|
3308
3461
|
admissionResult = null;
|
|
3309
3462
|
constructor(config) {
|
|
3310
3463
|
this.config = config;
|
|
3311
3464
|
this.logFilePath = (0, import_node_path3.join)(process.cwd(), LOG_FILE2);
|
|
3465
|
+
this.receiptFilePath = (0, import_node_path3.join)(process.cwd(), RECEIPTS_FILE);
|
|
3312
3466
|
this.evidenceStore = new EvidenceStore();
|
|
3313
3467
|
this.receiptBuffer = new ReceiptBuffer();
|
|
3314
3468
|
}
|
|
@@ -3332,12 +3486,15 @@ var ProtectGateway = class {
|
|
|
3332
3486
|
this.log(`External PDP: ${this.config.policy.external?.endpoint || "not configured"}`);
|
|
3333
3487
|
}
|
|
3334
3488
|
}
|
|
3489
|
+
this.log(`Approval nonce: ${this.approvalNonce}`);
|
|
3335
3490
|
const httpPort = parseInt(process.env.PROTECT_MCP_HTTP_PORT || "9876", 10);
|
|
3336
3491
|
if (httpPort > 0) {
|
|
3337
3492
|
try {
|
|
3338
3493
|
startStatusServer(
|
|
3339
3494
|
{ port: httpPort, mode, verbose },
|
|
3340
|
-
this.receiptBuffer
|
|
3495
|
+
this.receiptBuffer,
|
|
3496
|
+
this.approvalStore,
|
|
3497
|
+
this.approvalNonce
|
|
3341
3498
|
);
|
|
3342
3499
|
} catch {
|
|
3343
3500
|
if (verbose) this.log(`HTTP status server could not start on port ${httpPort}`);
|
|
@@ -3491,6 +3648,32 @@ var ProtectGateway = class {
|
|
|
3491
3648
|
}
|
|
3492
3649
|
return null;
|
|
3493
3650
|
}
|
|
3651
|
+
if (toolPolicy.require_approval) {
|
|
3652
|
+
const grant = this.approvalStore.get(requestId);
|
|
3653
|
+
const alwaysGrant = this.approvalStore.get(`always:${toolName}`);
|
|
3654
|
+
if (grant && Date.now() < grant.expires_at || alwaysGrant && Date.now() < alwaysGrant.expires_at) {
|
|
3655
|
+
if (grant && grant.mode === "once") this.approvalStore.delete(requestId);
|
|
3656
|
+
this.emitDecisionLog({ tool: toolName, decision: "allow", reason_code: "approval_granted", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
3657
|
+
return null;
|
|
3658
|
+
}
|
|
3659
|
+
this.emitDecisionLog({ tool: toolName, decision: "require_approval", reason_code: "requires_human_approval", request_id: requestId, tier: this.currentTier, credential_ref: credentialRef });
|
|
3660
|
+
if (this.config.enforce) {
|
|
3661
|
+
return {
|
|
3662
|
+
jsonrpc: "2.0",
|
|
3663
|
+
id: request.id,
|
|
3664
|
+
result: {
|
|
3665
|
+
content: [
|
|
3666
|
+
{
|
|
3667
|
+
type: "text",
|
|
3668
|
+
text: `REQUIRES_APPROVAL: The tool "${toolName}" requires human approval before execution. Request ID: ${requestId}. Approval nonce: ${this.approvalNonce}. Tell the user you need their approval to use "${toolName}" and will retry when granted. Do NOT retry this tool call until the user explicitly approves it.`
|
|
3669
|
+
}
|
|
3670
|
+
],
|
|
3671
|
+
isError: true
|
|
3672
|
+
}
|
|
3673
|
+
};
|
|
3674
|
+
}
|
|
3675
|
+
return null;
|
|
3676
|
+
}
|
|
3494
3677
|
const rateSpec = this.getTierRateLimit(toolPolicy, this.currentTier);
|
|
3495
3678
|
if (rateSpec) {
|
|
3496
3679
|
try {
|
|
@@ -3548,6 +3731,10 @@ var ProtectGateway = class {
|
|
|
3548
3731
|
if (signed.signed) {
|
|
3549
3732
|
process.stderr.write(`[PROTECT_MCP_RECEIPT] ${signed.signed}
|
|
3550
3733
|
`);
|
|
3734
|
+
try {
|
|
3735
|
+
(0, import_node_fs5.appendFileSync)(this.receiptFilePath, signed.signed + "\n");
|
|
3736
|
+
} catch {
|
|
3737
|
+
}
|
|
3551
3738
|
this.receiptBuffer.add(log.request_id, signed.signed);
|
|
3552
3739
|
if (this.admissionResult?.agent_id) {
|
|
3553
3740
|
this.evidenceStore.record(this.admissionResult.agent_id, this.config.signing?.issuer || "protect-mcp");
|
|
@@ -3586,7 +3773,6 @@ var ProtectGateway = class {
|
|
|
3586
3773
|
};
|
|
3587
3774
|
|
|
3588
3775
|
// src/cli.ts
|
|
3589
|
-
var import_meta = {};
|
|
3590
3776
|
function printHelp() {
|
|
3591
3777
|
process.stderr.write(`
|
|
3592
3778
|
protect-mcp \u2014 Shadow-mode security gateway for MCP servers
|
|
@@ -3596,6 +3782,9 @@ Usage:
|
|
|
3596
3782
|
protect-mcp init [--dir <path>]
|
|
3597
3783
|
protect-mcp demo
|
|
3598
3784
|
protect-mcp status [--dir <path>]
|
|
3785
|
+
protect-mcp digest [--today] [--dir <path>]
|
|
3786
|
+
protect-mcp receipts [--last <n>] [--dir <path>]
|
|
3787
|
+
protect-mcp bundle [--output <path>] [--dir <path>]
|
|
3599
3788
|
|
|
3600
3789
|
Options:
|
|
3601
3790
|
--policy <path> Policy/config JSON file (default: allow-all)
|
|
@@ -3608,6 +3797,9 @@ Commands:
|
|
|
3608
3797
|
init Generate config template, Ed25519 keypair, and sample policy
|
|
3609
3798
|
demo Start a demo server wrapped with protect-mcp (see receipts instantly)
|
|
3610
3799
|
status Show tool call statistics from the local decision log
|
|
3800
|
+
digest Generate a human-readable summary of agent activity
|
|
3801
|
+
receipts Show recent persisted signed receipts
|
|
3802
|
+
bundle Export an offline-verifiable audit bundle
|
|
3611
3803
|
|
|
3612
3804
|
Examples:
|
|
3613
3805
|
protect-mcp -- node my-server.js
|
|
@@ -3616,6 +3808,9 @@ Examples:
|
|
|
3616
3808
|
protect-mcp init
|
|
3617
3809
|
protect-mcp demo
|
|
3618
3810
|
protect-mcp status
|
|
3811
|
+
protect-mcp digest --today
|
|
3812
|
+
protect-mcp receipts --last 10
|
|
3813
|
+
protect-mcp bundle --output audit.json
|
|
3619
3814
|
|
|
3620
3815
|
`);
|
|
3621
3816
|
}
|
|
@@ -3680,10 +3875,10 @@ async function handleInit(argv) {
|
|
|
3680
3875
|
const artifacts = await import("@veritasacta/artifacts");
|
|
3681
3876
|
keypair = artifacts.generateKeypair();
|
|
3682
3877
|
} catch {
|
|
3683
|
-
const { randomBytes:
|
|
3878
|
+
const { randomBytes: randomBytes3 } = await import("crypto");
|
|
3684
3879
|
const { ed25519: ed255192 } = await Promise.resolve().then(() => (init_ed25519(), ed25519_exports));
|
|
3685
3880
|
const { bytesToHex: bytesToHex2 } = await Promise.resolve().then(() => (init_utils(), utils_exports));
|
|
3686
|
-
const privateKey =
|
|
3881
|
+
const privateKey = randomBytes3(32);
|
|
3687
3882
|
const publicKey = ed255192.getPublicKey(privateKey);
|
|
3688
3883
|
keypair = {
|
|
3689
3884
|
privateKey: bytesToHex2(privateKey),
|
|
@@ -3776,11 +3971,10 @@ Add --enforce when ready to block policy violations.
|
|
|
3776
3971
|
}
|
|
3777
3972
|
async function handleDemo() {
|
|
3778
3973
|
const { existsSync: existsSync4 } = await import("fs");
|
|
3779
|
-
const { join: join4, dirname } = await import("path");
|
|
3780
|
-
const
|
|
3781
|
-
const
|
|
3782
|
-
const
|
|
3783
|
-
const demoServerPath = join4(__dirname, "demo-server.js");
|
|
3974
|
+
const { join: join4, dirname, resolve } = await import("path");
|
|
3975
|
+
const cliPath = resolve(process.argv[1] || "dist/cli.js");
|
|
3976
|
+
const cliDir = dirname(cliPath);
|
|
3977
|
+
const demoServerPath = join4(cliDir, "demo-server.js");
|
|
3784
3978
|
const configPath = join4(process.cwd(), "protect-mcp.json");
|
|
3785
3979
|
const hasConfig = existsSync4(configPath);
|
|
3786
3980
|
if (!hasConfig) {
|
|
@@ -3965,6 +4159,27 @@ ${bold("protect-mcp status")}
|
|
|
3965
4159
|
} catch {
|
|
3966
4160
|
}
|
|
3967
4161
|
}
|
|
4162
|
+
const keyPath = join4(dir, "keys", "gateway.json");
|
|
4163
|
+
if (existsSync4(keyPath)) {
|
|
4164
|
+
try {
|
|
4165
|
+
const keyData = JSON.parse(readFileSync5(keyPath, "utf-8"));
|
|
4166
|
+
if (keyData.publicKey) {
|
|
4167
|
+
const fingerprint = keyData.publicKey.slice(0, 16) + "...";
|
|
4168
|
+
process.stdout.write(`
|
|
4169
|
+
${bold("\u{1F6E1}\uFE0F Passport identity:")}
|
|
4170
|
+
`);
|
|
4171
|
+
process.stdout.write(` Public key: ${fingerprint}
|
|
4172
|
+
`);
|
|
4173
|
+
if (keyData.kid) process.stdout.write(` Key ID: ${keyData.kid}
|
|
4174
|
+
`);
|
|
4175
|
+
process.stdout.write(` Issuer: ${keyData.issuer || "protect-mcp"}
|
|
4176
|
+
`);
|
|
4177
|
+
process.stdout.write(` Verify: ${dim("npx @veritasacta/verify <receipt.json>")}
|
|
4178
|
+
`);
|
|
4179
|
+
}
|
|
4180
|
+
} catch {
|
|
4181
|
+
}
|
|
4182
|
+
}
|
|
3968
4183
|
process.stdout.write(`
|
|
3969
4184
|
Log file: ${dim(logPath)}
|
|
3970
4185
|
|
|
@@ -3985,6 +4200,186 @@ function red(s) {
|
|
|
3985
4200
|
function yellow(s) {
|
|
3986
4201
|
return process.env.NO_COLOR ? s : `\x1B[33m${s}\x1B[0m`;
|
|
3987
4202
|
}
|
|
4203
|
+
async function handleDigest(argv) {
|
|
4204
|
+
const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
|
|
4205
|
+
const { join: join4 } = await import("path");
|
|
4206
|
+
let dir = process.cwd();
|
|
4207
|
+
const dirIdx = argv.indexOf("--dir");
|
|
4208
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4209
|
+
const today = argv.includes("--today");
|
|
4210
|
+
const logPath = join4(dir, ".protect-mcp-log.jsonl");
|
|
4211
|
+
if (!existsSync4(logPath)) {
|
|
4212
|
+
process.stderr.write(`${bold("protect-mcp digest")}
|
|
4213
|
+
|
|
4214
|
+
No log file found. Run protect-mcp first.
|
|
4215
|
+
`);
|
|
4216
|
+
process.exit(0);
|
|
4217
|
+
}
|
|
4218
|
+
const raw = readFileSync5(logPath, "utf-8");
|
|
4219
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4220
|
+
let entries = [];
|
|
4221
|
+
for (const line of lines) {
|
|
4222
|
+
try {
|
|
4223
|
+
entries.push(JSON.parse(line));
|
|
4224
|
+
} catch {
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
if (today) {
|
|
4228
|
+
const todayStart = /* @__PURE__ */ new Date();
|
|
4229
|
+
todayStart.setHours(0, 0, 0, 0);
|
|
4230
|
+
entries = entries.filter((e) => e.timestamp >= todayStart.getTime());
|
|
4231
|
+
}
|
|
4232
|
+
if (entries.length === 0) {
|
|
4233
|
+
process.stdout.write(`
|
|
4234
|
+
${bold("\u{1F6E1}\uFE0F Agent Digest")}
|
|
4235
|
+
|
|
4236
|
+
No activity${today ? " today" : ""}.
|
|
4237
|
+
|
|
4238
|
+
`);
|
|
4239
|
+
process.exit(0);
|
|
4240
|
+
}
|
|
4241
|
+
const allowed = entries.filter((e) => e.decision === "allow").length;
|
|
4242
|
+
const denied = entries.filter((e) => e.decision === "deny").length;
|
|
4243
|
+
const approvalRequired = entries.filter((e) => e.decision === "require_approval").length;
|
|
4244
|
+
const toolUsage = /* @__PURE__ */ new Map();
|
|
4245
|
+
for (const e of entries) {
|
|
4246
|
+
toolUsage.set(e.tool, (toolUsage.get(e.tool) || 0) + 1);
|
|
4247
|
+
}
|
|
4248
|
+
const sortedTools = [...toolUsage.entries()].sort((a, b) => b[1] - a[1]);
|
|
4249
|
+
const currentTier = entries[entries.length - 1]?.tier || "unknown";
|
|
4250
|
+
const firstTime = new Date(Math.min(...entries.map((e) => e.timestamp)));
|
|
4251
|
+
const lastTime = new Date(Math.max(...entries.map((e) => e.timestamp)));
|
|
4252
|
+
const durationMs = lastTime.getTime() - firstTime.getTime();
|
|
4253
|
+
const durationStr = durationMs < 6e4 ? `${Math.round(durationMs / 1e3)}s` : durationMs < 36e5 ? `${Math.round(durationMs / 6e4)}m` : `${(durationMs / 36e5).toFixed(1)}h`;
|
|
4254
|
+
process.stdout.write(`
|
|
4255
|
+
${bold("\u{1F6E1}\uFE0F Agent Daily Digest")}
|
|
4256
|
+
|
|
4257
|
+
`);
|
|
4258
|
+
process.stdout.write(` \u{1F4CA} ${bold(String(entries.length))} actions | `);
|
|
4259
|
+
process.stdout.write(`${green("\u2713 " + allowed)} allowed | `);
|
|
4260
|
+
process.stdout.write(`${red("\u2717 " + denied)} blocked`);
|
|
4261
|
+
if (approvalRequired > 0) process.stdout.write(` | ${yellow("\u23F3 " + approvalRequired)} awaiting approval`);
|
|
4262
|
+
process.stdout.write(`
|
|
4263
|
+
`);
|
|
4264
|
+
process.stdout.write(` \u{1F3C5} Trust tier: ${bold(currentTier)} | \u23F1 Active: ${durationStr}
|
|
4265
|
+
|
|
4266
|
+
`);
|
|
4267
|
+
process.stdout.write(` ${bold("Tools used:")}
|
|
4268
|
+
`);
|
|
4269
|
+
for (const [tool, count] of sortedTools.slice(0, 8)) {
|
|
4270
|
+
process.stdout.write(` ${tool.padEnd(22)} ${count}x
|
|
4271
|
+
`);
|
|
4272
|
+
}
|
|
4273
|
+
if (denied > 0) {
|
|
4274
|
+
const deniedTools = entries.filter((e) => e.decision === "deny");
|
|
4275
|
+
const deniedToolNames = [...new Set(deniedTools.map((e) => e.tool))];
|
|
4276
|
+
process.stdout.write(`
|
|
4277
|
+
${bold(red("Blocked tools:"))}
|
|
4278
|
+
`);
|
|
4279
|
+
for (const tool of deniedToolNames) {
|
|
4280
|
+
const reason = deniedTools.find((e) => e.tool === tool)?.reason_code || "policy";
|
|
4281
|
+
process.stdout.write(` ${red("\u2717")} ${tool} (${reason})
|
|
4282
|
+
`);
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
process.stdout.write(`
|
|
4286
|
+
${dim("Latest receipt: curl -s http://127.0.0.1:9876/receipts/latest | jq -r .receipt > receipt.json")}
|
|
4287
|
+
`);
|
|
4288
|
+
process.stdout.write(` ${dim("Verify: npx @veritasacta/verify receipt.json --key <public-key-hex>")}
|
|
4289
|
+
`);
|
|
4290
|
+
process.stdout.write(` ${dim("Export: npx protect-mcp bundle --output audit.json")}
|
|
4291
|
+
|
|
4292
|
+
`);
|
|
4293
|
+
}
|
|
4294
|
+
async function handleReceipts2(argv) {
|
|
4295
|
+
const { readFileSync: readFileSync5, existsSync: existsSync4 } = await import("fs");
|
|
4296
|
+
const { join: join4 } = await import("path");
|
|
4297
|
+
let dir = process.cwd();
|
|
4298
|
+
const dirIdx = argv.indexOf("--dir");
|
|
4299
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4300
|
+
const lastIdx = argv.indexOf("--last");
|
|
4301
|
+
const count = lastIdx !== -1 && argv[lastIdx + 1] ? parseInt(argv[lastIdx + 1], 10) : 20;
|
|
4302
|
+
const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
|
|
4303
|
+
if (!existsSync4(receiptsPath)) {
|
|
4304
|
+
process.stderr.write(`${bold("protect-mcp receipts")}
|
|
4305
|
+
|
|
4306
|
+
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
4307
|
+
`);
|
|
4308
|
+
process.exit(0);
|
|
4309
|
+
}
|
|
4310
|
+
const raw = readFileSync5(receiptsPath, "utf-8");
|
|
4311
|
+
const lines = raw.trim().split("\n").filter(Boolean);
|
|
4312
|
+
const recent = lines.slice(-count);
|
|
4313
|
+
process.stdout.write(`
|
|
4314
|
+
${bold("\u{1F6E1}\uFE0F Recent Receipts")} (last ${recent.length})
|
|
4315
|
+
|
|
4316
|
+
`);
|
|
4317
|
+
for (const line of recent) {
|
|
4318
|
+
try {
|
|
4319
|
+
const entry = JSON.parse(line);
|
|
4320
|
+
const payload = entry.payload || {};
|
|
4321
|
+
const time = typeof entry.issued_at === "string" ? new Date(entry.issued_at).toLocaleTimeString() : "unknown";
|
|
4322
|
+
const decision = payload.decision || "unknown";
|
|
4323
|
+
const icon = decision === "allow" ? green("\u2713") : decision === "require_approval" ? yellow("\u23F3") : red("\u2717");
|
|
4324
|
+
process.stdout.write(` ${dim(time)} ${icon} ${String(payload.tool || "unknown").padEnd(22)} ${String(entry.type || "receipt").padEnd(18)} ${dim(String(payload.reason_code || "signed"))}
|
|
4325
|
+
`);
|
|
4326
|
+
} catch {
|
|
4327
|
+
}
|
|
4328
|
+
}
|
|
4329
|
+
process.stdout.write(`
|
|
4330
|
+
`);
|
|
4331
|
+
}
|
|
4332
|
+
async function handleBundle(argv) {
|
|
4333
|
+
const { readFileSync: readFileSync5, writeFileSync: writeFileSync2, existsSync: existsSync4 } = await import("fs");
|
|
4334
|
+
const { join: join4 } = await import("path");
|
|
4335
|
+
const { createAuditBundle: createAuditBundle2 } = await Promise.resolve().then(() => (init_bundle(), bundle_exports));
|
|
4336
|
+
let dir = process.cwd();
|
|
4337
|
+
const dirIdx = argv.indexOf("--dir");
|
|
4338
|
+
if (dirIdx !== -1 && argv[dirIdx + 1]) dir = argv[dirIdx + 1];
|
|
4339
|
+
const outputIdx = argv.indexOf("--output");
|
|
4340
|
+
const outputPath = outputIdx !== -1 && argv[outputIdx + 1] ? argv[outputIdx + 1] : join4(dir, "audit-bundle.json");
|
|
4341
|
+
const receiptsPath = join4(dir, ".protect-mcp-receipts.jsonl");
|
|
4342
|
+
const keyPath = join4(dir, "keys", "gateway.json");
|
|
4343
|
+
if (!existsSync4(receiptsPath)) {
|
|
4344
|
+
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
4345
|
+
|
|
4346
|
+
No signed receipt file found. Run protect-mcp with signing enabled first.
|
|
4347
|
+
`);
|
|
4348
|
+
process.exit(0);
|
|
4349
|
+
}
|
|
4350
|
+
if (!existsSync4(keyPath)) {
|
|
4351
|
+
process.stderr.write(`${bold("protect-mcp bundle")}
|
|
4352
|
+
|
|
4353
|
+
No key file found at ${keyPath}
|
|
4354
|
+
`);
|
|
4355
|
+
process.exit(1);
|
|
4356
|
+
}
|
|
4357
|
+
const receipts = readFileSync5(receiptsPath, "utf-8").trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
4358
|
+
const keyData = JSON.parse(readFileSync5(keyPath, "utf-8"));
|
|
4359
|
+
const bundle = createAuditBundle2({
|
|
4360
|
+
tenant: keyData.issuer || "protect-mcp",
|
|
4361
|
+
receipts,
|
|
4362
|
+
signingKeys: [{
|
|
4363
|
+
kty: "OKP",
|
|
4364
|
+
crv: "Ed25519",
|
|
4365
|
+
kid: keyData.kid || "unknown",
|
|
4366
|
+
x: Buffer.from(keyData.publicKey, "hex").toString("base64url"),
|
|
4367
|
+
use: "sig"
|
|
4368
|
+
}]
|
|
4369
|
+
});
|
|
4370
|
+
writeFileSync2(outputPath, JSON.stringify(bundle, null, 2) + "\n");
|
|
4371
|
+
process.stdout.write(`
|
|
4372
|
+
${bold("protect-mcp bundle")}
|
|
4373
|
+
|
|
4374
|
+
`);
|
|
4375
|
+
process.stdout.write(` Receipts: ${receipts.length}
|
|
4376
|
+
`);
|
|
4377
|
+
process.stdout.write(` Output: ${outputPath}
|
|
4378
|
+
`);
|
|
4379
|
+
process.stdout.write(` Verify: npx @veritasacta/verify ${outputPath} --bundle
|
|
4380
|
+
|
|
4381
|
+
`);
|
|
4382
|
+
}
|
|
3988
4383
|
async function main() {
|
|
3989
4384
|
const args = process.argv.slice(2);
|
|
3990
4385
|
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
@@ -4003,6 +4398,18 @@ async function main() {
|
|
|
4003
4398
|
await handleStatus2(args.slice(1));
|
|
4004
4399
|
process.exit(0);
|
|
4005
4400
|
}
|
|
4401
|
+
if (args[0] === "digest") {
|
|
4402
|
+
await handleDigest(args.slice(1));
|
|
4403
|
+
process.exit(0);
|
|
4404
|
+
}
|
|
4405
|
+
if (args[0] === "receipts") {
|
|
4406
|
+
await handleReceipts2(args.slice(1));
|
|
4407
|
+
process.exit(0);
|
|
4408
|
+
}
|
|
4409
|
+
if (args[0] === "bundle") {
|
|
4410
|
+
await handleBundle(args.slice(1));
|
|
4411
|
+
process.exit(0);
|
|
4412
|
+
}
|
|
4006
4413
|
const { policyPath, slug, enforce, verbose, childCommand } = parseArgs(args);
|
|
4007
4414
|
let policy = null;
|
|
4008
4415
|
let policyDigest = "none";
|