protect-mcp 0.5.2 → 0.5.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 CHANGED
@@ -248,9 +248,9 @@ Self-contained offline-verifiable bundle with receipts + signing keys. Verify wi
248
248
 
249
249
  ## Standards & IP
250
250
 
251
- - **IETF Internet-Draft**: [draft-farley-acta-signed-receipts-00](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/) — Signed Decision Receipts for Machine-to-Machine Access Control
251
+ - **IETF Internet-Draft**: [draft-farley-acta-signed-receipts-01](https://datatracker.ietf.org/doc/draft-farley-acta-signed-receipts/) — Signed Decision Receipts for Machine-to-Machine Access Control
252
252
  - **Patent Status**: 4 Australian provisional patents pending (2025-2026) covering decision receipts with configurable disclosure, tool-calling gateway, agent manifests, and portable identity
253
- - **Verification**: MIT-licensed — `npx @veritasacta/verify --self-test`
253
+ - **Verification**: Apache-2.0 — `npx @veritasacta/verify --self-test`
254
254
  - **Microsoft AGT Integration**: [PR #667](https://github.com/microsoft/agent-governance-toolkit/pull/667) — Cedar policy bridge for Agent Governance Toolkit
255
255
 
256
256
  ## License
@@ -7,7 +7,7 @@ import {
7
7
  parseRateLimit,
8
8
  signDecision,
9
9
  startStatusServer
10
- } from "./chunk-N4F76LTC.mjs";
10
+ } from "./chunk-YTBC72JJ.mjs";
11
11
 
12
12
  // src/evidence-store.ts
13
13
  import { readFileSync, writeFileSync, existsSync } from "fs";
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  meetsMinTier
3
- } from "./chunk-IWDR5WU3.mjs";
3
+ } from "./chunk-BYYWYSHM.mjs";
4
4
  import {
5
5
  checkRateLimit,
6
6
  getToolPolicy,
7
7
  parseRateLimit
8
- } from "./chunk-N4F76LTC.mjs";
8
+ } from "./chunk-YTBC72JJ.mjs";
9
9
 
10
10
  // src/simulate.ts
11
11
  import { readFileSync } from "fs";
@@ -11,7 +11,7 @@ import {
11
11
  loadPolicy,
12
12
  parseRateLimit,
13
13
  signDecision
14
- } from "./chunk-N4F76LTC.mjs";
14
+ } from "./chunk-YTBC72JJ.mjs";
15
15
 
16
16
  // src/hook-server.ts
17
17
  import { createServer } from "http";
@@ -272,6 +272,8 @@ context: inline
272
272
 
273
273
  # ScopeBlind Receipt Verification
274
274
 
275
+ Every AI agent tool call gets a cryptographic receipt. Verify offline. No vendor trust required.
276
+
275
277
  When the user asks to verify receipts or check the audit trail:
276
278
 
277
279
  1. **Check for receipt files:**
@@ -130,7 +130,14 @@ function signDecision(entry) {
130
130
  scope: entry.request_id,
131
131
  // request scope
132
132
  mode: entry.mode,
133
- request_id: entry.request_id
133
+ request_id: entry.request_id,
134
+ // Spec version: ties every receipt to the IETF standard
135
+ spec: "draft-farley-acta-signed-receipts-01",
136
+ // Issuer certification: distinguishes VOPRF-backed receipts from self-signed ones
137
+ // - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
138
+ // - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
139
+ // - uncertified = unsigned receipt (shadow mode, no signing configured)
140
+ issuer_certification: signerState ? "self-signed" : "uncertified"
134
141
  };
135
142
  if (entry.tier) payload.tier = entry.tier;
136
143
  if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
@@ -138,6 +145,12 @@ function signDecision(entry) {
138
145
  payload.rate_limit_remaining = entry.rate_limit_remaining;
139
146
  }
140
147
  if (entry.policy_engine) payload.policy_engine = entry.policy_engine;
148
+ if (entry.hook_event) payload.hook_event = entry.hook_event;
149
+ if (entry.sandbox_state) payload.sandbox_state = entry.sandbox_state;
150
+ if (entry.timing) payload.timing = entry.timing;
151
+ if (entry.swarm) payload.swarm = entry.swarm;
152
+ if (entry.payload_digest) payload.payload_digest = entry.payload_digest;
153
+ if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
141
154
  const result = artifactsModule.createSignedArtifact(
142
155
  artifactType,
143
156
  payload,
package/dist/cli.js CHANGED
@@ -411,7 +411,14 @@ function signDecision(entry) {
411
411
  scope: entry.request_id,
412
412
  // request scope
413
413
  mode: entry.mode,
414
- request_id: entry.request_id
414
+ request_id: entry.request_id,
415
+ // Spec version: ties every receipt to the IETF standard
416
+ spec: "draft-farley-acta-signed-receipts-01",
417
+ // Issuer certification: distinguishes VOPRF-backed receipts from self-signed ones
418
+ // - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
419
+ // - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
420
+ // - uncertified = unsigned receipt (shadow mode, no signing configured)
421
+ issuer_certification: signerState ? "self-signed" : "uncertified"
415
422
  };
416
423
  if (entry.tier) payload.tier = entry.tier;
417
424
  if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
@@ -419,6 +426,12 @@ function signDecision(entry) {
419
426
  payload.rate_limit_remaining = entry.rate_limit_remaining;
420
427
  }
421
428
  if (entry.policy_engine) payload.policy_engine = entry.policy_engine;
429
+ if (entry.hook_event) payload.hook_event = entry.hook_event;
430
+ if (entry.sandbox_state) payload.sandbox_state = entry.sandbox_state;
431
+ if (entry.timing) payload.timing = entry.timing;
432
+ if (entry.swarm) payload.swarm = entry.swarm;
433
+ if (entry.payload_digest) payload.payload_digest = entry.payload_digest;
434
+ if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
422
435
  const result = artifactsModule.createSignedArtifact(
423
436
  artifactType,
424
437
  payload,
@@ -4544,6 +4557,8 @@ context: inline
4544
4557
 
4545
4558
  # ScopeBlind Receipt Verification
4546
4559
 
4560
+ Every AI agent tool call gets a cryptographic receipt. Verify offline. No vendor trust required.
4561
+
4547
4562
  When the user asks to verify receipts or check the audit trail:
4548
4563
 
4549
4564
  1. **Check for receipt files:**
@@ -6051,6 +6066,7 @@ function formatSimulation(summary) {
6051
6066
 
6052
6067
  // src/cli.ts
6053
6068
  init_cedar_evaluator();
6069
+ var import_meta = {};
6054
6070
  function printHelp() {
6055
6071
  process.stderr.write(`
6056
6072
  protect-mcp \u2014 Enterprise security gateway for MCP servers & Claude Code hooks
@@ -6059,7 +6075,8 @@ Usage:
6059
6075
  protect-mcp [options] -- <command> [args...]
6060
6076
  protect-mcp serve [--port <port>] [--enforce] [--policy <path>] [--cedar <dir>]
6061
6077
  protect-mcp init-hooks [--dir <path>] [--port <port>]
6062
- protect-mcp quickstart
6078
+ protect-mcp quickstart [--connect]
6079
+ protect-mcp connect
6063
6080
  protect-mcp init [--dir <path>]
6064
6081
  protect-mcp demo
6065
6082
  protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
@@ -6084,6 +6101,7 @@ Commands:
6084
6101
  serve Start HTTP hook server for Claude Code integration (port 9377)
6085
6102
  init-hooks Generate Claude Code hook config + skill + sample Cedar policy
6086
6103
  quickstart Zero-config onboarding: init + demo + show receipts in one command
6104
+ connect Create a ScopeBlind sandbox dashboard and configure receipt upload
6087
6105
  init Generate config template, Ed25519 keypair, and sample policy
6088
6106
  demo Start a demo server wrapped with protect-mcp (see receipts instantly)
6089
6107
  doctor Check your setup: keys, policies, verifier, API connectivity
@@ -6098,6 +6116,8 @@ Examples:
6098
6116
  protect-mcp serve --enforce --cedar ./cedar # Enforce Cedar policies
6099
6117
  protect-mcp init-hooks # One-command Claude Code setup
6100
6118
  protect-mcp quickstart
6119
+ protect-mcp quickstart --connect # Quickstart + create dashboard
6120
+ protect-mcp connect # Connect existing setup to dashboard
6101
6121
  protect-mcp -- node my-server.js
6102
6122
  protect-mcp init
6103
6123
  protect-mcp demo
@@ -6679,7 +6699,83 @@ ${bold("protect-mcp bundle")}
6679
6699
 
6680
6700
  `);
6681
6701
  }
6682
- async function handleQuickstart() {
6702
+ async function createSandbox() {
6703
+ const { mkdirSync, writeFileSync: writeFileSync2, existsSync: existsSync7, readFileSync: readFileSync9 } = await import("fs");
6704
+ const { join: join6 } = await import("path");
6705
+ const { homedir } = await import("os");
6706
+ let response;
6707
+ try {
6708
+ response = await fetch("https://api.scopeblind.com/sandbox/create", { method: "POST" });
6709
+ } catch {
6710
+ process.stderr.write(yellow(" \u26A0 Could not create dashboard (offline or server unavailable).\n"));
6711
+ process.stderr.write(` Run 'npx protect-mcp connect' later to set up the dashboard.
6712
+
6713
+ `);
6714
+ return null;
6715
+ }
6716
+ if (!response.ok) {
6717
+ process.stderr.write(yellow(" \u26A0 Could not create dashboard (offline or server unavailable).\n"));
6718
+ process.stderr.write(` Run 'npx protect-mcp connect' later to set up the dashboard.
6719
+
6720
+ `);
6721
+ return null;
6722
+ }
6723
+ let data;
6724
+ try {
6725
+ data = await response.json();
6726
+ } catch {
6727
+ process.stderr.write(yellow(" \u26A0 Could not create dashboard (unexpected response).\n"));
6728
+ process.stderr.write(` Run 'npx protect-mcp connect' later to set up the dashboard.
6729
+
6730
+ `);
6731
+ return null;
6732
+ }
6733
+ const dashboardUrl = `https://scopeblind.com/t/${data.slug}`;
6734
+ const configDir = join6(homedir(), ".protect-mcp");
6735
+ if (!existsSync7(configDir)) {
6736
+ mkdirSync(configDir, { recursive: true });
6737
+ }
6738
+ const configPath = join6(configDir, "config.json");
6739
+ let existing = {};
6740
+ if (existsSync7(configPath)) {
6741
+ try {
6742
+ existing = JSON.parse(readFileSync9(configPath, "utf-8"));
6743
+ } catch {
6744
+ }
6745
+ }
6746
+ writeFileSync2(configPath, JSON.stringify({
6747
+ ...existing,
6748
+ sandbox_slug: data.slug,
6749
+ dashboard_url: dashboardUrl
6750
+ }, null, 2) + "\n");
6751
+ return dashboardUrl;
6752
+ }
6753
+ async function handleConnect() {
6754
+ process.stderr.write(`
6755
+ ${bold("protect-mcp connect")}
6756
+ `);
6757
+ process.stderr.write(`${"\u2500".repeat(50)}
6758
+
6759
+ `);
6760
+ process.stderr.write(` Creating ScopeBlind sandbox dashboard...
6761
+
6762
+ `);
6763
+ const dashboardUrl = await createSandbox();
6764
+ if (dashboardUrl) {
6765
+ process.stderr.write(green(` \u2713 Dashboard created: ${dashboardUrl}
6766
+ `));
6767
+ process.stderr.write(` Receipts will be uploaded automatically.
6768
+ `);
6769
+ process.stderr.write(dim(` Free tier: 20,000 receipts/month \u2014 no credit card required.
6770
+ `));
6771
+ process.stderr.write(`
6772
+ ${"\u2500".repeat(50)}
6773
+
6774
+ `);
6775
+ }
6776
+ }
6777
+ async function handleQuickstart(argv) {
6778
+ const connectFlag = argv.includes("--connect");
6683
6779
  const { mkdtempSync, writeFileSync: writeFileSync2, existsSync: existsSync7, mkdirSync, readFileSync: readFileSync9 } = await import("fs");
6684
6780
  const { join: join6 } = await import("path");
6685
6781
  const { tmpdir } = await import("os");
@@ -6699,9 +6795,13 @@ ${bold("protect-mcp quickstart")}
6699
6795
  process.stdout.write(` 3. Start a demo MCP server with protect-mcp wrapping it
6700
6796
  `);
6701
6797
  process.stdout.write(` 4. Log signed receipts for every tool call
6702
-
6703
6798
  `);
6704
- process.stdout.write(` Working dir: ${dir}
6799
+ if (connectFlag) {
6800
+ process.stdout.write(` 5. Create a ScopeBlind dashboard for receipt viewing
6801
+ `);
6802
+ }
6803
+ process.stdout.write(`
6804
+ Working dir: ${dir}
6705
6805
 
6706
6806
  `);
6707
6807
  const keysDir = join6(dir, "keys");
@@ -6752,6 +6852,24 @@ ${bold("protect-mcp quickstart")}
6752
6852
  process.stdout.write(` \u2713 Signing enabled (Ed25519)
6753
6853
 
6754
6854
  `);
6855
+ if (connectFlag) {
6856
+ process.stdout.write(`${bold("Connecting to ScopeBlind dashboard...")}
6857
+
6858
+ `);
6859
+ const dashboardUrl = await createSandbox();
6860
+ if (dashboardUrl) {
6861
+ const updatedConfig = { ...config, dashboard_url: dashboardUrl };
6862
+ writeFileSync2(configPath, JSON.stringify(updatedConfig, null, 2) + "\n");
6863
+ process.stdout.write(green(` \u2713 Dashboard created: ${dashboardUrl}
6864
+ `));
6865
+ process.stdout.write(` Receipts will be uploaded automatically.
6866
+ `);
6867
+ process.stdout.write(dim(` Free tier: 20,000 receipts/month \u2014 no credit card required.
6868
+ `));
6869
+ process.stdout.write(`
6870
+ `);
6871
+ }
6872
+ }
6755
6873
  process.stdout.write(`${bold("Starting demo server...")}
6756
6874
 
6757
6875
  `);
@@ -7153,7 +7271,55 @@ ${bold("protect-mcp init-hooks")}
7153
7271
 
7154
7272
  `);
7155
7273
  }
7274
+ async function sendInstallTelemetry() {
7275
+ try {
7276
+ const { existsSync: existsSync7, mkdirSync, writeFileSync: writeFileSync2, readFileSync: readFileSync9 } = await import("fs");
7277
+ const { join: join6, dirname } = await import("path");
7278
+ const { homedir } = await import("os");
7279
+ const { fileURLToPath } = await import("url");
7280
+ const markerDir = join6(homedir(), ".protect-mcp");
7281
+ const markerFile = join6(markerDir, ".telemetry-sent");
7282
+ if (existsSync7(markerFile) || process.env.PROTECT_MCP_TELEMETRY === "off") {
7283
+ return;
7284
+ }
7285
+ let version = "unknown";
7286
+ try {
7287
+ const thisDir = dirname(fileURLToPath(import_meta.url));
7288
+ const pkgPath = join6(thisDir, "..", "package.json");
7289
+ if (existsSync7(pkgPath)) {
7290
+ version = JSON.parse(readFileSync9(pkgPath, "utf-8")).version;
7291
+ }
7292
+ } catch {
7293
+ }
7294
+ const controller = new AbortController();
7295
+ const timeout = setTimeout(() => controller.abort(), 3e3);
7296
+ fetch("https://api.scopeblind.com/telemetry/install", {
7297
+ method: "POST",
7298
+ headers: { "Content-Type": "application/json" },
7299
+ body: JSON.stringify({
7300
+ package: "protect-mcp",
7301
+ version,
7302
+ os: process.platform,
7303
+ arch: process.arch,
7304
+ node: process.version,
7305
+ ts: Date.now()
7306
+ }),
7307
+ signal: controller.signal
7308
+ }).catch(() => {
7309
+ }).finally(() => clearTimeout(timeout));
7310
+ if (!existsSync7(markerDir)) {
7311
+ mkdirSync(markerDir, { recursive: true });
7312
+ }
7313
+ writeFileSync2(markerFile, String(Date.now()), "utf-8");
7314
+ process.stderr.write(
7315
+ "[protect-mcp] Anonymous install telemetry sent (disable: PROTECT_MCP_TELEMETRY=off)\n"
7316
+ );
7317
+ } catch {
7318
+ }
7319
+ }
7156
7320
  async function main() {
7321
+ sendInstallTelemetry().catch(() => {
7322
+ });
7157
7323
  const args = process.argv.slice(2);
7158
7324
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
7159
7325
  printHelp();
@@ -7181,9 +7347,13 @@ async function main() {
7181
7347
  process.exit(0);
7182
7348
  }
7183
7349
  if (args[0] === "quickstart") {
7184
- await handleQuickstart();
7350
+ await handleQuickstart(args.slice(1));
7185
7351
  return;
7186
7352
  }
7353
+ if (args[0] === "connect") {
7354
+ await handleConnect();
7355
+ process.exit(0);
7356
+ }
7187
7357
  if (args[0] === "init") {
7188
7358
  await handleInit(args.slice(1));
7189
7359
  process.exit(0);
package/dist/cli.mjs CHANGED
@@ -3,17 +3,17 @@ import {
3
3
  formatSimulation,
4
4
  parseLogFile,
5
5
  simulate
6
- } from "./chunk-W4U5VNEC.mjs";
6
+ } from "./chunk-GQWJCHQV.mjs";
7
7
  import {
8
8
  ProtectGateway,
9
9
  validateCredentials
10
- } from "./chunk-IWDR5WU3.mjs";
10
+ } from "./chunk-BYYWYSHM.mjs";
11
11
  import {
12
12
  initSigning,
13
13
  isCedarAvailable,
14
14
  loadCedarPolicies,
15
15
  loadPolicy
16
- } from "./chunk-N4F76LTC.mjs";
16
+ } from "./chunk-YTBC72JJ.mjs";
17
17
  import "./chunk-PQJP2ZCI.mjs";
18
18
 
19
19
  // src/cli.ts
@@ -25,7 +25,8 @@ Usage:
25
25
  protect-mcp [options] -- <command> [args...]
26
26
  protect-mcp serve [--port <port>] [--enforce] [--policy <path>] [--cedar <dir>]
27
27
  protect-mcp init-hooks [--dir <path>] [--port <port>]
28
- protect-mcp quickstart
28
+ protect-mcp quickstart [--connect]
29
+ protect-mcp connect
29
30
  protect-mcp init [--dir <path>]
30
31
  protect-mcp demo
31
32
  protect-mcp trace <receipt_id> [--endpoint <url>] [--depth <n>]
@@ -50,6 +51,7 @@ Commands:
50
51
  serve Start HTTP hook server for Claude Code integration (port 9377)
51
52
  init-hooks Generate Claude Code hook config + skill + sample Cedar policy
52
53
  quickstart Zero-config onboarding: init + demo + show receipts in one command
54
+ connect Create a ScopeBlind sandbox dashboard and configure receipt upload
53
55
  init Generate config template, Ed25519 keypair, and sample policy
54
56
  demo Start a demo server wrapped with protect-mcp (see receipts instantly)
55
57
  doctor Check your setup: keys, policies, verifier, API connectivity
@@ -64,6 +66,8 @@ Examples:
64
66
  protect-mcp serve --enforce --cedar ./cedar # Enforce Cedar policies
65
67
  protect-mcp init-hooks # One-command Claude Code setup
66
68
  protect-mcp quickstart
69
+ protect-mcp quickstart --connect # Quickstart + create dashboard
70
+ protect-mcp connect # Connect existing setup to dashboard
67
71
  protect-mcp -- node my-server.js
68
72
  protect-mcp init
69
73
  protect-mcp demo
@@ -645,7 +649,83 @@ ${bold("protect-mcp bundle")}
645
649
 
646
650
  `);
647
651
  }
648
- async function handleQuickstart() {
652
+ async function createSandbox() {
653
+ const { mkdirSync, writeFileSync, existsSync, readFileSync } = await import("fs");
654
+ const { join } = await import("path");
655
+ const { homedir } = await import("os");
656
+ let response;
657
+ try {
658
+ response = await fetch("https://api.scopeblind.com/sandbox/create", { method: "POST" });
659
+ } catch {
660
+ process.stderr.write(yellow(" \u26A0 Could not create dashboard (offline or server unavailable).\n"));
661
+ process.stderr.write(` Run 'npx protect-mcp connect' later to set up the dashboard.
662
+
663
+ `);
664
+ return null;
665
+ }
666
+ if (!response.ok) {
667
+ process.stderr.write(yellow(" \u26A0 Could not create dashboard (offline or server unavailable).\n"));
668
+ process.stderr.write(` Run 'npx protect-mcp connect' later to set up the dashboard.
669
+
670
+ `);
671
+ return null;
672
+ }
673
+ let data;
674
+ try {
675
+ data = await response.json();
676
+ } catch {
677
+ process.stderr.write(yellow(" \u26A0 Could not create dashboard (unexpected response).\n"));
678
+ process.stderr.write(` Run 'npx protect-mcp connect' later to set up the dashboard.
679
+
680
+ `);
681
+ return null;
682
+ }
683
+ const dashboardUrl = `https://scopeblind.com/t/${data.slug}`;
684
+ const configDir = join(homedir(), ".protect-mcp");
685
+ if (!existsSync(configDir)) {
686
+ mkdirSync(configDir, { recursive: true });
687
+ }
688
+ const configPath = join(configDir, "config.json");
689
+ let existing = {};
690
+ if (existsSync(configPath)) {
691
+ try {
692
+ existing = JSON.parse(readFileSync(configPath, "utf-8"));
693
+ } catch {
694
+ }
695
+ }
696
+ writeFileSync(configPath, JSON.stringify({
697
+ ...existing,
698
+ sandbox_slug: data.slug,
699
+ dashboard_url: dashboardUrl
700
+ }, null, 2) + "\n");
701
+ return dashboardUrl;
702
+ }
703
+ async function handleConnect() {
704
+ process.stderr.write(`
705
+ ${bold("protect-mcp connect")}
706
+ `);
707
+ process.stderr.write(`${"\u2500".repeat(50)}
708
+
709
+ `);
710
+ process.stderr.write(` Creating ScopeBlind sandbox dashboard...
711
+
712
+ `);
713
+ const dashboardUrl = await createSandbox();
714
+ if (dashboardUrl) {
715
+ process.stderr.write(green(` \u2713 Dashboard created: ${dashboardUrl}
716
+ `));
717
+ process.stderr.write(` Receipts will be uploaded automatically.
718
+ `);
719
+ process.stderr.write(dim(` Free tier: 20,000 receipts/month \u2014 no credit card required.
720
+ `));
721
+ process.stderr.write(`
722
+ ${"\u2500".repeat(50)}
723
+
724
+ `);
725
+ }
726
+ }
727
+ async function handleQuickstart(argv) {
728
+ const connectFlag = argv.includes("--connect");
649
729
  const { mkdtempSync, writeFileSync, existsSync, mkdirSync, readFileSync } = await import("fs");
650
730
  const { join } = await import("path");
651
731
  const { tmpdir } = await import("os");
@@ -665,9 +745,13 @@ ${bold("protect-mcp quickstart")}
665
745
  process.stdout.write(` 3. Start a demo MCP server with protect-mcp wrapping it
666
746
  `);
667
747
  process.stdout.write(` 4. Log signed receipts for every tool call
668
-
669
748
  `);
670
- process.stdout.write(` Working dir: ${dir}
749
+ if (connectFlag) {
750
+ process.stdout.write(` 5. Create a ScopeBlind dashboard for receipt viewing
751
+ `);
752
+ }
753
+ process.stdout.write(`
754
+ Working dir: ${dir}
671
755
 
672
756
  `);
673
757
  const keysDir = join(dir, "keys");
@@ -718,6 +802,24 @@ ${bold("protect-mcp quickstart")}
718
802
  process.stdout.write(` \u2713 Signing enabled (Ed25519)
719
803
 
720
804
  `);
805
+ if (connectFlag) {
806
+ process.stdout.write(`${bold("Connecting to ScopeBlind dashboard...")}
807
+
808
+ `);
809
+ const dashboardUrl = await createSandbox();
810
+ if (dashboardUrl) {
811
+ const updatedConfig = { ...config, dashboard_url: dashboardUrl };
812
+ writeFileSync(configPath, JSON.stringify(updatedConfig, null, 2) + "\n");
813
+ process.stdout.write(green(` \u2713 Dashboard created: ${dashboardUrl}
814
+ `));
815
+ process.stdout.write(` Receipts will be uploaded automatically.
816
+ `);
817
+ process.stdout.write(dim(` Free tier: 20,000 receipts/month \u2014 no credit card required.
818
+ `));
819
+ process.stdout.write(`
820
+ `);
821
+ }
822
+ }
721
823
  process.stdout.write(`${bold("Starting demo server...")}
722
824
 
723
825
  `);
@@ -1119,7 +1221,55 @@ ${bold("protect-mcp init-hooks")}
1119
1221
 
1120
1222
  `);
1121
1223
  }
1224
+ async function sendInstallTelemetry() {
1225
+ try {
1226
+ const { existsSync, mkdirSync, writeFileSync, readFileSync } = await import("fs");
1227
+ const { join, dirname } = await import("path");
1228
+ const { homedir } = await import("os");
1229
+ const { fileURLToPath } = await import("url");
1230
+ const markerDir = join(homedir(), ".protect-mcp");
1231
+ const markerFile = join(markerDir, ".telemetry-sent");
1232
+ if (existsSync(markerFile) || process.env.PROTECT_MCP_TELEMETRY === "off") {
1233
+ return;
1234
+ }
1235
+ let version = "unknown";
1236
+ try {
1237
+ const thisDir = dirname(fileURLToPath(import.meta.url));
1238
+ const pkgPath = join(thisDir, "..", "package.json");
1239
+ if (existsSync(pkgPath)) {
1240
+ version = JSON.parse(readFileSync(pkgPath, "utf-8")).version;
1241
+ }
1242
+ } catch {
1243
+ }
1244
+ const controller = new AbortController();
1245
+ const timeout = setTimeout(() => controller.abort(), 3e3);
1246
+ fetch("https://api.scopeblind.com/telemetry/install", {
1247
+ method: "POST",
1248
+ headers: { "Content-Type": "application/json" },
1249
+ body: JSON.stringify({
1250
+ package: "protect-mcp",
1251
+ version,
1252
+ os: process.platform,
1253
+ arch: process.arch,
1254
+ node: process.version,
1255
+ ts: Date.now()
1256
+ }),
1257
+ signal: controller.signal
1258
+ }).catch(() => {
1259
+ }).finally(() => clearTimeout(timeout));
1260
+ if (!existsSync(markerDir)) {
1261
+ mkdirSync(markerDir, { recursive: true });
1262
+ }
1263
+ writeFileSync(markerFile, String(Date.now()), "utf-8");
1264
+ process.stderr.write(
1265
+ "[protect-mcp] Anonymous install telemetry sent (disable: PROTECT_MCP_TELEMETRY=off)\n"
1266
+ );
1267
+ } catch {
1268
+ }
1269
+ }
1122
1270
  async function main() {
1271
+ sendInstallTelemetry().catch(() => {
1272
+ });
1123
1273
  const args = process.argv.slice(2);
1124
1274
  if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
1125
1275
  printHelp();
@@ -1147,9 +1297,13 @@ async function main() {
1147
1297
  process.exit(0);
1148
1298
  }
1149
1299
  if (args[0] === "quickstart") {
1150
- await handleQuickstart();
1300
+ await handleQuickstart(args.slice(1));
1151
1301
  return;
1152
1302
  }
1303
+ if (args[0] === "connect") {
1304
+ await handleConnect();
1305
+ process.exit(0);
1306
+ }
1153
1307
  if (args[0] === "init") {
1154
1308
  await handleInit(args.slice(1));
1155
1309
  process.exit(0);
@@ -1282,7 +1436,7 @@ async function main() {
1282
1436
  if (useHttp) {
1283
1437
  const portIdx = args.indexOf("--port");
1284
1438
  const httpPort = portIdx >= 0 && args[portIdx + 1] ? parseInt(args[portIdx + 1]) : 3e3;
1285
- const { startHttpTransport } = await import("./http-transport-PWCK7JHZ.mjs");
1439
+ const { startHttpTransport } = await import("./http-transport-LNBENGXD.mjs");
1286
1440
  startHttpTransport({ port: httpPort, config, serverCommand: childCommand });
1287
1441
  return;
1288
1442
  }
@@ -299,6 +299,8 @@ context: inline
299
299
 
300
300
  # ScopeBlind Receipt Verification
301
301
 
302
+ Every AI agent tool call gets a cryptographic receipt. Verify offline. No vendor trust required.
303
+
302
304
  When the user asks to verify receipts or check the audit trail:
303
305
 
304
306
  1. **Check for receipt files:**
@@ -3,7 +3,7 @@ import {
3
3
  generateHookSettings,
4
4
  generateSampleCedarPolicy,
5
5
  generateVerifyReceiptSkill
6
- } from "./chunk-UEHLYOJY.mjs";
6
+ } from "./chunk-NMZPXXL3.mjs";
7
7
  import "./chunk-PQJP2ZCI.mjs";
8
8
  export {
9
9
  BUILTIN_PATTERNS,
@@ -257,7 +257,14 @@ function signDecision(entry) {
257
257
  scope: entry.request_id,
258
258
  // request scope
259
259
  mode: entry.mode,
260
- request_id: entry.request_id
260
+ request_id: entry.request_id,
261
+ // Spec version: ties every receipt to the IETF standard
262
+ spec: "draft-farley-acta-signed-receipts-01",
263
+ // Issuer certification: distinguishes VOPRF-backed receipts from self-signed ones
264
+ // - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
265
+ // - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
266
+ // - uncertified = unsigned receipt (shadow mode, no signing configured)
267
+ issuer_certification: signerState ? "self-signed" : "uncertified"
261
268
  };
262
269
  if (entry.tier) payload.tier = entry.tier;
263
270
  if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
@@ -265,6 +272,12 @@ function signDecision(entry) {
265
272
  payload.rate_limit_remaining = entry.rate_limit_remaining;
266
273
  }
267
274
  if (entry.policy_engine) payload.policy_engine = entry.policy_engine;
275
+ if (entry.hook_event) payload.hook_event = entry.hook_event;
276
+ if (entry.sandbox_state) payload.sandbox_state = entry.sandbox_state;
277
+ if (entry.timing) payload.timing = entry.timing;
278
+ if (entry.swarm) payload.swarm = entry.swarm;
279
+ if (entry.payload_digest) payload.payload_digest = entry.payload_digest;
280
+ if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
268
281
  const result = artifactsModule.createSignedArtifact(
269
282
  artifactType,
270
283
  payload,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  startHookServer
3
- } from "./chunk-Q4KOQUKV.mjs";
4
- import "./chunk-N4F76LTC.mjs";
3
+ } from "./chunk-HFM6K7E4.mjs";
4
+ import "./chunk-YTBC72JJ.mjs";
5
5
  import "./chunk-PQJP2ZCI.mjs";
6
6
  export {
7
7
  startHookServer
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  ProtectGateway
3
- } from "./chunk-IWDR5WU3.mjs";
4
- import "./chunk-N4F76LTC.mjs";
3
+ } from "./chunk-BYYWYSHM.mjs";
4
+ import "./chunk-YTBC72JJ.mjs";
5
5
  import "./chunk-PQJP2ZCI.mjs";
6
6
 
7
7
  // src/http-transport.ts
package/dist/index.d.mts CHANGED
@@ -167,6 +167,13 @@ interface DecisionLog {
167
167
  plan_receipt_id?: string;
168
168
  /** Hook event that triggered this log entry */
169
169
  hook_event?: HookEventName;
170
+ /** IETF specification version — ties every receipt to the standard */
171
+ spec?: string;
172
+ /** Issuer certification level:
173
+ * - "scopeblind:verified" = VOPRF-backed issuance (paid tier)
174
+ * - "self-signed" = local Ed25519 key (free tier, protect-mcp default)
175
+ * - "uncertified" = unsigned receipt (shadow mode) */
176
+ issuer_certification?: 'scopeblind:verified' | 'self-signed' | 'uncertified';
170
177
  }
171
178
  interface SwarmContext {
172
179
  /** Team name from CLAUDE_CODE_TEAM_NAME env var */
package/dist/index.d.ts CHANGED
@@ -167,6 +167,13 @@ interface DecisionLog {
167
167
  plan_receipt_id?: string;
168
168
  /** Hook event that triggered this log entry */
169
169
  hook_event?: HookEventName;
170
+ /** IETF specification version — ties every receipt to the standard */
171
+ spec?: string;
172
+ /** Issuer certification level:
173
+ * - "scopeblind:verified" = VOPRF-backed issuance (paid tier)
174
+ * - "self-signed" = local Ed25519 key (free tier, protect-mcp default)
175
+ * - "uncertified" = unsigned receipt (shadow mode) */
176
+ issuer_certification?: 'scopeblind:verified' | 'self-signed' | 'uncertified';
170
177
  }
171
178
  interface SwarmContext {
172
179
  /** Team name from CLAUDE_CODE_TEAM_NAME env var */
package/dist/index.js CHANGED
@@ -35474,7 +35474,14 @@ function signDecision(entry) {
35474
35474
  scope: entry.request_id,
35475
35475
  // request scope
35476
35476
  mode: entry.mode,
35477
- request_id: entry.request_id
35477
+ request_id: entry.request_id,
35478
+ // Spec version: ties every receipt to the IETF standard
35479
+ spec: "draft-farley-acta-signed-receipts-01",
35480
+ // Issuer certification: distinguishes VOPRF-backed receipts from self-signed ones
35481
+ // - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
35482
+ // - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
35483
+ // - uncertified = unsigned receipt (shadow mode, no signing configured)
35484
+ issuer_certification: signerState ? "self-signed" : "uncertified"
35478
35485
  };
35479
35486
  if (entry.tier) payload.tier = entry.tier;
35480
35487
  if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
@@ -35482,6 +35489,12 @@ function signDecision(entry) {
35482
35489
  payload.rate_limit_remaining = entry.rate_limit_remaining;
35483
35490
  }
35484
35491
  if (entry.policy_engine) payload.policy_engine = entry.policy_engine;
35492
+ if (entry.hook_event) payload.hook_event = entry.hook_event;
35493
+ if (entry.sandbox_state) payload.sandbox_state = entry.sandbox_state;
35494
+ if (entry.timing) payload.timing = entry.timing;
35495
+ if (entry.swarm) payload.swarm = entry.swarm;
35496
+ if (entry.payload_digest) payload.payload_digest = entry.payload_digest;
35497
+ if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
35485
35498
  const result = artifactsModule.createSignedArtifact(
35486
35499
  artifactType,
35487
35500
  payload,
@@ -38349,6 +38362,8 @@ context: inline
38349
38362
 
38350
38363
  # ScopeBlind Receipt Verification
38351
38364
 
38365
+ Every AI agent tool call gets a cryptographic receipt. Verify offline. No vendor trust required.
38366
+
38352
38367
  When the user asks to verify receipts or check the audit trail:
38353
38368
 
38354
38369
  1. **Check for receipt files:**
package/dist/index.mjs CHANGED
@@ -6,7 +6,7 @@ import {
6
6
  formatSimulation,
7
7
  parseLogFile,
8
8
  simulate
9
- } from "./chunk-W4U5VNEC.mjs";
9
+ } from "./chunk-GQWJCHQV.mjs";
10
10
  import {
11
11
  ProtectGateway,
12
12
  buildDecisionContext,
@@ -18,7 +18,7 @@ import {
18
18
  resolveCredential,
19
19
  sendApprovalNotification,
20
20
  validateCredentials
21
- } from "./chunk-IWDR5WU3.mjs";
21
+ } from "./chunk-BYYWYSHM.mjs";
22
22
  import {
23
23
  createSandboxServer
24
24
  } from "./chunk-SU2FZH7U.mjs";
@@ -27,10 +27,10 @@ import {
27
27
  generateHookSettings,
28
28
  generateSampleCedarPolicy,
29
29
  generateVerifyReceiptSkill
30
- } from "./chunk-UEHLYOJY.mjs";
30
+ } from "./chunk-NMZPXXL3.mjs";
31
31
  import {
32
32
  startHookServer
33
- } from "./chunk-Q4KOQUKV.mjs";
33
+ } from "./chunk-HFM6K7E4.mjs";
34
34
  import {
35
35
  checkRateLimit,
36
36
  evaluateCedar,
@@ -43,7 +43,7 @@ import {
43
43
  loadPolicy,
44
44
  parseRateLimit,
45
45
  signDecision
46
- } from "./chunk-N4F76LTC.mjs";
46
+ } from "./chunk-YTBC72JJ.mjs";
47
47
  import {
48
48
  collectSignedReceipts,
49
49
  createAuditBundle
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "protect-mcp",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "mcpName": "io.github.tomjwxf/protect-mcp",
5
5
  "description": "Enterprise security gateway for MCP servers and Claude Code hooks. Cedar policies, Ed25519-signed receipts, swarm tracking, and tamper detection. Shadow or enforce mode.",
6
6
  "main": "dist/index.js",
@@ -53,7 +53,7 @@
53
53
  ],
54
54
  "author": "Tom Farley <tommy@scopeblind.com>",
55
55
  "license": "MIT",
56
- "homepage": "https://www.scopeblind.com",
56
+ "homepage": "https://scopeblind.com",
57
57
  "repository": {
58
58
  "type": "git",
59
59
  "url": "git+https://github.com/scopeblind/scopeblind-gateway.git"
@@ -0,0 +1,113 @@
1
+ // ============================================================
2
+ // Secrets & Credential Protection — Cedar Edition
3
+ //
4
+ // Prevents AI agents from reading, writing, or exfiltrating
5
+ // sensitive files (SSH keys, env files, credentials, tokens).
6
+ //
7
+ // Controls: SOC2-CC6.1, SOC2-CC6.7, OWASP-A01, OWASP-A07
8
+ // MCP-01, MCP-04, NIST-GOVERN-1.1
9
+ // ============================================================
10
+
11
+ // Block reading SSH private keys
12
+ @id("sb-secrets-001")
13
+ forbid (
14
+ principal,
15
+ action == Action::"MCP::Tool::call",
16
+ resource == Tool::"read_file"
17
+ )
18
+ when {
19
+ context has "input" &&
20
+ context.input has "path" &&
21
+ context.input.path like "*/.ssh/*"
22
+ };
23
+
24
+ // Block reading .env files
25
+ @id("sb-secrets-002")
26
+ forbid (
27
+ principal,
28
+ action == Action::"MCP::Tool::call",
29
+ resource == Tool::"read_file"
30
+ )
31
+ when {
32
+ context has "input" &&
33
+ context.input has "path" &&
34
+ context.input.path like "*/.env*"
35
+ };
36
+
37
+ // Block reading credential files
38
+ @id("sb-secrets-003")
39
+ forbid (
40
+ principal,
41
+ action == Action::"MCP::Tool::call",
42
+ resource == Tool::"read_file"
43
+ )
44
+ when {
45
+ context has "input" &&
46
+ context.input has "path" &&
47
+ context.input.path like "*/credentials*"
48
+ };
49
+
50
+ // Block reading token files
51
+ @id("sb-secrets-004")
52
+ forbid (
53
+ principal,
54
+ action == Action::"MCP::Tool::call",
55
+ resource == Tool::"read_file"
56
+ )
57
+ when {
58
+ context has "input" &&
59
+ context.input has "path" &&
60
+ context.input.path like "*/.token*"
61
+ };
62
+
63
+ // Block reading AWS credentials
64
+ @id("sb-secrets-005")
65
+ forbid (
66
+ principal,
67
+ action == Action::"MCP::Tool::call",
68
+ resource == Tool::"read_file"
69
+ )
70
+ when {
71
+ context has "input" &&
72
+ context.input has "path" &&
73
+ context.input.path like "*/.aws/*"
74
+ };
75
+
76
+ // Block writing to sensitive directories
77
+ @id("sb-secrets-006")
78
+ forbid (
79
+ principal,
80
+ action == Action::"MCP::Tool::call",
81
+ resource == Tool::"write_file"
82
+ )
83
+ when {
84
+ context has "input" &&
85
+ context.input has "path" &&
86
+ context.input.path like "*/.ssh/*"
87
+ };
88
+
89
+ // Block exfiltrating files via curl upload
90
+ @id("sb-secrets-007")
91
+ forbid (
92
+ principal,
93
+ action == Action::"MCP::Tool::call",
94
+ resource == Tool::"bash"
95
+ )
96
+ when {
97
+ context has "input" &&
98
+ context.input has "command" &&
99
+ context.input.command like "*--upload-file*"
100
+ };
101
+
102
+ // Block piping secrets to network
103
+ @id("sb-secrets-008")
104
+ forbid (
105
+ principal,
106
+ action == Action::"MCP::Tool::call",
107
+ resource == Tool::"bash"
108
+ )
109
+ when {
110
+ context has "input" &&
111
+ context.input has "command" &&
112
+ context.input.command like "*| curl*"
113
+ };