protect-mcp 0.9.7 → 0.11.0

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/{chunk-AUUAVWQM.mjs → chunk-3FSKOGGK.mjs} +28 -8
  3. package/dist/{chunk-MOXINIMB.mjs → chunk-BXZUS6HS.mjs} +2 -2
  4. package/dist/{chunk-G6X763MH.mjs → chunk-CIWIK6BT.mjs} +5 -251
  5. package/dist/{chunk-MWXDXYWH.mjs → chunk-FGCNKEEW.mjs} +86 -9
  6. package/dist/{chunk-UWB5ALVO.mjs → chunk-K7AEEQWW.mjs} +17 -94
  7. package/dist/{chunk-WWPQNIVF.mjs → chunk-KRKZ2YX7.mjs} +2 -7
  8. package/dist/{chunk-YM6SOJBR.mjs → chunk-NQOKPBSQ.mjs} +19 -6
  9. package/dist/{chunk-JQDVKZBN.mjs → chunk-SOJBLPPX.mjs} +7 -2
  10. package/dist/chunk-WMIWQ6TS.mjs +266 -0
  11. package/dist/chunk-XOP3PEBM.mjs +114 -0
  12. package/dist/{chunk-SETXVE2K.mjs → chunk-ZG6NAATA.mjs} +17 -14
  13. package/dist/{claim-RIXFOQM7.mjs → claim-MPXJRCFC.mjs} +4 -12
  14. package/dist/cli.js +1166 -3391
  15. package/dist/cli.mjs +131 -37
  16. package/dist/coverage-NB55FTYL.mjs +237 -0
  17. package/dist/demo-server.js +17 -14
  18. package/dist/demo-server.mjs +1 -1
  19. package/dist/hook-server.js +177 -436
  20. package/dist/hook-server.mjs +6 -7
  21. package/dist/{http-transport-VHD3YBT5.mjs → http-transport-BHHLPYL2.mjs} +8 -3
  22. package/dist/index.d.mts +98 -7
  23. package/dist/index.d.ts +98 -7
  24. package/dist/index.js +777 -4007
  25. package/dist/index.mjs +39 -1131
  26. package/dist/mcp-server.js +154 -14
  27. package/dist/mcp-server.mjs +24 -14
  28. package/dist/{report-5XCNW6FB.mjs → report-5B7EO3QB.mjs} +2 -1
  29. package/dist/{sample-6LRP73ZD.mjs → sample-O3KYZWOX.mjs} +4 -12
  30. package/dist/{signing-committed-TGWXSLAO.mjs → signing-committed-7VEKAJ4A.mjs} +1 -5
  31. package/package.json +5 -5
  32. package/dist/chunk-AYNQIEN7.mjs +0 -10
  33. package/dist/chunk-D733KAPG.mjs +0 -252
  34. package/dist/chunk-FFVJL3KQ.mjs +0 -2039
  35. package/dist/chunk-JIDDQUSQ.mjs +0 -568
  36. package/dist/ed25519-SQA3S2RV.mjs +0 -39
  37. package/dist/utils-6AYZFE5A.mjs +0 -77
package/CHANGELOG.md CHANGED
@@ -1,5 +1,107 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.1: HTTP gateway mode enforces Cedar and emits chained receipts
4
+
5
+ Three fixes found by running the cloud-gateway path end to end before
6
+ documenting it:
7
+
8
+ - Fail-open fixed: in `--http` mode, loaded Cedar policies were never
9
+ attached to the gateway (the stdio path attached them after the HTTP
10
+ branch returned), so a remote gate started with `--enforce` and a Cedar
11
+ policy silently allowed everything. Cedar policies are now attached
12
+ before the child starts, and a forbidden tool call over HTTP is denied.
13
+ - Silent-unsigned fixed: a Cedar-mode gateway never read `signing` from
14
+ protect-mcp.json (only the JSON-policy branch did), so it emitted no
15
+ receipts while looking fully configured. Cedar mode now picks up signing
16
+ and credentials from protect-mcp.json when present and says so on stderr.
17
+ - Gateway receipts now chain: the wrapped-server path (stdio and HTTP)
18
+ threads previousReceiptHash per draft s5.7 and resumes the chain across
19
+ restarts from the log tail, matching the hook server. Verified end to
20
+ end: a cloud-style client over Streamable HTTP, one allow and one deny,
21
+ chained draft-02 receipts, replayed offline with the published
22
+ @veritasacta/verify with zero chain breaks.
23
+
24
+
25
+ ## 0.10.0: receipts migrate to the draft-02 Acta envelope
26
+
27
+ Also in this release, the policy digest becomes a recomputable commitment
28
+ (prompted by public review of opaque policy identifiers):
29
+
30
+ - One normative construction for every engine, `acta-policy-digest-v1`:
31
+ per-file SHA-256s into a sorted `{construction, engine, files}` manifest,
32
+ JCS, SHA-256, emitted as `"sha256:<64 hex>"`. Replaces two divergent
33
+ code-defined preimages that concatenated sources (ambiguous file
34
+ boundaries) and truncated to 16 hex characters with no prefix. Receipts
35
+ now carry the full-strength digest; pre-0.10 digests should be treated
36
+ as opaque labels.
37
+ - `protect-mcp policy digest` prints the digest, the per-file hashes, and
38
+ the recompute rule (`--expect` for CI, `--json` for tooling).
39
+ - `protect-mcp policy publish` writes the `acta.policy-bundle.v1` bundle
40
+ (policy bytes plus preimage spec, addressed by the digest) ready to host
41
+ at `.well-known/acta-policies/<hex>.json`; a verifier confirms which
42
+ policy governed a receipt from the bundle bytes alone, with no
43
+ communication with the issuer.
44
+ - The construction is specified normatively in
45
+ draft-farley-acta-signed-receipts-03 (section 5.8), alongside
46
+ `require_approval` as a decision value.
47
+
48
+ Receipts are now emitted in the envelope the IETF draft actually specifies
49
+ (draft-farley-acta-signed-receipts-02): a two-field
50
+ `{ payload, signature: { alg: "EdDSA", kid, sig } }` envelope, with the
51
+ access-decision fields from section 3.1 (`type: "protectmcp:decision"`,
52
+ `tool_name`, `decision`, `reason`, `policy_digest`), `issuer_id` equal to
53
+ `signature.kid` (section 2.2), the signature computed as PureEdDSA directly
54
+ over the JCS bytes of the payload with no pre-hash (section 5.6), and new
55
+ keys defaulting to the section 2.1.1 recommended
56
+ `sb:issuer:<base58-fingerprint>` kid format (existing key files keep their
57
+ explicit kid). Previously the package emitted an internal envelope shape
58
+ that predated the published draft.
59
+
60
+ Receipt logs are now hash-chained per section 5.7: each receipt's
61
+ `previousReceiptHash` is the bare-hex SHA-256 of the JCS bytes of the
62
+ previous log line exactly as written, signature included. The hook server
63
+ resumes the chain across restarts from the tail of the existing log, and
64
+ signing-failure tombstones participate in the chain so an unsigned gap
65
+ cannot be silently dropped.
66
+
67
+ Verification is dual-shape everywhere (the `verify_receipt` MCP tool, the
68
+ `serve --enforce` self-test, the embedded record viewer, and the exported
69
+ `verifyReceipt` API): receipts written by protect-mcp 0.9.x and earlier
70
+ (flat v1 artifacts and structured v2 envelopes with a top-level signature
71
+ string) continue to verify, and a mixed pre/post-migration log replays
72
+ cleanly, including the chain link that spans the boundary. Verified against
73
+ 3,352 real receipts from a production gate log: all verify. The published
74
+ `@veritasacta/verify` CLI (0.9.2) verifies the new envelopes as-is via its
75
+ passport path; no verifier upgrade is required.
76
+
77
+ New module `acta-envelope` (exported from the package root):
78
+ `createReceiptEnvelope`, `verifyReceipt`, `receiptHash`, `receiptIdentity`,
79
+ `computeSbIssuerKid`. `signDecision` accepts an optional previous-chain-hash
80
+ argument and returns the emitted receipt's chain hash. `@noble/curves` and
81
+ `@noble/hashes` moved from optional to regular dependencies.
82
+
83
+ Fixed: importing the package as a library no longer attaches the demo
84
+ server's stdin JSON-RPC listener (it now only starts when demo-server is
85
+ the process entry point). Previously any `require("protect-mcp")` kept the
86
+ host process's event loop alive and answered JSON-RPC on stdout.
87
+
88
+ Note for strict draft conformance: `decision` may be "require_approval" for
89
+ held-for-co-sign flows, an extension value beyond the section 3.1 set
90
+ (allow, deny, rate_limit), pending a revision of the draft.
91
+
92
+
93
+ ## 0.9.7: the gate as an MCP server
94
+
95
+ `protect-mcp mcp` boots a stdio MCP server exposing the gate itself as four
96
+ tools, so an agent can call the gate directly instead of only through the
97
+ PreToolUse/PostToolUse hooks: `evaluate_action` (Cedar decision, fail-closed),
98
+ `sign_decision` (Ed25519 receipt, byte-compatible with the runtime gate's),
99
+ `verify_receipt` (offline check), and `self_test` (proves a known-forbidden
100
+ action is denied and a signed receipt round-trips). All four are read-only
101
+ and annotated with what they return. A bare-spawnable bin,
102
+ `protect-mcp-mcp`, is included alongside the `mcp` subcommand so any MCP
103
+ host or registry can point at either.
104
+
3
105
  ## 0.9.6: policy you can see and change
4
106
 
5
107
  The gate is default-deny and fail-closed, but a deny used to be a dead end: an
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  buildEnrichment
3
- } from "./chunk-WWPQNIVF.mjs";
3
+ } from "./chunk-KRKZ2YX7.mjs";
4
4
  import {
5
5
  ReceiptBuffer,
6
- buildActionReadback,
6
+ buildActionReadback
7
+ } from "./chunk-CIWIK6BT.mjs";
8
+ import {
7
9
  checkRateLimit,
8
10
  getSignerInfo,
9
11
  getToolPolicy,
@@ -12,12 +14,15 @@ import {
12
14
  loadPolicy,
13
15
  parseRateLimit,
14
16
  signDecision
15
- } from "./chunk-G6X763MH.mjs";
17
+ } from "./chunk-WMIWQ6TS.mjs";
16
18
  import {
17
19
  evaluateCedar,
18
20
  isCedarAvailable,
19
21
  loadCedarPolicies
20
- } from "./chunk-MWXDXYWH.mjs";
22
+ } from "./chunk-FGCNKEEW.mjs";
23
+ import {
24
+ receiptHash
25
+ } from "./chunk-XOP3PEBM.mjs";
21
26
 
22
27
  // src/hook-server.ts
23
28
  import { createServer } from "http";
@@ -181,6 +186,16 @@ var DEFAULT_PORT = 9377;
181
186
  var LOG_FILE = ".protect-mcp-log.jsonl";
182
187
  var RECEIPTS_FILE = ".protect-mcp-receipts.jsonl";
183
188
  var PAYLOAD_HASH_THRESHOLD = 1024;
189
+ function resumeReceiptChain(receiptFilePath) {
190
+ try {
191
+ if (!existsSync(receiptFilePath)) return null;
192
+ const lines = readFileSync(receiptFilePath, "utf-8").split("\n").filter((l) => l.trim());
193
+ if (lines.length === 0) return null;
194
+ return receiptHash(JSON.parse(lines[lines.length - 1]));
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
184
199
  function detectSwarmContext() {
185
200
  const teamName = process.env.CLAUDE_CODE_TEAM_NAME;
186
201
  const agentId = process.env.CLAUDE_CODE_AGENT_ID;
@@ -683,10 +698,11 @@ function emitDecisionLog(state, entry) {
683
698
  } catch {
684
699
  }
685
700
  if (isSigningEnabled()) {
686
- const signed = signDecision(log);
701
+ const signed = signDecision(log, state.lastReceiptHash || void 0);
687
702
  if (signed.signed) {
688
703
  try {
689
704
  appendFileSync(state.receiptFilePath, signed.signed + "\n");
705
+ if (signed.receipt_hash) state.lastReceiptHash = signed.receipt_hash;
690
706
  } catch {
691
707
  }
692
708
  state.receiptBuffer.add(log.request_id, signed.signed);
@@ -701,16 +717,19 @@ function emitDecisionLog(state, entry) {
701
717
  `);
702
718
  }
703
719
  } else if (signed.error) {
704
- const tombstone = JSON.stringify({
720
+ const tombstoneObj = {
705
721
  type: "scopeblind.signing_failure.v1",
706
722
  request_id: log.request_id,
707
723
  tool: log.tool,
708
724
  decision: log.decision,
709
725
  error: signed.error,
710
- at: new Date(log.timestamp).toISOString()
711
- });
726
+ at: new Date(log.timestamp).toISOString(),
727
+ ...state.lastReceiptHash ? { previousReceiptHash: state.lastReceiptHash } : {}
728
+ };
729
+ const tombstone = JSON.stringify(tombstoneObj);
712
730
  try {
713
731
  appendFileSync(state.receiptFilePath, tombstone + "\n");
732
+ state.lastReceiptHash = receiptHash(tombstoneObj);
714
733
  } catch {
715
734
  }
716
735
  process.stderr.write(`[PROTECT_MCP_SIGNING_FAILURE] ${tombstone}
@@ -826,6 +845,7 @@ async function startHookServer(options = {}) {
826
845
  policyDigest,
827
846
  logFilePath: join(process.cwd(), LOG_FILE),
828
847
  receiptFilePath: join(process.cwd(), RECEIPTS_FILE),
848
+ lastReceiptHash: resumeReceiptChain(join(process.cwd(), RECEIPTS_FILE)),
829
849
  permissionSuggestions: /* @__PURE__ */ new Map(),
830
850
  configAlerts: []
831
851
  };
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  meetsMinTier
3
- } from "./chunk-YM6SOJBR.mjs";
3
+ } from "./chunk-NQOKPBSQ.mjs";
4
4
  import {
5
5
  checkRateLimit,
6
6
  getToolPolicy,
7
7
  parseRateLimit
8
- } from "./chunk-G6X763MH.mjs";
8
+ } from "./chunk-WMIWQ6TS.mjs";
9
9
 
10
10
  // src/simulate.ts
11
11
  import { readFileSync } from "fs";
@@ -1,244 +1,6 @@
1
- // src/policy.ts
2
- import { createHash } from "crypto";
3
- import { readFileSync } from "fs";
4
- function loadPolicy(path) {
5
- const raw = readFileSync(path, "utf-8");
6
- const parsed = JSON.parse(raw);
7
- if (!parsed.tools || typeof parsed.tools !== "object") {
8
- throw new Error(`Invalid policy file: missing "tools" object in ${path}`);
9
- }
10
- const policy = {
11
- tools: parsed.tools,
12
- default_tier: parsed.default_tier || "unknown",
13
- policy_engine: parsed.policy_engine || "built-in",
14
- ...parsed.external ? { external: parsed.external } : {}
15
- };
16
- const digest = computePolicyDigest(policy);
17
- return {
18
- policy,
19
- digest,
20
- credentials: parsed.credentials,
21
- signing: parsed.signing
22
- };
23
- }
24
- function computePolicyDigest(policy) {
25
- const canonical = JSON.stringify(sortKeysDeep(policy));
26
- return createHash("sha256").update(canonical).digest("hex").slice(0, 16);
27
- }
28
- function sortKeysDeep(obj) {
29
- if (obj === null || typeof obj !== "object") return obj;
30
- if (Array.isArray(obj)) return obj.map(sortKeysDeep);
31
- const sorted = {};
32
- for (const key of Object.keys(obj).sort()) {
33
- sorted[key] = sortKeysDeep(obj[key]);
34
- }
35
- return sorted;
36
- }
37
- function getToolPolicy(toolName, policy) {
38
- if (!policy) {
39
- return { require: "any" };
40
- }
41
- if (policy.tools[toolName]) {
42
- return policy.tools[toolName];
43
- }
44
- if (policy.tools["*"]) {
45
- return policy.tools["*"];
46
- }
47
- return { require: "any" };
48
- }
49
- function parseRateLimit(spec) {
50
- const match = spec.match(/^(\d+)\/(second|minute|hour|day)$/);
51
- if (!match) {
52
- throw new Error(`Invalid rate limit format: "${spec}". Expected "N/unit" (e.g. "5/hour")`);
53
- }
54
- const count = parseInt(match[1], 10);
55
- const unit = match[2];
56
- const windowMs = {
57
- second: 1e3,
58
- minute: 6e4,
59
- hour: 36e5,
60
- day: 864e5
61
- };
62
- return { count, windowMs: windowMs[unit] };
63
- }
64
- function checkRateLimit(key, limit, store) {
65
- const now = Date.now();
66
- const windowStart = now - limit.windowMs;
67
- const timestamps = (store.get(key) || []).filter((t) => t > windowStart);
68
- if (timestamps.length >= limit.count) {
69
- store.set(key, timestamps);
70
- return { allowed: false, remaining: 0 };
71
- }
72
- timestamps.push(now);
73
- store.set(key, timestamps);
74
- return { allowed: true, remaining: limit.count - timestamps.length };
75
- }
76
-
77
- // src/signing.ts
78
- import { readFileSync as readFileSync2, existsSync } from "fs";
79
- var signerState = null;
80
- var artifactsModule = null;
81
- var signingConfigured = false;
82
- var signingInitError = null;
83
- async function initSigning(config) {
84
- const warnings = [];
85
- signerState = null;
86
- artifactsModule = null;
87
- signingConfigured = Boolean(config && config.enabled !== false);
88
- signingInitError = null;
89
- if (!config || config.enabled === false) {
90
- return warnings;
91
- }
92
- if (!config.key_path) {
93
- signingInitError = "signing enabled but key_path is not configured";
94
- warnings.push(`signing: ${signingInitError}`);
95
- return warnings;
96
- }
97
- if (!existsSync(config.key_path)) {
98
- signingInitError = `key file not found at ${config.key_path}`;
99
- warnings.push(`signing: ${signingInitError} \u2014 run "protect-mcp init" to generate`);
100
- return warnings;
101
- }
102
- let keyData;
103
- try {
104
- keyData = JSON.parse(readFileSync2(config.key_path, "utf-8"));
105
- if (!keyData.privateKey || !keyData.publicKey) {
106
- signingInitError = "key file missing privateKey or publicKey fields";
107
- warnings.push(`signing: ${signingInitError}`);
108
- return warnings;
109
- }
110
- } catch (err) {
111
- signingInitError = `failed to load key file: ${err instanceof Error ? err.message : err}`;
112
- warnings.push(`signing: ${signingInitError}`);
113
- return warnings;
114
- }
115
- try {
116
- const moduleName = "@veritasacta/artifacts";
117
- artifactsModule = await import(
118
- /* @vite-ignore */
119
- moduleName
120
- );
121
- } catch {
122
- signingInitError = "@veritasacta/artifacts not available";
123
- warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
124
- return warnings;
125
- }
126
- try {
127
- signerState = {
128
- privateKey: keyData.privateKey,
129
- publicKey: keyData.publicKey,
130
- kid: keyData.kid || artifactsModule.computeKid(keyData.publicKey),
131
- issuer: config.issuer || keyData.issuer || "protect-mcp"
132
- };
133
- } catch (err) {
134
- signingInitError = `failed to initialize signer: ${err instanceof Error ? err.message : err}`;
135
- artifactsModule = null;
136
- warnings.push(`signing: ${signingInitError} \u2014 enforce mode will fail closed`);
137
- }
138
- return warnings;
139
- }
140
- function signDecision(entry) {
141
- const artifactType = entry.decision === "deny" ? "gateway_restraint" : "decision_receipt";
142
- if (signingConfigured && signingInitError) {
143
- return {
144
- ok: false,
145
- signed: null,
146
- artifact_type: artifactType,
147
- warning: `signing initialization failed: ${signingInitError}`,
148
- error: signingInitError
149
- };
150
- }
151
- if (signingConfigured && (!signerState || !artifactsModule)) {
152
- const error = "signing was configured but no signer is ready";
153
- return {
154
- ok: false,
155
- signed: null,
156
- artifact_type: artifactType,
157
- warning: error,
158
- error
159
- };
160
- }
161
- if (!signerState || !artifactsModule) {
162
- return { ok: false, signed: null, artifact_type: "none" };
163
- }
164
- try {
165
- const payload = {
166
- tool: entry.tool,
167
- decision: entry.decision,
168
- reason_code: entry.reason_code,
169
- policy_digest: entry.policy_digest,
170
- scope: entry.request_id,
171
- // request scope
172
- mode: entry.mode,
173
- request_id: entry.request_id,
174
- // Spec version: ties every receipt to the IETF standard
175
- spec: "draft-farley-acta-signed-receipts-01",
176
- // Issuer certification: distinguishes VOPRF-backed receipts from self-signed ones
177
- // - scopeblind:verified = issued via ScopeBlind VOPRF backend (paid tier)
178
- // - self-signed = signed with local Ed25519 key (free tier, protect-mcp default)
179
- // - uncertified = unsigned receipt (shadow mode, no signing configured)
180
- issuer_certification: signerState ? "self-signed" : "uncertified",
181
- // The signer's PUBLIC key, inside the signed payload, so a receipt is
182
- // self-contained: any verifier (including the record viewer, in-browser)
183
- // can check the signature without a side channel. Binding the key inside
184
- // the signature means it cannot be swapped without breaking the signature;
185
- // authenticity (that the key is YOUR gate's) still comes from pinning it.
186
- public_key: signerState.publicKey
187
- };
188
- if (entry.tier) payload.tier = entry.tier;
189
- if (entry.credential_ref) payload.credential_ref = entry.credential_ref;
190
- if (entry.rate_limit_remaining !== void 0) {
191
- payload.rate_limit_remaining = entry.rate_limit_remaining;
192
- }
193
- if (entry.policy_engine) payload.policy_engine = entry.policy_engine;
194
- if (entry.hook_event) payload.hook_event = entry.hook_event;
195
- if (entry.sandbox_state) payload.sandbox_state = entry.sandbox_state;
196
- if (entry.timing) payload.timing = entry.timing;
197
- if (entry.swarm) payload.swarm = entry.swarm;
198
- if (entry.payload_digest) payload.payload_digest = entry.payload_digest;
199
- if (entry.enrichment) payload.enrichment = entry.enrichment;
200
- if (entry.action_readback) payload.action_readback = entry.action_readback;
201
- if (entry.deny_iteration) payload.deny_iteration = entry.deny_iteration;
202
- const result = artifactsModule.createSignedArtifact(
203
- artifactType,
204
- payload,
205
- signerState.privateKey,
206
- {
207
- kid: signerState.kid,
208
- issuer: signerState.issuer
209
- }
210
- );
211
- return {
212
- ok: true,
213
- signed: JSON.stringify(result.artifact),
214
- artifact_type: artifactType
215
- };
216
- } catch (err) {
217
- const message = err instanceof Error ? err.message : "unknown error";
218
- return {
219
- ok: false,
220
- signed: null,
221
- artifact_type: artifactType,
222
- warning: `signing failed: ${message}`,
223
- error: message
224
- };
225
- }
226
- }
227
- function getSignerInfo() {
228
- if (!signerState) return null;
229
- return {
230
- publicKey: signerState.publicKey,
231
- kid: signerState.kid,
232
- issuer: signerState.issuer
233
- };
234
- }
235
- function isSigningEnabled() {
236
- return signingConfigured && signingInitError === null && signerState !== null && artifactsModule !== null;
237
- }
238
-
239
1
  // src/http-server.ts
240
2
  import { createServer } from "http";
241
- import { readFileSync as readFileSync3, existsSync as existsSync2 } from "fs";
3
+ import { readFileSync, existsSync } from "fs";
242
4
  import { join } from "path";
243
5
  var LOG_FILE = ".protect-mcp-log.jsonl";
244
6
  var MAX_RECEIPTS = 100;
@@ -333,12 +95,12 @@ function handleHealth(res, startTime, config) {
333
95
  }
334
96
  function handleStatus(res, logDir) {
335
97
  const logPath = join(logDir, LOG_FILE);
336
- if (!existsSync2(logPath)) {
98
+ if (!existsSync(logPath)) {
337
99
  res.writeHead(200);
338
100
  res.end(JSON.stringify({ entries: 0, message: "no log file yet" }));
339
101
  return;
340
102
  }
341
- const raw = readFileSync3(logPath, "utf-8");
103
+ const raw = readFileSync(logPath, "utf-8");
342
104
  const lines = raw.trim().split("\n").filter(Boolean);
343
105
  const entries = [];
344
106
  for (const line of lines) {
@@ -462,7 +224,7 @@ function handleListApprovals(res, approvalStore) {
462
224
  }
463
225
 
464
226
  // src/action-readback.ts
465
- import { createHash as createHash2 } from "crypto";
227
+ import { createHash } from "crypto";
466
228
  var SECRET_KEY_RE = /(api[_-]?key|authorization|bearer|credential|password|secret|session|token|private[_-]?key)/i;
467
229
  var DESTINATION_KEYS = [
468
230
  "path",
@@ -542,7 +304,7 @@ function buildActionReadback(tool, input) {
542
304
  action,
543
305
  destination,
544
306
  payload_preview: payloadPreview,
545
- payload_hash: createHash2("sha256").update(canonical).digest("hex"),
307
+ payload_hash: createHash("sha256").update(canonical).digest("hex"),
546
308
  payload_bytes: Buffer.byteLength(canonical, "utf-8"),
547
309
  disclosed_fields: [...new Set(disclosedFields)].slice(0, 80),
548
310
  redacted_fields: [...new Set(redactedFields)].slice(0, 80),
@@ -551,14 +313,6 @@ function buildActionReadback(tool, input) {
551
313
  }
552
314
 
553
315
  export {
554
- loadPolicy,
555
- getToolPolicy,
556
- parseRateLimit,
557
- checkRateLimit,
558
- initSigning,
559
- signDecision,
560
- getSignerInfo,
561
- isSigningEnabled,
562
316
  ReceiptBuffer,
563
317
  startStatusServer,
564
318
  buildActionReadback
@@ -1,7 +1,80 @@
1
- // src/cedar-evaluator.ts
1
+ import {
2
+ canonicalize
3
+ } from "./chunk-XOP3PEBM.mjs";
4
+
5
+ // src/policy-digest.ts
2
6
  import { createHash } from "crypto";
3
7
  import { readFileSync, readdirSync, existsSync } from "fs";
4
8
  import { join, extname } from "path";
9
+ var POLICY_DIGEST_CONSTRUCTION = "acta-policy-digest-v1";
10
+ var POLICY_BUNDLE_SCHEMA = "acta.policy-bundle.v1";
11
+ var sha256hex = (data) => createHash("sha256").update(data).digest("hex");
12
+ function digestPolicyFiles(engine, files) {
13
+ if (files.length === 0) throw new Error("policy digest requires at least one file");
14
+ const names = /* @__PURE__ */ new Set();
15
+ for (const f of files) {
16
+ if (!f.name) throw new Error("policy file entries require a name");
17
+ if (names.has(f.name)) throw new Error(`duplicate policy file name: ${f.name}`);
18
+ names.add(f.name);
19
+ }
20
+ const entries = files.map((f) => ({ name: f.name, sha256: sha256hex(Buffer.from(f.content, "utf-8")) })).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
21
+ const manifest = { construction: POLICY_DIGEST_CONSTRUCTION, engine, files: entries };
22
+ return {
23
+ policy_digest: `sha256:${sha256hex(Buffer.from(canonicalize(manifest), "utf-8"))}`,
24
+ construction: POLICY_DIGEST_CONSTRUCTION,
25
+ engine,
26
+ files: entries
27
+ };
28
+ }
29
+ function digestCedarDir(dirPath) {
30
+ if (!existsSync(dirPath)) throw new Error(`Cedar policy directory not found: ${dirPath}`);
31
+ const names = readdirSync(dirPath).filter((f) => extname(f) === ".cedar").sort();
32
+ if (names.length === 0) throw new Error(`No .cedar files found in: ${dirPath}`);
33
+ const files = names.map((name) => ({ name, content: readFileSync(join(dirPath, name), "utf-8") }));
34
+ return { ...digestPolicyFiles("cedar", files), dir: dirPath };
35
+ }
36
+ function digestCedarSource(source) {
37
+ return digestPolicyFiles("cedar", [{ name: "policy.cedar", content: source }]);
38
+ }
39
+ function digestBuiltinPolicy(policy) {
40
+ return digestPolicyFiles("builtin", [{ name: "policy.json", content: canonicalize(policy) }]);
41
+ }
42
+ function shortPolicyLabel(result) {
43
+ return `${result.engine}:${result.policy_digest.replace(/^sha256:/, "").slice(0, 16)}`;
44
+ }
45
+ function buildPolicyBundle(engine, files, generatedAt) {
46
+ const d = digestPolicyFiles(engine, files);
47
+ const byName = new Map(files.map((f) => [f.name, f.content]));
48
+ return {
49
+ schema: POLICY_BUNDLE_SCHEMA,
50
+ construction: POLICY_DIGEST_CONSTRUCTION,
51
+ engine,
52
+ policy_digest: d.policy_digest,
53
+ files: d.files.map((e) => ({ ...e, content: byName.get(e.name) })),
54
+ generated_at: generatedAt || (/* @__PURE__ */ new Date()).toISOString()
55
+ };
56
+ }
57
+ function verifyPolicyBundle(bundle) {
58
+ try {
59
+ const b = bundle;
60
+ if (!b || b.schema !== POLICY_BUNDLE_SCHEMA) return { valid: false, error: "unknown_schema" };
61
+ if (b.construction !== POLICY_DIGEST_CONSTRUCTION) return { valid: false, error: "unknown_construction" };
62
+ if (!Array.isArray(b.files) || b.files.length === 0) return { valid: false, error: "missing_files" };
63
+ for (const f of b.files) {
64
+ if (sha256hex(Buffer.from(f.content, "utf-8")) !== f.sha256) {
65
+ return { valid: false, error: `file_hash_mismatch:${f.name}` };
66
+ }
67
+ }
68
+ const recomputed = digestPolicyFiles(b.engine, b.files.map((f) => ({ name: f.name, content: f.content }))).policy_digest;
69
+ return recomputed === b.policy_digest ? { valid: true, recomputed } : { valid: false, recomputed, error: "digest_mismatch" };
70
+ } catch (err) {
71
+ return { valid: false, error: `verify_error:${err instanceof Error ? err.message : "unknown"}` };
72
+ }
73
+ }
74
+
75
+ // src/cedar-evaluator.ts
76
+ import { readFileSync as readFileSync2, readdirSync as readdirSync2, existsSync as existsSync2 } from "fs";
77
+ import { join as join2, extname as extname2 } from "path";
5
78
  var cedarWasm = null;
6
79
  var loadAttempted = false;
7
80
  async function ensureCedarWasm() {
@@ -20,20 +93,19 @@ async function ensureCedarWasm() {
20
93
  }
21
94
  }
22
95
  function loadCedarPolicies(dirPath) {
23
- if (!existsSync(dirPath)) {
96
+ if (!existsSync2(dirPath)) {
24
97
  throw new Error(`Cedar policy directory not found: ${dirPath}`);
25
98
  }
26
- const entries = readdirSync(dirPath).filter((f) => extname(f) === ".cedar").sort();
99
+ const entries = readdirSync2(dirPath).filter((f) => extname2(f) === ".cedar").sort();
27
100
  if (entries.length === 0) {
28
101
  throw new Error(`No .cedar files found in: ${dirPath}`);
29
102
  }
30
- const sources = [];
103
+ const files = [];
31
104
  for (const file of entries) {
32
- const content = readFileSync(join(dirPath, file), "utf-8");
33
- sources.push(content);
105
+ files.push({ name: file, content: readFileSync2(join2(dirPath, file), "utf-8") });
34
106
  }
35
- const concatenated = sources.join("\n\n");
36
- const digest = createHash("sha256").update(concatenated).digest("hex").slice(0, 16);
107
+ const concatenated = files.map((f) => f.content).join("\n\n");
108
+ const digest = digestPolicyFiles("cedar", files).policy_digest;
37
109
  return {
38
110
  source: concatenated,
39
111
  digest,
@@ -174,7 +246,7 @@ async function isCedarAvailable() {
174
246
  return ensureCedarWasm();
175
247
  }
176
248
  function policySetFromSource(source, name = "inline") {
177
- const digest = createHash("sha256").update(source).digest("hex").slice(0, 16);
249
+ const digest = digestCedarSource(source).policy_digest;
178
250
  return { source, digest, fileCount: 1, files: [name] };
179
251
  }
180
252
  async function runEvaluatorSelfTest() {
@@ -202,6 +274,11 @@ async function runEvaluatorSelfTest() {
202
274
  }
203
275
 
204
276
  export {
277
+ digestCedarDir,
278
+ digestBuiltinPolicy,
279
+ shortPolicyLabel,
280
+ buildPolicyBundle,
281
+ verifyPolicyBundle,
205
282
  loadCedarPolicies,
206
283
  evaluateCedar,
207
284
  isCedarAvailable,