@letterblack/lbe-core 1.3.4 → 1.3.5

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 (78) hide show
  1. package/.githooks/pre-commit +2 -0
  2. package/.githooks/pre-push +2 -0
  3. package/CHANGELOG.md +75 -0
  4. package/LICENSE +1 -1
  5. package/README.md +127 -154
  6. package/RELEASE_WORKSPACE_RULES.md +110 -0
  7. package/Release-README.md +65 -0
  8. package/WORKSPACE.md +422 -0
  9. package/_proof.mjs +246 -0
  10. package/assets/runtime-boundary.svg +36 -36
  11. package/bin/lbe.js +12 -0
  12. package/config/identity.config.json +3 -0
  13. package/config/policy.default.json +24 -0
  14. package/dist/cli/lbe.js +4432 -0
  15. package/dist/hooks/register.cjs +505 -0
  16. package/dist/state/appendCentral.cjs +87 -0
  17. package/dist/state/index.cjs +101 -0
  18. package/exec/cli.js +472 -0
  19. package/exec/index.js +2 -0
  20. package/index.js +24 -0
  21. package/lbe.audit.jsonl +46 -0
  22. package/package.json +48 -16
  23. package/release/README.md +216 -0
  24. package/release/TRUST.md +90 -0
  25. package/release/exec-README.md +215 -0
  26. package/release/exec-types.d.ts +50 -0
  27. package/release-exec/LICENSE +1 -0
  28. package/release-exec/README.md +215 -0
  29. package/release-exec/assets/lbe-gates.jpg +0 -0
  30. package/release-exec/assets/lbe-gates.png +0 -0
  31. package/release-exec/assets/runtime-boundary.svg +36 -0
  32. package/release-exec/assets/story-allow.jpg +0 -0
  33. package/release-exec/assets/story-allow.png +0 -0
  34. package/release-exec/assets/story-deny.jpg +0 -0
  35. package/release-exec/assets/story-deny.png +0 -0
  36. package/release-exec/dist/cli.js +2841 -0
  37. package/release-exec/dist/index.js +1835 -0
  38. package/release-exec/dist/lbe_engine.wasm +0 -0
  39. package/{dist → release-exec/dist}/wasm.lock.json +4 -5
  40. package/release-exec/hooks/register.cjs +473 -0
  41. package/release-exec/package.json +35 -0
  42. package/release-exec/types.d.ts +50 -0
  43. package/runtime/engine.js +322 -0
  44. package/runtime/lbe_engine.wasm +0 -0
  45. package/src/cli/commands/assertConsumer.js +198 -0
  46. package/src/cli/commands/auditVerify.js +36 -0
  47. package/src/cli/commands/dryrun.js +175 -0
  48. package/src/cli/commands/health.js +153 -0
  49. package/src/cli/commands/init.js +306 -0
  50. package/src/cli/commands/integrityCheck.js +57 -0
  51. package/src/cli/commands/logs.js +53 -0
  52. package/src/cli/commands/openState.js +44 -0
  53. package/src/cli/commands/policyAdd.js +8 -0
  54. package/src/cli/commands/policyMode.js +7 -0
  55. package/src/cli/commands/policySign.js +72 -0
  56. package/src/cli/commands/proof.js +122 -0
  57. package/src/cli/commands/run.js +342 -0
  58. package/src/cli/commands/status.js +73 -0
  59. package/src/cli/commands/verify.js +144 -0
  60. package/src/cli/main.js +181 -0
  61. package/src/cli/parseArgs.js +115 -0
  62. package/src/exec/localExecutor.js +289 -0
  63. package/src/hooks/register.cjs +505 -0
  64. package/src/state/appendCentral.cjs +87 -0
  65. package/src/state/fileIndex.js +140 -0
  66. package/src/state/index.cjs +101 -0
  67. package/src/state/index.js +65 -0
  68. package/src/state/intentRegistry.js +83 -0
  69. package/src/state/migration.js +112 -0
  70. package/src/state/proofRunner.js +246 -0
  71. package/src/state/stateRoot.js +40 -0
  72. package/src/state/targetRegistry.js +108 -0
  73. package/src/state/workspaceId.js +40 -0
  74. package/src/state/workspaceRegistry.js +65 -0
  75. package/types.d.ts +175 -2
  76. package/dist/cli.js +0 -141
  77. package/dist/index.js +0 -52
  78. /package/dist/{lbe_engine.wasm → cli/lbe_engine.wasm} +0 -0
@@ -0,0 +1,4432 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/main.js
4
+ import fs30 from "fs";
5
+ import path36 from "path";
6
+ import { fileURLToPath as fileURLToPath3 } from "url";
7
+
8
+ // src/cli/parseArgs.js
9
+ function parseArgs(argv) {
10
+ if (argv.length === 0) {
11
+ return { command: "help", opts: {} };
12
+ }
13
+ const command = argv[0];
14
+ const opts = {};
15
+ for (let i = 1; i < argv.length; i++) {
16
+ if (argv[i].startsWith("--")) {
17
+ const key = argv[i].substring(2);
18
+ if (key.includes("=")) {
19
+ const [k, v] = key.split("=");
20
+ opts[k] = v;
21
+ } else {
22
+ const nextArg = argv[i + 1];
23
+ if (!nextArg || nextArg.startsWith("-")) {
24
+ opts[key] = true;
25
+ } else {
26
+ opts[key] = nextArg;
27
+ i++;
28
+ }
29
+ }
30
+ } else if (argv[i].startsWith("-")) {
31
+ const key = argv[i].substring(1);
32
+ const nextArg = argv[i + 1];
33
+ if (!nextArg || nextArg.startsWith("-")) {
34
+ opts[key] = true;
35
+ } else {
36
+ opts[key] = nextArg;
37
+ i++;
38
+ }
39
+ }
40
+ }
41
+ return { command, opts };
42
+ }
43
+ function printHelp(version = "unknown") {
44
+ console.log(`
45
+ \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
46
+ \u2551 LetterBlack Sentinel \u2014 CLI Governance \u2551
47
+ \u2551 Local-first execution governance SDK v${version.padEnd(12)}\u2551
48
+ \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
49
+
50
+ USAGE:
51
+ lbe [command] [options]
52
+
53
+ COMMANDS:
54
+ init Initialize LetterBlack Sentinel environment
55
+ verify Verify a proposal (validate, don't execute)
56
+ dryrun Validate and simulate execution (no changes)
57
+ run Validate and execute a proposal
58
+ policy-sign Sign policy and write policy signature envelope
59
+ policy-add Controller-only: add a rule to lbe.policy.json
60
+ observe Set project-local policy to advisory mode
61
+ enforce Set project-local policy to blocking mode
62
+ health Run deployment/runtime health checks
63
+ integrity-check Verify controller integrity manifest
64
+ integrity-generate Generate controller integrity manifest
65
+ audit-verify Verify audit log hash-chain integrity
66
+ status Show central state summary for this workspace
67
+ logs Print recent entries from central event log
68
+ open-state Open the central state directory in the file manager
69
+ proof Show latest proof result (--json for raw JSON, --public for redacted)
70
+ assert-consumer Verify this project consumes LBE as an installed registry dependency
71
+ help Show this help message
72
+
73
+ OPTIONS:
74
+ --in Input file (JSON proposal)
75
+ --config Policy config file (default: ./.lbe/config/policy.default.json)
76
+ --policy Alias for --config
77
+ --policy-sig Policy signature file (default: ./.lbe/config/policy.sig.json)
78
+ --policy-state Policy monotonic state file (default: ./data/policy.state.json)
79
+ --policy-unsigned-ok Allow unsigned policy (dev-only; default: false)
80
+ --policy-key-id Signer keyId for policy-sign (default: policy-signer-v1-2026Q1)
81
+ --secret-key-file Secret key for policy-sign (default: ./keys/secret.key)
82
+ --data-dir Data directory for health checks (default: ./data)
83
+ --nonce-db Nonce DB path for health checks
84
+ --rate-db Rate-limit DB path for health checks
85
+ --keys-store Trusted keys store (default: ./.lbe/config/keys.json)
86
+ --pub-key Public key for verification (Ed25519 base64)
87
+ --pub-key-file Legacy single-key file path (fallback mode)
88
+ --integrity-strict Fail verify/dryrun/run if integrity check fails
89
+ --integrity-manifest Integrity manifest path (default: ./.lbe/config/integrity.manifest.json)
90
+ --manifest Manifest path override for integrity-check
91
+ --out Output path for integrity-generate
92
+ --audit Audit log file (default: ./data/audit.log.jsonl)
93
+ --root Project root for local policy commands (default: cwd)
94
+ --effect Local rule effect: allow or deny
95
+ --type Local rule type: path or command
96
+ --pattern Local rule glob/pattern
97
+ --from Required human-readable rule provenance
98
+ --json JSON output (default: true)
99
+ --fail-fast Stop at first audit integrity error (default: true)
100
+ --max Max audit entries to process (optional)
101
+ --version Show version
102
+ --help Show this help message
103
+
104
+ EXAMPLES:
105
+ lbe execute --input proposal.json
106
+ cat proposal.json | lbe execute
107
+ lbe policy-sign --config ./.lbe/config/policy.default.json --policy-sig ./.lbe/config/policy.sig.json
108
+ lbe integrity-generate --out ./.lbe/config/integrity.manifest.json
109
+ lbe integrity-check --integrity-strict --manifest ./.lbe/config/integrity.manifest.json
110
+ lbe audit-verify --audit ./data/audit.log.jsonl
111
+
112
+ For more info, visit: https://github.com/Letterblack0306/LetterBlack-LBE-Core
113
+ `);
114
+ }
115
+
116
+ // src/cli/commands/init.js
117
+ import fs4 from "fs";
118
+ import path4 from "path";
119
+ import readline from "readline";
120
+
121
+ // src/core/signature.js
122
+ import nacl from "tweetnacl";
123
+ import { canonicalize } from "json-canonicalize";
124
+ function bytesFromBase64(b64) {
125
+ return Buffer.from(b64, "base64");
126
+ }
127
+ function toBase64(bytes) {
128
+ return Buffer.from(bytes).toString("base64");
129
+ }
130
+ function verifyEd25519({ payloadObj, sigB64, pubKeyB64 }) {
131
+ try {
132
+ const msg = Buffer.from(canonicalize(payloadObj), "utf8");
133
+ const sig = bytesFromBase64(sigB64);
134
+ const pub = bytesFromBase64(pubKeyB64);
135
+ const isValid = nacl.sign.detached.verify(
136
+ new Uint8Array(msg),
137
+ new Uint8Array(sig),
138
+ new Uint8Array(pub)
139
+ );
140
+ return {
141
+ valid: isValid,
142
+ message: isValid ? "Signature verified" : "Signature verification failed"
143
+ };
144
+ } catch (err) {
145
+ return {
146
+ valid: false,
147
+ message: `Signature verification error: ${err.message}`
148
+ };
149
+ }
150
+ }
151
+ function generateKeyPair() {
152
+ const keyPair = nacl.sign.keyPair();
153
+ return {
154
+ publicKey: toBase64(keyPair.publicKey),
155
+ secretKey: toBase64(keyPair.secretKey)
156
+ };
157
+ }
158
+ function signEd25519({ payloadObj, secretKeyB64 }) {
159
+ try {
160
+ const msg = Buffer.from(canonicalize(payloadObj), "utf8");
161
+ const secretKey = bytesFromBase64(secretKeyB64);
162
+ const sig = nacl.sign.detached(new Uint8Array(msg), new Uint8Array(secretKey));
163
+ return {
164
+ signature: toBase64(sig),
165
+ error: null
166
+ };
167
+ } catch (err) {
168
+ return {
169
+ signature: null,
170
+ error: `Signing failed: ${err.message}`
171
+ };
172
+ }
173
+ }
174
+
175
+ // src/core/policySignature.js
176
+ import fs2 from "fs";
177
+ import path2 from "path";
178
+
179
+ // src/core/trustedKeys.js
180
+ import fs from "fs";
181
+ import path from "path";
182
+ var KEY_ID_PATTERN = /^[A-Za-z0-9:_-]{3,128}$/;
183
+ function isValidKeyId(keyId) {
184
+ return typeof keyId === "string" && KEY_ID_PATTERN.test(keyId) && keyId !== "default";
185
+ }
186
+ function loadKeysStore(keysStorePath) {
187
+ const resolvedPath = path.resolve(keysStorePath);
188
+ if (!fs.existsSync(resolvedPath)) {
189
+ return {
190
+ ok: false,
191
+ reason: "KEY_STORE_MISSING",
192
+ message: `Key store not found: ${resolvedPath}`,
193
+ store: null
194
+ };
195
+ }
196
+ try {
197
+ const content = fs.readFileSync(resolvedPath, "utf-8");
198
+ const parsed = JSON.parse(content);
199
+ if (!parsed || typeof parsed !== "object" || typeof parsed.trustedKeys !== "object") {
200
+ return {
201
+ ok: false,
202
+ reason: "KEY_STORE_INVALID",
203
+ message: `Invalid key store format: ${resolvedPath}`,
204
+ store: null
205
+ };
206
+ }
207
+ return {
208
+ ok: true,
209
+ reason: null,
210
+ message: "Key store loaded",
211
+ store: parsed
212
+ };
213
+ } catch (error) {
214
+ return {
215
+ ok: false,
216
+ reason: "KEY_STORE_INVALID_JSON",
217
+ message: `Unable to parse key store: ${error.message}`,
218
+ store: null
219
+ };
220
+ }
221
+ }
222
+ function resolveTrustedPublicKey({ keyStore, keyId, requesterId, now = /* @__PURE__ */ new Date() }) {
223
+ if (!keyStore || typeof keyStore !== "object") {
224
+ return {
225
+ ok: false,
226
+ reason: "KEY_STORE_UNAVAILABLE",
227
+ message: "Trusted key store is not available",
228
+ publicKey: null
229
+ };
230
+ }
231
+ if (!isValidKeyId(keyId)) {
232
+ return {
233
+ ok: false,
234
+ reason: "KEY_ID_INVALID",
235
+ message: `Invalid keyId '${keyId}'. Use versioned key IDs like 'agent:gpt-v1-2026Q1'`,
236
+ publicKey: null
237
+ };
238
+ }
239
+ const keyConfig = keyStore.trustedKeys?.[keyId];
240
+ if (!keyConfig) {
241
+ return {
242
+ ok: false,
243
+ reason: "KEY_NOT_TRUSTED",
244
+ message: `Key '${keyId}' is not in trusted key store`,
245
+ publicKey: null
246
+ };
247
+ }
248
+ if (keyConfig.deprecated) {
249
+ return {
250
+ ok: false,
251
+ reason: "KEY_DEPRECATED",
252
+ message: `Key '${keyId}' is deprecated`,
253
+ publicKey: null
254
+ };
255
+ }
256
+ if (keyConfig.requesterId && keyConfig.requesterId !== requesterId) {
257
+ return {
258
+ ok: false,
259
+ reason: "KEY_REQUESTER_MISMATCH",
260
+ message: `Key '${keyId}' is not authorized for requester '${requesterId}'`,
261
+ publicKey: null
262
+ };
263
+ }
264
+ const notBeforeRaw = keyConfig.notBefore || keyConfig.validFrom;
265
+ const expiresAtRaw = keyConfig.expiresAt || keyConfig.validUntil;
266
+ if (typeof notBeforeRaw !== "string" || typeof expiresAtRaw !== "string") {
267
+ return {
268
+ ok: false,
269
+ reason: "KEY_LIFECYCLE_INVALID",
270
+ message: `Key '${keyId}' must define lifecycle fields 'notBefore' and 'expiresAt'`,
271
+ publicKey: null
272
+ };
273
+ }
274
+ const notBefore = new Date(notBeforeRaw);
275
+ const expiresAt = new Date(expiresAtRaw);
276
+ if (Number.isNaN(notBefore.getTime()) || Number.isNaN(expiresAt.getTime())) {
277
+ return {
278
+ ok: false,
279
+ reason: "KEY_LIFECYCLE_INVALID",
280
+ message: `Key '${keyId}' has invalid lifecycle timestamp(s)`,
281
+ publicKey: null
282
+ };
283
+ }
284
+ if (notBefore >= expiresAt) {
285
+ return {
286
+ ok: false,
287
+ reason: "KEY_LIFECYCLE_INVALID",
288
+ message: `Key '${keyId}' has notBefore >= expiresAt`,
289
+ publicKey: null
290
+ };
291
+ }
292
+ if (now < notBefore) {
293
+ return {
294
+ ok: false,
295
+ reason: "KEY_NOT_YET_VALID",
296
+ message: `Key '${keyId}' not valid until ${notBeforeRaw}`,
297
+ publicKey: null
298
+ };
299
+ }
300
+ if (now > expiresAt) {
301
+ return {
302
+ ok: false,
303
+ reason: "KEY_EXPIRED",
304
+ message: `Key '${keyId}' expired on ${expiresAtRaw}`,
305
+ publicKey: null
306
+ };
307
+ }
308
+ if (!keyConfig.publicKey || typeof keyConfig.publicKey !== "string") {
309
+ return {
310
+ ok: false,
311
+ reason: "KEY_CONFIG_INVALID",
312
+ message: `Trusted key '${keyId}' is missing publicKey`,
313
+ publicKey: null
314
+ };
315
+ }
316
+ return {
317
+ ok: true,
318
+ reason: null,
319
+ message: "Trusted key resolved",
320
+ publicKey: keyConfig.publicKey
321
+ };
322
+ }
323
+
324
+ // src/core/policySignature.js
325
+ function createPolicySignatureEnvelope({ policyObj, secretKeyB64, keyId }) {
326
+ const signResult = signEd25519({
327
+ payloadObj: policyObj,
328
+ secretKeyB64
329
+ });
330
+ if (signResult.error) {
331
+ return {
332
+ ok: false,
333
+ reason: "POLICY_SIGNATURE_CREATE_FAILED",
334
+ message: signResult.error,
335
+ envelope: null
336
+ };
337
+ }
338
+ return {
339
+ ok: true,
340
+ reason: null,
341
+ message: "Policy signature created",
342
+ envelope: {
343
+ alg: "ed25519",
344
+ keyId,
345
+ sig: signResult.signature,
346
+ createdAt: Math.floor(Date.now() / 1e3)
347
+ }
348
+ };
349
+ }
350
+ function verifyPolicySignature({
351
+ policyObj,
352
+ keyStore,
353
+ policySigPath = "./.lbe/config/policy.sig.json",
354
+ allowUnsigned = false
355
+ }) {
356
+ const sigPathResolved = path2.resolve(policySigPath);
357
+ if (!fs2.existsSync(sigPathResolved)) {
358
+ if (allowUnsigned) {
359
+ return {
360
+ ok: true,
361
+ skipped: true,
362
+ reason: "POLICY_SIGNATURE_SKIPPED",
363
+ message: `Policy signature not found: ${sigPathResolved} (allowed by flag)`
364
+ };
365
+ }
366
+ return {
367
+ ok: false,
368
+ skipped: false,
369
+ reason: "POLICY_SIGNATURE_MISSING",
370
+ message: `Policy signature file not found: ${sigPathResolved}`
371
+ };
372
+ }
373
+ let envelope;
374
+ try {
375
+ envelope = JSON.parse(fs2.readFileSync(sigPathResolved, "utf-8"));
376
+ } catch (error) {
377
+ return {
378
+ ok: false,
379
+ skipped: false,
380
+ reason: "POLICY_SIGNATURE_INVALID",
381
+ message: `Unable to parse policy signature file: ${error.message}`
382
+ };
383
+ }
384
+ if (!envelope || envelope.alg !== "ed25519" || typeof envelope.keyId !== "string" || typeof envelope.sig !== "string") {
385
+ return {
386
+ ok: false,
387
+ skipped: false,
388
+ reason: "POLICY_SIGNATURE_INVALID",
389
+ message: "Policy signature envelope must include {alg, keyId, sig}"
390
+ };
391
+ }
392
+ if (!keyStore) {
393
+ return {
394
+ ok: false,
395
+ skipped: false,
396
+ reason: "POLICY_SIGNER_KEY_STORE_UNAVAILABLE",
397
+ message: "Trusted key store is required for policy signature verification"
398
+ };
399
+ }
400
+ const trustedKey = resolveTrustedPublicKey({
401
+ keyStore,
402
+ keyId: envelope.keyId,
403
+ requesterId: void 0
404
+ });
405
+ if (!trustedKey.ok) {
406
+ return {
407
+ ok: false,
408
+ skipped: false,
409
+ reason: "POLICY_SIGNER_NOT_TRUSTED",
410
+ message: trustedKey.message
411
+ };
412
+ }
413
+ const verification = verifyEd25519({
414
+ payloadObj: policyObj,
415
+ sigB64: envelope.sig,
416
+ pubKeyB64: trustedKey.publicKey
417
+ });
418
+ if (!verification.valid) {
419
+ return {
420
+ ok: false,
421
+ skipped: false,
422
+ reason: "POLICY_SIGNATURE_INVALID",
423
+ message: verification.message
424
+ };
425
+ }
426
+ return {
427
+ ok: true,
428
+ skipped: false,
429
+ reason: null,
430
+ message: "Policy signature verified",
431
+ keyId: envelope.keyId
432
+ };
433
+ }
434
+
435
+ // src/core/workspaceScanner.js
436
+ import fs3 from "fs";
437
+ import path3 from "path";
438
+ var PROJECT_SIGNALS = [
439
+ // Code types — ordered by priority for primaryType resolution
440
+ { file: "package.json", type: "node" },
441
+ { file: "pyproject.toml", type: "python" },
442
+ { file: "requirements.txt", type: "python" },
443
+ { file: "go.mod", type: "go" },
444
+ { file: "Cargo.toml", type: "rust" },
445
+ { file: "pom.xml", type: "java" },
446
+ { file: "build.gradle", type: "java" },
447
+ { file: "build.gradle.kts", type: "java" },
448
+ // Infrastructure types — supplementary, not primary
449
+ { file: "Dockerfile", type: "docker" },
450
+ { file: "docker-compose.yml", type: "docker" },
451
+ { dir: ".github/workflows", type: "ci" },
452
+ { file: ".gitlab-ci.yml", type: "ci" },
453
+ { dir: ".circleci", type: "ci" },
454
+ { file: "Jenkinsfile", type: "ci" },
455
+ { file: ".travis.yml", type: "ci" }
456
+ ];
457
+ var CODE_TYPES = ["node", "python", "go", "rust", "java"];
458
+ var SURFACE_MAP = {
459
+ source: ["src", "lib", "app", "pages", "components", "core", "api", "server", "client", "pkg", "cmd"],
460
+ generated: ["dist", "build", ".next", "out", "coverage", "target", ".cache", "__pycache__", ".turbo"],
461
+ tests: ["test", "tests", "__tests__", "spec", "e2e"],
462
+ docs: ["docs", "doc", "documentation"]
463
+ };
464
+ var SECRET_GLOBS = [".env", ".env.*", "keys/**", "secrets/**", "*.key", "*.pem", "*.p12", "*.pfx", "*.crt"];
465
+ var ALWAYS_DENY = ["node_modules/**", ".git/**"];
466
+ var LOCKFILES_BY_TYPE = {
467
+ node: ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"],
468
+ python: ["Pipfile.lock", "poetry.lock"],
469
+ go: ["go.sum"],
470
+ rust: ["Cargo.lock"],
471
+ java: ["gradle/wrapper/**"],
472
+ docker: [],
473
+ ci: [],
474
+ generic: []
475
+ };
476
+ var CONFIG_FILES_BY_TYPE = {
477
+ node: [
478
+ "package.json",
479
+ "tsconfig*.json",
480
+ "jest.config.*",
481
+ "vite.config.*",
482
+ "next.config.*",
483
+ "webpack.config.*",
484
+ ".eslintrc*",
485
+ ".eslint.config.*",
486
+ ".prettierrc*",
487
+ "babel.config.*"
488
+ ],
489
+ python: [
490
+ "pyproject.toml",
491
+ "setup.py",
492
+ "setup.cfg",
493
+ "tox.ini",
494
+ "pytest.ini",
495
+ "mypy.ini",
496
+ ".flake8",
497
+ ".pylintrc",
498
+ "Pipfile"
499
+ ],
500
+ go: ["go.mod", ".golangci.yml", ".golangci.yaml"],
501
+ rust: ["Cargo.toml", "rust-toolchain.toml", "clippy.toml", ".rustfmt.toml"],
502
+ java: [
503
+ "pom.xml",
504
+ "build.gradle",
505
+ "build.gradle.kts",
506
+ "gradle.properties",
507
+ "settings.gradle",
508
+ "settings.gradle.kts"
509
+ ],
510
+ docker: ["Dockerfile", "docker-compose.yml", ".dockerignore"],
511
+ ci: [".gitlab-ci.yml", "Jenkinsfile", ".travis.yml"],
512
+ generic: ["Makefile", "CMakeLists.txt", "meson.build"]
513
+ };
514
+ var CONFIG_FILES_UNIVERSAL = [".editorconfig", ".nvmrc", ".node-version", ".python-version"];
515
+ var CONFIG_DIRS_UNIVERSAL = ["config", ".github", ".gitlab", ".circleci", ".vscode"];
516
+ var CONFIG_LABEL = {
517
+ node: "dependency and build config",
518
+ python: "package and environment config",
519
+ go: "module definition",
520
+ rust: "crate manifest",
521
+ java: "build definition",
522
+ docker: "container config",
523
+ ci: "pipeline definition",
524
+ generic: "project config"
525
+ };
526
+ var LOCKFILE_LABEL = {
527
+ node: "package manager",
528
+ python: "dependency resolver",
529
+ go: "module checksums",
530
+ rust: "dependency resolver",
531
+ java: "Gradle wrapper"
532
+ };
533
+ var FALLBACK_MANIFESTS = [
534
+ "composer.json",
535
+ // PHP
536
+ "Gemfile",
537
+ // Ruby
538
+ "mix.exs",
539
+ // Elixir
540
+ "pubspec.yaml",
541
+ // Dart / Flutter
542
+ "Package.swift",
543
+ // Swift
544
+ "project.clj",
545
+ // Clojure
546
+ "build.sbt",
547
+ // Scala
548
+ "stack.yaml",
549
+ // Haskell
550
+ "deno.json",
551
+ "deno.jsonc",
552
+ // Deno
553
+ "Podfile"
554
+ // CocoaPods (iOS/macOS)
555
+ ];
556
+ var FALLBACK_LOCKFILES = [
557
+ "composer.lock",
558
+ // PHP
559
+ "Gemfile.lock",
560
+ // Ruby
561
+ "mix.lock",
562
+ // Elixir
563
+ "pubspec.lock",
564
+ // Dart / Flutter
565
+ "Package.resolved"
566
+ // Swift
567
+ ];
568
+ var FALLBACK_EXTENSIONS = [".csproj", ".fsproj", ".sln", ".cabal"];
569
+ function exists(p) {
570
+ return fs3.existsSync(p);
571
+ }
572
+ function detectDirs(root, names) {
573
+ return names.filter((n) => exists(path3.join(root, n))).map((n) => `${n}/**`);
574
+ }
575
+ function readGitignore(root) {
576
+ const p = path3.join(root, ".gitignore");
577
+ if (!exists(p)) return [];
578
+ return fs3.readFileSync(p, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("!")).map((l) => l.endsWith("/") ? l + "**" : l);
579
+ }
580
+ function dedup(arr) {
581
+ return arr.filter((v, i, a) => v && a.indexOf(v) === i);
582
+ }
583
+ function detectProjectTypes(root) {
584
+ const seen = /* @__PURE__ */ new Set();
585
+ const types = [];
586
+ for (const sig of PROJECT_SIGNALS) {
587
+ if (seen.has(sig.type)) continue;
588
+ const p = path3.join(root, sig.file || sig.dir);
589
+ if (exists(p)) {
590
+ seen.add(sig.type);
591
+ types.push(sig.type);
592
+ }
593
+ }
594
+ return types.length > 0 ? types : ["generic"];
595
+ }
596
+ function primaryType(projectTypes) {
597
+ return CODE_TYPES.find((t) => projectTypes.includes(t)) ?? "generic";
598
+ }
599
+ function scanFallbackManifests(root) {
600
+ const manifests = FALLBACK_MANIFESTS.filter((f) => exists(path3.join(root, f)));
601
+ const lockfiles = FALLBACK_LOCKFILES.filter((f) => exists(path3.join(root, f)));
602
+ try {
603
+ const entries = fs3.readdirSync(root);
604
+ for (const e of entries) {
605
+ if (FALLBACK_EXTENSIONS.some((ext) => e.endsWith(ext))) manifests.push(e);
606
+ }
607
+ } catch {
608
+ }
609
+ return { manifests, lockfiles };
610
+ }
611
+ function detectSurfaces(root, projectTypes) {
612
+ const s = {};
613
+ for (const [key, names] of Object.entries(SURFACE_MAP)) {
614
+ s[key] = detectDirs(root, names);
615
+ }
616
+ s.secrets = SECRET_GLOBS.filter((g) => {
617
+ const base = g.split("/")[0].replace(/\*.*/, "");
618
+ return base.includes("*") || exists(path3.join(root, base));
619
+ });
620
+ const typeConfigFiles = dedup(
621
+ projectTypes.flatMap((t) => CONFIG_FILES_BY_TYPE[t] || CONFIG_FILES_BY_TYPE.generic).concat(CONFIG_FILES_UNIVERSAL)
622
+ );
623
+ s.config = dedup([
624
+ ...typeConfigFiles.filter((f) => !f.includes("*") && !f.endsWith("/**") && exists(path3.join(root, f))),
625
+ ...typeConfigFiles.filter((f) => f.endsWith("/**") && exists(path3.join(root, f.replace("/**", "")))),
626
+ ...detectDirs(root, CONFIG_DIRS_UNIVERSAL)
627
+ ]);
628
+ s.lockfiles = dedup(
629
+ projectTypes.flatMap((t) => LOCKFILES_BY_TYPE[t] || []).filter((f) => {
630
+ const base = f.replace(/\*.*/, "").split("/")[0];
631
+ return base.includes("*") || exists(path3.join(root, base));
632
+ })
633
+ );
634
+ if (!projectTypes.some((t) => CODE_TYPES.includes(t))) {
635
+ const fb = scanFallbackManifests(root);
636
+ s.config = dedup([...s.config, ...fb.manifests]);
637
+ s.lockfiles = dedup([...s.lockfiles, ...fb.lockfiles]);
638
+ }
639
+ return s;
640
+ }
641
+ function buildSemantics(projectTypes, primary, surfaces) {
642
+ const sem = {};
643
+ sem.structure = "Preserve the existing folder structure. Add new files within established directories. Do not create top-level directories, reorganize, or rename existing folders.";
644
+ if (surfaces.source.length > 0) {
645
+ sem.source = `Source code lives in ${surfaces.source.join(", ")}. Make feature changes and bug fixes here only.`;
646
+ }
647
+ sem.secrets = `Never propose changes to credential or key files (${SECRET_GLOBS.slice(0, 4).join(", ")} \u2026). These are never task targets regardless of the instruction.`;
648
+ if (surfaces.generated.length > 0) {
649
+ sem.generated = `${surfaces.generated.join(", ")} contain generated output. Modify the source files that produce them; never write to generated directories directly.`;
650
+ }
651
+ if (surfaces.config.length > 0) {
652
+ const codeTypes = projectTypes.filter((t) => CODE_TYPES.includes(t));
653
+ const label = codeTypes.length === 1 ? CONFIG_LABEL[codeTypes[0]] : "project configuration";
654
+ const listed = surfaces.config.slice(0, 5).join(", ");
655
+ const trailer = surfaces.config.length > 5 ? " and related files" : "";
656
+ sem.config = `Treat ${listed}${trailer} as ${label} files. Do not modify them unless the task explicitly requires a configuration or dependency change.`;
657
+ }
658
+ if (surfaces.tests.length > 0) {
659
+ sem.tests = `Test files in ${surfaces.tests.join(", ")} validate behavior. Update them only when the behavior they cover changes.`;
660
+ }
661
+ if (surfaces.lockfiles?.length > 0) {
662
+ const label = LOCKFILE_LABEL[primary] || "tooling";
663
+ const listed = surfaces.lockfiles.slice(0, 3).join(", ");
664
+ sem.lockfiles = `${listed} are generated by the ${label}. Never edit them directly.`;
665
+ }
666
+ if (primary === "generic") {
667
+ const foundManifests = surfaces.config.filter((f) => !f.endsWith("/**"));
668
+ if (foundManifests.length > 0) {
669
+ sem.unknown = `This project uses an unrecognized toolchain. Treat ${foundManifests.slice(0, 3).join(", ")} as dependency/manifest files. Do not modify them unless the task explicitly requires a dependency change.`;
670
+ } else {
671
+ sem.unknown = "This project uses an unrecognized toolchain. Do not assume standard source layouts, dependency files, or build conventions apply. Confirm any structural assumption before acting.";
672
+ }
673
+ }
674
+ if (projectTypes.includes("docker")) {
675
+ sem.docker = "Dockerfile and docker-compose.yml define the container environment. Treat them as infrastructure config \u2014 only modify when the task explicitly involves container or environment changes.";
676
+ }
677
+ if (projectTypes.includes("ci")) {
678
+ sem.ci = "CI config files (.github/**, .gitlab-ci.yml, etc.) define the build and deployment pipeline. Do not modify them unless the task explicitly involves CI/CD changes.";
679
+ }
680
+ return sem;
681
+ }
682
+ function buildEnforcement(surfaces, gitignorePatterns) {
683
+ const allow = dedup([...surfaces.source, ...surfaces.docs, ...surfaces.tests]);
684
+ const approval = [...surfaces.config];
685
+ const deny = dedup([
686
+ ...surfaces.secrets,
687
+ ...surfaces.generated,
688
+ ...surfaces.lockfiles || [],
689
+ ...ALWAYS_DENY,
690
+ ...gitignorePatterns.filter((p) => p.endsWith("/**")).slice(0, 8)
691
+ ]);
692
+ return {
693
+ allow: allow.length > 0 ? allow : ["src/**"],
694
+ approval: approval.length > 0 ? approval : [],
695
+ deny
696
+ };
697
+ }
698
+ function scanWorkspace(rootDir) {
699
+ const root = path3.resolve(rootDir || process.cwd());
700
+ const projectTypes = detectProjectTypes(root);
701
+ const primary = primaryType(projectTypes);
702
+ const surfaces = detectSurfaces(root, projectTypes);
703
+ const gitignore = readGitignore(root);
704
+ const semantics = buildSemantics(projectTypes, primary, surfaces);
705
+ const enforcement = buildEnforcement(surfaces, gitignore);
706
+ return { projectTypes, primaryType: primary, surfaces, semantics, enforcement };
707
+ }
708
+ function formatSummary(projectTypes, semantics, enforcement) {
709
+ const lines = [];
710
+ const label = Array.isArray(projectTypes) ? projectTypes.join(" + ") : projectTypes;
711
+ lines.push(`Detected: ${label}`);
712
+ lines.push("");
713
+ lines.push("Agent semantics:");
714
+ for (const [, v] of Object.entries(semantics)) {
715
+ lines.push(` - ${v}`);
716
+ }
717
+ lines.push("");
718
+ lines.push("Enforcement:");
719
+ if (enforcement.allow.length) lines.push(` allow: ${enforcement.allow.join(", ")}`);
720
+ if (enforcement.approval.length) lines.push(` approval: ${enforcement.approval.join(", ")}`);
721
+ if (enforcement.deny.length) lines.push(` deny: ${enforcement.deny.slice(0, 6).join(", ")}${enforcement.deny.length > 6 ? " \u2026" : ""}`);
722
+ return lines.join("\n");
723
+ }
724
+
725
+ // src/cli/commands/init.js
726
+ function ask(question) {
727
+ if (!process.stdin.isTTY) return Promise.resolve("y");
728
+ return new Promise((resolve) => {
729
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
730
+ rl.question(question, (ans) => {
731
+ rl.close();
732
+ resolve(ans.trim().toLowerCase());
733
+ });
734
+ });
735
+ }
736
+ function applyStrict(enforcement) {
737
+ return {
738
+ ...enforcement,
739
+ deny: [.../* @__PURE__ */ new Set([...enforcement.deny, ...enforcement.approval, "*.json", "config/**"])],
740
+ approval: []
741
+ };
742
+ }
743
+ function applyRelaxed(enforcement) {
744
+ return { ...enforcement, approval: [] };
745
+ }
746
+ function setupCrypto(cwd) {
747
+ const nowIso = (/* @__PURE__ */ new Date()).toISOString();
748
+ const expiresAt = new Date(Date.now() + 180 * 24 * 60 * 60 * 1e3).toISOString();
749
+ const defaultKeyId = "agent:gpt-v1-2026Q1";
750
+ const signerKeyId = "policy-signer-v1-2026Q1";
751
+ const lbeDir = path4.join(cwd, ".lbe");
752
+ for (const d of ["config", "keys", "data"]) {
753
+ fs4.mkdirSync(path4.join(lbeDir, d), { recursive: true });
754
+ }
755
+ const dataFiles = {
756
+ ".lbe/data/nonce.db.json": JSON.stringify({ entries: [] }, null, 2),
757
+ ".lbe/data/rate-limit.db.json": JSON.stringify({ entries: [] }, null, 2),
758
+ ".lbe/data/policy.state.json": JSON.stringify({ schemaVersion: "1", lastAccepted: null, updatedAt: null }, null, 2),
759
+ ".lbe/data/audit.log.jsonl": ""
760
+ };
761
+ for (const [rel, content] of Object.entries(dataFiles)) {
762
+ const p = path4.join(cwd, rel);
763
+ if (!fs4.existsSync(p)) fs4.writeFileSync(p, content);
764
+ }
765
+ const keyDir = path4.join(lbeDir, "keys");
766
+ const pubPath = path4.join(keyDir, "public.key");
767
+ const secPath = path4.join(keyDir, "secret.key");
768
+ let publicKeyB64, secretKeyB64;
769
+ if (fs4.existsSync(pubPath) && fs4.existsSync(secPath)) {
770
+ publicKeyB64 = fs4.readFileSync(pubPath, "utf8").trim();
771
+ secretKeyB64 = fs4.readFileSync(secPath, "utf8").trim();
772
+ } else {
773
+ const kp = generateKeyPair();
774
+ publicKeyB64 = kp.publicKey;
775
+ secretKeyB64 = kp.secretKey;
776
+ fs4.writeFileSync(pubPath, publicKeyB64);
777
+ fs4.writeFileSync(secPath, secretKeyB64, { mode: 384 });
778
+ }
779
+ const keysPath = path4.join(lbeDir, "config/keys.json");
780
+ const keysStore = fs4.existsSync(keysPath) ? JSON.parse(fs4.readFileSync(keysPath, "utf8")) : { schemaVersion: "1", defaultKeyId, trustedKeys: {} };
781
+ for (const keyId of [defaultKeyId, signerKeyId]) {
782
+ if (!keysStore.trustedKeys[keyId]) {
783
+ keysStore.trustedKeys[keyId] = {
784
+ publicKey: publicKeyB64,
785
+ notBefore: nowIso,
786
+ expiresAt,
787
+ validFrom: nowIso,
788
+ validUntil: expiresAt,
789
+ deprecated: false
790
+ };
791
+ }
792
+ }
793
+ keysStore.defaultKeyId = defaultKeyId;
794
+ fs4.writeFileSync(keysPath, JSON.stringify(keysStore, null, 2));
795
+ const policyPath = path4.join(lbeDir, "config/policy.default.json");
796
+ let policyObj;
797
+ if (fs4.existsSync(policyPath)) {
798
+ policyObj = JSON.parse(fs4.readFileSync(policyPath, "utf8"));
799
+ } else {
800
+ policyObj = {
801
+ default: "DENY",
802
+ version: "1.0.0",
803
+ createdAt: nowIso,
804
+ security: {
805
+ maxClockSkewSec: 600,
806
+ maxPolicyCreatedAtSkewSec: 31536e3,
807
+ defaultRateLimit: { windowSec: 60, maxRequests: 30 }
808
+ },
809
+ requesters: {
810
+ "agent:gpt": {
811
+ allowAdapters: ["noop", "shell"],
812
+ allowCommands: ["RUN_SHELL"],
813
+ rateLimit: { windowSec: 60, maxRequests: 30 },
814
+ filesystem: { roots: [cwd], denyPatterns: ["**/.git/**", "**/secrets/**", "**/*.key"] },
815
+ exec: { allowCmds: ["ls", "node", "python", "echo"], denyCmds: ["rm", "chmod", "chown", "curl", "wget", "su", "sudo"] }
816
+ }
817
+ }
818
+ };
819
+ fs4.writeFileSync(policyPath, JSON.stringify(policyObj, null, 2));
820
+ }
821
+ const sigResult = createPolicySignatureEnvelope({ policyObj, secretKeyB64, keyId: signerKeyId });
822
+ if (sigResult.ok) {
823
+ fs4.writeFileSync(path4.join(lbeDir, "config/policy.sig.json"), JSON.stringify(sigResult.envelope, null, 2));
824
+ }
825
+ return { defaultKeyId, secretKeyB64, publicKeyB64 };
826
+ }
827
+ function agentContractContent() {
828
+ return `# LBE Governance Contract
829
+
830
+ This project has LetterBlack LBE (Local-first execution Governance) active.
831
+
832
+ ## Your role as an agent
833
+
834
+ Every file write, delete, and shell command must go through the LBE executor.
835
+ The executor validates each action against the project policy and audits it.
836
+
837
+ ## How to perform actions
838
+
839
+ \`\`\`js
840
+ import { createLocalExecutor } from '@letterblack/lbe-exec';
841
+ const lbe = createLocalExecutor({ rootDir: process.cwd() });
842
+
843
+ await lbe.writeFile('output/report.md', content);
844
+ await lbe.readFile('src/config.json');
845
+ await lbe.patchFile('src/index.js', patch);
846
+ await lbe.deleteFile('tmp/scratch.txt');
847
+ await lbe.runShell('node', ['scripts/build.js']);
848
+ \`\`\`
849
+
850
+ ## What you must never do
851
+
852
+ - Call fs.* or child_process.* directly
853
+ - Modify \`.lbe/policy.json\` directly \u2014 propose a rule via \`lbe.policy.proposeRule()\`
854
+ - Attempt to bypass or disable the executor
855
+
856
+ ## Proposing a policy rule
857
+
858
+ \`\`\`js
859
+ const proposal = lbe.policy.proposeRule({
860
+ effect: 'deny', type: 'path', pattern: 'secrets/**',
861
+ from: 'agent: these files should never be modified'
862
+ });
863
+ // Return proposal to the user \u2014 never call lbe.policy.addRule() yourself.
864
+ \`\`\`
865
+
866
+ ## Result shape
867
+
868
+ \`{ ok: boolean, decision: 'allow' | 'deny' | 'observe', executed: boolean }\`
869
+
870
+ ## Files
871
+
872
+ - Policy: \`.lbe/policy.json\`
873
+ - Audit: \`.lbe/audit.jsonl\`
874
+ - Status: \`npx lbe-exec status\`
875
+ `;
876
+ }
877
+ function writeAgentContract(cwd) {
878
+ const lbeDir = path4.join(cwd, ".lbe");
879
+ fs4.mkdirSync(lbeDir, { recursive: true });
880
+ fs4.writeFileSync(path4.join(lbeDir, "AGENT_CONTRACT.md"), agentContractContent());
881
+ }
882
+ function migrateLegacyRootFiles(cwd) {
883
+ const lbeDir = path4.join(cwd, ".lbe");
884
+ fs4.mkdirSync(lbeDir, { recursive: true });
885
+ const migrations = [
886
+ ["lbe.policy.json", ".lbe/policy.json"],
887
+ ["lbe.workspace.json", ".lbe/workspace.json"]
888
+ ];
889
+ const removed = [];
890
+ for (const [src, dest] of migrations) {
891
+ const srcPath = path4.join(cwd, src);
892
+ const destPath = path4.join(cwd, dest);
893
+ if (fs4.existsSync(srcPath) && !fs4.existsSync(destPath)) {
894
+ fs4.renameSync(srcPath, destPath);
895
+ removed.push(src + " \u2192 " + dest);
896
+ } else if (fs4.existsSync(srcPath)) {
897
+ fs4.unlinkSync(srcPath);
898
+ removed.push(src + " (removed \u2014 .lbe/ version exists)");
899
+ }
900
+ }
901
+ const toDelete = ["CLAUDE.md", path4.join(".github", "copilot-instructions.md")];
902
+ for (const rel of toDelete) {
903
+ const p = path4.join(cwd, rel);
904
+ if (fs4.existsSync(p)) {
905
+ const content = fs4.readFileSync(p, "utf8");
906
+ if (content.includes("lbe-governance") || content.includes("LetterBlack LBE")) {
907
+ fs4.unlinkSync(p);
908
+ removed.push(rel + " (removed \u2014 LBE-generated file)");
909
+ }
910
+ }
911
+ }
912
+ return removed;
913
+ }
914
+ async function initCommand(opts = {}) {
915
+ const cwd = process.cwd();
916
+ const yes = opts.yes || opts.y || !process.stdin.isTTY;
917
+ const lbeDir = path4.join(cwd, ".lbe");
918
+ fs4.mkdirSync(lbeDir, { recursive: true });
919
+ const outPath = path4.join(lbeDir, "workspace.json");
920
+ console.log("\nScanning workspace...\n");
921
+ const { projectTypes, primaryType: primaryType2, semantics, enforcement } = scanWorkspace(cwd);
922
+ console.log(formatSummary(projectTypes, semantics, enforcement));
923
+ console.log("");
924
+ let finalEnforcement = enforcement;
925
+ if (!yes) {
926
+ const answer = await ask("Accept? [Y = accept / s = strict / r = relaxed / n = cancel] ");
927
+ if (answer === "n") {
928
+ console.log("Cancelled.");
929
+ return { success: false };
930
+ }
931
+ if (answer === "s") finalEnforcement = applyStrict(enforcement);
932
+ if (answer === "r") finalEnforcement = applyRelaxed(enforcement);
933
+ }
934
+ const contract = {
935
+ lbe: true,
936
+ version: "0.4.0",
937
+ state: "local",
938
+ projectTypes,
939
+ primaryType: primaryType2,
940
+ semantics,
941
+ enforcement: finalEnforcement
942
+ };
943
+ fs4.writeFileSync(outPath, JSON.stringify(contract, null, 2));
944
+ console.log("\u2713 Wrote .lbe/workspace.json");
945
+ setupCrypto(cwd);
946
+ const localPolicyPath = path4.join(lbeDir, "policy.json");
947
+ if (!fs4.existsSync(localPolicyPath)) {
948
+ fs4.writeFileSync(localPolicyPath, JSON.stringify({ version: 1, mode: "observe", workspace: cwd, rules: [] }, null, 2) + "\n");
949
+ }
950
+ const localAuditPath = path4.join(lbeDir, "audit.jsonl");
951
+ if (!fs4.existsSync(localAuditPath)) fs4.writeFileSync(localAuditPath, "");
952
+ console.log("\u2713 Keys and policy ready (.lbe/)");
953
+ writeAgentContract(cwd);
954
+ console.log("\u2713 Agent contract written \u2192 .lbe/AGENT_CONTRACT.md");
955
+ const migrated = migrateLegacyRootFiles(cwd);
956
+ if (migrated.length) {
957
+ console.log("\n\u2713 Migrated legacy files:");
958
+ for (const m of migrated) console.log(" " + m);
959
+ }
960
+ console.log("\nDone. All LBE state is in .lbe/");
961
+ console.log("Run npx lbe-exec status to verify.\n");
962
+ return { success: true, contract };
963
+ }
964
+
965
+ // src/cli/commands/verify.js
966
+ import fs9 from "fs";
967
+ import path10 from "path";
968
+
969
+ // src/core/policyVersionGuard.js
970
+ import fs6 from "fs";
971
+ import path6 from "path";
972
+
973
+ // src/core/atomicWrite.js
974
+ import fs5 from "fs";
975
+ import path5 from "path";
976
+ import crypto from "crypto";
977
+ var DEFAULT_LOCK_OPTS = {
978
+ timeoutMs: 5e3,
979
+ // total wait before giving up
980
+ pollMs: 15,
981
+ // base poll interval (jittered)
982
+ staleMs: 3e4
983
+ // lock files older than this are presumed orphaned
984
+ };
985
+ function _lockPathFor(targetPath) {
986
+ return targetPath + ".lock";
987
+ }
988
+ function _tryAcquire(lockPath) {
989
+ try {
990
+ const fd = fs5.openSync(lockPath, "wx");
991
+ fs5.writeSync(fd, `pid:${process.pid}:${Date.now()}`);
992
+ fs5.closeSync(fd);
993
+ return true;
994
+ } catch (err) {
995
+ if (err.code === "EEXIST" || err.code === "EPERM" || err.code === "EBUSY" || err.code === "EACCES") {
996
+ return false;
997
+ }
998
+ throw err;
999
+ }
1000
+ }
1001
+ function _removeIfStale(lockPath, staleMs) {
1002
+ try {
1003
+ const stat = fs5.statSync(lockPath);
1004
+ const ageMs = Date.now() - stat.mtimeMs;
1005
+ if (ageMs > staleMs) {
1006
+ try {
1007
+ fs5.unlinkSync(lockPath);
1008
+ } catch {
1009
+ }
1010
+ }
1011
+ } catch {
1012
+ }
1013
+ }
1014
+ function _sleepSync(ms) {
1015
+ const end = Date.now() + ms;
1016
+ while (Date.now() < end) {
1017
+ try {
1018
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(1, end - Date.now()));
1019
+ } catch {
1020
+ }
1021
+ }
1022
+ }
1023
+ function withFileLock(targetPath, optsOrFn, maybeFn) {
1024
+ const fn = typeof optsOrFn === "function" ? optsOrFn : maybeFn;
1025
+ const opts = typeof optsOrFn === "function" ? {} : optsOrFn || {};
1026
+ const { timeoutMs, pollMs, staleMs } = { ...DEFAULT_LOCK_OPTS, ...opts };
1027
+ const dir = path5.dirname(targetPath);
1028
+ if (!fs5.existsSync(dir)) {
1029
+ fs5.mkdirSync(dir, { recursive: true });
1030
+ }
1031
+ const lockPath = _lockPathFor(targetPath);
1032
+ const deadline = Date.now() + timeoutMs;
1033
+ let acquired = false;
1034
+ while (!acquired) {
1035
+ acquired = _tryAcquire(lockPath);
1036
+ if (acquired) break;
1037
+ if (Date.now() >= deadline) {
1038
+ _removeIfStale(lockPath, staleMs);
1039
+ acquired = _tryAcquire(lockPath);
1040
+ if (acquired) break;
1041
+ const err = new Error(`withFileLock: timeout acquiring ${lockPath} after ${timeoutMs}ms`);
1042
+ err.code = "ELOCKTIMEOUT";
1043
+ throw err;
1044
+ }
1045
+ _removeIfStale(lockPath, staleMs);
1046
+ const jitter = Math.floor(Math.random() * pollMs);
1047
+ _sleepSync(pollMs + jitter);
1048
+ }
1049
+ try {
1050
+ return fn();
1051
+ } finally {
1052
+ try {
1053
+ fs5.unlinkSync(lockPath);
1054
+ } catch {
1055
+ }
1056
+ }
1057
+ }
1058
+ function atomicWriteFileSync(filePath, data, options = {}) {
1059
+ const dir = path5.dirname(filePath);
1060
+ if (!fs5.existsSync(dir)) {
1061
+ fs5.mkdirSync(dir, { recursive: true });
1062
+ }
1063
+ const tempFile = path5.join(dir, `.tmp-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`);
1064
+ try {
1065
+ fs5.writeFileSync(tempFile, data, options);
1066
+ fs5.renameSync(tempFile, filePath);
1067
+ } catch (error) {
1068
+ try {
1069
+ if (fs5.existsSync(tempFile)) {
1070
+ fs5.unlinkSync(tempFile);
1071
+ }
1072
+ } catch (cleanupError) {
1073
+ }
1074
+ throw error;
1075
+ }
1076
+ }
1077
+ function atomicAppendFileSync(filePath, data, options = {}) {
1078
+ const dir = path5.dirname(filePath);
1079
+ if (!fs5.existsSync(dir)) {
1080
+ fs5.mkdirSync(dir, { recursive: true });
1081
+ }
1082
+ withFileLock(filePath, () => {
1083
+ let existingContent = "";
1084
+ if (fs5.existsSync(filePath)) {
1085
+ existingContent = fs5.readFileSync(filePath, options.encoding || "utf8");
1086
+ }
1087
+ const combinedData = existingContent + data;
1088
+ atomicWriteFileSync(filePath, combinedData, options);
1089
+ });
1090
+ }
1091
+ function readJSONSafe(filePath) {
1092
+ try {
1093
+ if (!fs5.existsSync(filePath)) {
1094
+ return null;
1095
+ }
1096
+ const content = fs5.readFileSync(filePath, "utf8");
1097
+ return JSON.parse(content);
1098
+ } catch (e) {
1099
+ console.error(`[atomicWrite] Failed to read JSON from ${filePath}:`, e.message);
1100
+ return null;
1101
+ }
1102
+ }
1103
+
1104
+ // src/core/policyVersionGuard.js
1105
+ function parsePolicyVersion(version) {
1106
+ if (typeof version === "number" && Number.isFinite(version)) {
1107
+ return { ok: true, kind: "int", parts: [Math.floor(version)], raw: String(version) };
1108
+ }
1109
+ if (typeof version !== "string" || !version.trim()) {
1110
+ return { ok: false, reason: "POLICY_VERSION_INVALID", message: "Policy version is required" };
1111
+ }
1112
+ const trimmed = version.trim();
1113
+ if (/^\d+$/.test(trimmed)) {
1114
+ return { ok: true, kind: "int", parts: [Number(trimmed)], raw: trimmed };
1115
+ }
1116
+ const semver = trimmed.replace(/^v/i, "");
1117
+ if (/^\d+(\.\d+){0,2}$/.test(semver)) {
1118
+ const parsed = semver.split(".").map((n) => Number(n));
1119
+ while (parsed.length < 3) {
1120
+ parsed.push(0);
1121
+ }
1122
+ return { ok: true, kind: "semver", parts: parsed, raw: trimmed };
1123
+ }
1124
+ return {
1125
+ ok: false,
1126
+ reason: "POLICY_VERSION_INVALID",
1127
+ message: `Unsupported policy version format '${version}' (use integer or semver)`
1128
+ };
1129
+ }
1130
+ function compareVersions(a, b) {
1131
+ const len = Math.max(a.parts.length, b.parts.length);
1132
+ for (let i = 0; i < len; i++) {
1133
+ const av = a.parts[i] ?? 0;
1134
+ const bv = b.parts[i] ?? 0;
1135
+ if (av > bv) return 1;
1136
+ if (av < bv) return -1;
1137
+ }
1138
+ return 0;
1139
+ }
1140
+ function parseCreatedAt(createdAt) {
1141
+ if (typeof createdAt === "number" && Number.isFinite(createdAt)) {
1142
+ const sec = createdAt > 1e12 ? Math.floor(createdAt / 1e3) : Math.floor(createdAt);
1143
+ return { ok: true, epochSec: sec };
1144
+ }
1145
+ if (typeof createdAt !== "string" || !createdAt.trim()) {
1146
+ return {
1147
+ ok: false,
1148
+ reason: "POLICY_CREATED_AT_INVALID",
1149
+ message: "Policy createdAt is required"
1150
+ };
1151
+ }
1152
+ const ts = Date.parse(createdAt);
1153
+ if (Number.isNaN(ts)) {
1154
+ return {
1155
+ ok: false,
1156
+ reason: "POLICY_CREATED_AT_INVALID",
1157
+ message: `Invalid policy createdAt '${createdAt}'`
1158
+ };
1159
+ }
1160
+ return { ok: true, epochSec: Math.floor(ts / 1e3) };
1161
+ }
1162
+ function loadPolicyState(statePath) {
1163
+ if (!fs6.existsSync(statePath)) {
1164
+ return {
1165
+ schemaVersion: "1",
1166
+ lastAccepted: null,
1167
+ updatedAt: null
1168
+ };
1169
+ }
1170
+ try {
1171
+ const parsed = JSON.parse(fs6.readFileSync(statePath, "utf8"));
1172
+ if (!parsed || typeof parsed !== "object") {
1173
+ throw new Error("Policy state file has invalid structure");
1174
+ }
1175
+ return {
1176
+ schemaVersion: String(parsed.schemaVersion || "1"),
1177
+ lastAccepted: parsed.lastAccepted && typeof parsed.lastAccepted === "object" ? parsed.lastAccepted : null,
1178
+ updatedAt: parsed.updatedAt || null
1179
+ };
1180
+ } catch (err) {
1181
+ throw new Error(`Policy state at ${statePath} is corrupt or unreadable: ${err.message}`);
1182
+ }
1183
+ }
1184
+ function savePolicyState(statePath, stateObj) {
1185
+ const payload = JSON.stringify(stateObj, null, 2);
1186
+ atomicWriteFileSync(statePath, payload, { encoding: "utf8" });
1187
+ }
1188
+ function validateAndUpdatePolicyVersionState({
1189
+ policyObj,
1190
+ statePath = path6.resolve(".lbe/data/policy.state.json"),
1191
+ maxCreatedAtSkewSec = 31536e3,
1192
+ nowSec = Math.floor(Date.now() / 1e3),
1193
+ persist = true
1194
+ }) {
1195
+ const version = parsePolicyVersion(policyObj?.version);
1196
+ if (!version.ok) {
1197
+ return {
1198
+ ok: false,
1199
+ reason: version.reason,
1200
+ message: version.message,
1201
+ updated: false
1202
+ };
1203
+ }
1204
+ const createdAt = parseCreatedAt(policyObj?.createdAt);
1205
+ if (!createdAt.ok) {
1206
+ return {
1207
+ ok: false,
1208
+ reason: createdAt.reason,
1209
+ message: createdAt.message,
1210
+ updated: false
1211
+ };
1212
+ }
1213
+ const skew = Math.abs(nowSec - createdAt.epochSec);
1214
+ const allowedSkew = Number.isFinite(maxCreatedAtSkewSec) && maxCreatedAtSkewSec > 0 ? Math.floor(maxCreatedAtSkewSec) : 31536e3;
1215
+ if (skew > allowedSkew) {
1216
+ return {
1217
+ ok: false,
1218
+ reason: "POLICY_CREATED_AT_SKEW_EXCEEDED",
1219
+ message: `Policy createdAt skew ${skew}s exceeds allowed ${allowedSkew}s`,
1220
+ updated: false
1221
+ };
1222
+ }
1223
+ let state;
1224
+ try {
1225
+ state = loadPolicyState(statePath);
1226
+ } catch (err) {
1227
+ return {
1228
+ ok: false,
1229
+ reason: "POLICY_STATE_CORRUPT",
1230
+ message: err.message,
1231
+ updated: false
1232
+ };
1233
+ }
1234
+ const previous = state.lastAccepted;
1235
+ let previousVersion = null;
1236
+ let previousCreatedAt = null;
1237
+ let versionCompare = 0;
1238
+ if (previous) {
1239
+ previousVersion = parsePolicyVersion(previous.version);
1240
+ previousCreatedAt = parseCreatedAt(previous.createdAt);
1241
+ if (previousVersion.ok && previousCreatedAt.ok) {
1242
+ versionCompare = compareVersions(version, previousVersion);
1243
+ if (versionCompare < 0) {
1244
+ return {
1245
+ ok: false,
1246
+ reason: "POLICY_VERSION_REGRESSION",
1247
+ message: `Policy version regression: current '${version.raw}' < last '${previousVersion.raw}'`,
1248
+ updated: false
1249
+ };
1250
+ }
1251
+ if (versionCompare === 0 && createdAt.epochSec < previousCreatedAt.epochSec) {
1252
+ return {
1253
+ ok: false,
1254
+ reason: "POLICY_CREATED_AT_REGRESSION",
1255
+ message: `Policy createdAt regression: current '${policyObj.createdAt}' < last '${previous.createdAt}'`,
1256
+ updated: false
1257
+ };
1258
+ }
1259
+ if (versionCompare > 0 && createdAt.epochSec < previousCreatedAt.epochSec) {
1260
+ return {
1261
+ ok: false,
1262
+ reason: "POLICY_CREATED_AT_REGRESSION",
1263
+ message: `Policy createdAt must be monotonic when version increases`,
1264
+ updated: false
1265
+ };
1266
+ }
1267
+ }
1268
+ }
1269
+ const shouldUpdate = !previous || !previousVersion?.ok || !previousCreatedAt?.ok || versionCompare > 0 || versionCompare === 0 && createdAt.epochSec > previousCreatedAt.epochSec;
1270
+ if (persist && shouldUpdate) {
1271
+ const nextState = {
1272
+ schemaVersion: "1",
1273
+ lastAccepted: {
1274
+ version: policyObj.version,
1275
+ createdAt: policyObj.createdAt,
1276
+ environment: policyObj.environment || null
1277
+ },
1278
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1279
+ };
1280
+ savePolicyState(statePath, nextState);
1281
+ }
1282
+ return {
1283
+ ok: true,
1284
+ reason: null,
1285
+ message: "Policy version guard passed",
1286
+ updated: shouldUpdate
1287
+ };
1288
+ }
1289
+
1290
+ // runtime/engine.js
1291
+ import fs7 from "fs";
1292
+ import path7 from "path";
1293
+ import { fileURLToPath } from "url";
1294
+ var runtimeDir = path7.dirname(fileURLToPath(import.meta.url));
1295
+ var wasmPath = path7.join(runtimeDir, "lbe_engine.wasm");
1296
+ var POLICY_MESSAGES = {
1297
+ 0: { allowed: true, reason: null, message: "Policy check passed" },
1298
+ 1: { allowed: false, reason: "POLICY_NOT_CONFIGURED", message: "No policy configured" },
1299
+ 2: { allowed: false, reason: "REQUESTER_NOT_ALLOWED", message: "Requester not in policy" },
1300
+ 3: { allowed: false, reason: "COMMAND_NOT_ALLOWED", message: "Command not allowed for requester" },
1301
+ 4: { allowed: false, reason: "ADAPTER_NOT_ALLOWED", message: "Adapter not allowed" },
1302
+ 5: { allowed: false, reason: "NO_FILESYSTEM_ROOTS_DEFINED", message: "No filesystem roots defined for requester" },
1303
+ 6: { allowed: false, reason: "CWD_OUTSIDE_ALLOWED_ROOT", message: "Path not under allowed roots" },
1304
+ 7: { allowed: false, reason: "PATH_DENIED_BY_PATTERN", message: "Path matches deny pattern" },
1305
+ 8: { allowed: false, reason: "SHELL_CMD_DENIED", message: "Shell command not allowed" }
1306
+ };
1307
+ var SCHEMA_MESSAGES = {
1308
+ 0: { valid: true, error: null },
1309
+ 1: { valid: false, error: "Missing required field: id" },
1310
+ 2: { valid: false, error: "Missing required field: commandId" },
1311
+ 3: { valid: false, error: "Missing required field: requesterId" },
1312
+ 4: { valid: false, error: "Missing required field: sessionId" },
1313
+ 5: { valid: false, error: "Missing required field: timestamp" },
1314
+ 6: { valid: false, error: "Missing required field: nonce" },
1315
+ 7: { valid: false, error: "Missing required field: requires" },
1316
+ 8: { valid: false, error: "Missing required field: payload" },
1317
+ 9: { valid: false, error: "Missing required field: signature" },
1318
+ 10: { valid: false, error: "Field 'id' is invalid" },
1319
+ 11: { valid: false, error: "Field 'commandId' is invalid" },
1320
+ 12: { valid: false, error: "Field 'requesterId' is invalid" },
1321
+ 13: { valid: false, error: "Field 'sessionId' is invalid" },
1322
+ 14: { valid: false, error: "Field 'timestamp' is invalid" },
1323
+ 15: { valid: false, error: "Field 'nonce' is invalid" },
1324
+ 16: { valid: false, error: "Field 'requires' is invalid" },
1325
+ 17: { valid: false, error: "payload: missing required field: adapter" },
1326
+ 18: { valid: false, error: "payload: field 'adapter' is invalid" },
1327
+ 19: { valid: false, error: "signature: missing required field: alg" },
1328
+ 20: { valid: false, error: "signature: missing required field: keyId" },
1329
+ 21: { valid: false, error: "signature: missing required field: sig" },
1330
+ 22: { valid: false, error: "signature: field 'alg' must be ed25519" },
1331
+ 23: { valid: false, error: "signature: field 'sig' is invalid" },
1332
+ 24: { valid: false, error: "Field 'risk' is invalid" }
1333
+ };
1334
+ var KEY_REASONS = {
1335
+ 1: "KEY_ID_INVALID",
1336
+ 2: "KEY_NOT_TRUSTED",
1337
+ 3: "KEY_DEPRECATED",
1338
+ 4: "KEY_REQUESTER_MISMATCH",
1339
+ 5: "KEY_LIFECYCLE_INVALID",
1340
+ 6: "KEY_NOT_YET_VALID",
1341
+ 7: "KEY_EXPIRED"
1342
+ };
1343
+ var PIPELINE_STAGES = {
1344
+ 0: "schema",
1345
+ 1: "timestamp",
1346
+ 2: "key",
1347
+ 3: "signature",
1348
+ 4: "rate_limit",
1349
+ 5: "nonce",
1350
+ 6: "policy",
1351
+ 255: "ok"
1352
+ };
1353
+ var RISK_LABELS = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
1354
+ var COMMAND_TYPE = { ECHO: 0, READ_FILE: 1, WRITE_FILE: 2, PATCH_FILE: 3, DELETE_FILE: 4, RUN_SHELL: 5 };
1355
+ var _instance = null;
1356
+ function wasm() {
1357
+ if (_instance) return _instance;
1358
+ if (!fs7.existsSync(wasmPath)) throw new Error(`LBE engine missing: ${wasmPath}`);
1359
+ const bytes = fs7.readFileSync(wasmPath);
1360
+ _instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), {});
1361
+ return _instance;
1362
+ }
1363
+ function memory() {
1364
+ return new Uint8Array(wasm().exports.memory.buffer);
1365
+ }
1366
+ function inPtr() {
1367
+ return wasm().exports.lbe_in_ptr();
1368
+ }
1369
+ function outPtr() {
1370
+ return wasm().exports.lbe_out_ptr();
1371
+ }
1372
+ function bufSize() {
1373
+ return wasm().exports.lbe_buf_size();
1374
+ }
1375
+ function writeIn(str) {
1376
+ const enc = new TextEncoder().encode(str);
1377
+ const mem = memory();
1378
+ const ptr = inPtr();
1379
+ mem.set(enc, ptr);
1380
+ mem[ptr + enc.length] = 0;
1381
+ }
1382
+ function readOut() {
1383
+ const mem = memory();
1384
+ const ptr = outPtr();
1385
+ let end = ptr;
1386
+ while (mem[end] !== 0 && end - ptr < bufSize()) end++;
1387
+ return new TextDecoder().decode(mem.slice(ptr, end));
1388
+ }
1389
+ function writePipelineInput(fields) {
1390
+ const mem = memory();
1391
+ const ptr = inPtr();
1392
+ const view = new DataView(mem.buffer, ptr);
1393
+ fields.forEach((v, i) => view.setUint32(i * 4, v >>> 0, true));
1394
+ }
1395
+ function readPipelineOutput() {
1396
+ const mem = memory();
1397
+ const ptr = outPtr();
1398
+ const view = new DataView(mem.buffer, ptr);
1399
+ return { stage: view.getUint32(0, true), code: view.getUint32(4, true) };
1400
+ }
1401
+ function runValidationPipeline(flags) {
1402
+ writePipelineInput([
1403
+ // Schema flags [0..24]
1404
+ flags.hasId ? 1 : 0,
1405
+ flags.idValid ? 1 : 0,
1406
+ flags.hasCommandId ? 1 : 0,
1407
+ flags.commandIdValid ? 1 : 0,
1408
+ flags.hasRequesterId ? 1 : 0,
1409
+ flags.requesterIdValid ? 1 : 0,
1410
+ flags.hasSessionId ? 1 : 0,
1411
+ flags.sessionIdValid ? 1 : 0,
1412
+ flags.hasTimestamp ? 1 : 0,
1413
+ flags.timestampValid ? 1 : 0,
1414
+ flags.hasNonce ? 1 : 0,
1415
+ flags.nonceValid ? 1 : 0,
1416
+ flags.hasRequires ? 1 : 0,
1417
+ flags.requiresValid ? 1 : 0,
1418
+ flags.hasPayload ? 1 : 0,
1419
+ flags.hasPayloadAdapter ? 1 : 0,
1420
+ flags.payloadAdapterValid ? 1 : 0,
1421
+ flags.hasSignature ? 1 : 0,
1422
+ flags.hasSignatureAlg ? 1 : 0,
1423
+ flags.signatureAlgValid ? 1 : 0,
1424
+ flags.hasSignatureKeyId ? 1 : 0,
1425
+ flags.hasSignatureSig ? 1 : 0,
1426
+ flags.signatureSigValid ? 1 : 0,
1427
+ flags.hasRisk ? 1 : 0,
1428
+ flags.riskValid ? 1 : 0,
1429
+ // Timestamp [25..27]
1430
+ flags.cmdTimestamp >>> 0,
1431
+ flags.nowSec >>> 0,
1432
+ flags.maxClockSkewSec >>> 0,
1433
+ // Key lifecycle [28..34]
1434
+ flags.keyIdFormatValid ? 1 : 0,
1435
+ flags.keyFound ? 1 : 0,
1436
+ flags.keyNotDeprecated ? 1 : 0,
1437
+ flags.keyRequesterMatches ? 1 : 0,
1438
+ flags.keyNotBeforeOk ? 1 : 0,
1439
+ flags.keyNotExpired ? 1 : 0,
1440
+ flags.keyLifecycleFieldsPresent ? 1 : 0,
1441
+ // Signature [35]
1442
+ flags.signatureValid ? 1 : 0,
1443
+ // Rate limit [36..37]
1444
+ flags.rateLimitOk ? 1 : 0,
1445
+ flags.rateLimitRetryAfterSec >>> 0,
1446
+ // Nonce [38]
1447
+ flags.nonceOk ? 1 : 0,
1448
+ // Policy [39..48]
1449
+ flags.policyConfigured ? 1 : 0,
1450
+ flags.requesterConfigured ? 1 : 0,
1451
+ flags.commandAllowed ? 1 : 0,
1452
+ flags.adapterAllowed ? 1 : 0,
1453
+ flags.filesystemRequired ? 1 : 0,
1454
+ flags.filesystemRootsDefined ? 1 : 0,
1455
+ flags.filesystemOk ? 1 : 0,
1456
+ flags.pathDenied ? 1 : 0,
1457
+ flags.shellRequired ? 1 : 0,
1458
+ flags.shellCommandOk ? 1 : 0
1459
+ ]);
1460
+ wasm().exports.lbe_validate_pipeline();
1461
+ const { stage, code } = readPipelineOutput();
1462
+ const ok = stage === 255;
1463
+ return {
1464
+ ok,
1465
+ stage,
1466
+ stageLabel: PIPELINE_STAGES[stage] || "unknown",
1467
+ code,
1468
+ schemaError: stage === 0 ? SCHEMA_MESSAGES[code]?.error || "Schema invalid" : null,
1469
+ keyReason: stage === 2 ? KEY_REASONS[code] || "KEY_ERROR" : null,
1470
+ policyResult: stage === 6 ? { ...POLICY_MESSAGES[code] || POLICY_MESSAGES[1], code } : null,
1471
+ retryAfterSec: stage === 4 ? code : 0,
1472
+ skewSec: stage === 1 ? code : 0
1473
+ };
1474
+ }
1475
+ function checkNonce({ ttlSec, nowSec, newKey, existingEntries }) {
1476
+ const lines = [`${ttlSec}:${nowSec}`, newKey, ...existingEntries].join("\n") + "\n";
1477
+ writeIn(lines);
1478
+ const isReplay = wasm().exports.lbe_nonce_check() !== 0;
1479
+ if (isReplay) return { ok: false, updatedEntriesText: null };
1480
+ const out = readOut();
1481
+ return { ok: true, updatedEntriesText: out.startsWith("OK\n") ? out.slice(3) : out };
1482
+ }
1483
+ function checkRateLimit({ windowSec, maxRequests, nowSec, requesterId, existingEntries }) {
1484
+ const lines = [
1485
+ `${windowSec}:${maxRequests}:${nowSec}`,
1486
+ requesterId,
1487
+ ...existingEntries
1488
+ ].join("\n") + "\n";
1489
+ writeIn(lines);
1490
+ const exceeded = wasm().exports.lbe_rate_check() !== 0;
1491
+ const out = readOut();
1492
+ if (exceeded) {
1493
+ const retryAfterSec = parseInt(out.match(/^EXCEEDED:(\d+)/)?.[1] ?? "1", 10);
1494
+ const entriesText = out.replace(/^EXCEEDED:\d+\n/, "");
1495
+ return { ok: false, retryAfterSec, updatedEntriesText: entriesText };
1496
+ }
1497
+ return { ok: true, retryAfterSec: 0, updatedEntriesText: out.startsWith("OK\n") ? out.slice(3) : out };
1498
+ }
1499
+ function classifyRisk(commandId, shellCmdIsRm = false) {
1500
+ const typeCode = COMMAND_TYPE[commandId] ?? 0;
1501
+ const code = wasm().exports.lbe_classify_risk(typeCode, shellCmdIsRm ? 1 : 0);
1502
+ return RISK_LABELS[code] ?? "LOW";
1503
+ }
1504
+
1505
+ // src/core/validator.js
1506
+ import path8 from "path";
1507
+ function extractSchemaFlags(cmd) {
1508
+ const has = (k) => cmd != null && Object.prototype.hasOwnProperty.call(cmd, k);
1509
+ const isStr = (v) => typeof v === "string";
1510
+ const p = cmd?.payload;
1511
+ const sig = cmd?.signature;
1512
+ return {
1513
+ hasId: has("id"),
1514
+ idValid: isStr(cmd?.id) && /^[A-Z_]+$/.test(cmd.id) && cmd.id.length >= 1 && cmd.id.length <= 50,
1515
+ hasCommandId: has("commandId"),
1516
+ commandIdValid: isStr(cmd?.commandId) && /^[a-f0-9-]+$/.test(cmd.commandId) && cmd.commandId.length === 36,
1517
+ hasRequesterId: has("requesterId"),
1518
+ requesterIdValid: isStr(cmd?.requesterId) && cmd.requesterId.length >= 3 && cmd.requesterId.length <= 100,
1519
+ hasSessionId: has("sessionId"),
1520
+ sessionIdValid: isStr(cmd?.sessionId) && cmd.sessionId.length >= 3,
1521
+ hasTimestamp: has("timestamp"),
1522
+ timestampValid: typeof cmd?.timestamp === "number" && cmd.timestamp >= 1e9,
1523
+ hasNonce: has("nonce"),
1524
+ nonceValid: isStr(cmd?.nonce) && cmd.nonce.length >= 32 && cmd.nonce.length <= 128,
1525
+ hasRequires: has("requires"),
1526
+ requiresValid: Array.isArray(cmd?.requires) && cmd.requires.length >= 1 && cmd.requires.every(isStr),
1527
+ hasPayload: has("payload") && typeof p === "object" && p !== null && !Array.isArray(p),
1528
+ hasPayloadAdapter: p != null && Object.prototype.hasOwnProperty.call(p, "adapter"),
1529
+ payloadAdapterValid: isStr(p?.adapter),
1530
+ hasSignature: has("signature") && typeof sig === "object" && sig !== null && !Array.isArray(sig),
1531
+ hasSignatureAlg: sig != null && Object.prototype.hasOwnProperty.call(sig, "alg"),
1532
+ signatureAlgValid: sig?.alg === "ed25519",
1533
+ hasSignatureKeyId: sig != null && Object.prototype.hasOwnProperty.call(sig, "keyId"),
1534
+ hasSignatureSig: sig != null && Object.prototype.hasOwnProperty.call(sig, "sig"),
1535
+ signatureSigValid: isStr(sig?.sig) && sig.sig.length >= 10,
1536
+ hasRisk: has("risk"),
1537
+ riskValid: ["LOW", "MEDIUM", "HIGH", "CRITICAL"].includes(cmd?.risk)
1538
+ };
1539
+ }
1540
+ function extractPolicyFlags(policy, cmd) {
1541
+ const hasPolicy = !!(policy && policy.default === "DENY" && policy.requesters && typeof policy.requesters === "object");
1542
+ const rp = policy?.requesters?.[cmd.requesterId];
1543
+ const cmdId = cmd.id?.toLowerCase() ?? "";
1544
+ const commandAllowed = !!rp?.allowCommands?.some((c) => c.toLowerCase() === cmdId);
1545
+ const adapterAllowed = !!rp?.allowAdapters?.includes(cmd.payload?.adapter);
1546
+ const filesystemRequired = !!cmd.payload?.cwd;
1547
+ let filesystemRootsDefined = false;
1548
+ let filesystemOk = false;
1549
+ let pathDenied = false;
1550
+ if (filesystemRequired) {
1551
+ const roots = rp?.filesystem?.roots ?? [];
1552
+ filesystemRootsDefined = roots.length > 0;
1553
+ if (filesystemRootsDefined) {
1554
+ const cwd = path8.resolve(cmd.payload.cwd);
1555
+ filesystemOk = roots.some((r) => {
1556
+ const rr = path8.resolve(r);
1557
+ return cwd === rr || cwd.startsWith(rr + path8.sep);
1558
+ });
1559
+ const denyPatterns = rp?.filesystem?.denyPatterns ?? [];
1560
+ pathDenied = denyPatterns.some((pattern) => {
1561
+ const re = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$");
1562
+ return re.test(cwd);
1563
+ });
1564
+ }
1565
+ }
1566
+ let shellRequired = false;
1567
+ let shellCommandOk = true;
1568
+ if (cmd.id === "RUN_SHELL") {
1569
+ shellRequired = true;
1570
+ const allowCmds = rp?.exec?.allowCmds ?? [];
1571
+ const denyCmds = rp?.exec?.denyCmds ?? [];
1572
+ const shellCmd = cmd.payload?.cmd;
1573
+ if (denyCmds.includes(shellCmd)) {
1574
+ shellCommandOk = false;
1575
+ } else {
1576
+ shellCommandOk = allowCmds.length === 0 || allowCmds.includes(shellCmd);
1577
+ }
1578
+ }
1579
+ return {
1580
+ policyConfigured: hasPolicy,
1581
+ requesterConfigured: !!rp,
1582
+ commandAllowed,
1583
+ adapterAllowed,
1584
+ filesystemRequired,
1585
+ filesystemRootsDefined,
1586
+ filesystemOk,
1587
+ pathDenied,
1588
+ shellRequired,
1589
+ shellCommandOk
1590
+ };
1591
+ }
1592
+ function extractKeyFlags(keyStore, keyId, requesterId, now = /* @__PURE__ */ new Date()) {
1593
+ if (!keyStore || !keyId) {
1594
+ return {
1595
+ keyIdFormatValid: false,
1596
+ keyFound: false,
1597
+ keyNotDeprecated: false,
1598
+ keyRequesterMatches: false,
1599
+ keyNotBeforeOk: false,
1600
+ keyNotExpired: false,
1601
+ keyLifecycleFieldsPresent: false,
1602
+ publicKey: null
1603
+ };
1604
+ }
1605
+ const KEY_ID_RE = /^[A-Za-z0-9:_-]{3,128}$/;
1606
+ const keyIdFormatValid = KEY_ID_RE.test(keyId) && keyId !== "default";
1607
+ if (!keyIdFormatValid) {
1608
+ return {
1609
+ keyIdFormatValid,
1610
+ keyFound: false,
1611
+ keyNotDeprecated: false,
1612
+ keyRequesterMatches: false,
1613
+ keyNotBeforeOk: false,
1614
+ keyNotExpired: false,
1615
+ keyLifecycleFieldsPresent: false,
1616
+ publicKey: null
1617
+ };
1618
+ }
1619
+ const entry = keyStore.trustedKeys?.[keyId];
1620
+ const keyFound = !!entry;
1621
+ if (!keyFound) {
1622
+ return {
1623
+ keyIdFormatValid,
1624
+ keyFound,
1625
+ keyNotDeprecated: false,
1626
+ keyRequesterMatches: false,
1627
+ keyNotBeforeOk: false,
1628
+ keyNotExpired: false,
1629
+ keyLifecycleFieldsPresent: false,
1630
+ publicKey: null
1631
+ };
1632
+ }
1633
+ const keyNotDeprecated = !entry.deprecated;
1634
+ const keyRequesterMatches = !entry.requesterId || entry.requesterId === requesterId;
1635
+ const notBefore = entry.notBefore || entry.validFrom;
1636
+ const expiresAt = entry.expiresAt || entry.validUntil;
1637
+ const keyLifecycleFieldsPresent = typeof notBefore === "string" && typeof expiresAt === "string";
1638
+ let keyNotBeforeOk = false;
1639
+ let keyNotExpired = false;
1640
+ if (keyLifecycleFieldsPresent) {
1641
+ const nb = new Date(notBefore);
1642
+ const exp = new Date(expiresAt);
1643
+ if (!isNaN(nb.getTime()) && !isNaN(exp.getTime()) && nb < exp) {
1644
+ keyNotBeforeOk = now >= nb;
1645
+ keyNotExpired = now < exp;
1646
+ }
1647
+ }
1648
+ return {
1649
+ keyIdFormatValid,
1650
+ keyFound,
1651
+ keyNotDeprecated,
1652
+ keyRequesterMatches,
1653
+ keyNotBeforeOk,
1654
+ keyNotExpired,
1655
+ keyLifecycleFieldsPresent,
1656
+ publicKey: entry.publicKey ?? null
1657
+ };
1658
+ }
1659
+ function nonceEntriesToText(db) {
1660
+ return (db?.entries ?? []).map((e) => `${e.key}:${e.timestamp}`);
1661
+ }
1662
+ function textToNonceEntries(text) {
1663
+ return text.split("\n").filter(Boolean).map((line) => {
1664
+ const lastColon = line.lastIndexOf(":");
1665
+ return {
1666
+ key: line.slice(0, lastColon),
1667
+ timestamp: parseInt(line.slice(lastColon + 1), 10) || 0
1668
+ };
1669
+ });
1670
+ }
1671
+ function rateEntriesToText(db) {
1672
+ return (db?.entries ?? []).map((e) => `${e.requesterId}:${e.timestamp}`);
1673
+ }
1674
+ function textToRateEntries(text) {
1675
+ return text.split("\n").filter(Boolean).map((line) => {
1676
+ const lastColon = line.lastIndexOf(":");
1677
+ return {
1678
+ requesterId: line.slice(0, lastColon),
1679
+ timestamp: parseInt(line.slice(lastColon + 1), 10) || 0
1680
+ };
1681
+ });
1682
+ }
1683
+ function validateCommand({
1684
+ commandObj,
1685
+ pubKeyB64,
1686
+ keyStore,
1687
+ nonceDb,
1688
+ policy,
1689
+ rateLimiter,
1690
+ policyStatePath
1691
+ }) {
1692
+ const result = {
1693
+ valid: false,
1694
+ commandId: commandObj?.commandId,
1695
+ checks: {},
1696
+ errors: []
1697
+ };
1698
+ const nowSec = Math.floor(Date.now() / 1e3);
1699
+ const now = /* @__PURE__ */ new Date();
1700
+ const maxClockSkewSec = Number.isFinite(policy?.security?.maxClockSkewSec) ? policy.security.maxClockSkewSec : 600;
1701
+ if (policyStatePath && policy?.version !== void 0) {
1702
+ try {
1703
+ const vCheck = validateAndUpdatePolicyVersionState({ policyObj: policy, statePath: policyStatePath });
1704
+ result.checks.policyVersion = vCheck.ok;
1705
+ if (!vCheck.ok) {
1706
+ result.errors.push({ type: "POLICY_VERSION_INVALID", message: vCheck.message });
1707
+ return result;
1708
+ }
1709
+ } catch {
1710
+ result.checks.policyVersion = true;
1711
+ }
1712
+ } else {
1713
+ result.checks.policyVersion = true;
1714
+ }
1715
+ const schemaFlags = extractSchemaFlags(commandObj);
1716
+ const keyId = commandObj?.signature?.keyId;
1717
+ const keyFlags = extractKeyFlags(keyStore, keyId, commandObj?.requesterId, now);
1718
+ let signatureValid = false;
1719
+ let effectivePubKey = keyFlags.publicKey;
1720
+ if (!effectivePubKey && pubKeyB64) effectivePubKey = pubKeyB64;
1721
+ if (effectivePubKey) {
1722
+ const bodyWithoutSig = { ...commandObj };
1723
+ delete bodyWithoutSig.signature;
1724
+ const sigCheck = verifyEd25519({
1725
+ payloadObj: bodyWithoutSig,
1726
+ sigB64: commandObj?.signature?.sig,
1727
+ pubKeyB64: effectivePubKey
1728
+ });
1729
+ signatureValid = sigCheck.valid;
1730
+ }
1731
+ let rateLimitOk = true;
1732
+ let rateLimitRetryAfterSec = 0;
1733
+ if (signatureValid && rateLimiter && typeof rateLimiter.db !== "undefined") {
1734
+ const rateCfg = policy?.requesters?.[commandObj.requesterId]?.rateLimit || {};
1735
+ const dfltCfg = policy?.security?.defaultRateLimit || {};
1736
+ const windowSec = rateCfg.windowSec ?? dfltCfg.windowSec ?? 60;
1737
+ const maxRequests = rateCfg.maxRequests ?? dfltCfg.maxRequests ?? 30;
1738
+ const rateResult = checkRateLimit({
1739
+ windowSec,
1740
+ maxRequests,
1741
+ nowSec,
1742
+ requesterId: commandObj.requesterId,
1743
+ existingEntries: rateEntriesToText(rateLimiter.db)
1744
+ });
1745
+ rateLimitOk = rateResult.ok;
1746
+ rateLimitRetryAfterSec = rateResult.retryAfterSec;
1747
+ if (rateResult.ok) {
1748
+ rateLimiter.db.entries = textToRateEntries(rateResult.updatedEntriesText);
1749
+ }
1750
+ } else if (signatureValid && rateLimiter && typeof rateLimiter.checkAndRecord === "function") {
1751
+ const rateCfg = policy?.requesters?.[commandObj.requesterId]?.rateLimit || {};
1752
+ const dfltCfg = policy?.security?.defaultRateLimit || {};
1753
+ const rateCheck = rateLimiter.checkAndRecord({
1754
+ requesterId: commandObj.requesterId,
1755
+ nowSec,
1756
+ windowSec: rateCfg.windowSec ?? dfltCfg.windowSec ?? 60,
1757
+ maxRequests: rateCfg.maxRequests ?? dfltCfg.maxRequests ?? 30
1758
+ });
1759
+ rateLimitOk = rateCheck.ok;
1760
+ rateLimitRetryAfterSec = rateCheck.retryAfterSec ?? 0;
1761
+ }
1762
+ let nonceOk = true;
1763
+ const nonceKey = `${commandObj?.requesterId}|${commandObj?.sessionId}|${commandObj?.nonce}`;
1764
+ const ttlSec = 3600;
1765
+ if (signatureValid && rateLimitOk && nonceDb) {
1766
+ if (typeof nonceDb.checkAndRecord === "function") {
1767
+ if (nonceDb.db) {
1768
+ const nonceResult = checkNonce({
1769
+ ttlSec,
1770
+ nowSec,
1771
+ newKey: nonceKey,
1772
+ existingEntries: nonceEntriesToText(nonceDb.db)
1773
+ });
1774
+ nonceOk = nonceResult.ok;
1775
+ if (nonceResult.ok) {
1776
+ nonceDb.db.entries = textToNonceEntries(nonceResult.updatedEntriesText);
1777
+ }
1778
+ } else {
1779
+ const r = nonceDb.checkAndRecord({
1780
+ requesterId: commandObj.requesterId,
1781
+ sessionId: commandObj.sessionId,
1782
+ nonce: commandObj.nonce
1783
+ });
1784
+ nonceOk = r.ok;
1785
+ }
1786
+ } else {
1787
+ const nonceResult = checkNonce({
1788
+ ttlSec,
1789
+ nowSec,
1790
+ newKey: nonceKey,
1791
+ existingEntries: nonceEntriesToText(nonceDb)
1792
+ });
1793
+ nonceOk = nonceResult.ok;
1794
+ if (nonceResult.ok) {
1795
+ nonceDb.entries = textToNonceEntries(nonceResult.updatedEntriesText);
1796
+ }
1797
+ }
1798
+ }
1799
+ const policyFlags = extractPolicyFlags(policy, commandObj ?? {});
1800
+ const pipeline = runValidationPipeline({
1801
+ ...schemaFlags,
1802
+ cmdTimestamp: commandObj?.timestamp ?? 0,
1803
+ nowSec,
1804
+ maxClockSkewSec,
1805
+ ...keyFlags,
1806
+ signatureValid,
1807
+ rateLimitOk,
1808
+ rateLimitRetryAfterSec,
1809
+ nonceOk,
1810
+ ...policyFlags
1811
+ });
1812
+ const s = pipeline.stage;
1813
+ result.checks.schema = s !== 0;
1814
+ if (s >= 1) result.checks.timestamp = s !== 1;
1815
+ if (s >= 2) result.checks.keyId = s !== 2;
1816
+ if (s >= 2) result.checks.signature = s !== 2 && s !== 3;
1817
+ if (s >= 4) result.checks.rateLimit = s !== 4;
1818
+ if (s >= 5) result.checks.nonce = s !== 5;
1819
+ if (s >= 6 || pipeline.ok) result.checks.policy = s !== 6;
1820
+ if (!pipeline.ok) {
1821
+ const stage = pipeline.stageLabel;
1822
+ if (stage === "schema") {
1823
+ result.errors.push({ type: "SCHEMA_ERROR", message: pipeline.schemaError || "Schema invalid" });
1824
+ } else if (stage === "timestamp") {
1825
+ result.errors.push({ type: "TIMESTAMP_SKEW_EXCEEDED", message: `Command timestamp skew ${pipeline.skewSec}s exceeds allowed ${maxClockSkewSec}s` });
1826
+ } else if (stage === "key") {
1827
+ const reason = pipeline.keyReason || "KEY_ERROR";
1828
+ const msgs = {
1829
+ KEY_ID_INVALID: `Invalid keyId '${keyId}'`,
1830
+ KEY_NOT_TRUSTED: `Key '${keyId}' is not in trusted key store`,
1831
+ KEY_DEPRECATED: `Key '${keyId}' is deprecated`,
1832
+ KEY_REQUESTER_MISMATCH: `Key '${keyId}' is not authorized for requester '${commandObj?.requesterId}'`,
1833
+ KEY_LIFECYCLE_INVALID: `Key '${keyId}' must define notBefore and expiresAt`,
1834
+ KEY_NOT_YET_VALID: `Key '${keyId}' is not yet valid`,
1835
+ KEY_EXPIRED: `Key '${keyId}' has expired`
1836
+ };
1837
+ result.errors.push({ type: reason, message: msgs[reason] || reason });
1838
+ } else if (stage === "signature") {
1839
+ result.errors.push({ type: "SIGNATURE_INVALID", message: effectivePubKey ? "Signature verification failed" : "No public key available" });
1840
+ } else if (stage === "rate_limit") {
1841
+ result.errors.push({ type: "RATE_LIMIT_EXCEEDED", message: `Rate limit exceeded. Retry after ${pipeline.retryAfterSec}s` });
1842
+ } else if (stage === "nonce") {
1843
+ result.errors.push({ type: "REPLAY_NONCE", message: "Nonce has already been used" });
1844
+ } else if (stage === "policy" && pipeline.policyResult) {
1845
+ result.errors.push({ type: pipeline.policyResult.reason, message: pipeline.policyResult.message });
1846
+ } else {
1847
+ result.errors.push({ type: "VALIDATION_FAILED", message: `Failed at stage: ${stage}` });
1848
+ }
1849
+ return result;
1850
+ }
1851
+ result.valid = true;
1852
+ result.risk = classifyRisk(commandObj.id, commandObj.payload?.cmd === "rm");
1853
+ result.message = "Command validation successful";
1854
+ return result;
1855
+ }
1856
+
1857
+ // src/core/nonceStore.js
1858
+ import fs8 from "fs";
1859
+ import path9 from "path";
1860
+ var NonceStore = class {
1861
+ constructor(dbPath, ttlSec = 3600) {
1862
+ this.dbPath = dbPath;
1863
+ this.ttlSec = ttlSec;
1864
+ this.db = { entries: [] };
1865
+ }
1866
+ async load() {
1867
+ if (!fs8.existsSync(this.dbPath)) {
1868
+ this.db = { entries: [] };
1869
+ return;
1870
+ }
1871
+ try {
1872
+ const data = fs8.readFileSync(this.dbPath, "utf8");
1873
+ this.db = JSON.parse(data);
1874
+ this.prune();
1875
+ } catch (err) {
1876
+ throw new Error(`Nonce DB at ${this.dbPath} is corrupt or unreadable: ${err.message}`);
1877
+ }
1878
+ }
1879
+ async save() {
1880
+ try {
1881
+ const dir = path9.dirname(this.dbPath);
1882
+ if (!fs8.existsSync(dir)) {
1883
+ fs8.mkdirSync(dir, { recursive: true });
1884
+ }
1885
+ atomicWriteFileSync(this.dbPath, JSON.stringify(this.db, null, 2), { encoding: "utf8" });
1886
+ } catch (err) {
1887
+ throw new Error(`Failed to save nonce DB: ${err.message}`);
1888
+ }
1889
+ }
1890
+ checkAndRecord({ requesterId, sessionId, nonce }) {
1891
+ const now = Math.floor(Date.now() / 1e3);
1892
+ this.db.entries = this.db.entries.filter((e) => now - e.timestamp <= this.ttlSec);
1893
+ const key = `${requesterId}|${sessionId}|${nonce}`;
1894
+ if (this.db.entries.some((e) => e.key === key)) {
1895
+ return {
1896
+ ok: false,
1897
+ reason: "REPLAY_NONCE",
1898
+ message: "Nonce has already been used"
1899
+ };
1900
+ }
1901
+ this.db.entries.push({ key, timestamp: now });
1902
+ return {
1903
+ ok: true,
1904
+ reason: null,
1905
+ message: "Nonce accepted"
1906
+ };
1907
+ }
1908
+ prune() {
1909
+ const now = Math.floor(Date.now() / 1e3);
1910
+ const before = this.db.entries.length;
1911
+ this.db.entries = this.db.entries.filter((e) => now - e.timestamp <= this.ttlSec);
1912
+ const after = this.db.entries.length;
1913
+ return {
1914
+ prunedCount: before - after,
1915
+ remainingCount: after
1916
+ };
1917
+ }
1918
+ };
1919
+
1920
+ // src/cli/commands/verify.js
1921
+ async function verifyCommand(opts) {
1922
+ const { in: inFile } = opts;
1923
+ const config = opts.config || opts.policy;
1924
+ const pubKey = opts["pub-key"];
1925
+ const keysStorePath = opts["keys-store"] || path10.resolve(".lbe/config/keys.json");
1926
+ const policySigPath = opts["policy-sig"] || path10.resolve(".lbe/config/policy.sig.json");
1927
+ const policyStatePath = opts["policy-state"] || path10.resolve(".lbe/data/policy.state.json");
1928
+ const allowUnsignedPolicy = opts["policy-unsigned-ok"] === true || String(opts["policy-unsigned-ok"]).toLowerCase() === "true";
1929
+ if (!inFile) {
1930
+ console.error("Error: --in <file> is required");
1931
+ process.exit(1);
1932
+ }
1933
+ let proposal;
1934
+ try {
1935
+ const filePath = path10.resolve(inFile);
1936
+ const content = fs9.readFileSync(filePath, "utf-8");
1937
+ proposal = JSON.parse(content);
1938
+ } catch (error) {
1939
+ console.error(JSON.stringify({
1940
+ status: "error",
1941
+ error: "INVALID_PROPOSAL_FILE",
1942
+ message: error.message
1943
+ }));
1944
+ process.exit(5);
1945
+ }
1946
+ let policy;
1947
+ try {
1948
+ const policyPath = config || path10.resolve(".lbe/config/policy.default.json");
1949
+ if (!fs9.existsSync(policyPath)) {
1950
+ console.error(JSON.stringify({
1951
+ status: "error",
1952
+ error: "MISSING_POLICY",
1953
+ message: `Policy file not found: ${policyPath}`
1954
+ }));
1955
+ process.exit(1);
1956
+ }
1957
+ const policyContent = fs9.readFileSync(policyPath, "utf-8");
1958
+ policy = JSON.parse(policyContent);
1959
+ } catch (error) {
1960
+ console.error(JSON.stringify({
1961
+ status: "error",
1962
+ error: "INVALID_POLICY",
1963
+ message: error.message
1964
+ }));
1965
+ process.exit(1);
1966
+ }
1967
+ const keyStoreResult = loadKeysStore(keysStorePath);
1968
+ const keyStore = keyStoreResult.ok ? keyStoreResult.store : null;
1969
+ const policySigCheck = verifyPolicySignature({
1970
+ policyObj: policy,
1971
+ keyStore,
1972
+ policySigPath,
1973
+ allowUnsigned: allowUnsignedPolicy
1974
+ });
1975
+ if (!policySigCheck.ok) {
1976
+ console.error(JSON.stringify({
1977
+ status: "error",
1978
+ error: policySigCheck.reason,
1979
+ message: policySigCheck.message
1980
+ }, null, 2));
1981
+ process.exit(8);
1982
+ }
1983
+ const versionCheck = validateAndUpdatePolicyVersionState({
1984
+ policyObj: policy,
1985
+ statePath: policyStatePath,
1986
+ maxCreatedAtSkewSec: policy?.security?.maxPolicyCreatedAtSkewSec
1987
+ });
1988
+ if (!versionCheck.ok) {
1989
+ console.error(JSON.stringify({
1990
+ status: "error",
1991
+ error: versionCheck.reason,
1992
+ message: versionCheck.message
1993
+ }, null, 2));
1994
+ process.exit(8);
1995
+ }
1996
+ const nonceDb = new NonceStore(path10.resolve(".lbe/data/nonce.db.json"));
1997
+ await nonceDb.load();
1998
+ if (!keyStore && !pubKey) {
1999
+ console.error(JSON.stringify({
2000
+ status: "error",
2001
+ error: "MISSING_KEY_MATERIAL",
2002
+ message: `${keyStoreResult.message}. Provide --pub-key/--pub-key-file or create .lbe/config/keys.json`
2003
+ }));
2004
+ process.exit(1);
2005
+ }
2006
+ const result = validateCommand({
2007
+ commandObj: proposal,
2008
+ pubKeyB64: pubKey,
2009
+ keyStore,
2010
+ nonceDb,
2011
+ policy
2012
+ });
2013
+ const output = {
2014
+ status: result.valid ? "valid" : "invalid",
2015
+ commandId: proposal.commandId || "N/A",
2016
+ checks: result.checks,
2017
+ errors: result.errors || [],
2018
+ risk: result.risk || "UNKNOWN"
2019
+ };
2020
+ console.log(JSON.stringify(output, null, 2));
2021
+ if (!result.valid) {
2022
+ if (result.checks.schema === false) process.exit(5);
2023
+ if (result.checks.signature === false) process.exit(3);
2024
+ if (result.checks.nonce === false) process.exit(4);
2025
+ if (result.checks.timestamp === false) process.exit(6);
2026
+ if (result.checks.rateLimit === false) process.exit(7);
2027
+ if (result.checks.policy === false) process.exit(2);
2028
+ process.exit(9);
2029
+ }
2030
+ process.exit(0);
2031
+ }
2032
+
2033
+ // src/cli/commands/dryrun.js
2034
+ import fs13 from "fs";
2035
+ import path14 from "path";
2036
+
2037
+ // src/adapters/noopAdapter.js
2038
+ async function noopAdapter(cmd) {
2039
+ return {
2040
+ adapter: "noop",
2041
+ commandId: cmd.commandId || "unknown",
2042
+ command: cmd.id || "unknown",
2043
+ status: "completed",
2044
+ output: `[NOOP] Would execute: ${cmd.id || "unknown"} on adapter: ${cmd.payload?.adapter || "unknown"}`,
2045
+ exitCode: 0,
2046
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2047
+ };
2048
+ }
2049
+
2050
+ // src/adapters/shellAdapter.js
2051
+ import { spawnSync } from "child_process";
2052
+ import path11 from "path";
2053
+ import fs10 from "fs";
2054
+ function physicalPath(candidate) {
2055
+ try {
2056
+ return fs10.realpathSync(path11.resolve(candidate));
2057
+ } catch {
2058
+ return path11.resolve(candidate);
2059
+ }
2060
+ }
2061
+ function normalizeArgs(args) {
2062
+ if (args === void 0) return { ok: true, args: [] };
2063
+ if (!Array.isArray(args)) {
2064
+ return { ok: false, error: "payload.args must be an array" };
2065
+ }
2066
+ const normalized = [];
2067
+ for (const arg of args) {
2068
+ if (typeof arg !== "string" && typeof arg !== "number" && typeof arg !== "boolean") {
2069
+ return { ok: false, error: "payload.args may only contain string, number, or boolean values" };
2070
+ }
2071
+ normalized.push(String(arg));
2072
+ }
2073
+ return { ok: true, args: normalized };
2074
+ }
2075
+ async function shellAdapter(cmd, policy, requester) {
2076
+ const payload = cmd.payload;
2077
+ const timeout = Math.min(Math.max(Number(payload.timeoutMs) || 3e4, 1), 3e4);
2078
+ const maxOutputSize = Math.min(Math.max(Number(payload.maxOutputBytes) || 1024 * 1024, 1024), 1024 * 1024);
2079
+ if (payload.adapter !== "shell") {
2080
+ return {
2081
+ adapter: "shell",
2082
+ commandId: cmd.commandId,
2083
+ status: "error",
2084
+ error: "Adapter mismatch",
2085
+ exitCode: 1
2086
+ };
2087
+ }
2088
+ const allowedCmds = requester?.exec?.allowCmds || [];
2089
+ const deniedCmds = requester?.exec?.denyCmds || [];
2090
+ if (deniedCmds.includes(payload.cmd)) {
2091
+ return {
2092
+ adapter: "shell",
2093
+ commandId: cmd.commandId,
2094
+ status: "blocked",
2095
+ error: `Command '${payload.cmd}' is denied`,
2096
+ exitCode: 2
2097
+ };
2098
+ }
2099
+ if (allowedCmds.length > 0 && !allowedCmds.includes(payload.cmd)) {
2100
+ return {
2101
+ adapter: "shell",
2102
+ commandId: cmd.commandId,
2103
+ status: "blocked",
2104
+ error: `Command '${payload.cmd}' not in allowlist`,
2105
+ exitCode: 2
2106
+ };
2107
+ }
2108
+ const roots = requester?.filesystem?.roots || [];
2109
+ const cwdAllow = roots.some((r) => {
2110
+ const resolvedRoot = physicalPath(r);
2111
+ const norm = physicalPath(payload.cwd);
2112
+ return norm === resolvedRoot || norm.startsWith(resolvedRoot + path11.sep);
2113
+ });
2114
+ if (!cwdAllow) {
2115
+ return {
2116
+ adapter: "shell",
2117
+ commandId: cmd.commandId,
2118
+ status: "blocked",
2119
+ error: `CWD '${payload.cwd}' not authorized`,
2120
+ exitCode: 2
2121
+ };
2122
+ }
2123
+ const argCheck = normalizeArgs(payload.args);
2124
+ if (!argCheck.ok) {
2125
+ return {
2126
+ adapter: "shell",
2127
+ commandId: cmd.commandId,
2128
+ status: "blocked",
2129
+ error: argCheck.error,
2130
+ exitCode: 2
2131
+ };
2132
+ }
2133
+ try {
2134
+ const result = spawnSync(payload.cmd, argCheck.args, {
2135
+ cwd: payload.cwd,
2136
+ timeout,
2137
+ encoding: "utf8",
2138
+ maxBuffer: maxOutputSize,
2139
+ stdio: ["pipe", "pipe", "pipe"],
2140
+ shell: false
2141
+ });
2142
+ if (result.error) {
2143
+ throw result.error;
2144
+ }
2145
+ const output = `${result.stdout || ""}${result.stderr || ""}`;
2146
+ const exitCode = result.status ?? 1;
2147
+ if (exitCode !== 0) {
2148
+ return {
2149
+ adapter: "shell",
2150
+ commandId: cmd.commandId,
2151
+ command: payload.cmd,
2152
+ status: "error",
2153
+ error: output.substring(0, maxOutputSize) || `Command exited with code ${exitCode}`,
2154
+ exitCode,
2155
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2156
+ };
2157
+ }
2158
+ return {
2159
+ adapter: "shell",
2160
+ commandId: cmd.commandId,
2161
+ command: payload.cmd,
2162
+ status: "completed",
2163
+ output: output.substring(0, maxOutputSize),
2164
+ exitCode: 0,
2165
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2166
+ };
2167
+ } catch (err) {
2168
+ return {
2169
+ adapter: "shell",
2170
+ commandId: cmd.commandId,
2171
+ command: payload.cmd,
2172
+ status: "error",
2173
+ error: err.message,
2174
+ exitCode: err.status || 1,
2175
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2176
+ };
2177
+ }
2178
+ }
2179
+
2180
+ // src/adapters/fileAdapter.js
2181
+ import fs12 from "fs";
2182
+ import path13 from "path";
2183
+
2184
+ // src/core/backup.js
2185
+ import fs11 from "fs";
2186
+ import path12 from "path";
2187
+ import crypto2 from "crypto";
2188
+ function createBackup(filePath, backupDir) {
2189
+ const dir = backupDir || path12.resolve(".lbe/data/backups");
2190
+ if (!fs11.existsSync(dir)) {
2191
+ fs11.mkdirSync(dir, { recursive: true });
2192
+ }
2193
+ const target = path12.resolve(filePath);
2194
+ const existed = fs11.existsSync(target);
2195
+ let content = null;
2196
+ let hash = null;
2197
+ if (existed) {
2198
+ content = fs11.readFileSync(target);
2199
+ hash = crypto2.createHash("sha256").update(content).digest("hex");
2200
+ }
2201
+ const basename = path12.basename(target).replace(/[^a-zA-Z0-9._-]/g, "_");
2202
+ const backupName = `${Date.now()}-${hash ? hash.slice(0, 8) : "new"}-${basename}`;
2203
+ const backupPath = existed ? path12.join(dir, backupName) : null;
2204
+ if (existed && content !== null) {
2205
+ atomicWriteFileSync(backupPath, content);
2206
+ }
2207
+ return {
2208
+ originalPath: target,
2209
+ backupPath,
2210
+ existed,
2211
+ hash,
2212
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
2213
+ };
2214
+ }
2215
+ function restoreBackup(backupMeta) {
2216
+ if (!backupMeta) return { restored: false, error: "No backup metadata" };
2217
+ const { originalPath, backupPath, existed } = backupMeta;
2218
+ if (!existed) {
2219
+ try {
2220
+ if (fs11.existsSync(originalPath)) fs11.unlinkSync(originalPath);
2221
+ return { restored: true, action: "deleted" };
2222
+ } catch (e) {
2223
+ return { restored: false, error: e.message };
2224
+ }
2225
+ }
2226
+ if (!backupPath || !fs11.existsSync(backupPath)) {
2227
+ return { restored: false, error: "Backup file not found at: " + backupPath };
2228
+ }
2229
+ try {
2230
+ const content = fs11.readFileSync(backupPath);
2231
+ atomicWriteFileSync(originalPath, content);
2232
+ return { restored: true, action: "restored" };
2233
+ } catch (e) {
2234
+ return { restored: false, error: e.message };
2235
+ }
2236
+ }
2237
+
2238
+ // src/adapters/fileAdapter.js
2239
+ var MAX_READ_BYTES = 10 * 1024 * 1024;
2240
+ function resolvedTarget(target, cwd) {
2241
+ if (!target) return null;
2242
+ return path13.isAbsolute(target) ? path13.resolve(target) : path13.resolve(cwd || process.cwd(), target);
2243
+ }
2244
+ function isUnderRoot(targetPath, roots) {
2245
+ const norm = resolvePhysicalPath(targetPath);
2246
+ return roots.some((r) => {
2247
+ const root = resolvePhysicalPath(r);
2248
+ return norm === root || norm.startsWith(root + path13.sep);
2249
+ });
2250
+ }
2251
+ function resolvePhysicalPath(candidate) {
2252
+ let current = path13.resolve(candidate);
2253
+ const suffix = [];
2254
+ while (!fs12.existsSync(current)) {
2255
+ const parent = path13.dirname(current);
2256
+ if (parent === current) break;
2257
+ suffix.unshift(path13.basename(current));
2258
+ current = parent;
2259
+ }
2260
+ try {
2261
+ current = fs12.realpathSync(current);
2262
+ } catch {
2263
+ }
2264
+ return path13.join(current, ...suffix);
2265
+ }
2266
+ function matchesDenyPattern(str, patterns) {
2267
+ for (const pattern of patterns || []) {
2268
+ const rx = new RegExp(
2269
+ "^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/\\\\]*") + "$"
2270
+ );
2271
+ if (rx.test(str)) return pattern;
2272
+ }
2273
+ return null;
2274
+ }
2275
+ function blocked(cmd, code, message, exitCode = 2) {
2276
+ return {
2277
+ adapter: "file",
2278
+ commandId: cmd.commandId,
2279
+ status: "blocked",
2280
+ errorCode: code,
2281
+ error: message,
2282
+ exitCode
2283
+ };
2284
+ }
2285
+ function fail(cmd, code, message, backup = null, exitCode = 1) {
2286
+ return {
2287
+ adapter: "file",
2288
+ commandId: cmd.commandId,
2289
+ status: "error",
2290
+ errorCode: code,
2291
+ error: message,
2292
+ backup: backup ? summariseBackup(backup) : null,
2293
+ exitCode
2294
+ };
2295
+ }
2296
+ function summariseBackup(b) {
2297
+ return b ? { path: b.backupPath, existed: b.existed, hash: b.hash, createdAt: b.createdAt } : null;
2298
+ }
2299
+ async function fileAdapter(cmd, policy, requester) {
2300
+ const payload = cmd.payload;
2301
+ const action = payload.action;
2302
+ const cwd = payload.cwd || process.cwd();
2303
+ const target = resolvedTarget(payload.target, cwd);
2304
+ if (!action) return blocked(cmd, "FILE_NO_ACTION", "payload.action is required");
2305
+ if (!target && action !== "noop") return blocked(cmd, "FILE_NO_TARGET", "payload.target is required");
2306
+ const roots = requester?.filesystem?.roots || [];
2307
+ if (roots.length === 0) return blocked(cmd, "FILE_NO_ROOTS", "No filesystem roots defined for requester");
2308
+ if (!isUnderRoot(target, roots)) return blocked(cmd, "FILE_OUTSIDE_ROOT", `'${target}' is outside allowed roots`);
2309
+ const denied = matchesDenyPattern(target, requester?.filesystem?.denyPatterns);
2310
+ if (denied) return blocked(cmd, "FILE_PATH_DENIED", `'${target}' matches deny pattern: ${denied}`);
2311
+ switch (action) {
2312
+ case "read":
2313
+ return doRead(cmd, target);
2314
+ case "write":
2315
+ return doWrite(cmd, target, payload);
2316
+ case "patch":
2317
+ return doPatch(cmd, target, payload);
2318
+ case "delete":
2319
+ return doDelete(cmd, target);
2320
+ default:
2321
+ return blocked(cmd, "FILE_UNKNOWN_ACTION", `Unknown action: '${action}'`);
2322
+ }
2323
+ }
2324
+ function doRead(cmd, target) {
2325
+ if (!fs12.existsSync(target)) return fail(cmd, "FILE_NOT_FOUND", `Not found: ${target}`);
2326
+ try {
2327
+ const stat = fs12.statSync(target);
2328
+ if (stat.size > MAX_READ_BYTES) return fail(cmd, "FILE_TOO_LARGE", "File exceeds 10 MB read limit");
2329
+ const content = fs12.readFileSync(target, "utf8");
2330
+ return {
2331
+ adapter: "file",
2332
+ action: "read",
2333
+ commandId: cmd.commandId,
2334
+ status: "completed",
2335
+ target,
2336
+ output: content,
2337
+ bytesRead: stat.size,
2338
+ exitCode: 0
2339
+ };
2340
+ } catch (e) {
2341
+ return fail(cmd, "FILE_READ_ERROR", e.message);
2342
+ }
2343
+ }
2344
+ function doWrite(cmd, target, payload) {
2345
+ const content = payload.content;
2346
+ if (content === void 0 || content === null) {
2347
+ return fail(cmd, "FILE_MISSING_CONTENT", "payload.content is required for write");
2348
+ }
2349
+ const backup = tryBackup(target);
2350
+ try {
2351
+ atomicWriteFileSync(target, content, { encoding: "utf8" });
2352
+ return {
2353
+ adapter: "file",
2354
+ action: "write",
2355
+ commandId: cmd.commandId,
2356
+ status: "completed",
2357
+ target,
2358
+ backup: summariseBackup(backup),
2359
+ output: `Wrote ${Buffer.byteLength(content, "utf8")} bytes to ${target}`,
2360
+ exitCode: 0
2361
+ };
2362
+ } catch (e) {
2363
+ restoreBackup(backup);
2364
+ return fail(cmd, "FILE_WRITE_ERROR", e.message, backup);
2365
+ }
2366
+ }
2367
+ function doPatch(cmd, target, payload) {
2368
+ const content = payload.content;
2369
+ if (content === void 0 || content === null) {
2370
+ return fail(cmd, "FILE_MISSING_CONTENT", "payload.content is required for patch");
2371
+ }
2372
+ const backup = tryBackup(target);
2373
+ try {
2374
+ atomicWriteFileSync(target, content, { encoding: "utf8" });
2375
+ return {
2376
+ adapter: "file",
2377
+ action: "patch",
2378
+ commandId: cmd.commandId,
2379
+ status: "completed",
2380
+ target,
2381
+ backup: summariseBackup(backup),
2382
+ output: `Patched ${target} (${Buffer.byteLength(content, "utf8")} bytes)`,
2383
+ exitCode: 0
2384
+ };
2385
+ } catch (e) {
2386
+ restoreBackup(backup);
2387
+ return fail(cmd, "FILE_PATCH_ERROR", e.message, backup);
2388
+ }
2389
+ }
2390
+ function doDelete(cmd, target) {
2391
+ if (!fs12.existsSync(target)) return fail(cmd, "FILE_NOT_FOUND", `Not found: ${target}`);
2392
+ const backup = tryBackup(target);
2393
+ try {
2394
+ fs12.unlinkSync(target);
2395
+ return {
2396
+ adapter: "file",
2397
+ action: "delete",
2398
+ commandId: cmd.commandId,
2399
+ status: "completed",
2400
+ target,
2401
+ backup: summariseBackup(backup),
2402
+ output: `Deleted ${target}`,
2403
+ exitCode: 0
2404
+ };
2405
+ } catch (e) {
2406
+ restoreBackup(backup);
2407
+ return fail(cmd, "FILE_DELETE_ERROR", e.message, backup);
2408
+ }
2409
+ }
2410
+ function tryBackup(target) {
2411
+ try {
2412
+ return createBackup(target);
2413
+ } catch {
2414
+ return null;
2415
+ }
2416
+ }
2417
+
2418
+ // src/adapters/index.js
2419
+ var ADAPTERS = {
2420
+ noop: noopAdapter,
2421
+ shell: shellAdapter,
2422
+ file: fileAdapter
2423
+ };
2424
+ function getAdapter(name) {
2425
+ return ADAPTERS[name];
2426
+ }
2427
+ async function executeAdapter(adapterName, cmd, policy, requester) {
2428
+ const adapter = getAdapter(adapterName);
2429
+ if (!adapter) {
2430
+ return {
2431
+ adapter: adapterName,
2432
+ commandId: cmd.commandId,
2433
+ status: "error",
2434
+ error: `Adapter '${adapterName}' not found`,
2435
+ exitCode: 1
2436
+ };
2437
+ }
2438
+ try {
2439
+ return await adapter(cmd, policy, requester);
2440
+ } catch (err) {
2441
+ return {
2442
+ adapter: adapterName,
2443
+ commandId: cmd.commandId,
2444
+ status: "error",
2445
+ error: `Adapter execution failed: ${err.message}`,
2446
+ exitCode: 9
2447
+ };
2448
+ }
2449
+ }
2450
+ var AVAILABLE_ADAPTERS = Object.keys(ADAPTERS);
2451
+
2452
+ // src/cli/commands/dryrun.js
2453
+ async function dryrunCommand(opts) {
2454
+ const { in: inFile } = opts;
2455
+ const config = opts.config || opts.policy;
2456
+ const pubKey = opts["pub-key"];
2457
+ const keysStorePath = opts["keys-store"] || path14.resolve(".lbe/config/keys.json");
2458
+ const policySigPath = opts["policy-sig"] || path14.resolve(".lbe/config/policy.sig.json");
2459
+ const policyStatePath = opts["policy-state"] || path14.resolve(".lbe/data/policy.state.json");
2460
+ const allowUnsignedPolicy = opts["policy-unsigned-ok"] === true || String(opts["policy-unsigned-ok"]).toLowerCase() === "true";
2461
+ if (!inFile) {
2462
+ console.error("Error: --in <file> is required");
2463
+ process.exit(1);
2464
+ }
2465
+ let proposal;
2466
+ try {
2467
+ const filePath = path14.resolve(inFile);
2468
+ const content = fs13.readFileSync(filePath, "utf-8");
2469
+ proposal = JSON.parse(content);
2470
+ } catch (error) {
2471
+ console.error(JSON.stringify({
2472
+ status: "error",
2473
+ error: "INVALID_PROPOSAL_FILE",
2474
+ message: error.message
2475
+ }));
2476
+ process.exit(5);
2477
+ }
2478
+ let policy;
2479
+ try {
2480
+ const policyPath = config || path14.resolve(".lbe/config/policy.default.json");
2481
+ if (!fs13.existsSync(policyPath)) {
2482
+ console.error(JSON.stringify({
2483
+ status: "error",
2484
+ error: "MISSING_POLICY",
2485
+ message: `Policy file not found: ${policyPath}`
2486
+ }));
2487
+ process.exit(1);
2488
+ }
2489
+ const policyContent = fs13.readFileSync(policyPath, "utf-8");
2490
+ policy = JSON.parse(policyContent);
2491
+ } catch (error) {
2492
+ console.error(JSON.stringify({
2493
+ status: "error",
2494
+ error: "INVALID_POLICY",
2495
+ message: error.message
2496
+ }));
2497
+ process.exit(1);
2498
+ }
2499
+ const keyStoreResult = loadKeysStore(keysStorePath);
2500
+ const keyStore = keyStoreResult.ok ? keyStoreResult.store : null;
2501
+ const policySigCheck = verifyPolicySignature({
2502
+ policyObj: policy,
2503
+ keyStore,
2504
+ policySigPath,
2505
+ allowUnsigned: allowUnsignedPolicy
2506
+ });
2507
+ if (!policySigCheck.ok) {
2508
+ console.error(JSON.stringify({
2509
+ status: "error",
2510
+ error: policySigCheck.reason,
2511
+ message: policySigCheck.message
2512
+ }, null, 2));
2513
+ process.exit(8);
2514
+ }
2515
+ const versionCheck = validateAndUpdatePolicyVersionState({
2516
+ policyObj: policy,
2517
+ statePath: policyStatePath,
2518
+ maxCreatedAtSkewSec: policy?.security?.maxPolicyCreatedAtSkewSec
2519
+ });
2520
+ if (!versionCheck.ok) {
2521
+ console.error(JSON.stringify({
2522
+ status: "error",
2523
+ error: versionCheck.reason,
2524
+ message: versionCheck.message
2525
+ }, null, 2));
2526
+ process.exit(8);
2527
+ }
2528
+ const nonceDb = new NonceStore(path14.resolve(".lbe/data/nonce.db.json"));
2529
+ await nonceDb.load();
2530
+ if (!keyStore && !pubKey) {
2531
+ console.error(JSON.stringify({
2532
+ status: "error",
2533
+ error: "MISSING_KEY_MATERIAL",
2534
+ message: `${keyStoreResult.message}. Provide --pub-key/--pub-key-file or create .lbe/config/keys.json`
2535
+ }));
2536
+ process.exit(1);
2537
+ }
2538
+ const validateResult = validateCommand({
2539
+ commandObj: proposal,
2540
+ pubKeyB64: pubKey,
2541
+ keyStore,
2542
+ nonceDb,
2543
+ policy
2544
+ });
2545
+ if (!validateResult.valid) {
2546
+ const output2 = {
2547
+ status: "invalid",
2548
+ commandId: proposal.commandId || "N/A",
2549
+ checks: validateResult.checks,
2550
+ errors: validateResult.errors || [],
2551
+ executionResult: null
2552
+ };
2553
+ console.log(JSON.stringify(output2, null, 2));
2554
+ if (validateResult.checks.schema === false) process.exit(5);
2555
+ if (validateResult.checks.signature === false) process.exit(3);
2556
+ if (validateResult.checks.nonce === false) process.exit(4);
2557
+ if (validateResult.checks.timestamp === false) process.exit(6);
2558
+ if (validateResult.checks.rateLimit === false) process.exit(7);
2559
+ if (validateResult.checks.policy === false) process.exit(2);
2560
+ process.exit(9);
2561
+ }
2562
+ let executionResult;
2563
+ try {
2564
+ const requesterPolicy = policy.requesters?.[proposal.requesterId];
2565
+ executionResult = await executeAdapter(
2566
+ "noop",
2567
+ proposal,
2568
+ policy,
2569
+ requesterPolicy
2570
+ );
2571
+ } catch (error) {
2572
+ executionResult = {
2573
+ adapter: "noop",
2574
+ status: "error",
2575
+ error: error.message
2576
+ };
2577
+ }
2578
+ const output = {
2579
+ status: "valid_simulated",
2580
+ commandId: proposal.commandId || "N/A",
2581
+ checks: validateResult.checks,
2582
+ risk: validateResult.risk || "UNKNOWN",
2583
+ executionResult: {
2584
+ adapter: executionResult.adapter,
2585
+ status: executionResult.status,
2586
+ output: executionResult.output || executionResult.error || "",
2587
+ exitCode: executionResult.exitCode || 0,
2588
+ note: "This is a simulation using noop adapter. No actual execution occurred."
2589
+ }
2590
+ };
2591
+ console.log(JSON.stringify(output, null, 2));
2592
+ process.exit(0);
2593
+ }
2594
+
2595
+ // src/cli/commands/run.js
2596
+ import fs17 from "fs";
2597
+ import path19 from "path";
2598
+ import crypto6 from "crypto";
2599
+
2600
+ // src/core/auditLog.js
2601
+ import fs14 from "fs";
2602
+ import path15 from "path";
2603
+ import crypto3 from "crypto";
2604
+ function sha256(str) {
2605
+ return crypto3.createHash("sha256").update(str).digest("hex");
2606
+ }
2607
+ function getLastHash(logPath) {
2608
+ try {
2609
+ if (!fs14.existsSync(logPath)) return "GENESIS";
2610
+ const content = fs14.readFileSync(logPath, "utf8").trim();
2611
+ if (!content) return "GENESIS";
2612
+ const lines = content.split("\n");
2613
+ const lastLine = lines[lines.length - 1];
2614
+ try {
2615
+ const lastEntry = JSON.parse(lastLine);
2616
+ return lastEntry.hash || "GENESIS";
2617
+ } catch (err) {
2618
+ return "GENESIS";
2619
+ }
2620
+ } catch (err) {
2621
+ return "GENESIS";
2622
+ }
2623
+ }
2624
+ function appendAudit(logPath, entry) {
2625
+ const dir = path15.dirname(logPath);
2626
+ if (!fs14.existsSync(dir)) {
2627
+ fs14.mkdirSync(dir, { recursive: true });
2628
+ }
2629
+ let result;
2630
+ withFileLock(logPath, () => {
2631
+ const prevHash = getLastHash(logPath);
2632
+ const record = {
2633
+ ...entry,
2634
+ prevHash,
2635
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
2636
+ };
2637
+ delete record.hash;
2638
+ const recordStr = JSON.stringify(record);
2639
+ const hash = sha256(recordStr);
2640
+ const final = JSON.stringify({ ...record, hash });
2641
+ let existingContent = "";
2642
+ if (fs14.existsSync(logPath)) {
2643
+ existingContent = fs14.readFileSync(logPath, "utf8");
2644
+ }
2645
+ try {
2646
+ atomicWriteFileSync(logPath, existingContent + final + "\n", { encoding: "utf8" });
2647
+ } catch (err) {
2648
+ throw new Error(`Audit log write failed: ${err.message}`);
2649
+ }
2650
+ result = {
2651
+ success: true,
2652
+ hash,
2653
+ prevHash,
2654
+ message: "Audit entry appended"
2655
+ };
2656
+ });
2657
+ return result;
2658
+ }
2659
+ function verifyAuditLogIntegrity(logPath, options = {}) {
2660
+ const failFast = options.failFast !== false;
2661
+ const maxEntries = Number.isFinite(options.maxEntries) && options.maxEntries > 0 ? Math.floor(options.maxEntries) : null;
2662
+ const response = {
2663
+ ok: true,
2664
+ file: path15.resolve(logPath),
2665
+ entries: 0,
2666
+ valid: true,
2667
+ firstInvalidIndex: null,
2668
+ reason: null,
2669
+ errors: [],
2670
+ message: "Audit log verified"
2671
+ };
2672
+ try {
2673
+ if (!fs14.existsSync(logPath)) {
2674
+ response.message = "Audit log file not found (treated as empty)";
2675
+ return response;
2676
+ }
2677
+ const raw = fs14.readFileSync(logPath, "utf8").trim();
2678
+ if (!raw) {
2679
+ response.message = "Empty audit log";
2680
+ return response;
2681
+ }
2682
+ const allLines = raw.split("\n");
2683
+ const lines = maxEntries ? allLines.slice(0, maxEntries) : allLines;
2684
+ response.entries = lines.length;
2685
+ let expectedPrevHash = "GENESIS";
2686
+ for (let i = 0; i < lines.length; i++) {
2687
+ let entry;
2688
+ try {
2689
+ entry = JSON.parse(lines[i]);
2690
+ } catch {
2691
+ const errObj = {
2692
+ index: i,
2693
+ reason: "INVALID_JSON_LINE",
2694
+ message: `Line ${i} is not valid JSON`
2695
+ };
2696
+ response.valid = false;
2697
+ response.ok = false;
2698
+ response.firstInvalidIndex ??= i;
2699
+ response.reason ??= errObj.reason;
2700
+ response.errors.push(errObj);
2701
+ if (failFast) break;
2702
+ continue;
2703
+ }
2704
+ if (entry.prevHash !== expectedPrevHash) {
2705
+ const errObj = {
2706
+ index: i,
2707
+ reason: "PREV_HASH_MISMATCH",
2708
+ message: `Expected prevHash '${expectedPrevHash}', got '${entry.prevHash}'`
2709
+ };
2710
+ response.valid = false;
2711
+ response.ok = false;
2712
+ response.firstInvalidIndex ??= i;
2713
+ response.reason ??= errObj.reason;
2714
+ response.errors.push(errObj);
2715
+ if (failFast) break;
2716
+ }
2717
+ const recordCopy = { ...entry };
2718
+ const recordHash = recordCopy.hash;
2719
+ delete recordCopy.hash;
2720
+ const expectedHash = sha256(JSON.stringify(recordCopy));
2721
+ if (recordHash !== expectedHash) {
2722
+ const errObj = {
2723
+ index: i,
2724
+ reason: "HASH_MISMATCH",
2725
+ message: `Expected hash '${expectedHash}', got '${recordHash}'`
2726
+ };
2727
+ response.valid = false;
2728
+ response.ok = false;
2729
+ response.firstInvalidIndex ??= i;
2730
+ response.reason ??= errObj.reason;
2731
+ response.errors.push(errObj);
2732
+ if (failFast) break;
2733
+ }
2734
+ expectedPrevHash = recordHash;
2735
+ }
2736
+ response.message = response.valid ? `Audit log verified: ${response.entries} entries` : `Audit log integrity failed at index ${response.firstInvalidIndex}`;
2737
+ return response;
2738
+ } catch (err) {
2739
+ return {
2740
+ ok: false,
2741
+ file: path15.resolve(logPath),
2742
+ entries: 0,
2743
+ valid: false,
2744
+ firstInvalidIndex: null,
2745
+ reason: "AUDIT_VERIFY_ERROR",
2746
+ errors: [{ index: null, reason: "AUDIT_VERIFY_ERROR", message: err.message }],
2747
+ message: `Integrity check failed: ${err.message}`
2748
+ };
2749
+ }
2750
+ }
2751
+
2752
+ // src/core/requestRateLimiter.js
2753
+ import fs15 from "fs";
2754
+ import path16 from "path";
2755
+ var RequestRateLimiter = class {
2756
+ constructor(dbPath) {
2757
+ this.dbPath = dbPath;
2758
+ this.db = { entries: [] };
2759
+ }
2760
+ async load() {
2761
+ try {
2762
+ if (!fs15.existsSync(this.dbPath)) {
2763
+ this.db = { entries: [] };
2764
+ return;
2765
+ }
2766
+ const data = fs15.readFileSync(this.dbPath, "utf8");
2767
+ this.db = JSON.parse(data);
2768
+ if (!Array.isArray(this.db.entries)) {
2769
+ this.db = { entries: [] };
2770
+ }
2771
+ } catch {
2772
+ this.db = { entries: [] };
2773
+ }
2774
+ }
2775
+ async save() {
2776
+ const dir = path16.dirname(this.dbPath);
2777
+ if (!fs15.existsSync(dir)) {
2778
+ fs15.mkdirSync(dir, { recursive: true });
2779
+ }
2780
+ atomicWriteFileSync(this.dbPath, JSON.stringify(this.db, null, 2), { encoding: "utf8" });
2781
+ }
2782
+ checkAndRecord({ requesterId, nowSec, windowSec, maxRequests }) {
2783
+ const now = Number.isFinite(nowSec) ? nowSec : Math.floor(Date.now() / 1e3);
2784
+ const window = Number.isFinite(windowSec) && windowSec > 0 ? windowSec : 60;
2785
+ const limit = Number.isFinite(maxRequests) && maxRequests > 0 ? maxRequests : 30;
2786
+ const cutoff = now - window;
2787
+ this.db.entries = this.db.entries.filter((entry) => entry.timestamp >= cutoff);
2788
+ const requesterEntries = this.db.entries.filter((entry) => entry.requesterId === requesterId);
2789
+ if (requesterEntries.length >= limit) {
2790
+ const oldest = requesterEntries.sort((a, b) => a.timestamp - b.timestamp)[0];
2791
+ const retryAfterSec = Math.max(1, window - (now - oldest.timestamp));
2792
+ return {
2793
+ ok: false,
2794
+ reason: "RATE_LIMIT_EXCEEDED",
2795
+ message: `Rate limit exceeded for '${requesterId}' (${limit}/${window}s)`,
2796
+ retryAfterSec
2797
+ };
2798
+ }
2799
+ this.db.entries.push({ requesterId, timestamp: now });
2800
+ return {
2801
+ ok: true,
2802
+ reason: null,
2803
+ message: "Rate limit check passed",
2804
+ retryAfterSec: 0
2805
+ };
2806
+ }
2807
+ };
2808
+
2809
+ // src/core/approval-token.js
2810
+ import crypto4 from "crypto";
2811
+
2812
+ // src/core/checkpoint-store.js
2813
+ import path17 from "path";
2814
+ var CheckpointStore = class {
2815
+ constructor(dbPath) {
2816
+ this.dbPath = dbPath || path17.resolve(".lbe/data/checkpoints.db.json");
2817
+ this.store = { checkpoints: {}, tokens: {} };
2818
+ this._load();
2819
+ }
2820
+ _load() {
2821
+ const data = readJSONSafe(this.dbPath);
2822
+ if (data) {
2823
+ this.store = data;
2824
+ this.store.checkpoints = this.store.checkpoints || {};
2825
+ this.store.tokens = this.store.tokens || {};
2826
+ }
2827
+ }
2828
+ _save() {
2829
+ const jsonStr = JSON.stringify(this.store, null, 2);
2830
+ atomicWriteFileSync(this.dbPath, jsonStr, { encoding: "utf8" });
2831
+ }
2832
+ // --- Checkpoint Management ---
2833
+ saveCheckpoint(jobId, state) {
2834
+ this.store.checkpoints[jobId] = {
2835
+ jobId,
2836
+ ...state,
2837
+ updatedAt: Date.now()
2838
+ };
2839
+ this._save();
2840
+ }
2841
+ getCheckpoint(jobId) {
2842
+ return this.store.checkpoints[jobId] || null;
2843
+ }
2844
+ getAllCheckpoints() {
2845
+ return Object.values(this.store.checkpoints);
2846
+ }
2847
+ removeCheckpoint(jobId) {
2848
+ if (this.store.checkpoints[jobId]) {
2849
+ delete this.store.checkpoints[jobId];
2850
+ this._save();
2851
+ return true;
2852
+ }
2853
+ return false;
2854
+ }
2855
+ // --- Approval Token Management ---
2856
+ saveToken(tokenId, tokenData) {
2857
+ this.store.tokens[tokenId] = {
2858
+ tokenId,
2859
+ ...tokenData,
2860
+ createdAt: Date.now()
2861
+ };
2862
+ this._save();
2863
+ }
2864
+ getToken(tokenId) {
2865
+ return this.store.tokens[tokenId] || null;
2866
+ }
2867
+ getAllTokens() {
2868
+ return Object.values(this.store.tokens);
2869
+ }
2870
+ removeToken(tokenId) {
2871
+ if (this.store.tokens[tokenId]) {
2872
+ delete this.store.tokens[tokenId];
2873
+ this._save();
2874
+ return true;
2875
+ }
2876
+ return false;
2877
+ }
2878
+ };
2879
+ var instance = null;
2880
+ function getCheckpointStore(dbPath) {
2881
+ if (!instance) {
2882
+ instance = new CheckpointStore(dbPath);
2883
+ }
2884
+ return instance;
2885
+ }
2886
+
2887
+ // src/core/approval-token.js
2888
+ var ApprovalManager = class {
2889
+ constructor(storePath) {
2890
+ this.store = getCheckpointStore(storePath);
2891
+ this._pendingResolvers = /* @__PURE__ */ new Map();
2892
+ }
2893
+ /**
2894
+ * Create a new approval token and persist it
2895
+ * @param {string} jobId The workflow or job ID awaiting approval
2896
+ * @param {object} context Contextual data for the approver
2897
+ */
2898
+ createToken(jobId, context = {}) {
2899
+ const tokenId = crypto4.randomBytes(16).toString("hex");
2900
+ const tokenData = {
2901
+ jobId,
2902
+ context,
2903
+ status: "pending",
2904
+ expiresAt: Date.now() + 24 * 60 * 60 * 1e3
2905
+ // 24 hours
2906
+ };
2907
+ this.store.saveToken(tokenId, tokenData);
2908
+ return tokenId;
2909
+ }
2910
+ /**
2911
+ * Rehydrate an approval wait into memory.
2912
+ * This allows a resumed workflow to await a previously created token.
2913
+ * @param {string} tokenId The token to await
2914
+ * @returns {Promise} Resolves when approved, rejects when denied or expired
2915
+ */
2916
+ awaitApproval(tokenId) {
2917
+ const token = this.store.getToken(tokenId);
2918
+ if (!token) {
2919
+ return Promise.reject(new Error(`Approval token ${tokenId} not found`));
2920
+ }
2921
+ if (token.status !== "pending") {
2922
+ return Promise.reject(new Error(`Approval token ${tokenId} is no longer pending (status: ${token.status})`));
2923
+ }
2924
+ if (Date.now() > token.expiresAt) {
2925
+ this.store.removeToken(tokenId);
2926
+ return Promise.reject(new Error(`Approval token ${tokenId} expired`));
2927
+ }
2928
+ return new Promise((resolve, reject) => {
2929
+ this._pendingResolvers.set(tokenId, { resolve, reject });
2930
+ });
2931
+ }
2932
+ /**
2933
+ * Approve a pending token
2934
+ */
2935
+ approve(tokenId, approverData = {}) {
2936
+ const token = this.store.getToken(tokenId);
2937
+ if (!token) throw new Error("Token not found");
2938
+ if (token.status !== "pending") throw new Error("Token not pending");
2939
+ this.store.saveToken(tokenId, { ...token, status: "approved", approverData, resolvedAt: Date.now() });
2940
+ const resolver = this._pendingResolvers.get(tokenId);
2941
+ if (resolver) {
2942
+ resolver.resolve({ approved: true, approverData });
2943
+ this._pendingResolvers.delete(tokenId);
2944
+ }
2945
+ return true;
2946
+ }
2947
+ /**
2948
+ * Deny a pending token
2949
+ */
2950
+ deny(tokenId, reason = "Manually denied") {
2951
+ const token = this.store.getToken(tokenId);
2952
+ if (!token) throw new Error("Token not found");
2953
+ if (token.status !== "pending") throw new Error("Token not pending");
2954
+ this.store.saveToken(tokenId, { ...token, status: "denied", reason, resolvedAt: Date.now() });
2955
+ const resolver = this._pendingResolvers.get(tokenId);
2956
+ if (resolver) {
2957
+ resolver.reject(new Error(`Approval denied: ${reason}`));
2958
+ this._pendingResolvers.delete(tokenId);
2959
+ }
2960
+ return true;
2961
+ }
2962
+ };
2963
+ var instance2 = null;
2964
+ function getApprovalManager(storePath) {
2965
+ if (!instance2) {
2966
+ instance2 = new ApprovalManager(storePath);
2967
+ }
2968
+ return instance2;
2969
+ }
2970
+
2971
+ // src/core/localPolicy.js
2972
+ import fs16 from "fs";
2973
+ import path18 from "path";
2974
+ import crypto5 from "crypto";
2975
+ var POLICY_FILE = ".lbe/policy.json";
2976
+ var AUDIT_FILE = ".lbe/audit.jsonl";
2977
+ function glob(pattern) {
2978
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
2979
+ return new RegExp("^" + escaped.replace(/\*\*\//g, "(?:.*/)?").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$");
2980
+ }
2981
+ function relative(root, value) {
2982
+ const rel = path18.relative(root, path18.resolve(value));
2983
+ return rel.split(path18.sep).join("/");
2984
+ }
2985
+ function localPolicyPaths(rootDir) {
2986
+ const root = path18.resolve(rootDir || process.cwd());
2987
+ return { root, policyPath: path18.join(root, POLICY_FILE), auditPath: path18.join(root, AUDIT_FILE) };
2988
+ }
2989
+ function loadLocalPolicy(rootDir, mode = "observe") {
2990
+ const paths = localPolicyPaths(rootDir);
2991
+ if (!fs16.existsSync(paths.policyPath)) {
2992
+ return { ...paths, policy: { version: 1, mode, workspace: paths.root, rules: [] } };
2993
+ }
2994
+ const policy = JSON.parse(fs16.readFileSync(paths.policyPath, "utf8"));
2995
+ if (policy?.version !== 1 || !["observe", "enforce"].includes(policy.mode) || !Array.isArray(policy.rules)) {
2996
+ throw new Error(`Invalid ${POLICY_FILE}`);
2997
+ }
2998
+ return { ...paths, policy };
2999
+ }
3000
+ function writeLocalPolicy(rootDir, policy) {
3001
+ const { policyPath, root } = localPolicyPaths(rootDir);
3002
+ const next = { ...policy, version: 1, workspace: root, rules: Array.isArray(policy.rules) ? policy.rules : [] };
3003
+ atomicWriteFileSync(policyPath, JSON.stringify(next, null, 2) + "\n", { encoding: "utf8" });
3004
+ return next;
3005
+ }
3006
+ function addLocalPolicyRule(rootDir, rule, mode) {
3007
+ if (!rule || !["allow", "deny"].includes(rule.effect) || !["path", "command"].includes(rule.type) || typeof rule.pattern !== "string" || !rule.pattern || typeof rule.from !== "string" || !rule.from) {
3008
+ throw new Error("Rule requires effect, type, pattern, and from");
3009
+ }
3010
+ const loaded = loadLocalPolicy(rootDir, mode);
3011
+ const entry = {
3012
+ id: rule.id || crypto5.randomUUID(),
3013
+ effect: rule.effect,
3014
+ type: rule.type,
3015
+ pattern: rule.pattern,
3016
+ from: rule.from,
3017
+ at: rule.at || (/* @__PURE__ */ new Date()).toISOString()
3018
+ };
3019
+ writeLocalPolicy(loaded.root, { ...loaded.policy, mode: mode || loaded.policy.mode, rules: [...loaded.policy.rules, entry] });
3020
+ return { id: entry.id, added: true, rule: entry };
3021
+ }
3022
+ function evaluateLocalPolicy(policy, rootDir, { target, command } = {}) {
3023
+ const root = path18.resolve(rootDir);
3024
+ const candidates = [];
3025
+ if (target) candidates.push({ type: "path", value: relative(root, target) });
3026
+ if (command) candidates.push({ type: "command", value: command });
3027
+ const matched = policy.rules.filter((rule) => candidates.some((c) => c.type === rule.type && glob(rule.pattern).test(c.value)));
3028
+ const denied = matched.filter((rule) => rule.effect === "deny");
3029
+ return { allowed: denied.length === 0, matched, winningRules: denied.length ? denied : matched.filter((r) => r.effect === "allow"), reason: denied.length ? "LOCAL_POLICY_DENY" : null };
3030
+ }
3031
+ function auditLocalPolicy(rootDir, entry) {
3032
+ const { auditPath } = localPolicyPaths(rootDir);
3033
+ appendAudit(auditPath, { kind: "local_policy", timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
3034
+ }
3035
+
3036
+ // src/cli/commands/run.js
3037
+ function sha2562(obj) {
3038
+ return crypto6.createHash("sha256").update(JSON.stringify(obj)).digest("hex");
3039
+ }
3040
+ async function runCommand(opts) {
3041
+ const { in: inFile } = opts;
3042
+ const config = opts.config || opts.policy;
3043
+ const pubKey = opts["pub-key"];
3044
+ const keysStorePath = opts["keys-store"] || path19.resolve(".lbe/config/keys.json");
3045
+ const policySigPath = opts["policy-sig"] || path19.resolve(".lbe/config/policy.sig.json");
3046
+ const policyStatePath = opts["policy-state"] || path19.resolve(".lbe/data/policy.state.json");
3047
+ const allowUnsignedPolicy = opts["policy-unsigned-ok"] === true || String(opts["policy-unsigned-ok"]).toLowerCase() === "true";
3048
+ if (!inFile) {
3049
+ console.error("Error: --in <file> is required");
3050
+ process.exit(1);
3051
+ }
3052
+ let proposal;
3053
+ try {
3054
+ const filePath = path19.resolve(inFile);
3055
+ const content = fs17.readFileSync(filePath, "utf-8");
3056
+ proposal = JSON.parse(content);
3057
+ } catch (error) {
3058
+ console.error(JSON.stringify({
3059
+ status: "error",
3060
+ error: "INVALID_PROPOSAL_FILE",
3061
+ message: error.message
3062
+ }));
3063
+ process.exit(5);
3064
+ }
3065
+ let policy;
3066
+ try {
3067
+ const policyPath = config || path19.resolve(".lbe/config/policy.default.json");
3068
+ if (!fs17.existsSync(policyPath)) {
3069
+ console.error(JSON.stringify({
3070
+ status: "error",
3071
+ error: "MISSING_POLICY",
3072
+ message: `Policy file not found: ${policyPath}`
3073
+ }));
3074
+ process.exit(1);
3075
+ }
3076
+ const policyContent = fs17.readFileSync(policyPath, "utf-8");
3077
+ policy = JSON.parse(policyContent);
3078
+ } catch (error) {
3079
+ console.error(JSON.stringify({
3080
+ status: "error",
3081
+ error: "INVALID_POLICY",
3082
+ message: error.message
3083
+ }));
3084
+ process.exit(1);
3085
+ }
3086
+ const rootDir = process.cwd();
3087
+ let localPolicy;
3088
+ try {
3089
+ localPolicy = loadLocalPolicy(rootDir);
3090
+ } catch (error) {
3091
+ console.error(JSON.stringify({ status: "error", error: "LOCAL_POLICY_INVALID", message: error.message }));
3092
+ process.exit(1);
3093
+ }
3094
+ const localDecision = evaluateLocalPolicy(localPolicy.policy, rootDir, {
3095
+ target: proposal.payload?.target,
3096
+ command: proposal.payload?.cmd
3097
+ });
3098
+ const localBlocked = localPolicy.policy.mode === "enforce" && !localDecision.allowed;
3099
+ auditLocalPolicy(rootDir, {
3100
+ commandId: proposal.commandId || "N/A",
3101
+ requesterId: proposal.requesterId || "unknown",
3102
+ mode: localPolicy.policy.mode,
3103
+ decision: localBlocked ? "deny" : "allow",
3104
+ wouldDeny: !localDecision.allowed,
3105
+ ruleIds: localDecision.winningRules.map((rule) => rule.id)
3106
+ });
3107
+ if (localBlocked) {
3108
+ console.error(JSON.stringify({ status: "blocked", error: "LOCAL_POLICY_DENY", ruleIds: localDecision.winningRules.map((rule) => rule.id) }, null, 2));
3109
+ process.exit(2);
3110
+ }
3111
+ const keyStoreResult = loadKeysStore(keysStorePath);
3112
+ const keyStore = keyStoreResult.ok ? keyStoreResult.store : null;
3113
+ const policySigCheck = verifyPolicySignature({
3114
+ policyObj: policy,
3115
+ keyStore,
3116
+ policySigPath,
3117
+ allowUnsigned: allowUnsignedPolicy
3118
+ });
3119
+ if (!policySigCheck.ok) {
3120
+ console.error(JSON.stringify({
3121
+ status: "error",
3122
+ error: policySigCheck.reason,
3123
+ message: policySigCheck.message
3124
+ }, null, 2));
3125
+ process.exit(8);
3126
+ }
3127
+ const versionCheck = validateAndUpdatePolicyVersionState({
3128
+ policyObj: policy,
3129
+ statePath: policyStatePath,
3130
+ maxCreatedAtSkewSec: policy?.security?.maxPolicyCreatedAtSkewSec
3131
+ });
3132
+ if (!versionCheck.ok) {
3133
+ console.error(JSON.stringify({
3134
+ status: "error",
3135
+ error: versionCheck.reason,
3136
+ message: versionCheck.message
3137
+ }, null, 2));
3138
+ process.exit(8);
3139
+ }
3140
+ const nonceDb = new NonceStore(path19.resolve(".lbe/data/nonce.db.json"));
3141
+ await nonceDb.load();
3142
+ if (!keyStore && !pubKey) {
3143
+ console.error(JSON.stringify({
3144
+ status: "error",
3145
+ error: "MISSING_KEY_MATERIAL",
3146
+ message: `${keyStoreResult.message}. Provide --pub-key/--pub-key-file or create .lbe/config/keys.json`
3147
+ }));
3148
+ process.exit(1);
3149
+ }
3150
+ const rateLimiter = new RequestRateLimiter(path19.resolve(".lbe/data/rate-limit.db.json"));
3151
+ await rateLimiter.load();
3152
+ const validateResult = validateCommand({
3153
+ commandObj: proposal,
3154
+ pubKeyB64: pubKey,
3155
+ keyStore,
3156
+ nonceDb,
3157
+ policy,
3158
+ rateLimiter
3159
+ });
3160
+ if (!validateResult.valid) {
3161
+ try {
3162
+ await nonceDb.save();
3163
+ await rateLimiter.save();
3164
+ } catch {
3165
+ }
3166
+ const output2 = {
3167
+ status: "invalid",
3168
+ commandId: proposal.commandId || "N/A",
3169
+ checks: validateResult.checks,
3170
+ errors: validateResult.errors || [],
3171
+ executionResult: null
3172
+ };
3173
+ console.error(JSON.stringify(output2, null, 2));
3174
+ const auditPath2 = path19.resolve(".lbe/data/audit.log.jsonl");
3175
+ try {
3176
+ appendAudit(auditPath2, {
3177
+ commandId: proposal.commandId || "N/A",
3178
+ status: "rejected",
3179
+ requesterId: proposal.requesterId || "unknown",
3180
+ payloadHash: sha2562(proposal),
3181
+ reason: validateResult.checks,
3182
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3183
+ });
3184
+ } catch (auditErr) {
3185
+ console.error(JSON.stringify({
3186
+ status: "error",
3187
+ error: "AUDIT_WRITE_FAILED",
3188
+ message: auditErr.message
3189
+ }));
3190
+ process.exit(10);
3191
+ }
3192
+ if (validateResult.checks.schema === false) process.exit(5);
3193
+ if (validateResult.checks.signature === false) process.exit(3);
3194
+ if (validateResult.checks.nonce === false) process.exit(4);
3195
+ if (validateResult.checks.timestamp === false) process.exit(6);
3196
+ if (validateResult.checks.rateLimit === false) process.exit(7);
3197
+ if (validateResult.checks.policy === false) process.exit(2);
3198
+ process.exit(9);
3199
+ }
3200
+ const risk = validateResult.risk || "LOW";
3201
+ const adapterName = proposal.payload.adapter || "shell";
3202
+ const requesterPolicy = policy.requesters?.[proposal.requesterId];
3203
+ const approvalRule = requesterPolicy?.requireApproval;
3204
+ const approvalRequired = approvalRule === true || Array.isArray(approvalRule) && (approvalRule.includes(risk) || approvalRule.includes("*") || ["HIGH", "CRITICAL"].includes(risk) && approvalRule.includes("HIGH+"));
3205
+ if (approvalRequired) {
3206
+ const mgr = getApprovalManager();
3207
+ const tokenId = mgr.createToken(proposal.commandId, {
3208
+ requesterId: proposal.requesterId,
3209
+ adapter: adapterName,
3210
+ risk
3211
+ });
3212
+ await nonceDb.save().catch(() => {
3213
+ });
3214
+ await rateLimiter.save().catch(() => {
3215
+ });
3216
+ console.log(JSON.stringify({
3217
+ status: "approval_required",
3218
+ commandId: proposal.commandId || "N/A",
3219
+ risk,
3220
+ approvalToken: tokenId,
3221
+ message: `${risk} risk operation requires operator approval. Approve with: lbe approve --token ${tokenId}`
3222
+ }, null, 2));
3223
+ process.exit(11);
3224
+ }
3225
+ let backup = null;
3226
+ const shouldBackup = opts.backup === true || adapterName === "file";
3227
+ if (shouldBackup && proposal.payload.target) {
3228
+ try {
3229
+ backup = createBackup(path19.resolve(proposal.payload.target));
3230
+ } catch {
3231
+ }
3232
+ }
3233
+ let executionResult;
3234
+ try {
3235
+ executionResult = await executeAdapter(adapterName, proposal, policy, requesterPolicy);
3236
+ } catch (error) {
3237
+ executionResult = {
3238
+ adapter: adapterName,
3239
+ status: "error",
3240
+ error: error.message,
3241
+ exitCode: 1
3242
+ };
3243
+ }
3244
+ const executionFailed = executionResult.status === "error" || executionResult.exitCode !== 0;
3245
+ let rollbackResult = null;
3246
+ if (executionFailed && backup && opts["rollback-on-failure"] !== false) {
3247
+ try {
3248
+ rollbackResult = restoreBackup(backup);
3249
+ } catch (e) {
3250
+ rollbackResult = { restored: false, error: e.message };
3251
+ }
3252
+ }
3253
+ let postValidation = null;
3254
+ if (!executionFailed && proposal.payload.target) {
3255
+ const writeActions = ["write", "patch"];
3256
+ if (writeActions.includes(proposal.payload.action)) {
3257
+ const exists2 = fs17.existsSync(path19.resolve(proposal.payload.target));
3258
+ postValidation = { ok: exists2, check: "target_exists", target: proposal.payload.target };
3259
+ if (!exists2 && backup) {
3260
+ rollbackResult = restoreBackup(backup);
3261
+ executionResult.status = "error";
3262
+ }
3263
+ }
3264
+ }
3265
+ const auditPath = path19.resolve(".lbe/data/audit.log.jsonl");
3266
+ try {
3267
+ appendAudit(auditPath, {
3268
+ commandId: proposal.commandId || "N/A",
3269
+ status: rollbackResult?.restored ? "rolled_back" : executionResult.status || "completed",
3270
+ requesterId: proposal.requesterId || "unknown",
3271
+ payloadHash: sha2562(proposal),
3272
+ executionHash: sha2562(executionResult),
3273
+ adapter: executionResult.adapter,
3274
+ riskLevel: risk,
3275
+ exitCode: executionResult.exitCode || 0,
3276
+ rolledBack: rollbackResult?.restored || false,
3277
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
3278
+ });
3279
+ } catch (auditErr) {
3280
+ console.error(JSON.stringify({
3281
+ status: "error",
3282
+ error: "AUDIT_WRITE_FAILED",
3283
+ message: auditErr.message
3284
+ }));
3285
+ process.exit(10);
3286
+ }
3287
+ await nonceDb.save();
3288
+ await rateLimiter.save();
3289
+ const output = {
3290
+ status: executionFailed || postValidation && !postValidation.ok ? "failed" : "executed",
3291
+ commandId: proposal.commandId || "N/A",
3292
+ risk,
3293
+ checks: validateResult.checks,
3294
+ executionResult: {
3295
+ adapter: executionResult.adapter,
3296
+ status: executionResult.status || "completed",
3297
+ output: executionResult.output || executionResult.error || "",
3298
+ exitCode: executionResult.exitCode || 0
3299
+ },
3300
+ backup: backup ? { path: backup.backupPath, existed: backup.existed, hash: backup.hash } : null,
3301
+ rollback: rollbackResult,
3302
+ postValidation
3303
+ };
3304
+ console.log(JSON.stringify(output, null, 2));
3305
+ process.exit(executionResult.exitCode || 0);
3306
+ }
3307
+
3308
+ // src/cli/commands/auditVerify.js
3309
+ import path20 from "path";
3310
+ function toBoolean(value, defaultValue) {
3311
+ if (value === void 0) return defaultValue;
3312
+ if (value === true || value === false) return value;
3313
+ const normalized = String(value).trim().toLowerCase();
3314
+ if (normalized === "true" || normalized === "1" || normalized === "yes") return true;
3315
+ if (normalized === "false" || normalized === "0" || normalized === "no") return false;
3316
+ return defaultValue;
3317
+ }
3318
+ async function auditVerifyCommand(opts) {
3319
+ const auditPath = opts.audit ? path20.resolve(opts.audit) : path20.resolve(".lbe/data/audit.log.jsonl");
3320
+ const failFast = toBoolean(opts["fail-fast"], true);
3321
+ const jsonOutput = toBoolean(opts.json, true);
3322
+ const maxEntries = Number.isFinite(Number(opts.max)) ? Number(opts.max) : void 0;
3323
+ const result = verifyAuditLogIntegrity(auditPath, { failFast, maxEntries });
3324
+ if (jsonOutput) {
3325
+ console.log(JSON.stringify(result, null, 2));
3326
+ } else if (result.valid) {
3327
+ console.log(`OK: ${result.file}`);
3328
+ console.log(`Entries: ${result.entries}`);
3329
+ } else {
3330
+ console.log(`FAIL: ${result.file}`);
3331
+ console.log(`First invalid index: ${result.firstInvalidIndex}`);
3332
+ console.log(`Reason: ${result.reason}`);
3333
+ }
3334
+ process.exit(result.valid ? 0 : 8);
3335
+ }
3336
+
3337
+ // src/cli/commands/integrityCheck.js
3338
+ import path22 from "path";
3339
+
3340
+ // src/core/integrity.js
3341
+ import fs18 from "fs";
3342
+ import path21 from "path";
3343
+ import crypto7 from "crypto";
3344
+ import { fileURLToPath as fileURLToPath2 } from "url";
3345
+ var __dirname = path21.dirname(fileURLToPath2(import.meta.url));
3346
+ function sha256File(filePath) {
3347
+ try {
3348
+ const content = fs18.readFileSync(filePath);
3349
+ return crypto7.createHash("sha256").update(content).digest("hex");
3350
+ } catch (err) {
3351
+ return null;
3352
+ }
3353
+ }
3354
+ function generateIntegrityManifest(rootDir = path21.join(__dirname, "../..")) {
3355
+ const manifest = {};
3356
+ const criticalFiles = [
3357
+ "src/core/signature.js",
3358
+ "src/core/validator.js",
3359
+ "src/core/policyEngine.js",
3360
+ "src/core/nonceStore.js",
3361
+ "src/core/auditLog.js",
3362
+ "src/core/schema.js",
3363
+ "bin/lbe.js"
3364
+ ];
3365
+ for (const file of criticalFiles) {
3366
+ const filePath = path21.join(rootDir, file);
3367
+ const hash = sha256File(filePath);
3368
+ if (hash) {
3369
+ manifest[file] = hash;
3370
+ }
3371
+ }
3372
+ return manifest;
3373
+ }
3374
+ function verifyIntegrity(manifest, rootDir = path21.join(__dirname, "../..")) {
3375
+ const results = {
3376
+ valid: true,
3377
+ mismatches: [],
3378
+ missing: [],
3379
+ checkedFiles: 0
3380
+ };
3381
+ for (const [file, expectedHash] of Object.entries(manifest)) {
3382
+ const filePath = path21.join(rootDir, file);
3383
+ if (!fs18.existsSync(filePath)) {
3384
+ results.valid = false;
3385
+ results.missing.push(file);
3386
+ continue;
3387
+ }
3388
+ const actualHash = sha256File(filePath);
3389
+ results.checkedFiles++;
3390
+ if (actualHash !== expectedHash) {
3391
+ results.valid = false;
3392
+ results.mismatches.push({
3393
+ file,
3394
+ expected: expectedHash,
3395
+ actual: actualHash
3396
+ });
3397
+ }
3398
+ }
3399
+ return results;
3400
+ }
3401
+ async function performIntegrityCheck(options = {}) {
3402
+ const opts = typeof options === "string" || options === null ? { manifestPath: options } : options;
3403
+ const rootDir = opts.rootDir || path21.join(__dirname, "../..");
3404
+ const strict = opts.strict === true;
3405
+ const manifestPath = opts.manifestPath || path21.join(rootDir, ".lbe/config/integrity.manifest.json");
3406
+ if (!fs18.existsSync(manifestPath)) {
3407
+ if (strict) {
3408
+ return {
3409
+ valid: false,
3410
+ skipped: false,
3411
+ reason: "INTEGRITY_MANIFEST_MISSING",
3412
+ message: `Integrity manifest not found: ${manifestPath}`,
3413
+ checkedFiles: 0,
3414
+ mismatches: [],
3415
+ missing: []
3416
+ };
3417
+ }
3418
+ return {
3419
+ valid: true,
3420
+ skipped: true,
3421
+ reason: null,
3422
+ message: "Integrity manifest not found - check skipped",
3423
+ checkedFiles: 0,
3424
+ mismatches: [],
3425
+ missing: []
3426
+ };
3427
+ }
3428
+ try {
3429
+ const manifestContent = fs18.readFileSync(manifestPath, "utf8");
3430
+ const manifest = JSON.parse(manifestContent);
3431
+ const result = verifyIntegrity(manifest, rootDir);
3432
+ return {
3433
+ ...result,
3434
+ skipped: false,
3435
+ reason: result.valid ? null : "INTEGRITY_CHECK_FAILED",
3436
+ message: result.valid ? `Integrity check passed (${result.checkedFiles} files verified)` : "Runtime integrity check failed - system may be tampered"
3437
+ };
3438
+ } catch (err) {
3439
+ return {
3440
+ valid: false,
3441
+ skipped: false,
3442
+ reason: "INTEGRITY_CHECK_ERROR",
3443
+ message: `Integrity check error: ${err.message}`,
3444
+ checkedFiles: 0,
3445
+ mismatches: [],
3446
+ missing: []
3447
+ };
3448
+ }
3449
+ }
3450
+ function writeIntegrityManifest({
3451
+ outputPath = path21.join(__dirname, "../../.lbe/config/integrity.manifest.json"),
3452
+ rootDir = path21.join(__dirname, "../..")
3453
+ } = {}) {
3454
+ const manifest = generateIntegrityManifest(rootDir);
3455
+ const output = JSON.stringify(manifest, null, 2);
3456
+ const outDir = path21.dirname(outputPath);
3457
+ if (!fs18.existsSync(outDir)) {
3458
+ fs18.mkdirSync(outDir, { recursive: true });
3459
+ }
3460
+ fs18.writeFileSync(outputPath, output);
3461
+ return {
3462
+ outputPath,
3463
+ fileCount: Object.keys(manifest).length,
3464
+ manifest
3465
+ };
3466
+ }
3467
+
3468
+ // src/cli/commands/integrityCheck.js
3469
+ function toBoolean2(value, defaultValue = false) {
3470
+ if (value === void 0) return defaultValue;
3471
+ if (value === true || value === false) return value;
3472
+ const normalized = String(value).trim().toLowerCase();
3473
+ if (normalized === "true" || normalized === "1" || normalized === "yes") return true;
3474
+ if (normalized === "false" || normalized === "0" || normalized === "no") return false;
3475
+ return defaultValue;
3476
+ }
3477
+ async function integrityCheckCommand(opts) {
3478
+ const strict = toBoolean2(opts.strict, false) || toBoolean2(opts["integrity-strict"], false);
3479
+ const manifestPath = opts.manifest ? path22.resolve(opts.manifest) : path22.resolve(opts["integrity-manifest"] || ".lbe/config/integrity.manifest.json");
3480
+ const jsonOutput = toBoolean2(opts.json, true);
3481
+ const result = await performIntegrityCheck({
3482
+ manifestPath,
3483
+ strict
3484
+ });
3485
+ if (jsonOutput) {
3486
+ console.log(JSON.stringify({
3487
+ ok: result.valid,
3488
+ valid: result.valid,
3489
+ skipped: result.skipped === true,
3490
+ strict,
3491
+ manifestPath,
3492
+ checkedFiles: result.checkedFiles,
3493
+ mismatches: result.mismatches || [],
3494
+ missing: result.missing || [],
3495
+ reason: result.reason || null,
3496
+ message: result.message
3497
+ }, null, 2));
3498
+ } else {
3499
+ console.log(result.message);
3500
+ }
3501
+ process.exit(result.valid ? 0 : 8);
3502
+ }
3503
+ async function integrityGenerateCommand(opts) {
3504
+ const outputPath = path22.resolve(opts.out || opts.output || opts.manifest || ".lbe/config/integrity.manifest.json");
3505
+ const result = writeIntegrityManifest({ outputPath });
3506
+ console.log(JSON.stringify({
3507
+ ok: true,
3508
+ outputPath: result.outputPath,
3509
+ fileCount: result.fileCount
3510
+ }, null, 2));
3511
+ process.exit(0);
3512
+ }
3513
+
3514
+ // src/cli/commands/policySign.js
3515
+ import fs19 from "fs";
3516
+ import path23 from "path";
3517
+ async function policySignCommand(opts) {
3518
+ const policyPath = path23.resolve(opts.config || opts.policy || ".lbe/config/policy.default.json");
3519
+ const sigPath = path23.resolve(opts["policy-sig"] || ".lbe/config/policy.sig.json");
3520
+ const secretKeyPath = path23.resolve(opts["secret-key-file"] || ".lbe/keys/secret.key");
3521
+ const keyId = String(opts["policy-key-id"] || "policy-signer-v1-2026Q1");
3522
+ if (!fs19.existsSync(policyPath)) {
3523
+ console.error(JSON.stringify({
3524
+ status: "error",
3525
+ error: "POLICY_FILE_MISSING",
3526
+ message: `Policy file not found: ${policyPath}`
3527
+ }, null, 2));
3528
+ process.exit(1);
3529
+ }
3530
+ if (!fs19.existsSync(secretKeyPath)) {
3531
+ console.error(JSON.stringify({
3532
+ status: "error",
3533
+ error: "SECRET_KEY_MISSING",
3534
+ message: `Secret key file not found: ${secretKeyPath}`
3535
+ }, null, 2));
3536
+ process.exit(1);
3537
+ }
3538
+ const policyObj = JSON.parse(fs19.readFileSync(policyPath, "utf8"));
3539
+ if (typeof policyObj.version === "undefined" || typeof policyObj.createdAt === "undefined") {
3540
+ console.error(JSON.stringify({
3541
+ status: "error",
3542
+ error: "POLICY_VERSION_METADATA_MISSING",
3543
+ message: "Policy must include version and createdAt before signing"
3544
+ }, null, 2));
3545
+ process.exit(8);
3546
+ }
3547
+ const secretKeyB64 = fs19.readFileSync(secretKeyPath, "utf8").trim();
3548
+ const signResult = createPolicySignatureEnvelope({
3549
+ policyObj,
3550
+ secretKeyB64,
3551
+ keyId
3552
+ });
3553
+ if (!signResult.ok) {
3554
+ console.error(JSON.stringify({
3555
+ status: "error",
3556
+ error: signResult.reason || "POLICY_SIGN_FAILED",
3557
+ message: signResult.message
3558
+ }, null, 2));
3559
+ process.exit(8);
3560
+ }
3561
+ const outDir = path23.dirname(sigPath);
3562
+ if (!fs19.existsSync(outDir)) {
3563
+ fs19.mkdirSync(outDir, { recursive: true });
3564
+ }
3565
+ fs19.writeFileSync(sigPath, JSON.stringify(signResult.envelope, null, 2));
3566
+ console.log(JSON.stringify({
3567
+ status: "ok",
3568
+ message: "Policy signature written",
3569
+ policy: policyPath,
3570
+ policySig: sigPath,
3571
+ keyId
3572
+ }, null, 2));
3573
+ process.exit(0);
3574
+ }
3575
+
3576
+ // src/cli/commands/health.js
3577
+ import fs20 from "fs";
3578
+ import path24 from "path";
3579
+ function toBoolean3(value, defaultValue) {
3580
+ if (value === void 0) return defaultValue;
3581
+ if (value === true || value === false) return value;
3582
+ const normalized = String(value).trim().toLowerCase();
3583
+ if (normalized === "true" || normalized === "1" || normalized === "yes") return true;
3584
+ if (normalized === "false" || normalized === "0" || normalized === "no") return false;
3585
+ return defaultValue;
3586
+ }
3587
+ function addCheck(checks, name, ok, message) {
3588
+ checks[name] = { ok, message };
3589
+ }
3590
+ function canReadFile(filePath) {
3591
+ try {
3592
+ fs20.accessSync(filePath, fs20.constants.R_OK);
3593
+ return true;
3594
+ } catch {
3595
+ return false;
3596
+ }
3597
+ }
3598
+ function checkDataWritable(dataDir) {
3599
+ const marker = path24.join(dataDir, `.healthcheck-${Date.now()}`);
3600
+ try {
3601
+ fs20.mkdirSync(dataDir, { recursive: true });
3602
+ fs20.writeFileSync(marker, "ok", "utf8");
3603
+ fs20.unlinkSync(marker);
3604
+ return { ok: true, message: "Data directory writable" };
3605
+ } catch (error) {
3606
+ return { ok: false, message: `Data directory not writable: ${error.message}` };
3607
+ }
3608
+ }
3609
+ async function healthCommand(opts) {
3610
+ const jsonOutput = toBoolean3(opts.json, true);
3611
+ const policyPath = path24.resolve(opts.config || opts.policy || ".lbe/config/policy.default.json");
3612
+ const policySigPath = path24.resolve(opts["policy-sig"] || ".lbe/config/policy.sig.json");
3613
+ const keysStorePath = path24.resolve(opts["keys-store"] || ".lbe/config/keys.json");
3614
+ const dataDir = path24.resolve(opts["data-dir"] || ".lbe/data");
3615
+ const auditPath = path24.resolve(opts.audit || path24.join(dataDir, "audit.log.jsonl"));
3616
+ const noncePath = path24.resolve(opts["nonce-db"] || path24.join(dataDir, "nonce.db.json"));
3617
+ const ratePath = path24.resolve(opts["rate-db"] || path24.join(dataDir, "rate-limit.db.json"));
3618
+ const policyStatePath = path24.resolve(opts["policy-state"] || path24.join(dataDir, "policy.state.json"));
3619
+ const integrityStrict = toBoolean3(opts["integrity-strict"], false);
3620
+ const integrityManifestPath = path24.resolve(opts["integrity-manifest"] || ".lbe/config/integrity.manifest.json");
3621
+ const checks = {};
3622
+ addCheck(
3623
+ checks,
3624
+ "policy",
3625
+ fs20.existsSync(policyPath) && canReadFile(policyPath),
3626
+ fs20.existsSync(policyPath) ? `Policy file readable: ${policyPath}` : `Policy file missing: ${policyPath}`
3627
+ );
3628
+ addCheck(
3629
+ checks,
3630
+ "policySignature",
3631
+ fs20.existsSync(policySigPath) && canReadFile(policySigPath),
3632
+ fs20.existsSync(policySigPath) ? `Policy signature readable: ${policySigPath}` : `Policy signature missing: ${policySigPath}`
3633
+ );
3634
+ addCheck(
3635
+ checks,
3636
+ "trustedKeys",
3637
+ fs20.existsSync(keysStorePath) && canReadFile(keysStorePath),
3638
+ fs20.existsSync(keysStorePath) ? `Trusted keys readable: ${keysStorePath}` : `Trusted keys missing: ${keysStorePath}`
3639
+ );
3640
+ addCheck(
3641
+ checks,
3642
+ "auditLog",
3643
+ fs20.existsSync(auditPath) && canReadFile(auditPath),
3644
+ fs20.existsSync(auditPath) ? `Audit log readable: ${auditPath}` : `Audit log missing: ${auditPath}`
3645
+ );
3646
+ addCheck(
3647
+ checks,
3648
+ "nonceDb",
3649
+ fs20.existsSync(noncePath) && canReadFile(noncePath),
3650
+ fs20.existsSync(noncePath) ? `Nonce DB readable: ${noncePath}` : `Nonce DB missing: ${noncePath}`
3651
+ );
3652
+ addCheck(
3653
+ checks,
3654
+ "rateLimitDb",
3655
+ fs20.existsSync(ratePath) && canReadFile(ratePath),
3656
+ fs20.existsSync(ratePath) ? `Rate-limit DB readable: ${ratePath}` : `Rate-limit DB missing: ${ratePath}`
3657
+ );
3658
+ addCheck(
3659
+ checks,
3660
+ "policyState",
3661
+ fs20.existsSync(policyStatePath) && canReadFile(policyStatePath),
3662
+ fs20.existsSync(policyStatePath) ? `Policy state readable: ${policyStatePath}` : `Policy state missing: ${policyStatePath}`
3663
+ );
3664
+ const writable = checkDataWritable(dataDir);
3665
+ addCheck(checks, "dataWritable", writable.ok, writable.message);
3666
+ if (integrityStrict) {
3667
+ const integrity = await performIntegrityCheck({
3668
+ strict: true,
3669
+ manifestPath: integrityManifestPath
3670
+ });
3671
+ addCheck(
3672
+ checks,
3673
+ "integrity",
3674
+ integrity.valid,
3675
+ integrity.valid ? integrity.message : `${integrity.reason}: ${integrity.message}`
3676
+ );
3677
+ }
3678
+ const allOk = Object.values(checks).every((c) => c.ok === true);
3679
+ const output = {
3680
+ ok: allOk,
3681
+ status: allOk ? "healthy" : "unhealthy",
3682
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3683
+ checks
3684
+ };
3685
+ if (jsonOutput) {
3686
+ console.log(JSON.stringify(output, null, 2));
3687
+ } else {
3688
+ console.log(`${output.status.toUpperCase()}: ${Object.keys(checks).length} checks`);
3689
+ }
3690
+ process.exit(allOk ? 0 : 8);
3691
+ }
3692
+
3693
+ // src/cli/commands/policyAdd.js
3694
+ async function policyAddCommand(opts = {}) {
3695
+ const result = addLocalPolicyRule(opts.root || process.cwd(), {
3696
+ effect: opts.effect,
3697
+ type: opts.type,
3698
+ pattern: opts.pattern,
3699
+ from: opts.from
3700
+ }, opts.mode);
3701
+ console.log(JSON.stringify(result, null, 2));
3702
+ }
3703
+
3704
+ // src/cli/commands/policyMode.js
3705
+ async function policyModeCommand(mode, opts = {}) {
3706
+ const loaded = loadLocalPolicy(opts.root || process.cwd(), mode);
3707
+ writeLocalPolicy(loaded.root, { ...loaded.policy, mode });
3708
+ console.log(JSON.stringify({ mode, policy: loaded.policyPath }, null, 2));
3709
+ }
3710
+
3711
+ // src/cli/commands/status.js
3712
+ import fs25 from "node:fs";
3713
+ import path30 from "node:path";
3714
+
3715
+ // src/state/index.js
3716
+ import fs24 from "node:fs";
3717
+ import path29 from "node:path";
3718
+
3719
+ // src/state/workspaceId.js
3720
+ import crypto8 from "node:crypto";
3721
+ import fs21 from "node:fs";
3722
+ import path25 from "node:path";
3723
+ function canonicalWorkspacePath(workspaceRoot) {
3724
+ let resolved;
3725
+ try {
3726
+ resolved = fs21.realpathSync.native(workspaceRoot);
3727
+ } catch (_) {
3728
+ resolved = path25.resolve(workspaceRoot);
3729
+ }
3730
+ const normalised = path25.normalize(resolved);
3731
+ return process.platform === "win32" ? normalised.toLowerCase() : normalised;
3732
+ }
3733
+ function workspaceId(workspaceRoot) {
3734
+ return crypto8.createHash("sha256").update(canonicalWorkspacePath(workspaceRoot)).digest("hex");
3735
+ }
3736
+ function workspaceStateDir(stateRoot2, id) {
3737
+ return path25.join(stateRoot2, "workspaces", id.slice(0, 2), id.slice(2, 4), id.slice(4, 6), id);
3738
+ }
3739
+
3740
+ // src/state/stateRoot.js
3741
+ import path26 from "node:path";
3742
+ import os from "node:os";
3743
+ function stateRoot() {
3744
+ const home = os.homedir();
3745
+ if (process.platform === "win32") {
3746
+ const localAppData = process.env.LOCALAPPDATA || path26.join(home, "AppData", "Local");
3747
+ return path26.join(localAppData, "LetterBlack", "Sentinel");
3748
+ }
3749
+ if (process.platform === "darwin") {
3750
+ const appSupport = process.env.HOME ? path26.join(process.env.HOME, "Library", "Application Support") : path26.join(home, "Library", "Application Support");
3751
+ return path26.join(appSupport, "LetterBlack", "Sentinel");
3752
+ }
3753
+ const xdgData = process.env.XDG_DATA_HOME || path26.join(home, ".local", "share");
3754
+ return path26.join(xdgData, "LetterBlack", "Sentinel");
3755
+ }
3756
+
3757
+ // src/state/workspaceRegistry.js
3758
+ import fs22 from "node:fs";
3759
+ import path27 from "node:path";
3760
+ var FORMAT = 1;
3761
+ function emptyRegistry() {
3762
+ return { format: FORMAT, workspaces: {} };
3763
+ }
3764
+ function readRegistry(registryPath) {
3765
+ if (!fs22.existsSync(registryPath)) return { registry: emptyRegistry(), readable: true };
3766
+ try {
3767
+ const registry = JSON.parse(fs22.readFileSync(registryPath, "utf8"));
3768
+ if (!registry || typeof registry !== "object" || Array.isArray(registry) || registry.format !== FORMAT || !registry.workspaces || typeof registry.workspaces !== "object" || Array.isArray(registry.workspaces)) {
3769
+ return { registry: null, readable: false };
3770
+ }
3771
+ return { registry, readable: true };
3772
+ } catch (_) {
3773
+ return { registry: null, readable: false };
3774
+ }
3775
+ }
3776
+ function registerWorkspace(registryPath, workspaceId2, workspacePath) {
3777
+ return withFileLock(registryPath, () => {
3778
+ const { registry, readable } = readRegistry(registryPath);
3779
+ if (!readable) return null;
3780
+ const now = (/* @__PURE__ */ new Date()).toISOString();
3781
+ const existing = registry.workspaces[workspaceId2];
3782
+ registry.workspaces[workspaceId2] = {
3783
+ path: workspacePath,
3784
+ alias: existing?.alias || path27.basename(workspacePath),
3785
+ first_seen: existing?.first_seen || now,
3786
+ last_active: now
3787
+ };
3788
+ atomicWriteFileSync(registryPath, JSON.stringify(registry, null, 2) + "\n", "utf8");
3789
+ return registry.workspaces[workspaceId2];
3790
+ });
3791
+ }
3792
+ function listWorkspaces(registryPath) {
3793
+ const { registry, readable } = readRegistry(registryPath);
3794
+ if (!readable) return [];
3795
+ return Object.entries(registry.workspaces).map(([workspaceId2, workspace]) => ({
3796
+ workspaceId: workspaceId2,
3797
+ ...workspace
3798
+ }));
3799
+ }
3800
+ function isWorkspaceRegistryReadable(registryPath) {
3801
+ return readRegistry(registryPath).readable;
3802
+ }
3803
+
3804
+ // src/state/migration.js
3805
+ import crypto9 from "node:crypto";
3806
+ import fs23 from "node:fs";
3807
+ import path28 from "node:path";
3808
+ var SOURCE = ".lbe/events.jsonl";
3809
+ function sha2563(data) {
3810
+ return crypto9.createHash("sha256").update(data).digest("hex");
3811
+ }
3812
+ function readMarker(markerPath) {
3813
+ try {
3814
+ return JSON.parse(fs23.readFileSync(markerPath, "utf8"));
3815
+ } catch (_) {
3816
+ return null;
3817
+ }
3818
+ }
3819
+ function readCentralLines(eventsPath) {
3820
+ try {
3821
+ return new Set(fs23.readFileSync(eventsPath, "utf8").split(/\r?\n/).filter(Boolean));
3822
+ } catch (_) {
3823
+ return /* @__PURE__ */ new Set();
3824
+ }
3825
+ }
3826
+ function migrateLegacyEvents(workspaceRoot, stateDir) {
3827
+ const sourcePath = path28.join(workspaceRoot, ".lbe", "events.jsonl");
3828
+ const migrationDir = path28.join(stateDir, "migration");
3829
+ const markerPath = path28.join(migrationDir, "events-v1.json");
3830
+ const invalidPath = path28.join(migrationDir, "migration-invalid.jsonl");
3831
+ const result = {
3832
+ attempted: false,
3833
+ imported_count: 0,
3834
+ skipped_duplicate_count: 0,
3835
+ invalid_count: 0,
3836
+ markerPath,
3837
+ invalidPath
3838
+ };
3839
+ let source;
3840
+ try {
3841
+ if (!fs23.existsSync(sourcePath)) return result;
3842
+ source = fs23.readFileSync(sourcePath, "utf8");
3843
+ } catch (_) {
3844
+ return result;
3845
+ }
3846
+ result.attempted = true;
3847
+ const sourceSha256 = sha2563(source);
3848
+ const marker = readMarker(markerPath);
3849
+ if (marker?.format === 1 && marker.source_sha256 === sourceSha256) return result;
3850
+ const validLines = [];
3851
+ const invalidLines = [];
3852
+ for (const [index, rawLine] of source.split(/\r?\n/).entries()) {
3853
+ const line = rawLine.trim();
3854
+ if (!line) continue;
3855
+ try {
3856
+ JSON.parse(line);
3857
+ validLines.push(line);
3858
+ } catch (_) {
3859
+ invalidLines.push({ source: SOURCE, line_number: index + 1, line, source_sha256: sourceSha256 });
3860
+ }
3861
+ }
3862
+ const eventsPath = path28.join(stateDir, "lbe-events.jsonl");
3863
+ try {
3864
+ withFileLock(eventsPath, () => {
3865
+ const centralLines = readCentralLines(eventsPath);
3866
+ const imported = [];
3867
+ for (const line of validLines) {
3868
+ if (centralLines.has(line)) {
3869
+ result.skipped_duplicate_count++;
3870
+ continue;
3871
+ }
3872
+ centralLines.add(line);
3873
+ imported.push(line);
3874
+ result.imported_count++;
3875
+ }
3876
+ if (imported.length > 0) {
3877
+ const existing = fs23.existsSync(eventsPath) ? fs23.readFileSync(eventsPath, "utf8") : "";
3878
+ atomicWriteFileSync(eventsPath, existing + (existing && !existing.endsWith("\n") ? "\n" : "") + imported.join("\n") + "\n", "utf8");
3879
+ }
3880
+ });
3881
+ if (invalidLines.length > 0) {
3882
+ atomicAppendFileSync(invalidPath, invalidLines.map((line) => JSON.stringify(line) + "\n").join(""), { encoding: "utf8" });
3883
+ result.invalid_count = invalidLines.length;
3884
+ }
3885
+ fs23.mkdirSync(migrationDir, { recursive: true });
3886
+ atomicWriteFileSync(markerPath, JSON.stringify({
3887
+ format: 1,
3888
+ source: SOURCE,
3889
+ source_sha256: sourceSha256,
3890
+ migrated_at: (/* @__PURE__ */ new Date()).toISOString(),
3891
+ imported_count: result.imported_count,
3892
+ skipped_duplicate_count: result.skipped_duplicate_count,
3893
+ invalid_count: result.invalid_count
3894
+ }, null, 2) + "\n", "utf8");
3895
+ } catch (_) {
3896
+ }
3897
+ return result;
3898
+ }
3899
+
3900
+ // src/state/index.js
3901
+ function resolveWorkspaceState(workspaceRoot) {
3902
+ const root = stateRoot();
3903
+ const id = workspaceId(workspaceRoot);
3904
+ const dir = workspaceStateDir(root, id);
3905
+ fs24.mkdirSync(dir, { recursive: true });
3906
+ fs24.mkdirSync(path29.join(dir, "file-index"), { recursive: true });
3907
+ fs24.mkdirSync(path29.join(dir, "proof"), { recursive: true });
3908
+ registerWorkspace(path29.join(root, "registry.json"), id, workspaceRoot);
3909
+ migrateLegacyEvents(workspaceRoot, dir);
3910
+ return {
3911
+ stateDir: dir,
3912
+ workspaceId: id,
3913
+ paths: buildPaths(dir)
3914
+ };
3915
+ }
3916
+ function buildPaths(dir) {
3917
+ return {
3918
+ workspace: path29.join(dir, "workspace.json"),
3919
+ events: path29.join(dir, "lbe-events.jsonl"),
3920
+ intent: path29.join(dir, "intent.jsonl"),
3921
+ targetRegistry: path29.join(dir, "target_registry.jsonl"),
3922
+ fileIndexDir: path29.join(dir, "file-index"),
3923
+ fileIndexBefore: path29.join(dir, "file-index", "before.json"),
3924
+ fileIndexAfter: path29.join(dir, "file-index", "after.json"),
3925
+ proofDir: path29.join(dir, "proof"),
3926
+ proofLatest: path29.join(dir, "proof", "latest.json")
3927
+ };
3928
+ }
3929
+
3930
+ // src/cli/commands/status.js
3931
+ async function statusCommand(opts) {
3932
+ if (opts.all) {
3933
+ const registryPath = opts.registryPath || path30.join(stateRoot(), "registry.json");
3934
+ if (!isWorkspaceRegistryReadable(registryPath)) {
3935
+ console.log("Workspace registry unreadable");
3936
+ return { workspaces: [], registryReadable: false };
3937
+ }
3938
+ const workspaces = listWorkspaces(registryPath);
3939
+ if (workspaces.length === 0) {
3940
+ console.log("No known workspaces yet");
3941
+ return { workspaces, registryReadable: true };
3942
+ }
3943
+ console.log("\nKnown LBE workspaces");
3944
+ for (const workspace of workspaces) {
3945
+ console.log(` ${workspace.alias}`);
3946
+ console.log(` workspace_id ${workspace.workspaceId}`);
3947
+ console.log(` path ${workspace.path}`);
3948
+ console.log(` last_active ${workspace.last_active}`);
3949
+ }
3950
+ console.log("");
3951
+ return { workspaces, registryReadable: true };
3952
+ }
3953
+ const workspaceRoot = path30.resolve(opts.root || process.cwd());
3954
+ const { stateDir, workspaceId: workspaceId2, paths } = resolveWorkspaceState(workspaceRoot);
3955
+ const policyPath = path30.join(workspaceRoot, ".lbe", "policy.json");
3956
+ let policySource = "not found";
3957
+ let policyMode = "unknown";
3958
+ if (fs25.existsSync(policyPath)) {
3959
+ try {
3960
+ const policy = JSON.parse(fs25.readFileSync(policyPath, "utf8"));
3961
+ policyMode = policy.mode || "unknown";
3962
+ policySource = policyPath;
3963
+ } catch (_) {
3964
+ policySource = policyPath + " (unreadable)";
3965
+ }
3966
+ }
3967
+ const hasProof = fs25.existsSync(paths.proofLatest);
3968
+ const hasEvents = fs25.existsSync(paths.events);
3969
+ console.log(`
3970
+ LBE Central State \u2014 ${workspaceRoot}`);
3971
+ console.log(` workspace_id ${workspaceId2}`);
3972
+ console.log(` state_dir ${stateDir}`);
3973
+ console.log(` policy_source ${policySource}`);
3974
+ console.log(` policy_mode ${policyMode}`);
3975
+ console.log(` central_proof ${hasProof ? paths.proofLatest : "No central proof yet"}`);
3976
+ console.log(` central_logs ${hasEvents ? paths.events : "No central logs yet. Hook dual-write not enabled."}`);
3977
+ console.log("");
3978
+ return { workspaceId: workspaceId2, stateDir, policySource, policyMode, hasProof, hasEvents };
3979
+ }
3980
+
3981
+ // src/cli/commands/logs.js
3982
+ import fs26 from "node:fs";
3983
+ import path31 from "node:path";
3984
+ var DEFAULT_LIMIT = 20;
3985
+ async function logsCommand(opts) {
3986
+ const workspaceRoot = path31.resolve(opts.root || process.cwd());
3987
+ const { paths } = resolveWorkspaceState(workspaceRoot);
3988
+ const limit = opts.limit ? parseInt(opts.limit, 10) : DEFAULT_LIMIT;
3989
+ if (!fs26.existsSync(paths.events)) {
3990
+ console.log("\nLBE Central Logs");
3991
+ console.log(" No central logs yet. Hook dual-write not enabled.");
3992
+ console.log(` Expected at: ${paths.events}`);
3993
+ console.log("");
3994
+ return { eventsPath: paths.events, count: 0, entries: [], missing: true };
3995
+ }
3996
+ const raw = fs26.readFileSync(paths.events, "utf8").trim();
3997
+ const lines = raw ? raw.split("\n") : [];
3998
+ const entries = lines.map((line) => {
3999
+ try {
4000
+ return JSON.parse(line);
4001
+ } catch (_) {
4002
+ return null;
4003
+ }
4004
+ }).filter(Boolean);
4005
+ const tail = entries.slice(-limit);
4006
+ console.log(`
4007
+ LBE Central Logs \u2014 last ${tail.length} of ${entries.length} entries`);
4008
+ console.log(` source: ${paths.events}
4009
+ `);
4010
+ for (const entry of tail) {
4011
+ const ts = entry.ts ? new Date(entry.ts * 1e3).toISOString() : "?";
4012
+ const action = entry.action || "?";
4013
+ const dec = entry.decision || "?";
4014
+ const target = entry.path || entry.cmd || "";
4015
+ console.log(` [${ts}] ${dec.toUpperCase().padEnd(5)} ${action} ${target}`);
4016
+ }
4017
+ console.log("");
4018
+ return { eventsPath: paths.events, count: entries.length, entries: tail, missing: false };
4019
+ }
4020
+
4021
+ // src/cli/commands/openState.js
4022
+ import { spawnSync as spawnSync2 } from "node:child_process";
4023
+ import path32 from "node:path";
4024
+ async function openStateCommand(opts) {
4025
+ const workspaceRoot = path32.resolve(opts.root || process.cwd());
4026
+ const { stateDir } = resolveWorkspaceState(workspaceRoot);
4027
+ console.log(`
4028
+ LBE Central State Directory`);
4029
+ console.log(` ${stateDir}
4030
+ `);
4031
+ if (process.env.LBE_NO_OPEN === "1") {
4032
+ return { stateDir, opened: false };
4033
+ }
4034
+ let opened = false;
4035
+ try {
4036
+ if (process.platform === "win32") {
4037
+ spawnSync2("explorer.exe", [stateDir], { detached: true, stdio: "ignore" });
4038
+ opened = true;
4039
+ } else if (process.platform === "darwin") {
4040
+ spawnSync2("open", [stateDir], { detached: true, stdio: "ignore" });
4041
+ opened = true;
4042
+ } else {
4043
+ spawnSync2("xdg-open", [stateDir], { detached: true, stdio: "ignore" });
4044
+ opened = true;
4045
+ }
4046
+ } catch (_) {
4047
+ }
4048
+ return { stateDir, opened };
4049
+ }
4050
+
4051
+ // src/cli/commands/proof.js
4052
+ import fs28 from "node:fs";
4053
+ import path34 from "node:path";
4054
+ import os2 from "node:os";
4055
+
4056
+ // src/state/proofRunner.js
4057
+ import fs27 from "node:fs";
4058
+ import path33 from "node:path";
4059
+ function loadJson(filePath) {
4060
+ if (!filePath || !fs27.existsSync(filePath)) return null;
4061
+ try {
4062
+ return JSON.parse(fs27.readFileSync(filePath, "utf8"));
4063
+ } catch (_) {
4064
+ return null;
4065
+ }
4066
+ }
4067
+ function loadLatestProof(stateDir) {
4068
+ return loadJson(path33.join(stateDir, "proof", "latest.json"));
4069
+ }
4070
+
4071
+ // src/cli/commands/proof.js
4072
+ function buildPublicProof(proof, targets) {
4073
+ const lastTarget = Array.isArray(targets) && targets.length > 0 ? targets[targets.length - 1] : null;
4074
+ const pub = {
4075
+ result: proof.result,
4076
+ profile: proof.profile,
4077
+ checks: proof.checks_run || [],
4078
+ allow_deny: proof.failures && proof.failures.length > 0 ? "deny" : "allow"
4079
+ };
4080
+ if (lastTarget) {
4081
+ pub.target_type = lastTarget.kind || null;
4082
+ pub.target_label = lastTarget.label || null;
4083
+ pub.target_file = lastTarget.component_file || null;
4084
+ }
4085
+ if (proof.failures && proof.failures.length > 0) {
4086
+ pub.failure_reasons = proof.failures.map((f) => ({ check: f.check, reason: f.reason }));
4087
+ }
4088
+ return pub;
4089
+ }
4090
+ async function proofCommand(opts) {
4091
+ const workspaceRoot = path34.resolve(opts.root || process.cwd());
4092
+ const { stateDir, workspaceId: workspaceId2, paths } = resolveWorkspaceState(workspaceRoot);
4093
+ const proof = loadLatestProof(stateDir);
4094
+ const isPublic = opts.public === true || opts.public === "true";
4095
+ const isJson = opts.json === true || opts.json === "true" || isPublic;
4096
+ if (!proof) {
4097
+ if (isJson) {
4098
+ console.log(JSON.stringify({ found: false, message: "No proof record found. Run lbe proof after using the hook." }, null, 2));
4099
+ } else {
4100
+ console.log("\nNo proof record found.");
4101
+ console.log("Use the hook-protected workflow and then run: lbe proof\n");
4102
+ }
4103
+ return { found: false };
4104
+ }
4105
+ let targets = [];
4106
+ if (isPublic) {
4107
+ const targetPath = path34.join(stateDir, "target_registry.jsonl");
4108
+ if (fs28.existsSync(targetPath)) {
4109
+ const raw = fs28.readFileSync(targetPath, "utf8").trim();
4110
+ targets = raw ? raw.split("\n").reduce((acc, l) => {
4111
+ try {
4112
+ acc.push(JSON.parse(l));
4113
+ } catch (_) {
4114
+ }
4115
+ return acc;
4116
+ }, []) : [];
4117
+ }
4118
+ }
4119
+ if (isPublic) {
4120
+ const pub = buildPublicProof(proof, targets);
4121
+ console.log(JSON.stringify(pub, null, 2));
4122
+ return { found: true, result: proof.result, profile: proof.profile, public: true };
4123
+ }
4124
+ if (isJson) {
4125
+ console.log(JSON.stringify(proof, null, 2));
4126
+ return { found: true, result: proof.result, profile: proof.profile };
4127
+ }
4128
+ const resultMark = proof.result === "PASS" ? "\u2713" : proof.result === "WEAK_PROOF" ? "\u26A0" : "\u2717";
4129
+ const workspace = path34.basename(workspaceRoot);
4130
+ console.log(`
4131
+ LBE Proof \u2014 ${workspace}`);
4132
+ console.log(` Result ${resultMark} ${proof.result}`);
4133
+ console.log(` Profile ${proof.profile}`);
4134
+ console.log(` Changed files ${(proof.files_changed || []).length}`);
4135
+ console.log(` Checks run ${(proof.checks_run || []).join(", ")}`);
4136
+ if (proof.failures && proof.failures.length > 0) {
4137
+ console.log(` Failures ${proof.failures.length}`);
4138
+ for (const f of proof.failures) {
4139
+ console.log(` \u2022 [${f.check}] ${f.reason}${f.file ? ": " + f.file : ""}`);
4140
+ }
4141
+ }
4142
+ console.log(` Recorded at ${proof.ts}`);
4143
+ console.log("");
4144
+ return { found: true, result: proof.result, profile: proof.profile };
4145
+ }
4146
+
4147
+ // src/cli/commands/assertConsumer.js
4148
+ import fs29 from "fs";
4149
+ import path35 from "path";
4150
+ import { createRequire } from "module";
4151
+ var PACKAGE_NAME = "@letterblack/lbe-core";
4152
+ var DEP_SECTIONS = [
4153
+ "dependencies",
4154
+ "devDependencies",
4155
+ "optionalDependencies",
4156
+ "peerDependencies"
4157
+ ];
4158
+ function readJson(filePath) {
4159
+ try {
4160
+ return JSON.parse(fs29.readFileSync(filePath, "utf-8"));
4161
+ } catch {
4162
+ return null;
4163
+ }
4164
+ }
4165
+ function addCheck2(checks, name, ok, details = {}) {
4166
+ checks.push({ name, ok, ...details });
4167
+ return ok;
4168
+ }
4169
+ function findDependencySpec(packageJson2) {
4170
+ if (!packageJson2 || typeof packageJson2 !== "object") return null;
4171
+ for (const section of DEP_SECTIONS) {
4172
+ const value = packageJson2[section]?.[PACKAGE_NAME];
4173
+ if (value !== void 0) {
4174
+ return { section, spec: String(value) };
4175
+ }
4176
+ }
4177
+ return null;
4178
+ }
4179
+ function classifyDependencySpec(spec) {
4180
+ const raw = String(spec || "").trim();
4181
+ const lower = raw.toLowerCase();
4182
+ if (!raw) {
4183
+ return { ok: false, reason: "EMPTY_DEPENDENCY_SPEC" };
4184
+ }
4185
+ const blockedPrefixes = [
4186
+ "file:",
4187
+ "link:",
4188
+ "workspace:",
4189
+ "git+",
4190
+ "github:",
4191
+ "git://",
4192
+ "ssh://"
4193
+ ];
4194
+ for (const prefix of blockedPrefixes) {
4195
+ if (lower.startsWith(prefix)) {
4196
+ return { ok: false, reason: "LOCAL_OR_GIT_DEPENDENCY_SPEC", prefix };
4197
+ }
4198
+ }
4199
+ if (/^[a-z]:[\\/]/i.test(raw) || raw.startsWith("./") || raw.startsWith("../")) {
4200
+ return { ok: false, reason: "PATH_DEPENDENCY_SPEC" };
4201
+ }
4202
+ return { ok: true };
4203
+ }
4204
+ function findPackageRoot(startFile) {
4205
+ let dir = fs29.statSync(startFile).isDirectory() ? startFile : path35.dirname(startFile);
4206
+ while (true) {
4207
+ const candidate = path35.join(dir, "package.json");
4208
+ const pkg = readJson(candidate);
4209
+ if (pkg?.name === PACKAGE_NAME) return dir;
4210
+ const parent = path35.dirname(dir);
4211
+ if (parent === dir) return null;
4212
+ dir = parent;
4213
+ }
4214
+ }
4215
+ function inspectPackageLock(root) {
4216
+ const lockPath = path35.join(root, "package-lock.json");
4217
+ const lock = readJson(lockPath);
4218
+ if (!lock) return { present: false };
4219
+ const packageEntry = lock.packages?.[`node_modules/${PACKAGE_NAME}`];
4220
+ if (!packageEntry) return { present: true, packageEntryPresent: false };
4221
+ const resolved = String(packageEntry.resolved || "");
4222
+ const blockedResolved = packageEntry.link === true || resolved.startsWith("file:") || resolved.startsWith("git+") || resolved.startsWith("github:") || resolved.startsWith("ssh://") || /^[a-z]:[\\/]/i.test(resolved);
4223
+ return {
4224
+ present: true,
4225
+ packageEntryPresent: true,
4226
+ link: packageEntry.link === true,
4227
+ resolved: resolved || null,
4228
+ blockedResolved
4229
+ };
4230
+ }
4231
+ function getInstalledPackagePath(root) {
4232
+ return path35.join(root, "node_modules", ...PACKAGE_NAME.split("/"));
4233
+ }
4234
+ async function assertConsumerCommand(opts = {}) {
4235
+ const root = path35.resolve(opts.root || process.cwd());
4236
+ const packageJsonPath2 = path35.join(root, "package.json");
4237
+ const packageJson2 = readJson(packageJsonPath2);
4238
+ const checks = [];
4239
+ addCheck2(checks, "project-package-json-present", Boolean(packageJson2), { path: packageJsonPath2 });
4240
+ const dependency = findDependencySpec(packageJson2);
4241
+ addCheck2(checks, "declares-lbe-package-dependency", Boolean(dependency), dependency || {});
4242
+ const specCheck = dependency ? classifyDependencySpec(dependency.spec) : { ok: false, reason: "DEPENDENCY_NOT_DECLARED" };
4243
+ addCheck2(checks, "dependency-spec-is-registry-package", specCheck.ok, {
4244
+ spec: dependency?.spec || null,
4245
+ reason: specCheck.reason || null,
4246
+ blockedPrefix: specCheck.prefix || null
4247
+ });
4248
+ let resolvedEntry = null;
4249
+ let resolvedRoot = null;
4250
+ try {
4251
+ const req = createRequire(path35.join(root, "package.json"));
4252
+ resolvedEntry = req.resolve(PACKAGE_NAME);
4253
+ resolvedRoot = findPackageRoot(resolvedEntry);
4254
+ } catch (error) {
4255
+ addCheck2(checks, "lbe-package-resolves-from-project", false, { message: error.message });
4256
+ }
4257
+ if (resolvedEntry) {
4258
+ addCheck2(checks, "lbe-package-resolves-from-project", true, { resolvedEntry });
4259
+ }
4260
+ if (resolvedRoot) {
4261
+ const stat = fs29.lstatSync(resolvedRoot);
4262
+ addCheck2(checks, "lbe-package-is-not-symlink", !stat.isSymbolicLink(), { resolvedRoot });
4263
+ } else {
4264
+ addCheck2(checks, "lbe-package-is-not-symlink", false, { reason: "PACKAGE_ROOT_NOT_FOUND" });
4265
+ }
4266
+ const installedPackagePath = getInstalledPackagePath(root);
4267
+ if (fs29.existsSync(installedPackagePath)) {
4268
+ const installedStat = fs29.lstatSync(installedPackagePath);
4269
+ addCheck2(checks, "installed-node_modules-package-is-not-symlink", !installedStat.isSymbolicLink(), {
4270
+ installedPackagePath
4271
+ });
4272
+ } else {
4273
+ addCheck2(checks, "installed-node_modules-package-is-not-symlink", false, {
4274
+ installedPackagePath,
4275
+ reason: "PACKAGE_NOT_INSTALLED_LOCALLY"
4276
+ });
4277
+ }
4278
+ const lockInfo = inspectPackageLock(root);
4279
+ if (lockInfo.present && lockInfo.packageEntryPresent) {
4280
+ addCheck2(checks, "package-lock-does-not-link-local-lbe", !lockInfo.blockedResolved, lockInfo);
4281
+ } else {
4282
+ addCheck2(checks, "package-lock-does-not-link-local-lbe", true, lockInfo);
4283
+ }
4284
+ const ok = checks.every((check) => check.ok);
4285
+ const result = {
4286
+ ok,
4287
+ command: "assert-consumer",
4288
+ package: PACKAGE_NAME,
4289
+ root,
4290
+ classification: ok ? "consumer-project-using-installed-registry-dependency" : "not-proven-consumer-installed-dependency",
4291
+ releaseClaimsAllowed: false,
4292
+ message: ok ? "This project consumes LBE as an installed package dependency. This does not certify LBE release safety." : "This project is not proven to consume LBE only as an installed registry dependency.",
4293
+ checks
4294
+ };
4295
+ console.log(JSON.stringify(result, null, 2));
4296
+ if (!ok) {
4297
+ process.exitCode = 7;
4298
+ }
4299
+ }
4300
+
4301
+ // src/cli/main.js
4302
+ function toBoolean4(value, defaultValue = false) {
4303
+ if (value === void 0) return defaultValue;
4304
+ if (value === true || value === false) return value;
4305
+ const normalized = String(value).trim().toLowerCase();
4306
+ if (normalized === "true" || normalized === "1" || normalized === "yes") return true;
4307
+ if (normalized === "false" || normalized === "0" || normalized === "no") return false;
4308
+ return defaultValue;
4309
+ }
4310
+ var __dirname2 = path36.dirname(fileURLToPath3(import.meta.url));
4311
+ var packageJsonPath = path36.join(__dirname2, "../../package.json");
4312
+ var packageJson = JSON.parse(fs30.readFileSync(packageJsonPath, "utf-8"));
4313
+ async function main() {
4314
+ const argv = process.argv.slice(2);
4315
+ if (argv.includes("--version")) {
4316
+ console.log(`LetterBlack Sentinel v${packageJson.version}`);
4317
+ process.exit(0);
4318
+ }
4319
+ if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
4320
+ printHelp(packageJson.version);
4321
+ process.exit(0);
4322
+ }
4323
+ const { command, opts } = parseArgs(argv);
4324
+ if (opts.version) {
4325
+ console.log(`LetterBlack Sentinel v${packageJson.version}`);
4326
+ process.exit(0);
4327
+ }
4328
+ if (opts.help || !command || command === "help") {
4329
+ printHelp(packageJson.version);
4330
+ process.exit(0);
4331
+ }
4332
+ try {
4333
+ if (opts["pub-key-file"]) {
4334
+ try {
4335
+ opts["pub-key"] = fs30.readFileSync(path36.resolve(opts["pub-key-file"]), "utf-8").trim();
4336
+ } catch (error) {
4337
+ console.error(`Error reading public key file: ${error.message}`);
4338
+ process.exit(1);
4339
+ }
4340
+ }
4341
+ if (["verify", "dryrun", "run"].includes(command)) {
4342
+ const integrityStrict = toBoolean4(opts["integrity-strict"], false);
4343
+ const integrityManifestPath = path36.resolve(opts["integrity-manifest"] || ".lbe/config/integrity.manifest.json");
4344
+ const integrityResult = await performIntegrityCheck({
4345
+ strict: integrityStrict,
4346
+ manifestPath: integrityManifestPath
4347
+ });
4348
+ if (!integrityResult.valid) {
4349
+ console.error(JSON.stringify({
4350
+ status: "error",
4351
+ error: integrityResult.reason || "INTEGRITY_CHECK_FAILED",
4352
+ message: integrityResult.message
4353
+ }, null, 2));
4354
+ process.exit(8);
4355
+ }
4356
+ }
4357
+ switch (command) {
4358
+ case "init":
4359
+ await initCommand(opts);
4360
+ break;
4361
+ case "verify":
4362
+ await verifyCommand(opts);
4363
+ break;
4364
+ case "dryrun":
4365
+ await dryrunCommand(opts);
4366
+ break;
4367
+ case "run":
4368
+ await runCommand(opts);
4369
+ break;
4370
+ case "audit-verify":
4371
+ await auditVerifyCommand(opts);
4372
+ break;
4373
+ case "integrity-check":
4374
+ await integrityCheckCommand(opts);
4375
+ break;
4376
+ case "integrity-generate":
4377
+ await integrityGenerateCommand(opts);
4378
+ break;
4379
+ case "policy-sign":
4380
+ await policySignCommand(opts);
4381
+ break;
4382
+ case "health":
4383
+ await healthCommand(opts);
4384
+ break;
4385
+ case "policy-add":
4386
+ await policyAddCommand(opts);
4387
+ break;
4388
+ case "observe":
4389
+ case "enforce":
4390
+ await policyModeCommand(command, opts);
4391
+ break;
4392
+ case "status":
4393
+ await statusCommand(opts);
4394
+ break;
4395
+ case "logs":
4396
+ await logsCommand(opts);
4397
+ break;
4398
+ case "open-state":
4399
+ await openStateCommand(opts);
4400
+ break;
4401
+ case "proof":
4402
+ await proofCommand(opts);
4403
+ break;
4404
+ case "assert-consumer":
4405
+ await assertConsumerCommand(opts);
4406
+ break;
4407
+ default:
4408
+ console.error(`Unknown command: ${command}`);
4409
+ printHelp(packageJson.version);
4410
+ process.exit(1);
4411
+ }
4412
+ } catch (error) {
4413
+ console.error(JSON.stringify({
4414
+ status: "error",
4415
+ error: "INTERNAL_ERROR",
4416
+ message: error.message,
4417
+ stack: process.env.DEBUG ? error.stack : void 0
4418
+ }));
4419
+ process.exit(9);
4420
+ }
4421
+ }
4422
+ main().catch((error) => {
4423
+ console.error(JSON.stringify({
4424
+ status: "error",
4425
+ error: "FATAL_ERROR",
4426
+ message: error.message
4427
+ }));
4428
+ process.exit(9);
4429
+ });
4430
+ export {
4431
+ main
4432
+ };