@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,175 @@
1
+ // src/cli/commands/dryrun.js
2
+ // Validate proposal and simulate execution
3
+
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { validateCommand } from '../../core/validator.js';
7
+ import { NonceStore } from '../../core/nonceStore.js';
8
+ import { executeAdapter } from '../../adapters/index.js';
9
+ import { loadKeysStore } from '../../core/trustedKeys.js';
10
+ import { verifyPolicySignature } from '../../core/policySignature.js';
11
+ import { validateAndUpdatePolicyVersionState } from '../../core/policyVersionGuard.js';
12
+
13
+ export async function dryrunCommand(opts) {
14
+ const { in: inFile } = opts;
15
+ const config = opts.config || opts.policy;
16
+ const pubKey = opts['pub-key'];
17
+ const keysStorePath = opts['keys-store'] || path.resolve('.lbe/config/keys.json');
18
+ const policySigPath = opts['policy-sig'] || path.resolve('.lbe/config/policy.sig.json');
19
+ const policyStatePath = opts['policy-state'] || path.resolve('.lbe/data/policy.state.json');
20
+ const allowUnsignedPolicy = opts['policy-unsigned-ok'] === true || String(opts['policy-unsigned-ok']).toLowerCase() === 'true';
21
+ // Validate required arguments
22
+ if (!inFile) {
23
+ console.error('Error: --in <file> is required');
24
+ process.exit(1);
25
+ }
26
+
27
+ // Read proposal file
28
+ let proposal;
29
+ try {
30
+ const filePath = path.resolve(inFile);
31
+ const content = fs.readFileSync(filePath, 'utf-8');
32
+ proposal = JSON.parse(content);
33
+ } catch (error) {
34
+ console.error(JSON.stringify({
35
+ status: 'error',
36
+ error: 'INVALID_PROPOSAL_FILE',
37
+ message: error.message
38
+ }));
39
+ process.exit(5);
40
+ }
41
+
42
+ // Load policy
43
+ let policy;
44
+ try {
45
+ const policyPath = config || path.resolve('.lbe/config/policy.default.json');
46
+ if (!fs.existsSync(policyPath)) {
47
+ console.error(JSON.stringify({
48
+ status: 'error',
49
+ error: 'MISSING_POLICY',
50
+ message: `Policy file not found: ${policyPath}`
51
+ }));
52
+ process.exit(1);
53
+ }
54
+ const policyContent = fs.readFileSync(policyPath, 'utf-8');
55
+ policy = JSON.parse(policyContent);
56
+ } catch (error) {
57
+ console.error(JSON.stringify({
58
+ status: 'error',
59
+ error: 'INVALID_POLICY',
60
+ message: error.message
61
+ }));
62
+ process.exit(1);
63
+ }
64
+
65
+ // Load key store (preferred) with legacy pub-key fallback
66
+ const keyStoreResult = loadKeysStore(keysStorePath);
67
+ const keyStore = keyStoreResult.ok ? keyStoreResult.store : null;
68
+
69
+ // Preflight: policy signature verification (strict by default)
70
+ const policySigCheck = verifyPolicySignature({
71
+ policyObj: policy,
72
+ keyStore,
73
+ policySigPath,
74
+ allowUnsigned: allowUnsignedPolicy
75
+ });
76
+ if (!policySigCheck.ok) {
77
+ console.error(JSON.stringify({
78
+ status: 'error',
79
+ error: policySigCheck.reason,
80
+ message: policySigCheck.message
81
+ }, null, 2));
82
+ process.exit(8);
83
+ }
84
+
85
+ const versionCheck = validateAndUpdatePolicyVersionState({
86
+ policyObj: policy,
87
+ statePath: policyStatePath,
88
+ maxCreatedAtSkewSec: policy?.security?.maxPolicyCreatedAtSkewSec
89
+ });
90
+ if (!versionCheck.ok) {
91
+ console.error(JSON.stringify({
92
+ status: 'error',
93
+ error: versionCheck.reason,
94
+ message: versionCheck.message
95
+ }, null, 2));
96
+ process.exit(8);
97
+ }
98
+
99
+ // Load nonce store
100
+ const nonceDb = new NonceStore(path.resolve('.lbe/data/nonce.db.json'));
101
+ await nonceDb.load();
102
+
103
+ if (!keyStore && !pubKey) {
104
+ console.error(JSON.stringify({
105
+ status: 'error',
106
+ error: 'MISSING_KEY_MATERIAL',
107
+ message: `${keyStoreResult.message}. Provide --pub-key/--pub-key-file or create .lbe/config/keys.json`
108
+ }));
109
+ process.exit(1);
110
+ }
111
+
112
+ // Validate command
113
+ const validateResult = validateCommand({
114
+ commandObj: proposal,
115
+ pubKeyB64: pubKey,
116
+ keyStore,
117
+ nonceDb,
118
+ policy
119
+ });
120
+
121
+ if (!validateResult.valid) {
122
+ const output = {
123
+ status: 'invalid',
124
+ commandId: proposal.commandId || 'N/A',
125
+ checks: validateResult.checks,
126
+ errors: validateResult.errors || [],
127
+ executionResult: null
128
+ };
129
+ console.log(JSON.stringify(output, null, 2));
130
+
131
+ if (validateResult.checks.schema === false) process.exit(5);
132
+ if (validateResult.checks.signature === false) process.exit(3);
133
+ if (validateResult.checks.nonce === false) process.exit(4);
134
+ if (validateResult.checks.timestamp === false) process.exit(6);
135
+ if (validateResult.checks.rateLimit === false) process.exit(7);
136
+ if (validateResult.checks.policy === false) process.exit(2);
137
+ process.exit(9);
138
+ }
139
+
140
+ // Simulate execution with noop adapter
141
+ let executionResult;
142
+ try {
143
+ const requesterPolicy = policy.requesters?.[proposal.requesterId];
144
+ executionResult = await executeAdapter(
145
+ 'noop',
146
+ proposal,
147
+ policy,
148
+ requesterPolicy
149
+ );
150
+ } catch (error) {
151
+ executionResult = {
152
+ adapter: 'noop',
153
+ status: 'error',
154
+ error: error.message
155
+ };
156
+ }
157
+
158
+ // Output structured result
159
+ const output = {
160
+ status: 'valid_simulated',
161
+ commandId: proposal.commandId || 'N/A',
162
+ checks: validateResult.checks,
163
+ risk: validateResult.risk || 'UNKNOWN',
164
+ executionResult: {
165
+ adapter: executionResult.adapter,
166
+ status: executionResult.status,
167
+ output: executionResult.output || executionResult.error || '',
168
+ exitCode: executionResult.exitCode || 0,
169
+ note: 'This is a simulation using noop adapter. No actual execution occurred.'
170
+ }
171
+ };
172
+
173
+ console.log(JSON.stringify(output, null, 2));
174
+ process.exit(0);
175
+ }
@@ -0,0 +1,153 @@
1
+ // src/cli/commands/health.js
2
+ // Deployment health check command
3
+
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { performIntegrityCheck } from '../../core/integrity.js';
7
+
8
+ function toBoolean(value, defaultValue) {
9
+ if (value === undefined) return defaultValue;
10
+ if (value === true || value === false) return value;
11
+ const normalized = String(value).trim().toLowerCase();
12
+ if (normalized === 'true' || normalized === '1' || normalized === 'yes') return true;
13
+ if (normalized === 'false' || normalized === '0' || normalized === 'no') return false;
14
+ return defaultValue;
15
+ }
16
+
17
+ function addCheck(checks, name, ok, message) {
18
+ checks[name] = { ok, message };
19
+ }
20
+
21
+ function canReadFile(filePath) {
22
+ try {
23
+ fs.accessSync(filePath, fs.constants.R_OK);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ function checkDataWritable(dataDir) {
31
+ const marker = path.join(dataDir, `.healthcheck-${Date.now()}`);
32
+ try {
33
+ fs.mkdirSync(dataDir, { recursive: true });
34
+ fs.writeFileSync(marker, 'ok', 'utf8');
35
+ fs.unlinkSync(marker);
36
+ return { ok: true, message: 'Data directory writable' };
37
+ } catch (error) {
38
+ return { ok: false, message: `Data directory not writable: ${error.message}` };
39
+ }
40
+ }
41
+
42
+ export async function healthCommand(opts) {
43
+ const jsonOutput = toBoolean(opts.json, true);
44
+ const policyPath = path.resolve(opts.config || opts.policy || '.lbe/config/policy.default.json');
45
+ const policySigPath = path.resolve(opts['policy-sig'] || '.lbe/config/policy.sig.json');
46
+ const keysStorePath = path.resolve(opts['keys-store'] || '.lbe/config/keys.json');
47
+ const dataDir = path.resolve(opts['data-dir'] || '.lbe/data');
48
+ const auditPath = path.resolve(opts.audit || path.join(dataDir, 'audit.log.jsonl'));
49
+ const noncePath = path.resolve(opts['nonce-db'] || path.join(dataDir, 'nonce.db.json'));
50
+ const ratePath = path.resolve(opts['rate-db'] || path.join(dataDir, 'rate-limit.db.json'));
51
+ const policyStatePath = path.resolve(opts['policy-state'] || path.join(dataDir, 'policy.state.json'));
52
+ const integrityStrict = toBoolean(opts['integrity-strict'], false);
53
+ const integrityManifestPath = path.resolve(opts['integrity-manifest'] || '.lbe/config/integrity.manifest.json');
54
+
55
+ const checks = {};
56
+
57
+ addCheck(
58
+ checks,
59
+ 'policy',
60
+ fs.existsSync(policyPath) && canReadFile(policyPath),
61
+ fs.existsSync(policyPath)
62
+ ? `Policy file readable: ${policyPath}`
63
+ : `Policy file missing: ${policyPath}`
64
+ );
65
+
66
+ addCheck(
67
+ checks,
68
+ 'policySignature',
69
+ fs.existsSync(policySigPath) && canReadFile(policySigPath),
70
+ fs.existsSync(policySigPath)
71
+ ? `Policy signature readable: ${policySigPath}`
72
+ : `Policy signature missing: ${policySigPath}`
73
+ );
74
+
75
+ addCheck(
76
+ checks,
77
+ 'trustedKeys',
78
+ fs.existsSync(keysStorePath) && canReadFile(keysStorePath),
79
+ fs.existsSync(keysStorePath)
80
+ ? `Trusted keys readable: ${keysStorePath}`
81
+ : `Trusted keys missing: ${keysStorePath}`
82
+ );
83
+
84
+ addCheck(
85
+ checks,
86
+ 'auditLog',
87
+ fs.existsSync(auditPath) && canReadFile(auditPath),
88
+ fs.existsSync(auditPath)
89
+ ? `Audit log readable: ${auditPath}`
90
+ : `Audit log missing: ${auditPath}`
91
+ );
92
+
93
+ addCheck(
94
+ checks,
95
+ 'nonceDb',
96
+ fs.existsSync(noncePath) && canReadFile(noncePath),
97
+ fs.existsSync(noncePath)
98
+ ? `Nonce DB readable: ${noncePath}`
99
+ : `Nonce DB missing: ${noncePath}`
100
+ );
101
+
102
+ addCheck(
103
+ checks,
104
+ 'rateLimitDb',
105
+ fs.existsSync(ratePath) && canReadFile(ratePath),
106
+ fs.existsSync(ratePath)
107
+ ? `Rate-limit DB readable: ${ratePath}`
108
+ : `Rate-limit DB missing: ${ratePath}`
109
+ );
110
+
111
+ addCheck(
112
+ checks,
113
+ 'policyState',
114
+ fs.existsSync(policyStatePath) && canReadFile(policyStatePath),
115
+ fs.existsSync(policyStatePath)
116
+ ? `Policy state readable: ${policyStatePath}`
117
+ : `Policy state missing: ${policyStatePath}`
118
+ );
119
+
120
+ const writable = checkDataWritable(dataDir);
121
+ addCheck(checks, 'dataWritable', writable.ok, writable.message);
122
+
123
+ if (integrityStrict) {
124
+ const integrity = await performIntegrityCheck({
125
+ strict: true,
126
+ manifestPath: integrityManifestPath
127
+ });
128
+ addCheck(
129
+ checks,
130
+ 'integrity',
131
+ integrity.valid,
132
+ integrity.valid
133
+ ? integrity.message
134
+ : `${integrity.reason}: ${integrity.message}`
135
+ );
136
+ }
137
+
138
+ const allOk = Object.values(checks).every((c) => c.ok === true);
139
+ const output = {
140
+ ok: allOk,
141
+ status: allOk ? 'healthy' : 'unhealthy',
142
+ timestamp: new Date().toISOString(),
143
+ checks
144
+ };
145
+
146
+ if (jsonOutput) {
147
+ console.log(JSON.stringify(output, null, 2));
148
+ } else {
149
+ console.log(`${output.status.toUpperCase()}: ${Object.keys(checks).length} checks`);
150
+ }
151
+
152
+ process.exit(allOk ? 0 : 8);
153
+ }
@@ -0,0 +1,306 @@
1
+ // src/cli/commands/init.js
2
+ // Initialize LBE in a workspace:
3
+ // 1. Scan project → generate workspace contract (semantics + enforcement)
4
+ // 2. Show compact summary → ask once
5
+ // 3. Write lbe.workspace.json
6
+ // 4. Set up crypto infrastructure silently
7
+
8
+ import fs from 'fs';
9
+ import path from 'path';
10
+ import readline from 'readline';
11
+ import { generateKeyPair } from '../../core/signature.js';
12
+ import { createPolicySignatureEnvelope } from '../../core/policySignature.js';
13
+ import { scanWorkspace, formatSummary } from '../../core/workspaceScanner.js';
14
+
15
+ // ─── Interactive prompt ───────────────────────────────────────────────────────
16
+
17
+ function ask(question) {
18
+ // Non-interactive (CI / pipe) — default accept
19
+ if (!process.stdin.isTTY) return Promise.resolve('y');
20
+ return new Promise(resolve => {
21
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
22
+ rl.question(question, ans => { rl.close(); resolve(ans.trim().toLowerCase()); });
23
+ });
24
+ }
25
+
26
+ // ─── Strict / relaxed adjustments ────────────────────────────────────────────
27
+
28
+ function applyStrict(enforcement) {
29
+ // Strict: move approval items to deny, add common risky patterns
30
+ return {
31
+ ...enforcement,
32
+ deny: [...new Set([...enforcement.deny, ...enforcement.approval, '*.json', 'config/**'])],
33
+ approval: [],
34
+ };
35
+ }
36
+
37
+ function applyRelaxed(enforcement) {
38
+ // Relaxed: drop approval requirement, everything not denied is allowed
39
+ return { ...enforcement, approval: [] };
40
+ }
41
+
42
+ // ─── Crypto setup (silent — infrastructure only) ──────────────────────────────
43
+
44
+ function setupCrypto(cwd) {
45
+ const nowIso = new Date().toISOString();
46
+ const expiresAt = new Date(Date.now() + 180 * 24 * 60 * 60 * 1000).toISOString();
47
+ const defaultKeyId = 'agent:gpt-v1-2026Q1';
48
+ const signerKeyId = 'policy-signer-v1-2026Q1';
49
+
50
+ // All LBE infrastructure lives under .lbe/ — nothing pollutes the project root
51
+ const lbeDir = path.join(cwd, '.lbe');
52
+ for (const d of ['config', 'keys', 'data']) {
53
+ fs.mkdirSync(path.join(lbeDir, d), { recursive: true });
54
+ }
55
+
56
+ // Data stores
57
+ const dataFiles = {
58
+ '.lbe/data/nonce.db.json': JSON.stringify({ entries: [] }, null, 2),
59
+ '.lbe/data/rate-limit.db.json': JSON.stringify({ entries: [] }, null, 2),
60
+ '.lbe/data/policy.state.json': JSON.stringify({ schemaVersion: '1', lastAccepted: null, updatedAt: null }, null, 2),
61
+ '.lbe/data/audit.log.jsonl': '',
62
+ };
63
+ for (const [rel, content] of Object.entries(dataFiles)) {
64
+ const p = path.join(cwd, rel);
65
+ if (!fs.existsSync(p)) fs.writeFileSync(p, content);
66
+ }
67
+
68
+ // Keypair
69
+ const keyDir = path.join(lbeDir, 'keys');
70
+ const pubPath = path.join(keyDir, 'public.key');
71
+ const secPath = path.join(keyDir, 'secret.key');
72
+ let publicKeyB64, secretKeyB64;
73
+ if (fs.existsSync(pubPath) && fs.existsSync(secPath)) {
74
+ publicKeyB64 = fs.readFileSync(pubPath, 'utf8').trim();
75
+ secretKeyB64 = fs.readFileSync(secPath, 'utf8').trim();
76
+ } else {
77
+ const kp = generateKeyPair();
78
+ publicKeyB64 = kp.publicKey;
79
+ secretKeyB64 = kp.secretKey;
80
+ fs.writeFileSync(pubPath, publicKeyB64);
81
+ fs.writeFileSync(secPath, secretKeyB64, { mode: 0o600 });
82
+ }
83
+
84
+ // Keys store
85
+ const keysPath = path.join(lbeDir, 'config/keys.json');
86
+ const keysStore = fs.existsSync(keysPath)
87
+ ? JSON.parse(fs.readFileSync(keysPath, 'utf8'))
88
+ : { schemaVersion: '1', defaultKeyId, trustedKeys: {} };
89
+
90
+ for (const keyId of [defaultKeyId, signerKeyId]) {
91
+ if (!keysStore.trustedKeys[keyId]) {
92
+ keysStore.trustedKeys[keyId] = {
93
+ publicKey: publicKeyB64,
94
+ notBefore: nowIso,
95
+ expiresAt,
96
+ validFrom: nowIso,
97
+ validUntil: expiresAt,
98
+ deprecated: false,
99
+ };
100
+ }
101
+ }
102
+ keysStore.defaultKeyId = defaultKeyId;
103
+ fs.writeFileSync(keysPath, JSON.stringify(keysStore, null, 2));
104
+
105
+ // Policy
106
+ const policyPath = path.join(lbeDir, 'config/policy.default.json');
107
+ let policyObj;
108
+ if (fs.existsSync(policyPath)) {
109
+ policyObj = JSON.parse(fs.readFileSync(policyPath, 'utf8'));
110
+ } else {
111
+ policyObj = {
112
+ default: 'DENY',
113
+ version: '1.0.0',
114
+ createdAt: nowIso,
115
+ security: {
116
+ maxClockSkewSec: 600,
117
+ maxPolicyCreatedAtSkewSec: 31536000,
118
+ defaultRateLimit: { windowSec: 60, maxRequests: 30 }
119
+ },
120
+ requesters: {
121
+ 'agent:gpt': {
122
+ allowAdapters: ['noop', 'shell'],
123
+ allowCommands: ['RUN_SHELL'],
124
+ rateLimit: { windowSec: 60, maxRequests: 30 },
125
+ filesystem: { roots: [cwd], denyPatterns: ['**/.git/**', '**/secrets/**', '**/*.key'] },
126
+ exec: { allowCmds: ['ls', 'node', 'python', 'echo'], denyCmds: ['rm', 'chmod', 'chown', 'curl', 'wget', 'su', 'sudo'] }
127
+ }
128
+ }
129
+ };
130
+ fs.writeFileSync(policyPath, JSON.stringify(policyObj, null, 2));
131
+ }
132
+
133
+ // Policy signature
134
+ const sigResult = createPolicySignatureEnvelope({ policyObj, secretKeyB64, keyId: signerKeyId });
135
+ if (sigResult.ok) {
136
+ fs.writeFileSync(path.join(lbeDir, 'config/policy.sig.json'), JSON.stringify(sigResult.envelope, null, 2));
137
+ }
138
+
139
+ return { defaultKeyId, secretKeyB64, publicKeyB64 };
140
+ }
141
+
142
+ // ─── Agent governance files ───────────────────────────────────────────────────
143
+ // Generated once by init so every AI agent auto-discovers LBE without the user
144
+ // needing to explain it. Logic and internals are never mentioned — only the API.
145
+
146
+ function agentContractContent() {
147
+ return `# LBE Governance Contract
148
+
149
+ This project has LetterBlack LBE (Local-first execution Governance) active.
150
+
151
+ ## Your role as an agent
152
+
153
+ Every file write, delete, and shell command must go through the LBE executor.
154
+ The executor validates each action against the project policy and audits it.
155
+
156
+ ## How to perform actions
157
+
158
+ \`\`\`js
159
+ import { createLocalExecutor } from '@letterblack/lbe-exec';
160
+ const lbe = createLocalExecutor({ rootDir: process.cwd() });
161
+
162
+ await lbe.writeFile('output/report.md', content);
163
+ await lbe.readFile('src/config.json');
164
+ await lbe.patchFile('src/index.js', patch);
165
+ await lbe.deleteFile('tmp/scratch.txt');
166
+ await lbe.runShell('node', ['scripts/build.js']);
167
+ \`\`\`
168
+
169
+ ## What you must never do
170
+
171
+ - Call fs.* or child_process.* directly
172
+ - Modify \`.lbe/policy.json\` directly — propose a rule via \`lbe.policy.proposeRule()\`
173
+ - Attempt to bypass or disable the executor
174
+
175
+ ## Proposing a policy rule
176
+
177
+ \`\`\`js
178
+ const proposal = lbe.policy.proposeRule({
179
+ effect: 'deny', type: 'path', pattern: 'secrets/**',
180
+ from: 'agent: these files should never be modified'
181
+ });
182
+ // Return proposal to the user — never call lbe.policy.addRule() yourself.
183
+ \`\`\`
184
+
185
+ ## Result shape
186
+
187
+ \`{ ok: boolean, decision: 'allow' | 'deny' | 'observe', executed: boolean }\`
188
+
189
+ ## Files
190
+
191
+ - Policy: \`.lbe/policy.json\`
192
+ - Audit: \`.lbe/audit.jsonl\`
193
+ - Status: \`npx lbe-exec status\`
194
+ `;
195
+ }
196
+
197
+ function writeAgentContract(cwd) {
198
+ const lbeDir = path.join(cwd, '.lbe');
199
+ fs.mkdirSync(lbeDir, { recursive: true });
200
+ fs.writeFileSync(path.join(lbeDir, 'AGENT_CONTRACT.md'), agentContractContent());
201
+ }
202
+
203
+ // Migrate legacy root-level LBE files into .lbe/ so the workspace stays clean.
204
+ function migrateLegacyRootFiles(cwd) {
205
+ const lbeDir = path.join(cwd, '.lbe');
206
+ fs.mkdirSync(lbeDir, { recursive: true });
207
+ const migrations = [
208
+ ['lbe.policy.json', '.lbe/policy.json'],
209
+ ['lbe.workspace.json', '.lbe/workspace.json'],
210
+ ];
211
+ const removed = [];
212
+ for (const [src, dest] of migrations) {
213
+ const srcPath = path.join(cwd, src);
214
+ const destPath = path.join(cwd, dest);
215
+ if (fs.existsSync(srcPath) && !fs.existsSync(destPath)) {
216
+ fs.renameSync(srcPath, destPath);
217
+ removed.push(src + ' → ' + dest);
218
+ } else if (fs.existsSync(srcPath)) {
219
+ fs.unlinkSync(srcPath); // dest already exists — just drop the old file
220
+ removed.push(src + ' (removed — .lbe/ version exists)');
221
+ }
222
+ }
223
+ // Remove files that should never have been created in the project root
224
+ const toDelete = ['CLAUDE.md', path.join('.github', 'copilot-instructions.md')];
225
+ for (const rel of toDelete) {
226
+ const p = path.join(cwd, rel);
227
+ // Only remove if it contains the lbe-governance marker — don't touch user files
228
+ if (fs.existsSync(p)) {
229
+ const content = fs.readFileSync(p, 'utf8');
230
+ if (content.includes('lbe-governance') || content.includes('LetterBlack LBE')) {
231
+ fs.unlinkSync(p);
232
+ removed.push(rel + ' (removed — LBE-generated file)');
233
+ }
234
+ }
235
+ }
236
+ return removed;
237
+ }
238
+
239
+ // ─── Main ─────────────────────────────────────────────────────────────────────
240
+
241
+ export async function initCommand(opts = {}) {
242
+ const cwd = process.cwd();
243
+ const yes = opts.yes || opts.y || !process.stdin.isTTY;
244
+ const lbeDir = path.join(cwd, '.lbe');
245
+ fs.mkdirSync(lbeDir, { recursive: true });
246
+ const outPath = path.join(lbeDir, 'workspace.json');
247
+
248
+ // 1. Scan
249
+ console.log('\nScanning workspace...\n');
250
+ const { projectTypes, primaryType, semantics, enforcement } = scanWorkspace(cwd);
251
+
252
+ // 2. Show summary
253
+ console.log(formatSummary(projectTypes, semantics, enforcement));
254
+ console.log('');
255
+
256
+ // 3. Ask once (unless --yes or non-interactive)
257
+ let finalEnforcement = enforcement;
258
+ if (!yes) {
259
+ const answer = await ask('Accept? [Y = accept / s = strict / r = relaxed / n = cancel] ');
260
+ if (answer === 'n') {
261
+ console.log('Cancelled.');
262
+ return { success: false };
263
+ }
264
+ if (answer === 's') finalEnforcement = applyStrict(enforcement);
265
+ if (answer === 'r') finalEnforcement = applyRelaxed(enforcement);
266
+ }
267
+
268
+ // 4. Write lbe.workspace.json
269
+ const contract = {
270
+ lbe: true,
271
+ version: '0.4.0',
272
+ state: 'local',
273
+ projectTypes,
274
+ primaryType,
275
+ semantics,
276
+ enforcement: finalEnforcement,
277
+ };
278
+ fs.writeFileSync(outPath, JSON.stringify(contract, null, 2));
279
+ console.log('✓ Wrote .lbe/workspace.json');
280
+
281
+ // 5. Crypto setup (silent)
282
+ setupCrypto(cwd);
283
+ const localPolicyPath = path.join(lbeDir, 'policy.json');
284
+ if (!fs.existsSync(localPolicyPath)) {
285
+ fs.writeFileSync(localPolicyPath, JSON.stringify({ version: 1, mode: 'observe', workspace: cwd, rules: [] }, null, 2) + '\n');
286
+ }
287
+ const localAuditPath = path.join(lbeDir, 'audit.jsonl');
288
+ if (!fs.existsSync(localAuditPath)) fs.writeFileSync(localAuditPath, '');
289
+ console.log('✓ Keys and policy ready (.lbe/)');
290
+
291
+ // 6. Agent contract inside .lbe/ only — no CLAUDE.md, no .github/ changes
292
+ writeAgentContract(cwd);
293
+ console.log('✓ Agent contract written → .lbe/AGENT_CONTRACT.md');
294
+
295
+ // 7. Migrate any legacy root files from previous LBE versions
296
+ const migrated = migrateLegacyRootFiles(cwd);
297
+ if (migrated.length) {
298
+ console.log('\n✓ Migrated legacy files:');
299
+ for (const m of migrated) console.log(' ' + m);
300
+ }
301
+
302
+ console.log('\nDone. All LBE state is in .lbe/');
303
+ console.log('Run npx lbe-exec status to verify.\n');
304
+
305
+ return { success: true, contract };
306
+ }
@@ -0,0 +1,57 @@
1
+ // src/cli/commands/integrityCheck.js
2
+ // Controller integrity commands
3
+
4
+ import path from 'path';
5
+ import { performIntegrityCheck, writeIntegrityManifest } from '../../core/integrity.js';
6
+
7
+ function toBoolean(value, defaultValue = false) {
8
+ if (value === undefined) return defaultValue;
9
+ if (value === true || value === false) return value;
10
+ const normalized = String(value).trim().toLowerCase();
11
+ if (normalized === 'true' || normalized === '1' || normalized === 'yes') return true;
12
+ if (normalized === 'false' || normalized === '0' || normalized === 'no') return false;
13
+ return defaultValue;
14
+ }
15
+
16
+ export async function integrityCheckCommand(opts) {
17
+ const strict = toBoolean(opts.strict, false) || toBoolean(opts['integrity-strict'], false);
18
+ const manifestPath = opts.manifest
19
+ ? path.resolve(opts.manifest)
20
+ : path.resolve(opts['integrity-manifest'] || '.lbe/config/integrity.manifest.json');
21
+ const jsonOutput = toBoolean(opts.json, true);
22
+
23
+ const result = await performIntegrityCheck({
24
+ manifestPath,
25
+ strict
26
+ });
27
+
28
+ if (jsonOutput) {
29
+ console.log(JSON.stringify({
30
+ ok: result.valid,
31
+ valid: result.valid,
32
+ skipped: result.skipped === true,
33
+ strict,
34
+ manifestPath,
35
+ checkedFiles: result.checkedFiles,
36
+ mismatches: result.mismatches || [],
37
+ missing: result.missing || [],
38
+ reason: result.reason || null,
39
+ message: result.message
40
+ }, null, 2));
41
+ } else {
42
+ console.log(result.message);
43
+ }
44
+
45
+ process.exit(result.valid ? 0 : 8);
46
+ }
47
+
48
+ export async function integrityGenerateCommand(opts) {
49
+ const outputPath = path.resolve(opts.out || opts.output || opts.manifest || '.lbe/config/integrity.manifest.json');
50
+ const result = writeIntegrityManifest({ outputPath });
51
+ console.log(JSON.stringify({
52
+ ok: true,
53
+ outputPath: result.outputPath,
54
+ fileCount: result.fileCount
55
+ }, null, 2));
56
+ process.exit(0);
57
+ }