@letterblack/lbe-core 1.3.5 → 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.
- package/CHANGELOG.md +6 -0
- package/README.md +181 -166
- package/RELEASE_WORKSPACE_RULES.md +179 -110
- package/Release-README.md +2 -0
- package/_proof.mjs +246 -246
- package/config/identity.config.json +2 -2
- package/config/policy.default.json +24 -24
- package/dist/cli/lbe.js +2 -3
- package/dist/hooks/register.cjs +505 -505
- package/exec/cli.js +472 -472
- package/npm-pack.json +0 -0
- package/package.json +77 -77
- package/release/TRUST.md +88 -88
- package/release-exec/dist/cli.js +2841 -2841
- package/release-exec/dist/index.js +1835 -1835
- package/release-exec/hooks/register.cjs +473 -473
- package/release-exec/package.json +35 -35
- package/src/cli/commands/assertConsumer.js +164 -164
- package/src/cli/commands/init.js +306 -306
- package/src/cli/commands/proof.js +2 -22
- package/src/cli/main.js +181 -181
- package/src/hooks/register.cjs +505 -505
- package/src/state/fileIndex.js +1 -1
- package/src/state/intentRegistry.js +3 -2
- package/src/state/proofRunner.js +1 -1
- package/src/state/targetRegistry.js +3 -2
- package/lbe.audit.jsonl +0 -46
package/_proof.mjs
CHANGED
|
@@ -1,246 +1,246 @@
|
|
|
1
|
-
// Proof matrix for LBE preload hook
|
|
2
|
-
// Run with: node _proof.mjs
|
|
3
|
-
// All tests use isolated temp dirs. No shared state.
|
|
4
|
-
|
|
5
|
-
import fs from 'fs';
|
|
6
|
-
import path from 'path';
|
|
7
|
-
import os from 'os';
|
|
8
|
-
import { spawnSync, spawn } from 'child_process';
|
|
9
|
-
import { fileURLToPath } from 'url';
|
|
10
|
-
|
|
11
|
-
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
const HOOK = path.join(ROOT, 'src', 'hooks', 'register.cjs');
|
|
13
|
-
const HOOK_FWD = HOOK.replace(/\\/g, '/');
|
|
14
|
-
|
|
15
|
-
let passed = 0, failed = 0;
|
|
16
|
-
|
|
17
|
-
function result(name, ok, detail = '') {
|
|
18
|
-
const mark = ok ? '✓' : '✗';
|
|
19
|
-
console.log(` ${mark} ${name}${detail ? ' — ' + detail : ''}`);
|
|
20
|
-
if (ok) passed++; else failed++;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function tmpDir(policy = null) {
|
|
24
|
-
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'lbe-proof-'));
|
|
25
|
-
fs.mkdirSync(path.join(d, '.lbe'), { recursive: true });
|
|
26
|
-
fs.writeFileSync(path.join(d, '.lbe', 'policy.json'),
|
|
27
|
-
JSON.stringify(policy || { version: 1, mode: 'observe', workspace: d, rules: [] }));
|
|
28
|
-
return d;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function runNode(dir, code, mode = 'observe') {
|
|
32
|
-
return spawnSync(process.execPath, ['-e', code], {
|
|
33
|
-
encoding: 'utf8',
|
|
34
|
-
env: {
|
|
35
|
-
...process.env,
|
|
36
|
-
NODE_OPTIONS: `--require "${HOOK_FWD}"`,
|
|
37
|
-
LBE_ROOT: dir,
|
|
38
|
-
LBE_MODE: mode,
|
|
39
|
-
},
|
|
40
|
-
cwd: dir,
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function auditEntries(dir) {
|
|
45
|
-
const f = path.join(dir, '.lbe', 'events.jsonl');
|
|
46
|
-
if (!fs.existsSync(f)) return [];
|
|
47
|
-
return fs.readFileSync(f, 'utf8').split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ── 1. fs.writeFile (callback) ──────────────────────────────────────────────
|
|
51
|
-
{
|
|
52
|
-
const dir = tmpDir();
|
|
53
|
-
const r = runNode(dir, `
|
|
54
|
-
const fs = require('fs');
|
|
55
|
-
fs.writeFile('out.txt', 'hello', err => { if (err) process.exit(1); });
|
|
56
|
-
`);
|
|
57
|
-
const entries = auditEntries(dir);
|
|
58
|
-
const wrote = fs.existsSync(path.join(dir, 'out.txt'));
|
|
59
|
-
const logged = entries.some(e => e.action === 'file_write');
|
|
60
|
-
result('fs.writeFile', wrote && logged && r.status === 0,
|
|
61
|
-
wrote ? (logged ? 'file written + logged' : 'file written, NOT logged') : 'file not written');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ── 2. fs.promises.writeFile ────────────────────────────────────────────────
|
|
65
|
-
{
|
|
66
|
-
const dir = tmpDir();
|
|
67
|
-
const r = runNode(dir, `
|
|
68
|
-
const fs = require('fs');
|
|
69
|
-
(async () => {
|
|
70
|
-
await fs.promises.writeFile('out2.txt', 'async');
|
|
71
|
-
})().catch(() => process.exit(1));
|
|
72
|
-
`);
|
|
73
|
-
// Give async time to flush
|
|
74
|
-
const entries = auditEntries(dir);
|
|
75
|
-
const wrote = fs.existsSync(path.join(dir, 'out2.txt'));
|
|
76
|
-
const logged = entries.some(e => e.action === 'file_write');
|
|
77
|
-
result('fs.promises.writeFile', wrote && logged && r.status === 0,
|
|
78
|
-
wrote ? (logged ? 'file written + logged' : 'file written, NOT logged') : 'file not written');
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// ── 3. fs.rm ────────────────────────────────────────────────────────────────
|
|
82
|
-
{
|
|
83
|
-
const dir = tmpDir();
|
|
84
|
-
fs.writeFileSync(path.join(dir, 'del.txt'), 'x');
|
|
85
|
-
const r = runNode(dir, `
|
|
86
|
-
const fs = require('fs');
|
|
87
|
-
fs.rm('del.txt', err => { if (err) process.exit(1); });
|
|
88
|
-
`);
|
|
89
|
-
const entries = auditEntries(dir);
|
|
90
|
-
const deleted = !fs.existsSync(path.join(dir, 'del.txt'));
|
|
91
|
-
const logged = entries.some(e => e.action === 'file_delete');
|
|
92
|
-
result('fs.rm', deleted && logged && r.status === 0,
|
|
93
|
-
deleted ? (logged ? 'file deleted + logged' : 'file deleted, NOT logged') : 'file not deleted');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ── 4. fs.rename ────────────────────────────────────────────────────────────
|
|
97
|
-
{
|
|
98
|
-
const dir = tmpDir();
|
|
99
|
-
fs.writeFileSync(path.join(dir, 'a.txt'), 'x');
|
|
100
|
-
const r = runNode(dir, `
|
|
101
|
-
const fs = require('fs');
|
|
102
|
-
fs.rename('a.txt', 'b.txt', err => { if (err) process.exit(1); });
|
|
103
|
-
`);
|
|
104
|
-
const entries = auditEntries(dir);
|
|
105
|
-
const renamed = !fs.existsSync(path.join(dir, 'a.txt')) && fs.existsSync(path.join(dir, 'b.txt'));
|
|
106
|
-
const logged = entries.some(e => e.action === 'file_rename');
|
|
107
|
-
result('fs.rename', renamed && logged && r.status === 0,
|
|
108
|
-
renamed ? (logged ? 'renamed + logged' : 'renamed, NOT logged') : 'rename failed');
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ── 5. child_process.spawn ──────────────────────────────────────────────────
|
|
112
|
-
{
|
|
113
|
-
const dir = tmpDir();
|
|
114
|
-
const r = runNode(dir, `
|
|
115
|
-
const { spawn } = require('child_process');
|
|
116
|
-
const child = spawn(process.execPath, ['--version']);
|
|
117
|
-
let out = '';
|
|
118
|
-
child.stdout.on('data', d => out += d);
|
|
119
|
-
child.on('close', code => {
|
|
120
|
-
if (code !== 0) process.exit(1);
|
|
121
|
-
});
|
|
122
|
-
`);
|
|
123
|
-
const entries = auditEntries(dir);
|
|
124
|
-
const logged = entries.some(e => e.action === 'run_shell');
|
|
125
|
-
result('spawn', r.status === 0 && logged,
|
|
126
|
-
logged ? 'spawn logged' : 'spawn NOT logged');
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ── 6. child_process.exec ───────────────────────────────────────────────────
|
|
130
|
-
{
|
|
131
|
-
const dir = tmpDir();
|
|
132
|
-
// execSync is synchronous — quote path for Windows spaces
|
|
133
|
-
const r = runNode(dir, `
|
|
134
|
-
const { execSync } = require('child_process');
|
|
135
|
-
execSync('"' + process.execPath + '" --version');
|
|
136
|
-
`);
|
|
137
|
-
const entries = auditEntries(dir);
|
|
138
|
-
const logged = entries.some(e => e.action === 'run_shell');
|
|
139
|
-
result('exec / execSync', r.status === 0 && logged,
|
|
140
|
-
logged ? 'execSync logged' : 'execSync NOT logged');
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// ── 7. observe mode — writes still execute ──────────────────────────────────
|
|
144
|
-
{
|
|
145
|
-
const dir = tmpDir();
|
|
146
|
-
const r = runNode(dir, `
|
|
147
|
-
require('fs').writeFileSync('observe.txt', 'x');
|
|
148
|
-
`, 'observe');
|
|
149
|
-
const entries = auditEntries(dir);
|
|
150
|
-
const wrote = fs.existsSync(path.join(dir, 'observe.txt'));
|
|
151
|
-
const e = entries.find(e => e.action === 'file_write');
|
|
152
|
-
const enforced = e && e.enforced === false;
|
|
153
|
-
result('observe mode (write executes)', wrote && enforced,
|
|
154
|
-
wrote ? (enforced ? 'executed=true, enforced=false' : 'wrong enforced flag') : 'file not written');
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// ── 8. enforce mode — deny rule blocks write ─────────────────────────────────
|
|
158
|
-
{
|
|
159
|
-
const dir = tmpDir();
|
|
160
|
-
// Overwrite policy with deny-all enforce rule
|
|
161
|
-
fs.writeFileSync(path.join(dir, '.lbe', 'policy.json'), JSON.stringify({
|
|
162
|
-
version: 1, mode: 'enforce', workspace: dir,
|
|
163
|
-
rules: [{ id: 'r1', effect: 'deny', type: 'path', pattern: '**', from: 'test' }]
|
|
164
|
-
}));
|
|
165
|
-
const r = runNode(dir, `
|
|
166
|
-
try { require('fs').writeFileSync('blocked.txt', 'x'); }
|
|
167
|
-
catch(e) { /* expected */ }
|
|
168
|
-
`, 'enforce');
|
|
169
|
-
const entries = auditEntries(dir);
|
|
170
|
-
const notWritten = !fs.existsSync(path.join(dir, 'blocked.txt'));
|
|
171
|
-
const e = entries.find(e => e.decision === 'deny');
|
|
172
|
-
result('enforce mode (deny blocks)', notWritten && !!e,
|
|
173
|
-
notWritten ? (e ? 'blocked + logged' : 'blocked, NOT logged') : 'file was written (not blocked)');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// ── 9. no recursion — one write = one audit entry ───────────────────────────
|
|
177
|
-
{
|
|
178
|
-
const dir = tmpDir();
|
|
179
|
-
const r = runNode(dir, `
|
|
180
|
-
require('fs').writeFileSync('single.txt', 'x');
|
|
181
|
-
`);
|
|
182
|
-
const entries = auditEntries(dir).filter(e => e.action === 'file_write' && e.path && e.path.endsWith('single.txt'));
|
|
183
|
-
result('no recursion', entries.length === 1,
|
|
184
|
-
`${entries.length} audit entries for 1 writeFileSync (expected 1)`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ── 10. status PID detection ────────────────────────────────────────────────
|
|
188
|
-
{
|
|
189
|
-
const dir = tmpDir();
|
|
190
|
-
// Start a background node process with the hook
|
|
191
|
-
const child = spawn(process.execPath, ['-e', `
|
|
192
|
-
// keep alive for 3 seconds
|
|
193
|
-
setTimeout(() => {}, 3000);
|
|
194
|
-
`], {
|
|
195
|
-
env: { ...process.env, NODE_OPTIONS: `--require "${HOOK_FWD}"`, LBE_ROOT: dir, LBE_MODE: 'observe' },
|
|
196
|
-
cwd: dir,
|
|
197
|
-
stdio: 'ignore',
|
|
198
|
-
detached: false,
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
// Wait briefly for hook to write status file
|
|
202
|
-
await new Promise(r => setTimeout(r, 800));
|
|
203
|
-
|
|
204
|
-
const statusFile = path.join(dir, '.lbe', 'runtime', 'hook-status.json');
|
|
205
|
-
let pidAlive = false;
|
|
206
|
-
let statusOk = false;
|
|
207
|
-
if (fs.existsSync(statusFile)) {
|
|
208
|
-
const s = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
209
|
-
statusOk = true;
|
|
210
|
-
try { process.kill(s.pid, 0); pidAlive = true; } catch (_) {}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
result('status PID (alive)', statusOk && pidAlive,
|
|
214
|
-
statusOk ? (pidAlive ? `PID ${JSON.parse(fs.readFileSync(statusFile,'utf8')).pid} alive` : 'PID dead') : 'hook-status.json missing');
|
|
215
|
-
|
|
216
|
-
child.kill();
|
|
217
|
-
await new Promise(r => setTimeout(r, 300));
|
|
218
|
-
|
|
219
|
-
// After kill, PID should be gone
|
|
220
|
-
let pidGone = false;
|
|
221
|
-
if (fs.existsSync(statusFile)) {
|
|
222
|
-
const s = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
223
|
-
try { process.kill(s.pid, 0); } catch (_) { pidGone = true; }
|
|
224
|
-
}
|
|
225
|
-
result('status PID (stale after kill)', statusOk && pidGone,
|
|
226
|
-
pidGone ? 'PID correctly gone' : 'PID still appears alive');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ── 11. clean workspace ──────────────────────────────────────────────────────
|
|
230
|
-
{
|
|
231
|
-
const dir = tmpDir();
|
|
232
|
-
runNode(dir, `require('fs').writeFileSync('x.txt','x');`);
|
|
233
|
-
|
|
234
|
-
const rootFiles = fs.readdirSync(dir);
|
|
235
|
-
const lbeFiles = fs.existsSync(path.join(dir, '.lbe')) ? fs.readdirSync(path.join(dir, '.lbe')) : [];
|
|
236
|
-
const badRootFiles = rootFiles.filter(f =>
|
|
237
|
-
f !== '.lbe' && f !== 'x.txt' &&
|
|
238
|
-
(f.startsWith('lbe') || f === 'CLAUDE.md' || f.startsWith('_lbe'))
|
|
239
|
-
);
|
|
240
|
-
result('clean workspace', badRootFiles.length === 0,
|
|
241
|
-
badRootFiles.length ? 'unexpected root files: ' + badRootFiles.join(', ') : 'no LBE pollution in root');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
245
|
-
console.log(`\n${passed + failed} tests ✓ ${passed} passed ${failed > 0 ? '✗ ' + failed + ' failed' : ''}`);
|
|
246
|
-
if (failed > 0) process.exit(1);
|
|
1
|
+
// Proof matrix for LBE preload hook
|
|
2
|
+
// Run with: node _proof.mjs
|
|
3
|
+
// All tests use isolated temp dirs. No shared state.
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import { spawnSync, spawn } from 'child_process';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
|
|
11
|
+
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const HOOK = path.join(ROOT, 'src', 'hooks', 'register.cjs');
|
|
13
|
+
const HOOK_FWD = HOOK.replace(/\\/g, '/');
|
|
14
|
+
|
|
15
|
+
let passed = 0, failed = 0;
|
|
16
|
+
|
|
17
|
+
function result(name, ok, detail = '') {
|
|
18
|
+
const mark = ok ? '✓' : '✗';
|
|
19
|
+
console.log(` ${mark} ${name}${detail ? ' — ' + detail : ''}`);
|
|
20
|
+
if (ok) passed++; else failed++;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function tmpDir(policy = null) {
|
|
24
|
+
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'lbe-proof-'));
|
|
25
|
+
fs.mkdirSync(path.join(d, '.lbe'), { recursive: true });
|
|
26
|
+
fs.writeFileSync(path.join(d, '.lbe', 'policy.json'),
|
|
27
|
+
JSON.stringify(policy || { version: 1, mode: 'observe', workspace: d, rules: [] }));
|
|
28
|
+
return d;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function runNode(dir, code, mode = 'observe') {
|
|
32
|
+
return spawnSync(process.execPath, ['-e', code], {
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
NODE_OPTIONS: `--require "${HOOK_FWD}"`,
|
|
37
|
+
LBE_ROOT: dir,
|
|
38
|
+
LBE_MODE: mode,
|
|
39
|
+
},
|
|
40
|
+
cwd: dir,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function auditEntries(dir) {
|
|
45
|
+
const f = path.join(dir, '.lbe', 'events.jsonl');
|
|
46
|
+
if (!fs.existsSync(f)) return [];
|
|
47
|
+
return fs.readFileSync(f, 'utf8').split('\n').filter(l => l.trim()).map(l => JSON.parse(l));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ── 1. fs.writeFile (callback) ──────────────────────────────────────────────
|
|
51
|
+
{
|
|
52
|
+
const dir = tmpDir();
|
|
53
|
+
const r = runNode(dir, `
|
|
54
|
+
const fs = require('fs');
|
|
55
|
+
fs.writeFile('out.txt', 'hello', err => { if (err) process.exit(1); });
|
|
56
|
+
`);
|
|
57
|
+
const entries = auditEntries(dir);
|
|
58
|
+
const wrote = fs.existsSync(path.join(dir, 'out.txt'));
|
|
59
|
+
const logged = entries.some(e => e.action === 'file_write');
|
|
60
|
+
result('fs.writeFile', wrote && logged && r.status === 0,
|
|
61
|
+
wrote ? (logged ? 'file written + logged' : 'file written, NOT logged') : 'file not written');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── 2. fs.promises.writeFile ────────────────────────────────────────────────
|
|
65
|
+
{
|
|
66
|
+
const dir = tmpDir();
|
|
67
|
+
const r = runNode(dir, `
|
|
68
|
+
const fs = require('fs');
|
|
69
|
+
(async () => {
|
|
70
|
+
await fs.promises.writeFile('out2.txt', 'async');
|
|
71
|
+
})().catch(() => process.exit(1));
|
|
72
|
+
`);
|
|
73
|
+
// Give async time to flush
|
|
74
|
+
const entries = auditEntries(dir);
|
|
75
|
+
const wrote = fs.existsSync(path.join(dir, 'out2.txt'));
|
|
76
|
+
const logged = entries.some(e => e.action === 'file_write');
|
|
77
|
+
result('fs.promises.writeFile', wrote && logged && r.status === 0,
|
|
78
|
+
wrote ? (logged ? 'file written + logged' : 'file written, NOT logged') : 'file not written');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ── 3. fs.rm ────────────────────────────────────────────────────────────────
|
|
82
|
+
{
|
|
83
|
+
const dir = tmpDir();
|
|
84
|
+
fs.writeFileSync(path.join(dir, 'del.txt'), 'x');
|
|
85
|
+
const r = runNode(dir, `
|
|
86
|
+
const fs = require('fs');
|
|
87
|
+
fs.rm('del.txt', err => { if (err) process.exit(1); });
|
|
88
|
+
`);
|
|
89
|
+
const entries = auditEntries(dir);
|
|
90
|
+
const deleted = !fs.existsSync(path.join(dir, 'del.txt'));
|
|
91
|
+
const logged = entries.some(e => e.action === 'file_delete');
|
|
92
|
+
result('fs.rm', deleted && logged && r.status === 0,
|
|
93
|
+
deleted ? (logged ? 'file deleted + logged' : 'file deleted, NOT logged') : 'file not deleted');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ── 4. fs.rename ────────────────────────────────────────────────────────────
|
|
97
|
+
{
|
|
98
|
+
const dir = tmpDir();
|
|
99
|
+
fs.writeFileSync(path.join(dir, 'a.txt'), 'x');
|
|
100
|
+
const r = runNode(dir, `
|
|
101
|
+
const fs = require('fs');
|
|
102
|
+
fs.rename('a.txt', 'b.txt', err => { if (err) process.exit(1); });
|
|
103
|
+
`);
|
|
104
|
+
const entries = auditEntries(dir);
|
|
105
|
+
const renamed = !fs.existsSync(path.join(dir, 'a.txt')) && fs.existsSync(path.join(dir, 'b.txt'));
|
|
106
|
+
const logged = entries.some(e => e.action === 'file_rename');
|
|
107
|
+
result('fs.rename', renamed && logged && r.status === 0,
|
|
108
|
+
renamed ? (logged ? 'renamed + logged' : 'renamed, NOT logged') : 'rename failed');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── 5. child_process.spawn ──────────────────────────────────────────────────
|
|
112
|
+
{
|
|
113
|
+
const dir = tmpDir();
|
|
114
|
+
const r = runNode(dir, `
|
|
115
|
+
const { spawn } = require('child_process');
|
|
116
|
+
const child = spawn(process.execPath, ['--version']);
|
|
117
|
+
let out = '';
|
|
118
|
+
child.stdout.on('data', d => out += d);
|
|
119
|
+
child.on('close', code => {
|
|
120
|
+
if (code !== 0) process.exit(1);
|
|
121
|
+
});
|
|
122
|
+
`);
|
|
123
|
+
const entries = auditEntries(dir);
|
|
124
|
+
const logged = entries.some(e => e.action === 'run_shell');
|
|
125
|
+
result('spawn', r.status === 0 && logged,
|
|
126
|
+
logged ? 'spawn logged' : 'spawn NOT logged');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ── 6. child_process.exec ───────────────────────────────────────────────────
|
|
130
|
+
{
|
|
131
|
+
const dir = tmpDir();
|
|
132
|
+
// execSync is synchronous — quote path for Windows spaces
|
|
133
|
+
const r = runNode(dir, `
|
|
134
|
+
const { execSync } = require('child_process');
|
|
135
|
+
execSync('"' + process.execPath + '" --version');
|
|
136
|
+
`);
|
|
137
|
+
const entries = auditEntries(dir);
|
|
138
|
+
const logged = entries.some(e => e.action === 'run_shell');
|
|
139
|
+
result('exec / execSync', r.status === 0 && logged,
|
|
140
|
+
logged ? 'execSync logged' : 'execSync NOT logged');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ── 7. observe mode — writes still execute ──────────────────────────────────
|
|
144
|
+
{
|
|
145
|
+
const dir = tmpDir();
|
|
146
|
+
const r = runNode(dir, `
|
|
147
|
+
require('fs').writeFileSync('observe.txt', 'x');
|
|
148
|
+
`, 'observe');
|
|
149
|
+
const entries = auditEntries(dir);
|
|
150
|
+
const wrote = fs.existsSync(path.join(dir, 'observe.txt'));
|
|
151
|
+
const e = entries.find(e => e.action === 'file_write');
|
|
152
|
+
const enforced = e && e.enforced === false;
|
|
153
|
+
result('observe mode (write executes)', wrote && enforced,
|
|
154
|
+
wrote ? (enforced ? 'executed=true, enforced=false' : 'wrong enforced flag') : 'file not written');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ── 8. enforce mode — deny rule blocks write ─────────────────────────────────
|
|
158
|
+
{
|
|
159
|
+
const dir = tmpDir();
|
|
160
|
+
// Overwrite policy with deny-all enforce rule
|
|
161
|
+
fs.writeFileSync(path.join(dir, '.lbe', 'policy.json'), JSON.stringify({
|
|
162
|
+
version: 1, mode: 'enforce', workspace: dir,
|
|
163
|
+
rules: [{ id: 'r1', effect: 'deny', type: 'path', pattern: '**', from: 'test' }]
|
|
164
|
+
}));
|
|
165
|
+
const r = runNode(dir, `
|
|
166
|
+
try { require('fs').writeFileSync('blocked.txt', 'x'); }
|
|
167
|
+
catch(e) { /* expected */ }
|
|
168
|
+
`, 'enforce');
|
|
169
|
+
const entries = auditEntries(dir);
|
|
170
|
+
const notWritten = !fs.existsSync(path.join(dir, 'blocked.txt'));
|
|
171
|
+
const e = entries.find(e => e.decision === 'deny');
|
|
172
|
+
result('enforce mode (deny blocks)', notWritten && !!e,
|
|
173
|
+
notWritten ? (e ? 'blocked + logged' : 'blocked, NOT logged') : 'file was written (not blocked)');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ── 9. no recursion — one write = one audit entry ───────────────────────────
|
|
177
|
+
{
|
|
178
|
+
const dir = tmpDir();
|
|
179
|
+
const r = runNode(dir, `
|
|
180
|
+
require('fs').writeFileSync('single.txt', 'x');
|
|
181
|
+
`);
|
|
182
|
+
const entries = auditEntries(dir).filter(e => e.action === 'file_write' && e.path && e.path.endsWith('single.txt'));
|
|
183
|
+
result('no recursion', entries.length === 1,
|
|
184
|
+
`${entries.length} audit entries for 1 writeFileSync (expected 1)`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── 10. status PID detection ────────────────────────────────────────────────
|
|
188
|
+
{
|
|
189
|
+
const dir = tmpDir();
|
|
190
|
+
// Start a background node process with the hook
|
|
191
|
+
const child = spawn(process.execPath, ['-e', `
|
|
192
|
+
// keep alive for 3 seconds
|
|
193
|
+
setTimeout(() => {}, 3000);
|
|
194
|
+
`], {
|
|
195
|
+
env: { ...process.env, NODE_OPTIONS: `--require "${HOOK_FWD}"`, LBE_ROOT: dir, LBE_MODE: 'observe' },
|
|
196
|
+
cwd: dir,
|
|
197
|
+
stdio: 'ignore',
|
|
198
|
+
detached: false,
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Wait briefly for hook to write status file
|
|
202
|
+
await new Promise(r => setTimeout(r, 800));
|
|
203
|
+
|
|
204
|
+
const statusFile = path.join(dir, '.lbe', 'runtime', 'hook-status.json');
|
|
205
|
+
let pidAlive = false;
|
|
206
|
+
let statusOk = false;
|
|
207
|
+
if (fs.existsSync(statusFile)) {
|
|
208
|
+
const s = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
209
|
+
statusOk = true;
|
|
210
|
+
try { process.kill(s.pid, 0); pidAlive = true; } catch (_) {}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
result('status PID (alive)', statusOk && pidAlive,
|
|
214
|
+
statusOk ? (pidAlive ? `PID ${JSON.parse(fs.readFileSync(statusFile,'utf8')).pid} alive` : 'PID dead') : 'hook-status.json missing');
|
|
215
|
+
|
|
216
|
+
child.kill();
|
|
217
|
+
await new Promise(r => setTimeout(r, 300));
|
|
218
|
+
|
|
219
|
+
// After kill, PID should be gone
|
|
220
|
+
let pidGone = false;
|
|
221
|
+
if (fs.existsSync(statusFile)) {
|
|
222
|
+
const s = JSON.parse(fs.readFileSync(statusFile, 'utf8'));
|
|
223
|
+
try { process.kill(s.pid, 0); } catch (_) { pidGone = true; }
|
|
224
|
+
}
|
|
225
|
+
result('status PID (stale after kill)', statusOk && pidGone,
|
|
226
|
+
pidGone ? 'PID correctly gone' : 'PID still appears alive');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── 11. clean workspace ──────────────────────────────────────────────────────
|
|
230
|
+
{
|
|
231
|
+
const dir = tmpDir();
|
|
232
|
+
runNode(dir, `require('fs').writeFileSync('x.txt','x');`);
|
|
233
|
+
|
|
234
|
+
const rootFiles = fs.readdirSync(dir);
|
|
235
|
+
const lbeFiles = fs.existsSync(path.join(dir, '.lbe')) ? fs.readdirSync(path.join(dir, '.lbe')) : [];
|
|
236
|
+
const badRootFiles = rootFiles.filter(f =>
|
|
237
|
+
f !== '.lbe' && f !== 'x.txt' &&
|
|
238
|
+
(f.startsWith('lbe') || f === 'CLAUDE.md' || f.startsWith('_lbe'))
|
|
239
|
+
);
|
|
240
|
+
result('clean workspace', badRootFiles.length === 0,
|
|
241
|
+
badRootFiles.length ? 'unexpected root files: ' + badRootFiles.join(', ') : 'no LBE pollution in root');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── Summary ──────────────────────────────────────────────────────────────────
|
|
245
|
+
console.log(`\n${passed + failed} tests ✓ ${passed} passed ${failed > 0 ? '✗ ' + failed + ' failed' : ''}`);
|
|
246
|
+
if (failed > 0) process.exit(1);
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "0.2.0"
|
|
1
|
+
{
|
|
2
|
+
"version": "0.2.0"
|
|
3
3
|
}
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 1,
|
|
3
|
-
"createdAt": "2026-06-19T00:00:00Z",
|
|
4
|
-
"default": "DENY",
|
|
5
|
-
"requesters": {
|
|
6
|
-
"agent:sdk": {
|
|
7
|
-
"allowAdapters": ["file", "shell", "noop"],
|
|
8
|
-
"allowCommands": ["READ_FILE", "WRITE_FILE", "PATCH_FILE", "DELETE_FILE", "RUN_SHELL", "ECHO"],
|
|
9
|
-
"filesystem": {
|
|
10
|
-
"roots": ["."],
|
|
11
|
-
"denyPatterns": ["**/.git/**", "**/keys/**", "**/*.key", "**/.env*"]
|
|
12
|
-
},
|
|
13
|
-
"exec": {
|
|
14
|
-
"allowCmds": ["node", "echo"],
|
|
15
|
-
"denyCmds": ["rm", "del", "format", "curl", "wget", "sudo"]
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
},
|
|
19
|
-
"security": {
|
|
20
|
-
"maxClockSkewSec": 600,
|
|
21
|
-
"maxPolicyCreatedAtSkewSec": 31536000,
|
|
22
|
-
"defaultRateLimit": { "windowSec": 60, "maxRequests": 30 }
|
|
23
|
-
}
|
|
24
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"createdAt": "2026-06-19T00:00:00Z",
|
|
4
|
+
"default": "DENY",
|
|
5
|
+
"requesters": {
|
|
6
|
+
"agent:sdk": {
|
|
7
|
+
"allowAdapters": ["file", "shell", "noop"],
|
|
8
|
+
"allowCommands": ["READ_FILE", "WRITE_FILE", "PATCH_FILE", "DELETE_FILE", "RUN_SHELL", "ECHO"],
|
|
9
|
+
"filesystem": {
|
|
10
|
+
"roots": ["."],
|
|
11
|
+
"denyPatterns": ["**/.git/**", "**/keys/**", "**/*.key", "**/.env*"]
|
|
12
|
+
},
|
|
13
|
+
"exec": {
|
|
14
|
+
"allowCmds": ["node", "echo"],
|
|
15
|
+
"denyCmds": ["rm", "del", "format", "curl", "wget", "sudo"]
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"security": {
|
|
20
|
+
"maxClockSkewSec": 600,
|
|
21
|
+
"maxPolicyCreatedAtSkewSec": 31536000,
|
|
22
|
+
"defaultRateLimit": { "windowSec": 60, "maxRequests": 30 }
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/cli/lbe.js
CHANGED
|
@@ -4051,7 +4051,6 @@ LBE Central State Directory`);
|
|
|
4051
4051
|
// src/cli/commands/proof.js
|
|
4052
4052
|
import fs28 from "node:fs";
|
|
4053
4053
|
import path34 from "node:path";
|
|
4054
|
-
import os2 from "node:os";
|
|
4055
4054
|
|
|
4056
4055
|
// src/state/proofRunner.js
|
|
4057
4056
|
import fs27 from "node:fs";
|
|
@@ -4089,7 +4088,7 @@ function buildPublicProof(proof, targets) {
|
|
|
4089
4088
|
}
|
|
4090
4089
|
async function proofCommand(opts) {
|
|
4091
4090
|
const workspaceRoot = path34.resolve(opts.root || process.cwd());
|
|
4092
|
-
const { stateDir
|
|
4091
|
+
const { stateDir } = resolveWorkspaceState(workspaceRoot);
|
|
4093
4092
|
const proof = loadLatestProof(stateDir);
|
|
4094
4093
|
const isPublic = opts.public === true || opts.public === "true";
|
|
4095
4094
|
const isJson = opts.json === true || opts.json === "true" || isPublic;
|
|
@@ -4203,7 +4202,7 @@ function classifyDependencySpec(spec) {
|
|
|
4203
4202
|
}
|
|
4204
4203
|
function findPackageRoot(startFile) {
|
|
4205
4204
|
let dir = fs29.statSync(startFile).isDirectory() ? startFile : path35.dirname(startFile);
|
|
4206
|
-
while (
|
|
4205
|
+
while (dir) {
|
|
4207
4206
|
const candidate = path35.join(dir, "package.json");
|
|
4208
4207
|
const pkg = readJson(candidate);
|
|
4209
4208
|
if (pkg?.name === PACKAGE_NAME) return dir;
|