@letterblack/lbe-core 1.3.4 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.githooks/pre-commit +2 -0
  2. package/.githooks/pre-push +2 -0
  3. package/CHANGELOG.md +75 -0
  4. package/LICENSE +1 -1
  5. package/README.md +127 -154
  6. package/RELEASE_WORKSPACE_RULES.md +110 -0
  7. package/Release-README.md +65 -0
  8. package/WORKSPACE.md +422 -0
  9. package/_proof.mjs +246 -0
  10. package/assets/runtime-boundary.svg +36 -36
  11. package/bin/lbe.js +12 -0
  12. package/config/identity.config.json +3 -0
  13. package/config/policy.default.json +24 -0
  14. package/dist/cli/lbe.js +4432 -0
  15. package/dist/hooks/register.cjs +505 -0
  16. package/dist/state/appendCentral.cjs +87 -0
  17. package/dist/state/index.cjs +101 -0
  18. package/exec/cli.js +472 -0
  19. package/exec/index.js +2 -0
  20. package/index.js +24 -0
  21. package/lbe.audit.jsonl +46 -0
  22. package/package.json +48 -16
  23. package/release/README.md +216 -0
  24. package/release/TRUST.md +90 -0
  25. package/release/exec-README.md +215 -0
  26. package/release/exec-types.d.ts +50 -0
  27. package/release-exec/LICENSE +1 -0
  28. package/release-exec/README.md +215 -0
  29. package/release-exec/assets/lbe-gates.jpg +0 -0
  30. package/release-exec/assets/lbe-gates.png +0 -0
  31. package/release-exec/assets/runtime-boundary.svg +36 -0
  32. package/release-exec/assets/story-allow.jpg +0 -0
  33. package/release-exec/assets/story-allow.png +0 -0
  34. package/release-exec/assets/story-deny.jpg +0 -0
  35. package/release-exec/assets/story-deny.png +0 -0
  36. package/release-exec/dist/cli.js +2841 -0
  37. package/release-exec/dist/index.js +1835 -0
  38. package/release-exec/dist/lbe_engine.wasm +0 -0
  39. package/{dist → release-exec/dist}/wasm.lock.json +4 -5
  40. package/release-exec/hooks/register.cjs +473 -0
  41. package/release-exec/package.json +35 -0
  42. package/release-exec/types.d.ts +50 -0
  43. package/runtime/engine.js +322 -0
  44. package/runtime/lbe_engine.wasm +0 -0
  45. package/src/cli/commands/assertConsumer.js +198 -0
  46. package/src/cli/commands/auditVerify.js +36 -0
  47. package/src/cli/commands/dryrun.js +175 -0
  48. package/src/cli/commands/health.js +153 -0
  49. package/src/cli/commands/init.js +306 -0
  50. package/src/cli/commands/integrityCheck.js +57 -0
  51. package/src/cli/commands/logs.js +53 -0
  52. package/src/cli/commands/openState.js +44 -0
  53. package/src/cli/commands/policyAdd.js +8 -0
  54. package/src/cli/commands/policyMode.js +7 -0
  55. package/src/cli/commands/policySign.js +72 -0
  56. package/src/cli/commands/proof.js +122 -0
  57. package/src/cli/commands/run.js +342 -0
  58. package/src/cli/commands/status.js +73 -0
  59. package/src/cli/commands/verify.js +144 -0
  60. package/src/cli/main.js +181 -0
  61. package/src/cli/parseArgs.js +115 -0
  62. package/src/exec/localExecutor.js +289 -0
  63. package/src/hooks/register.cjs +505 -0
  64. package/src/state/appendCentral.cjs +87 -0
  65. package/src/state/fileIndex.js +140 -0
  66. package/src/state/index.cjs +101 -0
  67. package/src/state/index.js +65 -0
  68. package/src/state/intentRegistry.js +83 -0
  69. package/src/state/migration.js +112 -0
  70. package/src/state/proofRunner.js +246 -0
  71. package/src/state/stateRoot.js +40 -0
  72. package/src/state/targetRegistry.js +108 -0
  73. package/src/state/workspaceId.js +40 -0
  74. package/src/state/workspaceRegistry.js +65 -0
  75. package/types.d.ts +175 -2
  76. package/dist/cli.js +0 -141
  77. package/dist/index.js +0 -52
  78. /package/dist/{lbe_engine.wasm → cli/lbe_engine.wasm} +0 -0
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+ // CJS mirror of src/state/index.js — minimal subset for register.cjs preload use.
3
+ //
4
+ // ESM modules cannot be require()'d synchronously. This file duplicates the
5
+ // pure path-computation functions inline so register.cjs has no ESM dependency.
6
+ //
7
+ // Rule: this file must remain pure CJS. No import(), no top-level await.
8
+ // Policy authority: .lbe/policy.json remains authoritative. This file resolves
9
+ // central state paths only — it does not read or write policy.
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const os = require('os');
14
+ const crypto = require('crypto');
15
+
16
+ // ── Inline mirrors of ESM functions (pure, no side effects) ─────────────────
17
+
18
+ function canonicalWorkspacePath(workspaceRoot) {
19
+ var resolved;
20
+ try {
21
+ resolved = fs.realpathSync.native(workspaceRoot);
22
+ } catch (_) {
23
+ resolved = path.resolve(workspaceRoot);
24
+ }
25
+ var normalised = path.normalize(resolved);
26
+ return process.platform === 'win32' ? normalised.toLowerCase() : normalised;
27
+ }
28
+
29
+ function workspaceId(workspaceRoot) {
30
+ return crypto.createHash('sha256').update(canonicalWorkspacePath(workspaceRoot)).digest('hex');
31
+ }
32
+
33
+ function stateRoot() {
34
+ var home = os.homedir();
35
+
36
+ if (process.platform === 'win32') {
37
+ var localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
38
+ return path.join(localAppData, 'LetterBlack', 'Sentinel');
39
+ }
40
+
41
+ if (process.platform === 'darwin') {
42
+ var appSupport = process.env.HOME
43
+ ? path.join(process.env.HOME, 'Library', 'Application Support')
44
+ : path.join(home, 'Library', 'Application Support');
45
+ return path.join(appSupport, 'LetterBlack', 'Sentinel');
46
+ }
47
+
48
+ // Linux / other POSIX
49
+ var xdgData = process.env.XDG_DATA_HOME || path.join(home, '.local', 'share');
50
+ return path.join(xdgData, 'LetterBlack', 'Sentinel');
51
+ }
52
+
53
+ function workspaceStateDir(root, id) {
54
+ return path.join(root, 'workspaces', id.slice(0, 2), id.slice(2, 4), id.slice(4, 6), id);
55
+ }
56
+
57
+ function buildPaths(dir) {
58
+ return {
59
+ workspace: path.join(dir, 'workspace.json'),
60
+ events: path.join(dir, 'lbe-events.jsonl'),
61
+ intent: path.join(dir, 'intent.jsonl'),
62
+ targetRegistry: path.join(dir, 'target_registry.jsonl'),
63
+ fileIndexDir: path.join(dir, 'file-index'),
64
+ fileIndexBefore: path.join(dir, 'file-index', 'before.json'),
65
+ fileIndexAfter: path.join(dir, 'file-index', 'after.json'),
66
+ proofDir: path.join(dir, 'proof'),
67
+ proofLatest: path.join(dir, 'proof', 'latest.json'),
68
+ };
69
+ }
70
+
71
+ // ── Public API ───────────────────────────────────────────────────────────────
72
+
73
+ /**
74
+ * Resolves (and creates) the central state directory for a workspace.
75
+ * Safe to call at preload time — only mkdirSync, no registry or audit writes.
76
+ *
77
+ * @param {string} workspaceRoot Absolute path to the workspace root.
78
+ * @returns {{ stateDir: string, workspaceId: string, paths: object }}
79
+ */
80
+ function resolveWorkspaceStateSyncCjs(workspaceRoot) {
81
+ var root = stateRoot();
82
+ var id = workspaceId(workspaceRoot);
83
+ var dir = workspaceStateDir(root, id);
84
+
85
+ fs.mkdirSync(dir, { recursive: true });
86
+ fs.mkdirSync(path.join(dir, 'file-index'), { recursive: true });
87
+ fs.mkdirSync(path.join(dir, 'proof'), { recursive: true });
88
+
89
+ return {
90
+ stateDir: dir,
91
+ workspaceId: id,
92
+ paths: buildPaths(dir),
93
+ };
94
+ }
95
+
96
+ module.exports = {
97
+ resolveWorkspaceStateSyncCjs,
98
+ stateRoot,
99
+ workspaceId,
100
+ workspaceStateDir,
101
+ };
package/exec/cli.js ADDED
@@ -0,0 +1,472 @@
1
+ #!/usr/bin/env node
2
+ // CLI for @letterblack/lbe-exec
3
+ import fs from 'fs';
4
+ import path from 'path';
5
+ import { spawnSync, spawn } from 'child_process';
6
+ import { fileURLToPath } from 'url';
7
+ import { initCommand } from '../src/cli/commands/init.js';
8
+ import { policyModeCommand } from '../src/cli/commands/policyMode.js';
9
+
10
+ const [, , cmd, ...rest] = process.argv;
11
+ const opts = Object.fromEntries(
12
+ rest.flatMap((v, i, a) => v.startsWith('--') ? [[v.slice(2), a[i + 1] ?? true]] : [])
13
+ );
14
+ const positional = rest.filter(v => !v.startsWith('--') && rest[rest.indexOf(v) - 1]?.startsWith('--') === false);
15
+
16
+ const __dir = path.dirname(fileURLToPath(import.meta.url));
17
+
18
+ function loadPolicy() {
19
+ const cwd = process.cwd();
20
+ // .lbe/policy.json is canonical; fall back to legacy lbe.policy.json in root.
21
+ const p = fs.existsSync(path.join(cwd, '.lbe', 'policy.json'))
22
+ ? path.join(cwd, '.lbe', 'policy.json')
23
+ : path.join(cwd, 'lbe.policy.json');
24
+ return fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, 'utf8')) : null;
25
+ }
26
+
27
+ function countAudit() {
28
+ const p = path.join(process.cwd(), '.lbe', 'audit.jsonl');
29
+ if (!fs.existsSync(p)) return 0;
30
+ return fs.readFileSync(p, 'utf8').split('\n').filter(l => l.trim()).length;
31
+ }
32
+
33
+ function findHookPath() {
34
+ const candidates = [
35
+ path.resolve(__dir, '../hooks/register.cjs'), // npm: dist/ → ../hooks/
36
+ path.resolve(__dir, '../src/hooks/register.cjs'), // dev: exec/ → ../src/hooks/
37
+ ];
38
+ return candidates.find(p => fs.existsSync(p)) || candidates[0];
39
+ }
40
+
41
+ // ── init helpers — non-destructive script injection ──────────────────────────
42
+
43
+ function detectNodeScripts(scripts) {
44
+ const pattern = /(?:^|\s)node\s+(\S+)/;
45
+ return Object.entries(scripts || {}).filter(([name, cmd]) => {
46
+ if (name.includes(':lbe') || name.startsWith('lbe')) return false;
47
+ return pattern.test(cmd);
48
+ });
49
+ }
50
+
51
+ function extractNodeArgs(cmd) {
52
+ const match = cmd.match(/(?:^|\s)node\s+(.+)/);
53
+ return match ? match[1].trim() : null;
54
+ }
55
+
56
+ function injectScripts(wrapScript) {
57
+ const pkgPath = path.join(process.cwd(), 'package.json');
58
+ if (!fs.existsSync(pkgPath)) return [];
59
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
60
+ const scripts = pkg.scripts || {};
61
+ const added = [];
62
+
63
+ if (wrapScript) {
64
+ // --wrap <name>: destructive rewrite of that one script
65
+ const original = scripts[wrapScript];
66
+ if (!original) { console.error(`No script named "${wrapScript}" found.`); return []; }
67
+ const args = extractNodeArgs(original);
68
+ if (!args) { console.error(`Script "${wrapScript}" does not look like a node script.`); return []; }
69
+ scripts[wrapScript] = `lbe-exec run-node --mode observe ${args}`;
70
+ added.push(wrapScript);
71
+ } else {
72
+ // Non-destructive: add :lbe variants alongside each node script
73
+ const candidates = detectNodeScripts(scripts);
74
+ for (const [name, scriptCmd] of candidates) {
75
+ const args = extractNodeArgs(scriptCmd);
76
+ if (!args) continue;
77
+ const lbeName = name + ':lbe';
78
+ const lbeEnforceName = name + ':lbe:enforce';
79
+ if (!scripts[lbeName]) {
80
+ scripts[lbeName] = `lbe-exec run-node --mode observe ${args}`;
81
+ added.push(lbeName);
82
+ }
83
+ if (!scripts[lbeEnforceName]) {
84
+ scripts[lbeEnforceName] = `lbe-exec run-node --mode enforce ${args}`;
85
+ added.push(lbeEnforceName);
86
+ }
87
+ }
88
+ }
89
+
90
+ // Always add lbe:status and lbe:audit if not present
91
+ if (!scripts['lbe:status']) { scripts['lbe:status'] = 'lbe-exec status'; added.push('lbe:status'); }
92
+ if (!scripts['lbe:audit']) { scripts['lbe:audit'] = 'lbe-exec audit'; added.push('lbe:audit'); }
93
+
94
+ if (added.length) {
95
+ pkg.scripts = scripts;
96
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
97
+ for (const s of added) console.log(` added: ${s}`);
98
+ }
99
+ return added;
100
+ }
101
+
102
+ // ── Commands ──────────────────────────────────────────────────────────────────
103
+
104
+ switch (cmd) {
105
+
106
+ case 'run-node': {
107
+ // npx lbe-exec run-node [--mode observe|enforce] ./agent.js [...scriptArgs]
108
+ const mode = opts.mode || 'observe';
109
+ if (!['observe', 'enforce'].includes(mode)) {
110
+ console.error('--mode must be observe or enforce'); process.exit(1);
111
+ }
112
+ // Find the first non-flag positional argument as the script
113
+ const scriptIdx = rest.findIndex((v, i) => !v.startsWith('--') && (i === 0 || !rest[i - 1].startsWith('--')));
114
+ if (scriptIdx === -1) {
115
+ console.error('Usage: lbe-exec run-node [--mode observe|enforce] <script> [...args]'); process.exit(1);
116
+ }
117
+ const scriptAndArgs = rest.slice(scriptIdx);
118
+ const hookPath = findHookPath();
119
+ if (!fs.existsSync(hookPath)) {
120
+ console.error('Hook not found: ' + hookPath + '\nRun: npm install @letterblack/lbe-exec'); process.exit(1);
121
+ }
122
+ const child = spawn(process.execPath, ['--require', hookPath, ...scriptAndArgs], {
123
+ stdio: 'inherit',
124
+ env: { ...process.env, LBE_MODE: mode, LBE_ROOT: process.cwd() },
125
+ });
126
+ child.on('close', code => process.exit(code ?? 0));
127
+ break;
128
+ }
129
+
130
+ case 'npm': {
131
+ // npx lbe-exec npm <...args> — sets NODE_OPTIONS for hook preload
132
+ console.error('[lbe] Note: Use "lbe-exec run-node" for reliable hook preload.');
133
+ console.error('[lbe] NODE_OPTIONS --require may not fire for all npm lifecycle hooks.\n');
134
+ const hookPath = findHookPath();
135
+ if (!fs.existsSync(hookPath)) {
136
+ console.error('Hook not found: ' + hookPath); process.exit(1);
137
+ }
138
+ const existing = process.env.NODE_OPTIONS || '';
139
+ // Forward slashes: NODE_OPTIONS parser treats backslashes as escapes.
140
+ const hookPathFwd = hookPath.replace(/\\/g, '/');
141
+ const hookFlag = '--require "' + hookPathFwd + '"';
142
+ const nodeOptions = existing.includes(hookPathFwd) ? existing : (existing + ' ' + hookFlag).trim();
143
+ const npmArgs = rest.filter(v => !v.startsWith('--mode') && v !== opts.mode);
144
+ const child = spawn('npm', npmArgs, {
145
+ stdio: 'inherit',
146
+ shell: true,
147
+ env: { ...process.env, NODE_OPTIONS: nodeOptions, LBE_MODE: opts.mode || 'observe', LBE_ROOT: process.cwd() },
148
+ });
149
+ child.on('close', code => process.exit(code ?? 0));
150
+ break;
151
+ }
152
+
153
+ case 'status': {
154
+ const root = process.cwd();
155
+ console.log('── LBE Status ───────────────────────────────────');
156
+ console.log('workspace: ' + root);
157
+
158
+ // 1. Hook file path
159
+ const hookPath = findHookPath();
160
+ console.log('hook file: ' + hookPath + (fs.existsSync(hookPath) ? ' (found)' : ' (MISSING)'));
161
+
162
+ // 2. LBE_ROOT from env (set when running inside lbe-exec shell)
163
+ const lbeRoot = process.env.LBE_ROOT || '';
164
+ console.log('LBE_ROOT: ' + (lbeRoot || '(not set)'));
165
+
166
+ // 3. NODE_OPTIONS contains hook
167
+ const nodeOpts = process.env.NODE_OPTIONS || '';
168
+ const hookInPath = nodeOpts.includes('register.cjs');
169
+ console.log('NODE_OPTIONS contains hook: ' + (hookInPath ? 'yes' : 'no'));
170
+
171
+ // 4. Audit log
172
+ const eventsFile = path.join(root, '.lbe', 'events.jsonl');
173
+ const auditExists = fs.existsSync(eventsFile);
174
+ console.log('audit log: ' + (auditExists ? eventsFile : '(none yet)'));
175
+
176
+ // 5. Last audit event
177
+ if (auditExists) {
178
+ try {
179
+ const lines = fs.readFileSync(eventsFile, 'utf8').split('\n').filter(l => l.trim());
180
+ if (lines.length) {
181
+ const last = JSON.parse(lines[lines.length - 1]);
182
+ const ts = new Date((last.ts || 0) * 1000).toISOString().replace('T', ' ').slice(0, 19);
183
+ const target = last.path || last.cmd || '?';
184
+ console.log('last event: ' + ts + ' ' + last.action + ' ' + target +
185
+ ' → ' + (last.decision || '?'));
186
+ } else {
187
+ console.log('last event: (none)');
188
+ }
189
+ } catch (_) { console.log('last event: (unreadable)'); }
190
+ }
191
+
192
+ // 6. Hook process status (from hook-status.json written by register.cjs)
193
+ const statusFile = path.join(root, '.lbe', 'runtime', 'hook-status.json');
194
+ if (fs.existsSync(statusFile)) {
195
+ let h;
196
+ try { h = JSON.parse(fs.readFileSync(statusFile, 'utf8')); } catch (_) {}
197
+ if (h) {
198
+ let pidAlive = false;
199
+ try { process.kill(h.pid, 0); pidAlive = true; } catch (_) {}
200
+ console.log('\nhook process: ' + (pidAlive ? 'ACTIVE' : 'stale (process exited)'));
201
+ console.log('hook pid: ' + h.pid + (pidAlive ? ' (alive)' : ' (gone)'));
202
+ console.log('hook mode: ' + h.mode);
203
+ console.log('hook started: ' + h.started_at);
204
+ if (h.patched) {
205
+ console.log('\nPatched functions:');
206
+ for (const [fn, active] of Object.entries(h.patched)) {
207
+ console.log(' ' + (active ? '✓' : '–') + ' ' + fn);
208
+ }
209
+ }
210
+ }
211
+ } else {
212
+ console.log('\nhook process: inactive — run: lbe-exec run-node ./agent.js');
213
+ console.log(' or: lbe-exec activate then lbe-exec shell');
214
+ }
215
+ break;
216
+ }
217
+
218
+ case 'audit': {
219
+ const eventsPath = path.join(process.cwd(), '.lbe', 'events.jsonl');
220
+ if (!fs.existsSync(eventsPath)) {
221
+ console.log('No events log found. Run an agent with: npx lbe-exec run-node ./agent.js');
222
+ break;
223
+ }
224
+ const lines = fs.readFileSync(eventsPath, 'utf8').split('\n').filter(l => l.trim());
225
+ if (!lines.length) { console.log('No events recorded yet.'); break; }
226
+ console.log('── LBE Event Log (' + lines.length + ' entries) ──────────────────');
227
+ for (const line of lines) {
228
+ try {
229
+ const e = JSON.parse(line);
230
+ const ts = new Date(e.ts * 1000).toISOString().replace('T', ' ').slice(0, 19);
231
+ const target = e.path || e.cmd || '?';
232
+ const status = e.enforced && e.decision === 'deny' ? 'BLOCKED' :
233
+ e.decision === 'deny' ? 'WOULD-BLOCK' : 'allowed';
234
+ console.log(`${ts} [${e.mode}] ${e.action} ${target} → ${status}`);
235
+ } catch (_) { /* skip malformed lines */ }
236
+ }
237
+ break;
238
+ }
239
+
240
+ case 'init':
241
+ initCommand(opts)
242
+ .then(() => {
243
+ // Inject :lbe script variants after normal init
244
+ const added = injectScripts(opts.wrap || null);
245
+ if (added.length) {
246
+ console.log('\n✓ Added LBE script variants to package.json');
247
+ console.log(' Run your agent through LBE: npm run <name>:lbe');
248
+ } else {
249
+ console.log('\nNo node agent scripts detected in package.json.');
250
+ console.log('Use: npx lbe-exec run-node [--mode observe|enforce] ./your-agent.js');
251
+ }
252
+ })
253
+ .catch(e => { console.error(e.message); process.exit(1); });
254
+ break;
255
+
256
+ case 'activate': {
257
+ // Write workspace-local activation record only.
258
+ // No global env changes. No registry writes.
259
+ // Scope: Node.js processes only. Python/Go/native binaries are NOT governed.
260
+ const hookPath = findHookPath();
261
+ if (!fs.existsSync(hookPath)) {
262
+ console.error('Hook not found: ' + hookPath);
263
+ console.error('Run: npm install @letterblack/lbe-exec');
264
+ process.exit(1);
265
+ }
266
+ const mode = opts.mode || 'observe';
267
+ const root = process.cwd();
268
+ const lbeDir = path.join(root, '.lbe');
269
+ fs.mkdirSync(lbeDir, { recursive: true });
270
+ fs.writeFileSync(path.join(lbeDir, 'activation.json'), JSON.stringify({
271
+ activated: true,
272
+ activatedAt: new Date().toISOString(),
273
+ hookPath,
274
+ mode,
275
+ root,
276
+ }, null, 2) + '\n');
277
+ console.log('── LBE workspace activated ───────────────────────');
278
+ console.log('workspace: ' + root);
279
+ console.log('hook: ' + hookPath);
280
+ console.log('mode: ' + mode);
281
+ console.log('\nNext: open a governed shell session:');
282
+ console.log(' lbe-exec shell');
283
+ console.log('\nAny Node.js agent run inside that shell is intercepted.');
284
+ console.log('Python, Go, native binaries, and PowerShell are NOT governed.');
285
+ break;
286
+ }
287
+
288
+ case 'shell': {
289
+ // Spawn an interactive shell with NODE_OPTIONS pre-loaded.
290
+ // Every node/npm/npx command inside inherits the governance hook.
291
+ // Scope: Node.js only. Exits when the user types "exit".
292
+ const activationFile = path.join(process.cwd(), '.lbe', 'activation.json');
293
+ let activation = null;
294
+ if (fs.existsSync(activationFile)) {
295
+ try { activation = JSON.parse(fs.readFileSync(activationFile, 'utf8')); } catch (_) {}
296
+ }
297
+ const hookPath = (activation && activation.hookPath) || findHookPath();
298
+ if (!fs.existsSync(hookPath)) {
299
+ console.error('Hook not found. Run: lbe-exec activate');
300
+ process.exit(1);
301
+ }
302
+ const mode = opts.mode || (activation && activation.mode) || 'observe';
303
+ const root = (activation && activation.root) || process.cwd();
304
+ // Node.js NODE_OPTIONS parser treats backslashes as escapes — use forward slashes on all platforms.
305
+ const hookPathFwd = hookPath.replace(/\\/g, '/');
306
+ const nodeOpts = '--require "' + hookPathFwd + '"';
307
+ const shellEnv = { ...process.env, NODE_OPTIONS: nodeOpts, LBE_ROOT: root, LBE_MODE: mode };
308
+
309
+ console.log('[lbe] Opening governed shell — mode: ' + mode);
310
+ console.log('[lbe] NODE_OPTIONS set. Node.js agents are intercepted.');
311
+ console.log('[lbe] Python / Go / native binaries are NOT governed.');
312
+ console.log('[lbe] Type "exit" to close.\n');
313
+
314
+ let shellProc;
315
+ if (process.platform === 'win32') {
316
+ const banner = [
317
+ `$env:NODE_OPTIONS='--require "${hookPathFwd}"'`,
318
+ `$env:LBE_ROOT='${root}'`,
319
+ `$env:LBE_MODE='${mode}'`,
320
+ `Write-Host '[lbe] Shell armed — mode: ${mode}' -ForegroundColor Green`,
321
+ ].join('; ');
322
+ shellProc = spawn('powershell.exe', ['-NoExit', '-Command', banner],
323
+ { stdio: 'inherit', env: shellEnv });
324
+ } else {
325
+ const sh = process.env.SHELL || '/bin/bash';
326
+ shellProc = spawn(sh, [], { stdio: 'inherit', env: shellEnv });
327
+ }
328
+ shellProc.on('close', code => {
329
+ console.log('\n[lbe] Governed shell closed.');
330
+ process.exit(code ?? 0);
331
+ });
332
+ break;
333
+ }
334
+
335
+ case 'deactivate': {
336
+ const root = process.cwd();
337
+ const files = [
338
+ path.join(root, '.lbe', 'activation.json'),
339
+ path.join(root, '.lbe', 'runtime', 'hook-status.json'),
340
+ ];
341
+ let removed = 0;
342
+ for (const f of files) { if (fs.existsSync(f)) { fs.unlinkSync(f); removed++; } }
343
+ if (removed) {
344
+ console.log('✓ LBE deactivated — workspace activation files removed.');
345
+ } else {
346
+ console.log('Nothing to deactivate (workspace was not activated).');
347
+ }
348
+ console.log('Close any open "lbe-exec shell" sessions to fully disarm.');
349
+ break;
350
+ }
351
+
352
+ case 'observe':
353
+ case 'enforce':
354
+ policyModeCommand(cmd, opts).catch(e => { console.error(e.message); process.exit(1); });
355
+ break;
356
+
357
+ case 'policy': {
358
+ const policy = loadPolicy();
359
+ if (!policy) { console.log('No policy found. Run: npx lbe-exec init'); break; }
360
+ if (!policy.rules?.length) { console.log('No rules defined.'); break; }
361
+ for (const r of policy.rules) {
362
+ console.log(`[${r.effect.toUpperCase()}] ${r.type}:${r.pattern} — ${r.from || ''} (${r.id || '?'})`);
363
+ }
364
+ break;
365
+ }
366
+
367
+ case 'execute': {
368
+ import('../src/exec/localExecutor.js').then(async ({ createLocalExecutor }) => {
369
+ const lbe = createLocalExecutor({ rootDir: process.cwd() });
370
+ let raw = '';
371
+ if (opts.input) {
372
+ raw = fs.readFileSync(path.resolve(opts.input), 'utf8');
373
+ } else {
374
+ for await (const chunk of process.stdin) raw += chunk;
375
+ }
376
+ const request = JSON.parse(raw);
377
+ const result = await lbe.execute(request);
378
+ console.log(JSON.stringify(result, null, 2));
379
+ process.exit(result.ok ? 0 : result.decision === 'deny' ? 1 : 2);
380
+ }).catch(e => { console.error(e.message); process.exit(2); });
381
+ break;
382
+ }
383
+
384
+ case 'integrate': {
385
+ // Create tool-specific instruction files — only when user explicitly asks.
386
+ // Default init never touches these locations.
387
+ const tool = rest[0];
388
+ const root = process.cwd();
389
+ const contract = '.lbe/AGENT_CONTRACT.md';
390
+ const blurb = [
391
+ 'This workspace uses LBE execution governance.',
392
+ 'Run Node agents through: npx lbe-exec run-node ./agent.js',
393
+ 'Governance state lives in .lbe/ — do not create LBE files outside it.',
394
+ `Full contract: ${contract}`,
395
+ ].join('\n');
396
+
397
+ const integrations = {
398
+ claude: {
399
+ file: 'CLAUDE.md',
400
+ marker: '<!-- lbe-governance -->',
401
+ content: `<!-- lbe-governance -->\n## LBE Governance\n\n${blurb}\n<!-- /lbe-governance -->`,
402
+ },
403
+ copilot: {
404
+ file: '.github/copilot-instructions.md',
405
+ marker: 'lbe-governance',
406
+ content: `<!-- lbe-governance -->\n## LBE Governance\n\n${blurb}\n<!-- /lbe-governance -->`,
407
+ },
408
+ cursor: {
409
+ file: '.cursor/rules',
410
+ marker: 'lbe-governance',
411
+ content: `# lbe-governance\n\n${blurb}`,
412
+ },
413
+ gemini: {
414
+ file: 'GEMINI.md',
415
+ marker: 'lbe-governance',
416
+ content: `<!-- lbe-governance -->\n## LBE Governance\n\n${blurb}\n<!-- /lbe-governance -->`,
417
+ },
418
+ };
419
+
420
+ const known = Object.keys(integrations).join(', ');
421
+ if (!tool || !integrations[tool]) {
422
+ console.log('Usage: lbe-exec integrate <tool>');
423
+ console.log('Available: ' + known);
424
+ break;
425
+ }
426
+
427
+ const { file, marker, content } = integrations[tool];
428
+ const filePath = path.join(root, file);
429
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
430
+
431
+ if (fs.existsSync(filePath)) {
432
+ const existing = fs.readFileSync(filePath, 'utf8');
433
+ if (existing.includes(marker)) {
434
+ console.log('Already integrated: ' + file);
435
+ break;
436
+ }
437
+ fs.appendFileSync(filePath, '\n\n' + content + '\n');
438
+ console.log('✓ Appended LBE section to ' + file);
439
+ } else {
440
+ fs.writeFileSync(filePath, content + '\n');
441
+ console.log('✓ Created ' + file);
442
+ }
443
+ console.log(' Agents reading that file will see LBE instructions.');
444
+ break;
445
+ }
446
+
447
+ default:
448
+ console.log('Usage: lbe-exec <command>\n');
449
+ console.log(' init Bootstrap governance — policy, keys, agent files');
450
+ console.log(' run-node Run a Node.js agent under LBE governance');
451
+ console.log(' [--mode observe|enforce] <script> [...args]');
452
+ console.log(' npm Wrap npm command with LBE hook (via NODE_OPTIONS)');
453
+ console.log(' [...npm-args]');
454
+ console.log(' status Show workspace, mode, hook state, patched functions');
455
+ console.log(' audit Show unified event log (.lbe/events.jsonl)');
456
+ console.log(' policy List active policy rules');
457
+ console.log(' activate Write workspace activation record (Node.js only)');
458
+ console.log(' [--mode observe|enforce]');
459
+ console.log(' shell Open a governed terminal (NODE_OPTIONS pre-set)');
460
+ console.log(' [--mode observe|enforce]');
461
+ console.log(' deactivate Remove workspace activation files');
462
+ console.log(' integrate Create tool-specific instruction file (opt-in)');
463
+ console.log(' claude | copilot | cursor | gemini');
464
+ console.log(' observe Switch to observer mode (log only, nothing blocked)');
465
+ console.log(' enforce Switch to enforcement mode (violations blocked)');
466
+ console.log(' execute Send a JSON request from stdin or --input file');
467
+ console.log('\nCLI: npx lbe-exec <command>');
468
+ if (cmd && cmd !== '--help' && cmd !== 'help') {
469
+ console.error('\nUnknown command: ' + cmd);
470
+ process.exit(1);
471
+ }
472
+ }
package/exec/index.js ADDED
@@ -0,0 +1,2 @@
1
+ // @letterblack/lbe-exec local controller entrypoint.
2
+ export { createLocalExecutor } from '../src/exec/localExecutor.js';
package/index.js ADDED
@@ -0,0 +1,24 @@
1
+ // Public SDK surface for @letterblack/lbe-core.
2
+ // Keep consumers on this boundary while internals move toward compiled runtime.
3
+
4
+ export {
5
+ createLBE,
6
+ sandbox,
7
+ createKeyStore,
8
+ validateCommand,
9
+ appendAudit,
10
+ createBackup,
11
+ restoreBackup,
12
+ signEd25519,
13
+ verifyEd25519,
14
+ generateKeyPair,
15
+ createLogger,
16
+ deepFreeze,
17
+ checkInvariants,
18
+ assertInvariants,
19
+ InvariantGateError
20
+ } from './src/core/index.js';
21
+ export { addLocalPolicyRule, loadLocalPolicy, evaluateLocalPolicy, proposePolicyRule } from './src/core/localPolicy.js';
22
+
23
+ export { getRuntimeInfo, loadWasmEngine } from './runtime/engine.js';
24
+ export { createLocalExecutor } from './src/exec/localExecutor.js';