@letterblack/lbe-core 1.3.4 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.githooks/pre-commit +2 -0
  2. package/.githooks/pre-push +2 -0
  3. package/CHANGELOG.md +81 -0
  4. package/LICENSE +1 -1
  5. package/README.md +158 -170
  6. package/RELEASE_WORKSPACE_RULES.md +179 -0
  7. package/Release-README.md +67 -0
  8. package/WORKSPACE.md +422 -0
  9. package/_proof.mjs +246 -0
  10. package/assets/runtime-boundary.svg +36 -36
  11. package/bin/lbe.js +12 -0
  12. package/config/identity.config.json +3 -0
  13. package/config/policy.default.json +24 -0
  14. package/dist/cli/lbe.js +4431 -0
  15. package/dist/hooks/register.cjs +505 -0
  16. package/dist/state/appendCentral.cjs +87 -0
  17. package/dist/state/index.cjs +101 -0
  18. package/exec/cli.js +472 -0
  19. package/exec/index.js +2 -0
  20. package/index.js +24 -0
  21. package/npm-pack.json +0 -0
  22. package/package.json +77 -45
  23. package/release/README.md +216 -0
  24. package/release/TRUST.md +90 -0
  25. package/release/exec-README.md +215 -0
  26. package/release/exec-types.d.ts +50 -0
  27. package/release-exec/LICENSE +1 -0
  28. package/release-exec/README.md +215 -0
  29. package/release-exec/assets/lbe-gates.jpg +0 -0
  30. package/release-exec/assets/lbe-gates.png +0 -0
  31. package/release-exec/assets/runtime-boundary.svg +36 -0
  32. package/release-exec/assets/story-allow.jpg +0 -0
  33. package/release-exec/assets/story-allow.png +0 -0
  34. package/release-exec/assets/story-deny.jpg +0 -0
  35. package/release-exec/assets/story-deny.png +0 -0
  36. package/release-exec/dist/cli.js +2841 -0
  37. package/release-exec/dist/index.js +1835 -0
  38. package/release-exec/dist/lbe_engine.wasm +0 -0
  39. package/{dist → release-exec/dist}/wasm.lock.json +4 -5
  40. package/release-exec/hooks/register.cjs +473 -0
  41. package/release-exec/package.json +35 -0
  42. package/release-exec/types.d.ts +50 -0
  43. package/runtime/engine.js +322 -0
  44. package/runtime/lbe_engine.wasm +0 -0
  45. package/src/cli/commands/assertConsumer.js +198 -0
  46. package/src/cli/commands/auditVerify.js +36 -0
  47. package/src/cli/commands/dryrun.js +175 -0
  48. package/src/cli/commands/health.js +153 -0
  49. package/src/cli/commands/init.js +306 -0
  50. package/src/cli/commands/integrityCheck.js +57 -0
  51. package/src/cli/commands/logs.js +53 -0
  52. package/src/cli/commands/openState.js +44 -0
  53. package/src/cli/commands/policyAdd.js +8 -0
  54. package/src/cli/commands/policyMode.js +7 -0
  55. package/src/cli/commands/policySign.js +72 -0
  56. package/src/cli/commands/proof.js +102 -0
  57. package/src/cli/commands/run.js +342 -0
  58. package/src/cli/commands/status.js +73 -0
  59. package/src/cli/commands/verify.js +144 -0
  60. package/src/cli/main.js +181 -0
  61. package/src/cli/parseArgs.js +115 -0
  62. package/src/exec/localExecutor.js +289 -0
  63. package/src/hooks/register.cjs +505 -0
  64. package/src/state/appendCentral.cjs +87 -0
  65. package/src/state/fileIndex.js +140 -0
  66. package/src/state/index.cjs +101 -0
  67. package/src/state/index.js +65 -0
  68. package/src/state/intentRegistry.js +84 -0
  69. package/src/state/migration.js +112 -0
  70. package/src/state/proofRunner.js +246 -0
  71. package/src/state/stateRoot.js +40 -0
  72. package/src/state/targetRegistry.js +109 -0
  73. package/src/state/workspaceId.js +40 -0
  74. package/src/state/workspaceRegistry.js +65 -0
  75. package/types.d.ts +175 -2
  76. package/dist/cli.js +0 -141
  77. package/dist/index.js +0 -52
  78. /package/dist/{lbe_engine.wasm → cli/lbe_engine.wasm} +0 -0
@@ -0,0 +1,53 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolveWorkspaceState } from '../../state/index.js';
4
+
5
+ const DEFAULT_LIMIT = 20;
6
+
7
+ /**
8
+ * lbe logs [--limit <n>]
9
+ *
10
+ * Reads central lbe-events.jsonl and prints the last N entries.
11
+ * If the file does not exist, prints a clear "not yet" message — it means
12
+ * hook dual-write (alpha2 of the hook change) has not been enabled yet.
13
+ *
14
+ * Reads only. Does not write, migrate, or modify anything.
15
+ *
16
+ * @returns {{ eventsPath, count, entries, missing }}
17
+ */
18
+ export async function logsCommand(opts) {
19
+ const workspaceRoot = path.resolve(opts.root || process.cwd());
20
+ const { paths } = resolveWorkspaceState(workspaceRoot);
21
+ const limit = opts.limit ? parseInt(opts.limit, 10) : DEFAULT_LIMIT;
22
+
23
+ if (!fs.existsSync(paths.events)) {
24
+ console.log('\nLBE Central Logs');
25
+ console.log(' No central logs yet. Hook dual-write not enabled.');
26
+ console.log(` Expected at: ${paths.events}`);
27
+ console.log('');
28
+ return { eventsPath: paths.events, count: 0, entries: [], missing: true };
29
+ }
30
+
31
+ const raw = fs.readFileSync(paths.events, 'utf8').trim();
32
+ const lines = raw ? raw.split('\n') : [];
33
+
34
+ const entries = lines
35
+ .map(line => { try { return JSON.parse(line); } catch (_) { return null; } })
36
+ .filter(Boolean);
37
+
38
+ const tail = entries.slice(-limit);
39
+
40
+ console.log(`\nLBE Central Logs — last ${tail.length} of ${entries.length} entries`);
41
+ console.log(` source: ${paths.events}\n`);
42
+
43
+ for (const entry of tail) {
44
+ const ts = entry.ts ? new Date(entry.ts * 1000).toISOString() : '?';
45
+ const action = entry.action || '?';
46
+ const dec = entry.decision || '?';
47
+ const target = entry.path || entry.cmd || '';
48
+ console.log(` [${ts}] ${dec.toUpperCase().padEnd(5)} ${action} ${target}`);
49
+ }
50
+ console.log('');
51
+
52
+ return { eventsPath: paths.events, count: entries.length, entries: tail, missing: false };
53
+ }
@@ -0,0 +1,44 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import path from 'node:path';
3
+ import { resolveWorkspaceState } from '../../state/index.js';
4
+
5
+ /**
6
+ * lbe open-state
7
+ *
8
+ * Resolves the central state directory and opens it in the system file manager.
9
+ * Always prints the path regardless of whether the open succeeds — so the user
10
+ * can copy-paste it even if the file manager call fails.
11
+ *
12
+ * Set env LBE_NO_OPEN=1 to skip the subprocess (used in tests and CI).
13
+ *
14
+ * @returns {{ stateDir, opened }}
15
+ */
16
+ export async function openStateCommand(opts) {
17
+ const workspaceRoot = path.resolve(opts.root || process.cwd());
18
+ const { stateDir } = resolveWorkspaceState(workspaceRoot);
19
+
20
+ console.log(`\nLBE Central State Directory`);
21
+ console.log(` ${stateDir}\n`);
22
+
23
+ if (process.env.LBE_NO_OPEN === '1') {
24
+ return { stateDir, opened: false };
25
+ }
26
+
27
+ let opened = false;
28
+ try {
29
+ if (process.platform === 'win32') {
30
+ spawnSync('explorer.exe', [stateDir], { detached: true, stdio: 'ignore' });
31
+ opened = true;
32
+ } else if (process.platform === 'darwin') {
33
+ spawnSync('open', [stateDir], { detached: true, stdio: 'ignore' });
34
+ opened = true;
35
+ } else {
36
+ spawnSync('xdg-open', [stateDir], { detached: true, stdio: 'ignore' });
37
+ opened = true;
38
+ }
39
+ } catch (_) {
40
+ // Non-fatal — path was already printed above.
41
+ }
42
+
43
+ return { stateDir, opened };
44
+ }
@@ -0,0 +1,8 @@
1
+ import { addLocalPolicyRule } from '../../core/localPolicy.js';
2
+
3
+ export async function policyAddCommand(opts = {}) {
4
+ const result = addLocalPolicyRule(opts.root || process.cwd(), {
5
+ effect: opts.effect, type: opts.type, pattern: opts.pattern, from: opts.from
6
+ }, opts.mode);
7
+ console.log(JSON.stringify(result, null, 2));
8
+ }
@@ -0,0 +1,7 @@
1
+ import { loadLocalPolicy, writeLocalPolicy } from '../../core/localPolicy.js';
2
+
3
+ export async function policyModeCommand(mode, opts = {}) {
4
+ const loaded = loadLocalPolicy(opts.root || process.cwd(), mode);
5
+ writeLocalPolicy(loaded.root, { ...loaded.policy, mode });
6
+ console.log(JSON.stringify({ mode, policy: loaded.policyPath }, null, 2));
7
+ }
@@ -0,0 +1,72 @@
1
+ // src/cli/commands/policySign.js
2
+ // Sign policy.json and write signature envelope
3
+
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { createPolicySignatureEnvelope } from '../../core/policySignature.js';
7
+
8
+ export async function policySignCommand(opts) {
9
+ const policyPath = path.resolve(opts.config || opts.policy || '.lbe/config/policy.default.json');
10
+ const sigPath = path.resolve(opts['policy-sig'] || '.lbe/config/policy.sig.json');
11
+ const secretKeyPath = path.resolve(opts['secret-key-file'] || '.lbe/keys/secret.key');
12
+ const keyId = String(opts['policy-key-id'] || 'policy-signer-v1-2026Q1');
13
+
14
+ if (!fs.existsSync(policyPath)) {
15
+ console.error(JSON.stringify({
16
+ status: 'error',
17
+ error: 'POLICY_FILE_MISSING',
18
+ message: `Policy file not found: ${policyPath}`
19
+ }, null, 2));
20
+ process.exit(1);
21
+ }
22
+
23
+ if (!fs.existsSync(secretKeyPath)) {
24
+ console.error(JSON.stringify({
25
+ status: 'error',
26
+ error: 'SECRET_KEY_MISSING',
27
+ message: `Secret key file not found: ${secretKeyPath}`
28
+ }, null, 2));
29
+ process.exit(1);
30
+ }
31
+
32
+ const policyObj = JSON.parse(fs.readFileSync(policyPath, 'utf8'));
33
+ if (typeof policyObj.version === 'undefined' || typeof policyObj.createdAt === 'undefined') {
34
+ console.error(JSON.stringify({
35
+ status: 'error',
36
+ error: 'POLICY_VERSION_METADATA_MISSING',
37
+ message: 'Policy must include version and createdAt before signing'
38
+ }, null, 2));
39
+ process.exit(8);
40
+ }
41
+
42
+ const secretKeyB64 = fs.readFileSync(secretKeyPath, 'utf8').trim();
43
+ const signResult = createPolicySignatureEnvelope({
44
+ policyObj,
45
+ secretKeyB64,
46
+ keyId
47
+ });
48
+
49
+ if (!signResult.ok) {
50
+ console.error(JSON.stringify({
51
+ status: 'error',
52
+ error: signResult.reason || 'POLICY_SIGN_FAILED',
53
+ message: signResult.message
54
+ }, null, 2));
55
+ process.exit(8);
56
+ }
57
+
58
+ const outDir = path.dirname(sigPath);
59
+ if (!fs.existsSync(outDir)) {
60
+ fs.mkdirSync(outDir, { recursive: true });
61
+ }
62
+ fs.writeFileSync(sigPath, JSON.stringify(signResult.envelope, null, 2));
63
+
64
+ console.log(JSON.stringify({
65
+ status: 'ok',
66
+ message: 'Policy signature written',
67
+ policy: policyPath,
68
+ policySig: sigPath,
69
+ keyId
70
+ }, null, 2));
71
+ process.exit(0);
72
+ }
@@ -0,0 +1,102 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { resolveWorkspaceState } from '../../state/index.js';
4
+ import { loadLatestProof } from '../../state/proofRunner.js';
5
+
6
+ function buildPublicProof(proof, targets) {
7
+ const lastTarget = Array.isArray(targets) && targets.length > 0
8
+ ? targets[targets.length - 1]
9
+ : null;
10
+
11
+ // Redact: only expose safe, non-identifying fields
12
+ const pub = {
13
+ result: proof.result,
14
+ profile: proof.profile,
15
+ checks: proof.checks_run || [],
16
+ allow_deny: (proof.failures && proof.failures.length > 0) ? 'deny' : 'allow',
17
+ };
18
+
19
+ if (lastTarget) {
20
+ pub.target_type = lastTarget.kind || null;
21
+ pub.target_label = lastTarget.label || null;
22
+ // component_file is a relative path — safe to include as-is
23
+ pub.target_file = lastTarget.component_file || null;
24
+ }
25
+
26
+ // Include failure reasons but NOT the raw file paths
27
+ if (proof.failures && proof.failures.length > 0) {
28
+ pub.failure_reasons = proof.failures.map(f => ({ check: f.check, reason: f.reason }));
29
+ }
30
+
31
+ return pub;
32
+ }
33
+
34
+ /**
35
+ * lbe proof [--json] [--public] [--root <path>]
36
+ *
37
+ * Reads proof/latest.json from the central state store and prints it.
38
+ * Does not re-run proof; use the programmatic API for that.
39
+ *
40
+ * @returns {{ result, profile, found } | null}
41
+ */
42
+ export async function proofCommand(opts) {
43
+ const workspaceRoot = path.resolve(opts.root || process.cwd());
44
+ const { stateDir } = resolveWorkspaceState(workspaceRoot);
45
+
46
+ const proof = loadLatestProof(stateDir);
47
+ const isPublic = opts.public === true || opts.public === 'true';
48
+ const isJson = opts.json === true || opts.json === 'true' || isPublic;
49
+
50
+ if (!proof) {
51
+ if (isJson) {
52
+ console.log(JSON.stringify({ found: false, message: 'No proof record found. Run lbe proof after using the hook.' }, null, 2));
53
+ } else {
54
+ console.log('\nNo proof record found.');
55
+ console.log('Use the hook-protected workflow and then run: lbe proof\n');
56
+ }
57
+ return { found: false };
58
+ }
59
+
60
+ // Load targets for public redaction
61
+ let targets = [];
62
+ if (isPublic) {
63
+ const targetPath = path.join(stateDir, 'target_registry.jsonl');
64
+ if (fs.existsSync(targetPath)) {
65
+ const raw = fs.readFileSync(targetPath, 'utf8').trim();
66
+ targets = raw ? raw.split('\n').reduce((acc, l) => {
67
+ try { acc.push(JSON.parse(l)); } catch (_) { /* ignore */ }
68
+ return acc;
69
+ }, []) : [];
70
+ }
71
+ }
72
+
73
+ if (isPublic) {
74
+ const pub = buildPublicProof(proof, targets);
75
+ console.log(JSON.stringify(pub, null, 2));
76
+ return { found: true, result: proof.result, profile: proof.profile, public: true };
77
+ }
78
+
79
+ if (isJson) {
80
+ console.log(JSON.stringify(proof, null, 2));
81
+ return { found: true, result: proof.result, profile: proof.profile };
82
+ }
83
+
84
+ // Human-readable output
85
+ const resultMark = proof.result === 'PASS' ? '✓' : proof.result === 'WEAK_PROOF' ? '⚠' : '✗';
86
+ const workspace = path.basename(workspaceRoot);
87
+ console.log(`\nLBE Proof — ${workspace}`);
88
+ console.log(` Result ${resultMark} ${proof.result}`);
89
+ console.log(` Profile ${proof.profile}`);
90
+ console.log(` Changed files ${(proof.files_changed || []).length}`);
91
+ console.log(` Checks run ${(proof.checks_run || []).join(', ')}`);
92
+ if (proof.failures && proof.failures.length > 0) {
93
+ console.log(` Failures ${proof.failures.length}`);
94
+ for (const f of proof.failures) {
95
+ console.log(` • [${f.check}] ${f.reason}${f.file ? ': ' + f.file : ''}`);
96
+ }
97
+ }
98
+ console.log(` Recorded at ${proof.ts}`);
99
+ console.log('');
100
+
101
+ return { found: true, result: proof.result, profile: proof.profile };
102
+ }
@@ -0,0 +1,342 @@
1
+ // src/cli/commands/run.js
2
+ // Validate and execute a proposal
3
+
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import crypto from 'crypto';
7
+ import { validateCommand } from '../../core/validator.js';
8
+ import { NonceStore } from '../../core/nonceStore.js';
9
+ import { executeAdapter } from '../../adapters/index.js';
10
+ import { appendAudit } from '../../core/auditLog.js';
11
+ import { loadKeysStore } from '../../core/trustedKeys.js';
12
+ import { RequestRateLimiter } from '../../core/requestRateLimiter.js';
13
+ import { verifyPolicySignature } from '../../core/policySignature.js';
14
+ import { validateAndUpdatePolicyVersionState } from '../../core/policyVersionGuard.js';
15
+ import { getApprovalManager } from '../../core/approval-token.js';
16
+ import { createBackup, restoreBackup } from '../../core/backup.js';
17
+ import { loadLocalPolicy, evaluateLocalPolicy, auditLocalPolicy } from '../../core/localPolicy.js';
18
+
19
+ function sha256(obj) {
20
+ return crypto.createHash('sha256').update(JSON.stringify(obj)).digest('hex');
21
+ }
22
+
23
+ export async function runCommand(opts) {
24
+ const { in: inFile } = opts;
25
+ const config = opts.config || opts.policy;
26
+ const pubKey = opts['pub-key'];
27
+ const keysStorePath = opts['keys-store'] || path.resolve('.lbe/config/keys.json');
28
+ const policySigPath = opts['policy-sig'] || path.resolve('.lbe/config/policy.sig.json');
29
+ const policyStatePath = opts['policy-state'] || path.resolve('.lbe/data/policy.state.json');
30
+ const allowUnsignedPolicy = opts['policy-unsigned-ok'] === true || String(opts['policy-unsigned-ok']).toLowerCase() === 'true';
31
+ // Validate required arguments
32
+ if (!inFile) {
33
+ console.error('Error: --in <file> is required');
34
+ process.exit(1);
35
+ }
36
+
37
+ // Read proposal file
38
+ let proposal;
39
+ try {
40
+ const filePath = path.resolve(inFile);
41
+ const content = fs.readFileSync(filePath, 'utf-8');
42
+ proposal = JSON.parse(content);
43
+ } catch (error) {
44
+ console.error(JSON.stringify({
45
+ status: 'error',
46
+ error: 'INVALID_PROPOSAL_FILE',
47
+ message: error.message
48
+ }));
49
+ process.exit(5);
50
+ }
51
+
52
+ // Load policy
53
+ let policy;
54
+ try {
55
+ const policyPath = config || path.resolve('.lbe/config/policy.default.json');
56
+ if (!fs.existsSync(policyPath)) {
57
+ console.error(JSON.stringify({
58
+ status: 'error',
59
+ error: 'MISSING_POLICY',
60
+ message: `Policy file not found: ${policyPath}`
61
+ }));
62
+ process.exit(1);
63
+ }
64
+ const policyContent = fs.readFileSync(policyPath, 'utf-8');
65
+ policy = JSON.parse(policyContent);
66
+ } catch (error) {
67
+ console.error(JSON.stringify({
68
+ status: 'error',
69
+ error: 'INVALID_POLICY',
70
+ message: error.message
71
+ }));
72
+ process.exit(1);
73
+ }
74
+
75
+ // Project-local rules are controller-owned and take precedence over any
76
+ // observer allow. Observe mode records a would-deny decision only.
77
+ const rootDir = process.cwd();
78
+ let localPolicy;
79
+ try {
80
+ localPolicy = loadLocalPolicy(rootDir);
81
+ } catch (error) {
82
+ console.error(JSON.stringify({ status: 'error', error: 'LOCAL_POLICY_INVALID', message: error.message }));
83
+ process.exit(1);
84
+ }
85
+ const localDecision = evaluateLocalPolicy(localPolicy.policy, rootDir, {
86
+ target: proposal.payload?.target,
87
+ command: proposal.payload?.cmd
88
+ });
89
+ const localBlocked = localPolicy.policy.mode === 'enforce' && !localDecision.allowed;
90
+ auditLocalPolicy(rootDir, {
91
+ commandId: proposal.commandId || 'N/A', requesterId: proposal.requesterId || 'unknown',
92
+ mode: localPolicy.policy.mode, decision: localBlocked ? 'deny' : 'allow',
93
+ wouldDeny: !localDecision.allowed, ruleIds: localDecision.winningRules.map(rule => rule.id)
94
+ });
95
+ if (localBlocked) {
96
+ console.error(JSON.stringify({ status: 'blocked', error: 'LOCAL_POLICY_DENY', ruleIds: localDecision.winningRules.map(rule => rule.id) }, null, 2));
97
+ process.exit(2);
98
+ }
99
+
100
+ // Load key store (preferred) with legacy pub-key fallback
101
+ const keyStoreResult = loadKeysStore(keysStorePath);
102
+ const keyStore = keyStoreResult.ok ? keyStoreResult.store : null;
103
+
104
+ // Preflight: policy signature verification (strict by default)
105
+ const policySigCheck = verifyPolicySignature({
106
+ policyObj: policy,
107
+ keyStore,
108
+ policySigPath,
109
+ allowUnsigned: allowUnsignedPolicy
110
+ });
111
+ if (!policySigCheck.ok) {
112
+ console.error(JSON.stringify({
113
+ status: 'error',
114
+ error: policySigCheck.reason,
115
+ message: policySigCheck.message
116
+ }, null, 2));
117
+ process.exit(8);
118
+ }
119
+
120
+ const versionCheck = validateAndUpdatePolicyVersionState({
121
+ policyObj: policy,
122
+ statePath: policyStatePath,
123
+ maxCreatedAtSkewSec: policy?.security?.maxPolicyCreatedAtSkewSec
124
+ });
125
+ if (!versionCheck.ok) {
126
+ console.error(JSON.stringify({
127
+ status: 'error',
128
+ error: versionCheck.reason,
129
+ message: versionCheck.message
130
+ }, null, 2));
131
+ process.exit(8);
132
+ }
133
+
134
+ // Load nonce store
135
+ const nonceDb = new NonceStore(path.resolve('.lbe/data/nonce.db.json'));
136
+ await nonceDb.load();
137
+
138
+ if (!keyStore && !pubKey) {
139
+ console.error(JSON.stringify({
140
+ status: 'error',
141
+ error: 'MISSING_KEY_MATERIAL',
142
+ message: `${keyStoreResult.message}. Provide --pub-key/--pub-key-file or create .lbe/config/keys.json`
143
+ }));
144
+ process.exit(1);
145
+ }
146
+
147
+ // Load requester rate limiter
148
+ const rateLimiter = new RequestRateLimiter(path.resolve('.lbe/data/rate-limit.db.json'));
149
+ await rateLimiter.load();
150
+
151
+ // Validate command
152
+ const validateResult = validateCommand({
153
+ commandObj: proposal,
154
+ pubKeyB64: pubKey,
155
+ keyStore,
156
+ nonceDb,
157
+ policy,
158
+ rateLimiter
159
+ });
160
+
161
+ if (!validateResult.valid) {
162
+ // Persist state from checks that may record entries prior to rejection.
163
+ try {
164
+ await nonceDb.save();
165
+ await rateLimiter.save();
166
+ } catch {
167
+ // Continue with rejection path even if state persistence fails.
168
+ }
169
+
170
+ const output = {
171
+ status: 'invalid',
172
+ commandId: proposal.commandId || 'N/A',
173
+ checks: validateResult.checks,
174
+ errors: validateResult.errors || [],
175
+ executionResult: null
176
+ };
177
+ console.error(JSON.stringify(output, null, 2));
178
+
179
+ // Load audit log and append this rejection
180
+ const auditPath = path.resolve('.lbe/data/audit.log.jsonl');
181
+ try {
182
+ appendAudit(auditPath, {
183
+ commandId: proposal.commandId || 'N/A',
184
+ status: 'rejected',
185
+ requesterId: proposal.requesterId || 'unknown',
186
+ payloadHash: sha256(proposal),
187
+ reason: validateResult.checks,
188
+ timestamp: new Date().toISOString()
189
+ });
190
+ } catch (auditErr) {
191
+ console.error(JSON.stringify({
192
+ status: 'error',
193
+ error: 'AUDIT_WRITE_FAILED',
194
+ message: auditErr.message
195
+ }));
196
+ process.exit(10);
197
+ }
198
+
199
+ if (validateResult.checks.schema === false) process.exit(5);
200
+ if (validateResult.checks.signature === false) process.exit(3);
201
+ if (validateResult.checks.nonce === false) process.exit(4);
202
+ if (validateResult.checks.timestamp === false) process.exit(6);
203
+ if (validateResult.checks.rateLimit === false) process.exit(7);
204
+ if (validateResult.checks.policy === false) process.exit(2);
205
+ process.exit(9);
206
+ }
207
+
208
+ const risk = validateResult.risk || 'LOW';
209
+ const adapterName = proposal.payload.adapter || 'shell';
210
+ const requesterPolicy = policy.requesters?.[proposal.requesterId];
211
+
212
+ // Approval gate — pause if the requester policy marks this risk level for approval
213
+ const approvalRule = requesterPolicy?.requireApproval;
214
+ const approvalRequired = approvalRule === true
215
+ || (Array.isArray(approvalRule) && (
216
+ approvalRule.includes(risk)
217
+ || approvalRule.includes('*')
218
+ || (['HIGH', 'CRITICAL'].includes(risk) && approvalRule.includes('HIGH+'))
219
+ ));
220
+
221
+ if (approvalRequired) {
222
+ const mgr = getApprovalManager();
223
+ const tokenId = mgr.createToken(proposal.commandId, {
224
+ requesterId: proposal.requesterId,
225
+ adapter: adapterName,
226
+ risk
227
+ });
228
+
229
+ await nonceDb.save().catch(() => {});
230
+ await rateLimiter.save().catch(() => {});
231
+
232
+ console.log(JSON.stringify({
233
+ status: 'approval_required',
234
+ commandId: proposal.commandId || 'N/A',
235
+ risk,
236
+ approvalToken: tokenId,
237
+ message: `${risk} risk operation requires operator approval. Approve with: lbe approve --token ${tokenId}`
238
+ }, null, 2));
239
+ process.exit(11);
240
+ }
241
+
242
+ // Backup — create before execution for file adapter or when --backup flag is set
243
+ let backup = null;
244
+ const shouldBackup = opts.backup === true || adapterName === 'file';
245
+ if (shouldBackup && proposal.payload.target) {
246
+ try {
247
+ backup = createBackup(path.resolve(proposal.payload.target));
248
+ } catch {
249
+ // Non-fatal — execution continues without backup
250
+ }
251
+ }
252
+
253
+ // Execute with appropriate adapter
254
+ let executionResult;
255
+ try {
256
+ executionResult = await executeAdapter(adapterName, proposal, policy, requesterPolicy);
257
+ } catch (error) {
258
+ executionResult = {
259
+ adapter: adapterName,
260
+ status: 'error',
261
+ error: error.message,
262
+ exitCode: 1
263
+ };
264
+ }
265
+
266
+ const executionFailed = executionResult.status === 'error' || executionResult.exitCode !== 0;
267
+
268
+ // Rollback on failure — restore backup if execution failed and we have one
269
+ let rollbackResult = null;
270
+ if (executionFailed && backup && opts['rollback-on-failure'] !== false) {
271
+ try {
272
+ rollbackResult = restoreBackup(backup);
273
+ } catch (e) {
274
+ rollbackResult = { restored: false, error: e.message };
275
+ }
276
+ }
277
+
278
+ // Post-execution validation — verify target exists after a write/patch
279
+ let postValidation = null;
280
+ if (!executionFailed && proposal.payload.target) {
281
+ const writeActions = ['write', 'patch'];
282
+ if (writeActions.includes(proposal.payload.action)) {
283
+ const exists = fs.existsSync(path.resolve(proposal.payload.target));
284
+ postValidation = { ok: exists, check: 'target_exists', target: proposal.payload.target };
285
+ if (!exists && backup) {
286
+ rollbackResult = restoreBackup(backup);
287
+ executionResult.status = 'error';
288
+ }
289
+ }
290
+ }
291
+
292
+ // Log to audit trail
293
+ // payloadHash: SHA-256 of the validated proposal — proves what the adapter received
294
+ // executionHash: SHA-256 of the adapter result — proves what the adapter returned
295
+ // Together these bind the validation result to the execution result in the immutable log
296
+ const auditPath = path.resolve('.lbe/data/audit.log.jsonl');
297
+ try {
298
+ appendAudit(auditPath, {
299
+ commandId: proposal.commandId || 'N/A',
300
+ status: rollbackResult?.restored ? 'rolled_back' : (executionResult.status || 'completed'),
301
+ requesterId: proposal.requesterId || 'unknown',
302
+ payloadHash: sha256(proposal),
303
+ executionHash: sha256(executionResult),
304
+ adapter: executionResult.adapter,
305
+ riskLevel: risk,
306
+ exitCode: executionResult.exitCode || 0,
307
+ rolledBack: rollbackResult?.restored || false,
308
+ timestamp: new Date().toISOString()
309
+ });
310
+ } catch (auditErr) {
311
+ console.error(JSON.stringify({
312
+ status: 'error',
313
+ error: 'AUDIT_WRITE_FAILED',
314
+ message: auditErr.message
315
+ }));
316
+ process.exit(10);
317
+ }
318
+
319
+ // Save nonce DB (records the nonce as used)
320
+ await nonceDb.save();
321
+ await rateLimiter.save();
322
+
323
+ // Output structured result
324
+ const output = {
325
+ status: executionFailed || (postValidation && !postValidation.ok) ? 'failed' : 'executed',
326
+ commandId: proposal.commandId || 'N/A',
327
+ risk,
328
+ checks: validateResult.checks,
329
+ executionResult: {
330
+ adapter: executionResult.adapter,
331
+ status: executionResult.status || 'completed',
332
+ output: executionResult.output || executionResult.error || '',
333
+ exitCode: executionResult.exitCode || 0
334
+ },
335
+ backup: backup ? { path: backup.backupPath, existed: backup.existed, hash: backup.hash } : null,
336
+ rollback: rollbackResult,
337
+ postValidation
338
+ };
339
+
340
+ console.log(JSON.stringify(output, null, 2));
341
+ process.exit(executionResult.exitCode || 0);
342
+ }