@letterblack/lbe-core 1.3.3 → 1.3.4
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/LICENSE +1 -1
- package/README.md +130 -442
- package/assets/runtime-boundary.svg +36 -36
- package/dist/cli.js +141 -0
- package/dist/index.js +52 -0
- package/{release-exec/dist → dist}/wasm.lock.json +5 -4
- package/package.json +23 -54
- package/types.d.ts +2 -175
- package/.githooks/pre-commit +0 -2
- package/.githooks/pre-push +0 -2
- package/CHANGELOG.md +0 -75
- package/Release-README.md +0 -65
- package/WORKSPACE.md +0 -422
- package/_proof.mjs +0 -246
- package/bin/lbe.js +0 -12
- package/config/identity.config.json +0 -3
- package/config/policy.default.json +0 -24
- package/dist/cli/lbe.js +0 -4274
- package/dist/hooks/register.cjs +0 -505
- package/dist/state/appendCentral.cjs +0 -87
- package/dist/state/index.cjs +0 -101
- package/exec/cli.js +0 -472
- package/exec/index.js +0 -2
- package/index.js +0 -24
- package/lbe.audit.jsonl +0 -46
- package/release/README.md +0 -216
- package/release/TRUST.md +0 -90
- package/release/exec-README.md +0 -215
- package/release/exec-types.d.ts +0 -50
- package/release-exec/LICENSE +0 -1
- package/release-exec/README.md +0 -215
- package/release-exec/assets/lbe-gates.jpg +0 -0
- package/release-exec/assets/lbe-gates.png +0 -0
- package/release-exec/assets/runtime-boundary.svg +0 -36
- package/release-exec/assets/story-allow.jpg +0 -0
- package/release-exec/assets/story-allow.png +0 -0
- package/release-exec/assets/story-deny.jpg +0 -0
- package/release-exec/assets/story-deny.png +0 -0
- package/release-exec/dist/cli.js +0 -2841
- package/release-exec/dist/index.js +0 -1835
- package/release-exec/hooks/register.cjs +0 -473
- package/release-exec/package.json +0 -35
- package/release-exec/types.d.ts +0 -50
- package/runtime/engine.js +0 -322
- package/runtime/lbe_engine.wasm +0 -0
- package/src/cli/commands/auditVerify.js +0 -36
- package/src/cli/commands/dryrun.js +0 -175
- package/src/cli/commands/health.js +0 -153
- package/src/cli/commands/init.js +0 -306
- package/src/cli/commands/integrityCheck.js +0 -57
- package/src/cli/commands/logs.js +0 -53
- package/src/cli/commands/openState.js +0 -44
- package/src/cli/commands/policyAdd.js +0 -8
- package/src/cli/commands/policyMode.js +0 -7
- package/src/cli/commands/policySign.js +0 -72
- package/src/cli/commands/proof.js +0 -122
- package/src/cli/commands/run.js +0 -342
- package/src/cli/commands/status.js +0 -73
- package/src/cli/commands/verify.js +0 -144
- package/src/cli/main.js +0 -176
- package/src/cli/parseArgs.js +0 -114
- package/src/exec/localExecutor.js +0 -289
- package/src/hooks/register.cjs +0 -505
- package/src/state/appendCentral.cjs +0 -87
- package/src/state/fileIndex.js +0 -140
- package/src/state/index.cjs +0 -101
- package/src/state/index.js +0 -65
- package/src/state/intentRegistry.js +0 -83
- package/src/state/migration.js +0 -112
- package/src/state/proofRunner.js +0 -246
- package/src/state/stateRoot.js +0 -40
- package/src/state/targetRegistry.js +0 -108
- package/src/state/workspaceId.js +0 -40
- package/src/state/workspaceRegistry.js +0 -65
- /package/{release-exec/dist → dist}/lbe_engine.wasm +0 -0
package/src/cli/commands/run.js
DELETED
|
@@ -1,342 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { resolveWorkspaceState } from '../../state/index.js';
|
|
4
|
-
import { stateRoot } from '../../state/stateRoot.js';
|
|
5
|
-
import { isWorkspaceRegistryReadable, listWorkspaces } from '../../state/workspaceRegistry.js';
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* lbe status
|
|
9
|
-
*
|
|
10
|
-
* Resolves central state for the workspace and prints a summary.
|
|
11
|
-
* Policy authority stays in .lbe/policy.json — this command reads it
|
|
12
|
-
* for display only. It does not modify or migrate anything.
|
|
13
|
-
*
|
|
14
|
-
* @returns {{ workspaceId, stateDir, policySource, policyMode, hasProof, hasEvents }}
|
|
15
|
-
*/
|
|
16
|
-
export async function statusCommand(opts) {
|
|
17
|
-
if (opts.all) {
|
|
18
|
-
const registryPath = opts.registryPath || path.join(stateRoot(), 'registry.json');
|
|
19
|
-
if (!isWorkspaceRegistryReadable(registryPath)) {
|
|
20
|
-
console.log('Workspace registry unreadable');
|
|
21
|
-
return { workspaces: [], registryReadable: false };
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const workspaces = listWorkspaces(registryPath);
|
|
25
|
-
if (workspaces.length === 0) {
|
|
26
|
-
console.log('No known workspaces yet');
|
|
27
|
-
return { workspaces, registryReadable: true };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
console.log('\nKnown LBE workspaces');
|
|
31
|
-
for (const workspace of workspaces) {
|
|
32
|
-
console.log(` ${workspace.alias}`);
|
|
33
|
-
console.log(` workspace_id ${workspace.workspaceId}`);
|
|
34
|
-
console.log(` path ${workspace.path}`);
|
|
35
|
-
console.log(` last_active ${workspace.last_active}`);
|
|
36
|
-
}
|
|
37
|
-
console.log('');
|
|
38
|
-
return { workspaces, registryReadable: true };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const workspaceRoot = path.resolve(opts.root || process.cwd());
|
|
42
|
-
const { stateDir, workspaceId, paths } = resolveWorkspaceState(workspaceRoot);
|
|
43
|
-
|
|
44
|
-
// ── Policy source (read-only, .lbe/policy.json is authoritative) ──────────
|
|
45
|
-
const policyPath = path.join(workspaceRoot, '.lbe', 'policy.json');
|
|
46
|
-
let policySource = 'not found';
|
|
47
|
-
let policyMode = 'unknown';
|
|
48
|
-
if (fs.existsSync(policyPath)) {
|
|
49
|
-
try {
|
|
50
|
-
const policy = JSON.parse(fs.readFileSync(policyPath, 'utf8'));
|
|
51
|
-
policyMode = policy.mode || 'unknown';
|
|
52
|
-
policySource = policyPath;
|
|
53
|
-
} catch (_) {
|
|
54
|
-
policySource = policyPath + ' (unreadable)';
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ── Central state presence ─────────────────────────────────────────────────
|
|
59
|
-
const hasProof = fs.existsSync(paths.proofLatest);
|
|
60
|
-
const hasEvents = fs.existsSync(paths.events);
|
|
61
|
-
|
|
62
|
-
// ── Output ─────────────────────────────────────────────────────────────────
|
|
63
|
-
console.log(`\nLBE Central State — ${workspaceRoot}`);
|
|
64
|
-
console.log(` workspace_id ${workspaceId}`);
|
|
65
|
-
console.log(` state_dir ${stateDir}`);
|
|
66
|
-
console.log(` policy_source ${policySource}`);
|
|
67
|
-
console.log(` policy_mode ${policyMode}`);
|
|
68
|
-
console.log(` central_proof ${hasProof ? paths.proofLatest : 'No central proof yet'}`);
|
|
69
|
-
console.log(` central_logs ${hasEvents ? paths.events : 'No central logs yet. Hook dual-write not enabled.'}`);
|
|
70
|
-
console.log('');
|
|
71
|
-
|
|
72
|
-
return { workspaceId, stateDir, policySource, policyMode, hasProof, hasEvents };
|
|
73
|
-
}
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
// src/cli/commands/verify.js
|
|
2
|
-
// Validate a proposal without executing
|
|
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 { loadKeysStore } from '../../core/trustedKeys.js';
|
|
9
|
-
import { verifyPolicySignature } from '../../core/policySignature.js';
|
|
10
|
-
import { validateAndUpdatePolicyVersionState } from '../../core/policyVersionGuard.js';
|
|
11
|
-
|
|
12
|
-
export async function verifyCommand(opts) {
|
|
13
|
-
const { in: inFile } = opts;
|
|
14
|
-
const config = opts.config || opts.policy;
|
|
15
|
-
const pubKey = opts['pub-key'];
|
|
16
|
-
const keysStorePath = opts['keys-store'] || path.resolve('.lbe/config/keys.json');
|
|
17
|
-
const policySigPath = opts['policy-sig'] || path.resolve('.lbe/config/policy.sig.json');
|
|
18
|
-
const policyStatePath = opts['policy-state'] || path.resolve('.lbe/data/policy.state.json');
|
|
19
|
-
const allowUnsignedPolicy = opts['policy-unsigned-ok'] === true || String(opts['policy-unsigned-ok']).toLowerCase() === 'true';
|
|
20
|
-
// Validate required arguments
|
|
21
|
-
if (!inFile) {
|
|
22
|
-
console.error('Error: --in <file> is required');
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Read proposal file
|
|
27
|
-
let proposal;
|
|
28
|
-
try {
|
|
29
|
-
const filePath = path.resolve(inFile);
|
|
30
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
31
|
-
proposal = JSON.parse(content);
|
|
32
|
-
} catch (error) {
|
|
33
|
-
console.error(JSON.stringify({
|
|
34
|
-
status: 'error',
|
|
35
|
-
error: 'INVALID_PROPOSAL_FILE',
|
|
36
|
-
message: error.message
|
|
37
|
-
}));
|
|
38
|
-
process.exit(5);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Load policy
|
|
42
|
-
let policy;
|
|
43
|
-
try {
|
|
44
|
-
const policyPath = config || path.resolve('.lbe/config/policy.default.json');
|
|
45
|
-
if (!fs.existsSync(policyPath)) {
|
|
46
|
-
console.error(JSON.stringify({
|
|
47
|
-
status: 'error',
|
|
48
|
-
error: 'MISSING_POLICY',
|
|
49
|
-
message: `Policy file not found: ${policyPath}`
|
|
50
|
-
}));
|
|
51
|
-
process.exit(1);
|
|
52
|
-
}
|
|
53
|
-
const policyContent = fs.readFileSync(policyPath, 'utf-8');
|
|
54
|
-
policy = JSON.parse(policyContent);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
console.error(JSON.stringify({
|
|
57
|
-
status: 'error',
|
|
58
|
-
error: 'INVALID_POLICY',
|
|
59
|
-
message: error.message
|
|
60
|
-
}));
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Load key store (preferred) with legacy pub-key fallback
|
|
65
|
-
const keyStoreResult = loadKeysStore(keysStorePath);
|
|
66
|
-
const keyStore = keyStoreResult.ok ? keyStoreResult.store : null;
|
|
67
|
-
|
|
68
|
-
// Preflight: policy signature verification (strict by default)
|
|
69
|
-
const policySigCheck = verifyPolicySignature({
|
|
70
|
-
policyObj: policy,
|
|
71
|
-
keyStore,
|
|
72
|
-
policySigPath,
|
|
73
|
-
allowUnsigned: allowUnsignedPolicy
|
|
74
|
-
});
|
|
75
|
-
if (!policySigCheck.ok) {
|
|
76
|
-
console.error(JSON.stringify({
|
|
77
|
-
status: 'error',
|
|
78
|
-
error: policySigCheck.reason,
|
|
79
|
-
message: policySigCheck.message
|
|
80
|
-
}, null, 2));
|
|
81
|
-
process.exit(8);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const versionCheck = validateAndUpdatePolicyVersionState({
|
|
85
|
-
policyObj: policy,
|
|
86
|
-
statePath: policyStatePath,
|
|
87
|
-
maxCreatedAtSkewSec: policy?.security?.maxPolicyCreatedAtSkewSec
|
|
88
|
-
});
|
|
89
|
-
if (!versionCheck.ok) {
|
|
90
|
-
console.error(JSON.stringify({
|
|
91
|
-
status: 'error',
|
|
92
|
-
error: versionCheck.reason,
|
|
93
|
-
message: versionCheck.message
|
|
94
|
-
}, null, 2));
|
|
95
|
-
process.exit(8);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Load nonce store
|
|
99
|
-
const nonceDb = new NonceStore(path.resolve('.lbe/data/nonce.db.json'));
|
|
100
|
-
await nonceDb.load();
|
|
101
|
-
|
|
102
|
-
if (!keyStore && !pubKey) {
|
|
103
|
-
console.error(JSON.stringify({
|
|
104
|
-
status: 'error',
|
|
105
|
-
error: 'MISSING_KEY_MATERIAL',
|
|
106
|
-
message: `${keyStoreResult.message}. Provide --pub-key/--pub-key-file or create .lbe/config/keys.json`
|
|
107
|
-
}));
|
|
108
|
-
process.exit(1);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// Validate command
|
|
112
|
-
const result = validateCommand({
|
|
113
|
-
commandObj: proposal,
|
|
114
|
-
pubKeyB64: pubKey,
|
|
115
|
-
keyStore,
|
|
116
|
-
nonceDb,
|
|
117
|
-
policy
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Output structured result
|
|
121
|
-
const output = {
|
|
122
|
-
status: result.valid ? 'valid' : 'invalid',
|
|
123
|
-
commandId: proposal.commandId || 'N/A',
|
|
124
|
-
checks: result.checks,
|
|
125
|
-
errors: result.errors || [],
|
|
126
|
-
risk: result.risk || 'UNKNOWN'
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
console.log(JSON.stringify(output, null, 2));
|
|
130
|
-
|
|
131
|
-
// Exit with appropriate code
|
|
132
|
-
if (!result.valid) {
|
|
133
|
-
// Determine which validation failed for exit code
|
|
134
|
-
if (result.checks.schema === false) process.exit(5); // Schema error
|
|
135
|
-
if (result.checks.signature === false) process.exit(3); // Signature error
|
|
136
|
-
if (result.checks.nonce === false) process.exit(4); // Replay detected
|
|
137
|
-
if (result.checks.timestamp === false) process.exit(6); // Clock skew
|
|
138
|
-
if (result.checks.rateLimit === false) process.exit(7); // Rate limited
|
|
139
|
-
if (result.checks.policy === false) process.exit(2); // Policy blocked
|
|
140
|
-
process.exit(9); // Generic error
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
process.exit(0);
|
|
144
|
-
}
|