@letterblack/lbe-core 1.3.0

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