@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.
- package/.githooks/pre-commit +2 -0
- package/.githooks/pre-push +2 -0
- package/CHANGELOG.md +75 -0
- package/LICENSE +1 -1
- package/README.md +127 -154
- package/RELEASE_WORKSPACE_RULES.md +110 -0
- package/Release-README.md +65 -0
- package/WORKSPACE.md +422 -0
- package/_proof.mjs +246 -0
- package/assets/runtime-boundary.svg +36 -36
- package/bin/lbe.js +12 -0
- package/config/identity.config.json +3 -0
- package/config/policy.default.json +24 -0
- package/dist/cli/lbe.js +4432 -0
- package/dist/hooks/register.cjs +505 -0
- package/dist/state/appendCentral.cjs +87 -0
- package/dist/state/index.cjs +101 -0
- package/exec/cli.js +472 -0
- package/exec/index.js +2 -0
- package/index.js +24 -0
- package/lbe.audit.jsonl +46 -0
- package/package.json +48 -16
- package/release/README.md +216 -0
- package/release/TRUST.md +90 -0
- package/release/exec-README.md +215 -0
- package/release/exec-types.d.ts +50 -0
- package/release-exec/LICENSE +1 -0
- package/release-exec/README.md +215 -0
- 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 +36 -0
- 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 +2841 -0
- package/release-exec/dist/index.js +1835 -0
- package/release-exec/dist/lbe_engine.wasm +0 -0
- package/{dist → release-exec/dist}/wasm.lock.json +4 -5
- package/release-exec/hooks/register.cjs +473 -0
- package/release-exec/package.json +35 -0
- package/release-exec/types.d.ts +50 -0
- package/runtime/engine.js +322 -0
- package/runtime/lbe_engine.wasm +0 -0
- package/src/cli/commands/assertConsumer.js +198 -0
- package/src/cli/commands/auditVerify.js +36 -0
- package/src/cli/commands/dryrun.js +175 -0
- package/src/cli/commands/health.js +153 -0
- package/src/cli/commands/init.js +306 -0
- package/src/cli/commands/integrityCheck.js +57 -0
- package/src/cli/commands/logs.js +53 -0
- package/src/cli/commands/openState.js +44 -0
- package/src/cli/commands/policyAdd.js +8 -0
- package/src/cli/commands/policyMode.js +7 -0
- package/src/cli/commands/policySign.js +72 -0
- package/src/cli/commands/proof.js +122 -0
- package/src/cli/commands/run.js +342 -0
- package/src/cli/commands/status.js +73 -0
- package/src/cli/commands/verify.js +144 -0
- package/src/cli/main.js +181 -0
- package/src/cli/parseArgs.js +115 -0
- package/src/exec/localExecutor.js +289 -0
- package/src/hooks/register.cjs +505 -0
- package/src/state/appendCentral.cjs +87 -0
- package/src/state/fileIndex.js +140 -0
- package/src/state/index.cjs +101 -0
- package/src/state/index.js +65 -0
- package/src/state/intentRegistry.js +83 -0
- package/src/state/migration.js +112 -0
- package/src/state/proofRunner.js +246 -0
- package/src/state/stateRoot.js +40 -0
- package/src/state/targetRegistry.js +108 -0
- package/src/state/workspaceId.js +40 -0
- package/src/state/workspaceRegistry.js +65 -0
- package/types.d.ts +175 -2
- package/dist/cli.js +0 -141
- package/dist/index.js +0 -52
- /package/dist/{lbe_engine.wasm → cli/lbe_engine.wasm} +0 -0
package/dist/cli/lbe.js
ADDED
|
@@ -0,0 +1,4432 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli/main.js
|
|
4
|
+
import fs30 from "fs";
|
|
5
|
+
import path36 from "path";
|
|
6
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7
|
+
|
|
8
|
+
// src/cli/parseArgs.js
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
if (argv.length === 0) {
|
|
11
|
+
return { command: "help", opts: {} };
|
|
12
|
+
}
|
|
13
|
+
const command = argv[0];
|
|
14
|
+
const opts = {};
|
|
15
|
+
for (let i = 1; i < argv.length; i++) {
|
|
16
|
+
if (argv[i].startsWith("--")) {
|
|
17
|
+
const key = argv[i].substring(2);
|
|
18
|
+
if (key.includes("=")) {
|
|
19
|
+
const [k, v] = key.split("=");
|
|
20
|
+
opts[k] = v;
|
|
21
|
+
} else {
|
|
22
|
+
const nextArg = argv[i + 1];
|
|
23
|
+
if (!nextArg || nextArg.startsWith("-")) {
|
|
24
|
+
opts[key] = true;
|
|
25
|
+
} else {
|
|
26
|
+
opts[key] = nextArg;
|
|
27
|
+
i++;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} else if (argv[i].startsWith("-")) {
|
|
31
|
+
const key = argv[i].substring(1);
|
|
32
|
+
const nextArg = argv[i + 1];
|
|
33
|
+
if (!nextArg || nextArg.startsWith("-")) {
|
|
34
|
+
opts[key] = true;
|
|
35
|
+
} else {
|
|
36
|
+
opts[key] = nextArg;
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { command, opts };
|
|
42
|
+
}
|
|
43
|
+
function printHelp(version = "unknown") {
|
|
44
|
+
console.log(`
|
|
45
|
+
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
46
|
+
\u2551 LetterBlack Sentinel \u2014 CLI Governance \u2551
|
|
47
|
+
\u2551 Local-first execution governance SDK v${version.padEnd(12)}\u2551
|
|
48
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
49
|
+
|
|
50
|
+
USAGE:
|
|
51
|
+
lbe [command] [options]
|
|
52
|
+
|
|
53
|
+
COMMANDS:
|
|
54
|
+
init Initialize LetterBlack Sentinel environment
|
|
55
|
+
verify Verify a proposal (validate, don't execute)
|
|
56
|
+
dryrun Validate and simulate execution (no changes)
|
|
57
|
+
run Validate and execute a proposal
|
|
58
|
+
policy-sign Sign policy and write policy signature envelope
|
|
59
|
+
policy-add Controller-only: add a rule to lbe.policy.json
|
|
60
|
+
observe Set project-local policy to advisory mode
|
|
61
|
+
enforce Set project-local policy to blocking mode
|
|
62
|
+
health Run deployment/runtime health checks
|
|
63
|
+
integrity-check Verify controller integrity manifest
|
|
64
|
+
integrity-generate Generate controller integrity manifest
|
|
65
|
+
audit-verify Verify audit log hash-chain integrity
|
|
66
|
+
status Show central state summary for this workspace
|
|
67
|
+
logs Print recent entries from central event log
|
|
68
|
+
open-state Open the central state directory in the file manager
|
|
69
|
+
proof Show latest proof result (--json for raw JSON, --public for redacted)
|
|
70
|
+
assert-consumer Verify this project consumes LBE as an installed registry dependency
|
|
71
|
+
help Show this help message
|
|
72
|
+
|
|
73
|
+
OPTIONS:
|
|
74
|
+
--in Input file (JSON proposal)
|
|
75
|
+
--config Policy config file (default: ./.lbe/config/policy.default.json)
|
|
76
|
+
--policy Alias for --config
|
|
77
|
+
--policy-sig Policy signature file (default: ./.lbe/config/policy.sig.json)
|
|
78
|
+
--policy-state Policy monotonic state file (default: ./data/policy.state.json)
|
|
79
|
+
--policy-unsigned-ok Allow unsigned policy (dev-only; default: false)
|
|
80
|
+
--policy-key-id Signer keyId for policy-sign (default: policy-signer-v1-2026Q1)
|
|
81
|
+
--secret-key-file Secret key for policy-sign (default: ./keys/secret.key)
|
|
82
|
+
--data-dir Data directory for health checks (default: ./data)
|
|
83
|
+
--nonce-db Nonce DB path for health checks
|
|
84
|
+
--rate-db Rate-limit DB path for health checks
|
|
85
|
+
--keys-store Trusted keys store (default: ./.lbe/config/keys.json)
|
|
86
|
+
--pub-key Public key for verification (Ed25519 base64)
|
|
87
|
+
--pub-key-file Legacy single-key file path (fallback mode)
|
|
88
|
+
--integrity-strict Fail verify/dryrun/run if integrity check fails
|
|
89
|
+
--integrity-manifest Integrity manifest path (default: ./.lbe/config/integrity.manifest.json)
|
|
90
|
+
--manifest Manifest path override for integrity-check
|
|
91
|
+
--out Output path for integrity-generate
|
|
92
|
+
--audit Audit log file (default: ./data/audit.log.jsonl)
|
|
93
|
+
--root Project root for local policy commands (default: cwd)
|
|
94
|
+
--effect Local rule effect: allow or deny
|
|
95
|
+
--type Local rule type: path or command
|
|
96
|
+
--pattern Local rule glob/pattern
|
|
97
|
+
--from Required human-readable rule provenance
|
|
98
|
+
--json JSON output (default: true)
|
|
99
|
+
--fail-fast Stop at first audit integrity error (default: true)
|
|
100
|
+
--max Max audit entries to process (optional)
|
|
101
|
+
--version Show version
|
|
102
|
+
--help Show this help message
|
|
103
|
+
|
|
104
|
+
EXAMPLES:
|
|
105
|
+
lbe execute --input proposal.json
|
|
106
|
+
cat proposal.json | lbe execute
|
|
107
|
+
lbe policy-sign --config ./.lbe/config/policy.default.json --policy-sig ./.lbe/config/policy.sig.json
|
|
108
|
+
lbe integrity-generate --out ./.lbe/config/integrity.manifest.json
|
|
109
|
+
lbe integrity-check --integrity-strict --manifest ./.lbe/config/integrity.manifest.json
|
|
110
|
+
lbe audit-verify --audit ./data/audit.log.jsonl
|
|
111
|
+
|
|
112
|
+
For more info, visit: https://github.com/Letterblack0306/LetterBlack-LBE-Core
|
|
113
|
+
`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/cli/commands/init.js
|
|
117
|
+
import fs4 from "fs";
|
|
118
|
+
import path4 from "path";
|
|
119
|
+
import readline from "readline";
|
|
120
|
+
|
|
121
|
+
// src/core/signature.js
|
|
122
|
+
import nacl from "tweetnacl";
|
|
123
|
+
import { canonicalize } from "json-canonicalize";
|
|
124
|
+
function bytesFromBase64(b64) {
|
|
125
|
+
return Buffer.from(b64, "base64");
|
|
126
|
+
}
|
|
127
|
+
function toBase64(bytes) {
|
|
128
|
+
return Buffer.from(bytes).toString("base64");
|
|
129
|
+
}
|
|
130
|
+
function verifyEd25519({ payloadObj, sigB64, pubKeyB64 }) {
|
|
131
|
+
try {
|
|
132
|
+
const msg = Buffer.from(canonicalize(payloadObj), "utf8");
|
|
133
|
+
const sig = bytesFromBase64(sigB64);
|
|
134
|
+
const pub = bytesFromBase64(pubKeyB64);
|
|
135
|
+
const isValid = nacl.sign.detached.verify(
|
|
136
|
+
new Uint8Array(msg),
|
|
137
|
+
new Uint8Array(sig),
|
|
138
|
+
new Uint8Array(pub)
|
|
139
|
+
);
|
|
140
|
+
return {
|
|
141
|
+
valid: isValid,
|
|
142
|
+
message: isValid ? "Signature verified" : "Signature verification failed"
|
|
143
|
+
};
|
|
144
|
+
} catch (err) {
|
|
145
|
+
return {
|
|
146
|
+
valid: false,
|
|
147
|
+
message: `Signature verification error: ${err.message}`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function generateKeyPair() {
|
|
152
|
+
const keyPair = nacl.sign.keyPair();
|
|
153
|
+
return {
|
|
154
|
+
publicKey: toBase64(keyPair.publicKey),
|
|
155
|
+
secretKey: toBase64(keyPair.secretKey)
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function signEd25519({ payloadObj, secretKeyB64 }) {
|
|
159
|
+
try {
|
|
160
|
+
const msg = Buffer.from(canonicalize(payloadObj), "utf8");
|
|
161
|
+
const secretKey = bytesFromBase64(secretKeyB64);
|
|
162
|
+
const sig = nacl.sign.detached(new Uint8Array(msg), new Uint8Array(secretKey));
|
|
163
|
+
return {
|
|
164
|
+
signature: toBase64(sig),
|
|
165
|
+
error: null
|
|
166
|
+
};
|
|
167
|
+
} catch (err) {
|
|
168
|
+
return {
|
|
169
|
+
signature: null,
|
|
170
|
+
error: `Signing failed: ${err.message}`
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// src/core/policySignature.js
|
|
176
|
+
import fs2 from "fs";
|
|
177
|
+
import path2 from "path";
|
|
178
|
+
|
|
179
|
+
// src/core/trustedKeys.js
|
|
180
|
+
import fs from "fs";
|
|
181
|
+
import path from "path";
|
|
182
|
+
var KEY_ID_PATTERN = /^[A-Za-z0-9:_-]{3,128}$/;
|
|
183
|
+
function isValidKeyId(keyId) {
|
|
184
|
+
return typeof keyId === "string" && KEY_ID_PATTERN.test(keyId) && keyId !== "default";
|
|
185
|
+
}
|
|
186
|
+
function loadKeysStore(keysStorePath) {
|
|
187
|
+
const resolvedPath = path.resolve(keysStorePath);
|
|
188
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
189
|
+
return {
|
|
190
|
+
ok: false,
|
|
191
|
+
reason: "KEY_STORE_MISSING",
|
|
192
|
+
message: `Key store not found: ${resolvedPath}`,
|
|
193
|
+
store: null
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
198
|
+
const parsed = JSON.parse(content);
|
|
199
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.trustedKeys !== "object") {
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
reason: "KEY_STORE_INVALID",
|
|
203
|
+
message: `Invalid key store format: ${resolvedPath}`,
|
|
204
|
+
store: null
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
ok: true,
|
|
209
|
+
reason: null,
|
|
210
|
+
message: "Key store loaded",
|
|
211
|
+
store: parsed
|
|
212
|
+
};
|
|
213
|
+
} catch (error) {
|
|
214
|
+
return {
|
|
215
|
+
ok: false,
|
|
216
|
+
reason: "KEY_STORE_INVALID_JSON",
|
|
217
|
+
message: `Unable to parse key store: ${error.message}`,
|
|
218
|
+
store: null
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function resolveTrustedPublicKey({ keyStore, keyId, requesterId, now = /* @__PURE__ */ new Date() }) {
|
|
223
|
+
if (!keyStore || typeof keyStore !== "object") {
|
|
224
|
+
return {
|
|
225
|
+
ok: false,
|
|
226
|
+
reason: "KEY_STORE_UNAVAILABLE",
|
|
227
|
+
message: "Trusted key store is not available",
|
|
228
|
+
publicKey: null
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
if (!isValidKeyId(keyId)) {
|
|
232
|
+
return {
|
|
233
|
+
ok: false,
|
|
234
|
+
reason: "KEY_ID_INVALID",
|
|
235
|
+
message: `Invalid keyId '${keyId}'. Use versioned key IDs like 'agent:gpt-v1-2026Q1'`,
|
|
236
|
+
publicKey: null
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const keyConfig = keyStore.trustedKeys?.[keyId];
|
|
240
|
+
if (!keyConfig) {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
reason: "KEY_NOT_TRUSTED",
|
|
244
|
+
message: `Key '${keyId}' is not in trusted key store`,
|
|
245
|
+
publicKey: null
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (keyConfig.deprecated) {
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
reason: "KEY_DEPRECATED",
|
|
252
|
+
message: `Key '${keyId}' is deprecated`,
|
|
253
|
+
publicKey: null
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
if (keyConfig.requesterId && keyConfig.requesterId !== requesterId) {
|
|
257
|
+
return {
|
|
258
|
+
ok: false,
|
|
259
|
+
reason: "KEY_REQUESTER_MISMATCH",
|
|
260
|
+
message: `Key '${keyId}' is not authorized for requester '${requesterId}'`,
|
|
261
|
+
publicKey: null
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
const notBeforeRaw = keyConfig.notBefore || keyConfig.validFrom;
|
|
265
|
+
const expiresAtRaw = keyConfig.expiresAt || keyConfig.validUntil;
|
|
266
|
+
if (typeof notBeforeRaw !== "string" || typeof expiresAtRaw !== "string") {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
reason: "KEY_LIFECYCLE_INVALID",
|
|
270
|
+
message: `Key '${keyId}' must define lifecycle fields 'notBefore' and 'expiresAt'`,
|
|
271
|
+
publicKey: null
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
const notBefore = new Date(notBeforeRaw);
|
|
275
|
+
const expiresAt = new Date(expiresAtRaw);
|
|
276
|
+
if (Number.isNaN(notBefore.getTime()) || Number.isNaN(expiresAt.getTime())) {
|
|
277
|
+
return {
|
|
278
|
+
ok: false,
|
|
279
|
+
reason: "KEY_LIFECYCLE_INVALID",
|
|
280
|
+
message: `Key '${keyId}' has invalid lifecycle timestamp(s)`,
|
|
281
|
+
publicKey: null
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
if (notBefore >= expiresAt) {
|
|
285
|
+
return {
|
|
286
|
+
ok: false,
|
|
287
|
+
reason: "KEY_LIFECYCLE_INVALID",
|
|
288
|
+
message: `Key '${keyId}' has notBefore >= expiresAt`,
|
|
289
|
+
publicKey: null
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
if (now < notBefore) {
|
|
293
|
+
return {
|
|
294
|
+
ok: false,
|
|
295
|
+
reason: "KEY_NOT_YET_VALID",
|
|
296
|
+
message: `Key '${keyId}' not valid until ${notBeforeRaw}`,
|
|
297
|
+
publicKey: null
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
if (now > expiresAt) {
|
|
301
|
+
return {
|
|
302
|
+
ok: false,
|
|
303
|
+
reason: "KEY_EXPIRED",
|
|
304
|
+
message: `Key '${keyId}' expired on ${expiresAtRaw}`,
|
|
305
|
+
publicKey: null
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
if (!keyConfig.publicKey || typeof keyConfig.publicKey !== "string") {
|
|
309
|
+
return {
|
|
310
|
+
ok: false,
|
|
311
|
+
reason: "KEY_CONFIG_INVALID",
|
|
312
|
+
message: `Trusted key '${keyId}' is missing publicKey`,
|
|
313
|
+
publicKey: null
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
ok: true,
|
|
318
|
+
reason: null,
|
|
319
|
+
message: "Trusted key resolved",
|
|
320
|
+
publicKey: keyConfig.publicKey
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/core/policySignature.js
|
|
325
|
+
function createPolicySignatureEnvelope({ policyObj, secretKeyB64, keyId }) {
|
|
326
|
+
const signResult = signEd25519({
|
|
327
|
+
payloadObj: policyObj,
|
|
328
|
+
secretKeyB64
|
|
329
|
+
});
|
|
330
|
+
if (signResult.error) {
|
|
331
|
+
return {
|
|
332
|
+
ok: false,
|
|
333
|
+
reason: "POLICY_SIGNATURE_CREATE_FAILED",
|
|
334
|
+
message: signResult.error,
|
|
335
|
+
envelope: null
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
ok: true,
|
|
340
|
+
reason: null,
|
|
341
|
+
message: "Policy signature created",
|
|
342
|
+
envelope: {
|
|
343
|
+
alg: "ed25519",
|
|
344
|
+
keyId,
|
|
345
|
+
sig: signResult.signature,
|
|
346
|
+
createdAt: Math.floor(Date.now() / 1e3)
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function verifyPolicySignature({
|
|
351
|
+
policyObj,
|
|
352
|
+
keyStore,
|
|
353
|
+
policySigPath = "./.lbe/config/policy.sig.json",
|
|
354
|
+
allowUnsigned = false
|
|
355
|
+
}) {
|
|
356
|
+
const sigPathResolved = path2.resolve(policySigPath);
|
|
357
|
+
if (!fs2.existsSync(sigPathResolved)) {
|
|
358
|
+
if (allowUnsigned) {
|
|
359
|
+
return {
|
|
360
|
+
ok: true,
|
|
361
|
+
skipped: true,
|
|
362
|
+
reason: "POLICY_SIGNATURE_SKIPPED",
|
|
363
|
+
message: `Policy signature not found: ${sigPathResolved} (allowed by flag)`
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
return {
|
|
367
|
+
ok: false,
|
|
368
|
+
skipped: false,
|
|
369
|
+
reason: "POLICY_SIGNATURE_MISSING",
|
|
370
|
+
message: `Policy signature file not found: ${sigPathResolved}`
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
let envelope;
|
|
374
|
+
try {
|
|
375
|
+
envelope = JSON.parse(fs2.readFileSync(sigPathResolved, "utf-8"));
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return {
|
|
378
|
+
ok: false,
|
|
379
|
+
skipped: false,
|
|
380
|
+
reason: "POLICY_SIGNATURE_INVALID",
|
|
381
|
+
message: `Unable to parse policy signature file: ${error.message}`
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (!envelope || envelope.alg !== "ed25519" || typeof envelope.keyId !== "string" || typeof envelope.sig !== "string") {
|
|
385
|
+
return {
|
|
386
|
+
ok: false,
|
|
387
|
+
skipped: false,
|
|
388
|
+
reason: "POLICY_SIGNATURE_INVALID",
|
|
389
|
+
message: "Policy signature envelope must include {alg, keyId, sig}"
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (!keyStore) {
|
|
393
|
+
return {
|
|
394
|
+
ok: false,
|
|
395
|
+
skipped: false,
|
|
396
|
+
reason: "POLICY_SIGNER_KEY_STORE_UNAVAILABLE",
|
|
397
|
+
message: "Trusted key store is required for policy signature verification"
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
const trustedKey = resolveTrustedPublicKey({
|
|
401
|
+
keyStore,
|
|
402
|
+
keyId: envelope.keyId,
|
|
403
|
+
requesterId: void 0
|
|
404
|
+
});
|
|
405
|
+
if (!trustedKey.ok) {
|
|
406
|
+
return {
|
|
407
|
+
ok: false,
|
|
408
|
+
skipped: false,
|
|
409
|
+
reason: "POLICY_SIGNER_NOT_TRUSTED",
|
|
410
|
+
message: trustedKey.message
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const verification = verifyEd25519({
|
|
414
|
+
payloadObj: policyObj,
|
|
415
|
+
sigB64: envelope.sig,
|
|
416
|
+
pubKeyB64: trustedKey.publicKey
|
|
417
|
+
});
|
|
418
|
+
if (!verification.valid) {
|
|
419
|
+
return {
|
|
420
|
+
ok: false,
|
|
421
|
+
skipped: false,
|
|
422
|
+
reason: "POLICY_SIGNATURE_INVALID",
|
|
423
|
+
message: verification.message
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
ok: true,
|
|
428
|
+
skipped: false,
|
|
429
|
+
reason: null,
|
|
430
|
+
message: "Policy signature verified",
|
|
431
|
+
keyId: envelope.keyId
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// src/core/workspaceScanner.js
|
|
436
|
+
import fs3 from "fs";
|
|
437
|
+
import path3 from "path";
|
|
438
|
+
var PROJECT_SIGNALS = [
|
|
439
|
+
// Code types — ordered by priority for primaryType resolution
|
|
440
|
+
{ file: "package.json", type: "node" },
|
|
441
|
+
{ file: "pyproject.toml", type: "python" },
|
|
442
|
+
{ file: "requirements.txt", type: "python" },
|
|
443
|
+
{ file: "go.mod", type: "go" },
|
|
444
|
+
{ file: "Cargo.toml", type: "rust" },
|
|
445
|
+
{ file: "pom.xml", type: "java" },
|
|
446
|
+
{ file: "build.gradle", type: "java" },
|
|
447
|
+
{ file: "build.gradle.kts", type: "java" },
|
|
448
|
+
// Infrastructure types — supplementary, not primary
|
|
449
|
+
{ file: "Dockerfile", type: "docker" },
|
|
450
|
+
{ file: "docker-compose.yml", type: "docker" },
|
|
451
|
+
{ dir: ".github/workflows", type: "ci" },
|
|
452
|
+
{ file: ".gitlab-ci.yml", type: "ci" },
|
|
453
|
+
{ dir: ".circleci", type: "ci" },
|
|
454
|
+
{ file: "Jenkinsfile", type: "ci" },
|
|
455
|
+
{ file: ".travis.yml", type: "ci" }
|
|
456
|
+
];
|
|
457
|
+
var CODE_TYPES = ["node", "python", "go", "rust", "java"];
|
|
458
|
+
var SURFACE_MAP = {
|
|
459
|
+
source: ["src", "lib", "app", "pages", "components", "core", "api", "server", "client", "pkg", "cmd"],
|
|
460
|
+
generated: ["dist", "build", ".next", "out", "coverage", "target", ".cache", "__pycache__", ".turbo"],
|
|
461
|
+
tests: ["test", "tests", "__tests__", "spec", "e2e"],
|
|
462
|
+
docs: ["docs", "doc", "documentation"]
|
|
463
|
+
};
|
|
464
|
+
var SECRET_GLOBS = [".env", ".env.*", "keys/**", "secrets/**", "*.key", "*.pem", "*.p12", "*.pfx", "*.crt"];
|
|
465
|
+
var ALWAYS_DENY = ["node_modules/**", ".git/**"];
|
|
466
|
+
var LOCKFILES_BY_TYPE = {
|
|
467
|
+
node: ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"],
|
|
468
|
+
python: ["Pipfile.lock", "poetry.lock"],
|
|
469
|
+
go: ["go.sum"],
|
|
470
|
+
rust: ["Cargo.lock"],
|
|
471
|
+
java: ["gradle/wrapper/**"],
|
|
472
|
+
docker: [],
|
|
473
|
+
ci: [],
|
|
474
|
+
generic: []
|
|
475
|
+
};
|
|
476
|
+
var CONFIG_FILES_BY_TYPE = {
|
|
477
|
+
node: [
|
|
478
|
+
"package.json",
|
|
479
|
+
"tsconfig*.json",
|
|
480
|
+
"jest.config.*",
|
|
481
|
+
"vite.config.*",
|
|
482
|
+
"next.config.*",
|
|
483
|
+
"webpack.config.*",
|
|
484
|
+
".eslintrc*",
|
|
485
|
+
".eslint.config.*",
|
|
486
|
+
".prettierrc*",
|
|
487
|
+
"babel.config.*"
|
|
488
|
+
],
|
|
489
|
+
python: [
|
|
490
|
+
"pyproject.toml",
|
|
491
|
+
"setup.py",
|
|
492
|
+
"setup.cfg",
|
|
493
|
+
"tox.ini",
|
|
494
|
+
"pytest.ini",
|
|
495
|
+
"mypy.ini",
|
|
496
|
+
".flake8",
|
|
497
|
+
".pylintrc",
|
|
498
|
+
"Pipfile"
|
|
499
|
+
],
|
|
500
|
+
go: ["go.mod", ".golangci.yml", ".golangci.yaml"],
|
|
501
|
+
rust: ["Cargo.toml", "rust-toolchain.toml", "clippy.toml", ".rustfmt.toml"],
|
|
502
|
+
java: [
|
|
503
|
+
"pom.xml",
|
|
504
|
+
"build.gradle",
|
|
505
|
+
"build.gradle.kts",
|
|
506
|
+
"gradle.properties",
|
|
507
|
+
"settings.gradle",
|
|
508
|
+
"settings.gradle.kts"
|
|
509
|
+
],
|
|
510
|
+
docker: ["Dockerfile", "docker-compose.yml", ".dockerignore"],
|
|
511
|
+
ci: [".gitlab-ci.yml", "Jenkinsfile", ".travis.yml"],
|
|
512
|
+
generic: ["Makefile", "CMakeLists.txt", "meson.build"]
|
|
513
|
+
};
|
|
514
|
+
var CONFIG_FILES_UNIVERSAL = [".editorconfig", ".nvmrc", ".node-version", ".python-version"];
|
|
515
|
+
var CONFIG_DIRS_UNIVERSAL = ["config", ".github", ".gitlab", ".circleci", ".vscode"];
|
|
516
|
+
var CONFIG_LABEL = {
|
|
517
|
+
node: "dependency and build config",
|
|
518
|
+
python: "package and environment config",
|
|
519
|
+
go: "module definition",
|
|
520
|
+
rust: "crate manifest",
|
|
521
|
+
java: "build definition",
|
|
522
|
+
docker: "container config",
|
|
523
|
+
ci: "pipeline definition",
|
|
524
|
+
generic: "project config"
|
|
525
|
+
};
|
|
526
|
+
var LOCKFILE_LABEL = {
|
|
527
|
+
node: "package manager",
|
|
528
|
+
python: "dependency resolver",
|
|
529
|
+
go: "module checksums",
|
|
530
|
+
rust: "dependency resolver",
|
|
531
|
+
java: "Gradle wrapper"
|
|
532
|
+
};
|
|
533
|
+
var FALLBACK_MANIFESTS = [
|
|
534
|
+
"composer.json",
|
|
535
|
+
// PHP
|
|
536
|
+
"Gemfile",
|
|
537
|
+
// Ruby
|
|
538
|
+
"mix.exs",
|
|
539
|
+
// Elixir
|
|
540
|
+
"pubspec.yaml",
|
|
541
|
+
// Dart / Flutter
|
|
542
|
+
"Package.swift",
|
|
543
|
+
// Swift
|
|
544
|
+
"project.clj",
|
|
545
|
+
// Clojure
|
|
546
|
+
"build.sbt",
|
|
547
|
+
// Scala
|
|
548
|
+
"stack.yaml",
|
|
549
|
+
// Haskell
|
|
550
|
+
"deno.json",
|
|
551
|
+
"deno.jsonc",
|
|
552
|
+
// Deno
|
|
553
|
+
"Podfile"
|
|
554
|
+
// CocoaPods (iOS/macOS)
|
|
555
|
+
];
|
|
556
|
+
var FALLBACK_LOCKFILES = [
|
|
557
|
+
"composer.lock",
|
|
558
|
+
// PHP
|
|
559
|
+
"Gemfile.lock",
|
|
560
|
+
// Ruby
|
|
561
|
+
"mix.lock",
|
|
562
|
+
// Elixir
|
|
563
|
+
"pubspec.lock",
|
|
564
|
+
// Dart / Flutter
|
|
565
|
+
"Package.resolved"
|
|
566
|
+
// Swift
|
|
567
|
+
];
|
|
568
|
+
var FALLBACK_EXTENSIONS = [".csproj", ".fsproj", ".sln", ".cabal"];
|
|
569
|
+
function exists(p) {
|
|
570
|
+
return fs3.existsSync(p);
|
|
571
|
+
}
|
|
572
|
+
function detectDirs(root, names) {
|
|
573
|
+
return names.filter((n) => exists(path3.join(root, n))).map((n) => `${n}/**`);
|
|
574
|
+
}
|
|
575
|
+
function readGitignore(root) {
|
|
576
|
+
const p = path3.join(root, ".gitignore");
|
|
577
|
+
if (!exists(p)) return [];
|
|
578
|
+
return fs3.readFileSync(p, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("!")).map((l) => l.endsWith("/") ? l + "**" : l);
|
|
579
|
+
}
|
|
580
|
+
function dedup(arr) {
|
|
581
|
+
return arr.filter((v, i, a) => v && a.indexOf(v) === i);
|
|
582
|
+
}
|
|
583
|
+
function detectProjectTypes(root) {
|
|
584
|
+
const seen = /* @__PURE__ */ new Set();
|
|
585
|
+
const types = [];
|
|
586
|
+
for (const sig of PROJECT_SIGNALS) {
|
|
587
|
+
if (seen.has(sig.type)) continue;
|
|
588
|
+
const p = path3.join(root, sig.file || sig.dir);
|
|
589
|
+
if (exists(p)) {
|
|
590
|
+
seen.add(sig.type);
|
|
591
|
+
types.push(sig.type);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return types.length > 0 ? types : ["generic"];
|
|
595
|
+
}
|
|
596
|
+
function primaryType(projectTypes) {
|
|
597
|
+
return CODE_TYPES.find((t) => projectTypes.includes(t)) ?? "generic";
|
|
598
|
+
}
|
|
599
|
+
function scanFallbackManifests(root) {
|
|
600
|
+
const manifests = FALLBACK_MANIFESTS.filter((f) => exists(path3.join(root, f)));
|
|
601
|
+
const lockfiles = FALLBACK_LOCKFILES.filter((f) => exists(path3.join(root, f)));
|
|
602
|
+
try {
|
|
603
|
+
const entries = fs3.readdirSync(root);
|
|
604
|
+
for (const e of entries) {
|
|
605
|
+
if (FALLBACK_EXTENSIONS.some((ext) => e.endsWith(ext))) manifests.push(e);
|
|
606
|
+
}
|
|
607
|
+
} catch {
|
|
608
|
+
}
|
|
609
|
+
return { manifests, lockfiles };
|
|
610
|
+
}
|
|
611
|
+
function detectSurfaces(root, projectTypes) {
|
|
612
|
+
const s = {};
|
|
613
|
+
for (const [key, names] of Object.entries(SURFACE_MAP)) {
|
|
614
|
+
s[key] = detectDirs(root, names);
|
|
615
|
+
}
|
|
616
|
+
s.secrets = SECRET_GLOBS.filter((g) => {
|
|
617
|
+
const base = g.split("/")[0].replace(/\*.*/, "");
|
|
618
|
+
return base.includes("*") || exists(path3.join(root, base));
|
|
619
|
+
});
|
|
620
|
+
const typeConfigFiles = dedup(
|
|
621
|
+
projectTypes.flatMap((t) => CONFIG_FILES_BY_TYPE[t] || CONFIG_FILES_BY_TYPE.generic).concat(CONFIG_FILES_UNIVERSAL)
|
|
622
|
+
);
|
|
623
|
+
s.config = dedup([
|
|
624
|
+
...typeConfigFiles.filter((f) => !f.includes("*") && !f.endsWith("/**") && exists(path3.join(root, f))),
|
|
625
|
+
...typeConfigFiles.filter((f) => f.endsWith("/**") && exists(path3.join(root, f.replace("/**", "")))),
|
|
626
|
+
...detectDirs(root, CONFIG_DIRS_UNIVERSAL)
|
|
627
|
+
]);
|
|
628
|
+
s.lockfiles = dedup(
|
|
629
|
+
projectTypes.flatMap((t) => LOCKFILES_BY_TYPE[t] || []).filter((f) => {
|
|
630
|
+
const base = f.replace(/\*.*/, "").split("/")[0];
|
|
631
|
+
return base.includes("*") || exists(path3.join(root, base));
|
|
632
|
+
})
|
|
633
|
+
);
|
|
634
|
+
if (!projectTypes.some((t) => CODE_TYPES.includes(t))) {
|
|
635
|
+
const fb = scanFallbackManifests(root);
|
|
636
|
+
s.config = dedup([...s.config, ...fb.manifests]);
|
|
637
|
+
s.lockfiles = dedup([...s.lockfiles, ...fb.lockfiles]);
|
|
638
|
+
}
|
|
639
|
+
return s;
|
|
640
|
+
}
|
|
641
|
+
function buildSemantics(projectTypes, primary, surfaces) {
|
|
642
|
+
const sem = {};
|
|
643
|
+
sem.structure = "Preserve the existing folder structure. Add new files within established directories. Do not create top-level directories, reorganize, or rename existing folders.";
|
|
644
|
+
if (surfaces.source.length > 0) {
|
|
645
|
+
sem.source = `Source code lives in ${surfaces.source.join(", ")}. Make feature changes and bug fixes here only.`;
|
|
646
|
+
}
|
|
647
|
+
sem.secrets = `Never propose changes to credential or key files (${SECRET_GLOBS.slice(0, 4).join(", ")} \u2026). These are never task targets regardless of the instruction.`;
|
|
648
|
+
if (surfaces.generated.length > 0) {
|
|
649
|
+
sem.generated = `${surfaces.generated.join(", ")} contain generated output. Modify the source files that produce them; never write to generated directories directly.`;
|
|
650
|
+
}
|
|
651
|
+
if (surfaces.config.length > 0) {
|
|
652
|
+
const codeTypes = projectTypes.filter((t) => CODE_TYPES.includes(t));
|
|
653
|
+
const label = codeTypes.length === 1 ? CONFIG_LABEL[codeTypes[0]] : "project configuration";
|
|
654
|
+
const listed = surfaces.config.slice(0, 5).join(", ");
|
|
655
|
+
const trailer = surfaces.config.length > 5 ? " and related files" : "";
|
|
656
|
+
sem.config = `Treat ${listed}${trailer} as ${label} files. Do not modify them unless the task explicitly requires a configuration or dependency change.`;
|
|
657
|
+
}
|
|
658
|
+
if (surfaces.tests.length > 0) {
|
|
659
|
+
sem.tests = `Test files in ${surfaces.tests.join(", ")} validate behavior. Update them only when the behavior they cover changes.`;
|
|
660
|
+
}
|
|
661
|
+
if (surfaces.lockfiles?.length > 0) {
|
|
662
|
+
const label = LOCKFILE_LABEL[primary] || "tooling";
|
|
663
|
+
const listed = surfaces.lockfiles.slice(0, 3).join(", ");
|
|
664
|
+
sem.lockfiles = `${listed} are generated by the ${label}. Never edit them directly.`;
|
|
665
|
+
}
|
|
666
|
+
if (primary === "generic") {
|
|
667
|
+
const foundManifests = surfaces.config.filter((f) => !f.endsWith("/**"));
|
|
668
|
+
if (foundManifests.length > 0) {
|
|
669
|
+
sem.unknown = `This project uses an unrecognized toolchain. Treat ${foundManifests.slice(0, 3).join(", ")} as dependency/manifest files. Do not modify them unless the task explicitly requires a dependency change.`;
|
|
670
|
+
} else {
|
|
671
|
+
sem.unknown = "This project uses an unrecognized toolchain. Do not assume standard source layouts, dependency files, or build conventions apply. Confirm any structural assumption before acting.";
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
if (projectTypes.includes("docker")) {
|
|
675
|
+
sem.docker = "Dockerfile and docker-compose.yml define the container environment. Treat them as infrastructure config \u2014 only modify when the task explicitly involves container or environment changes.";
|
|
676
|
+
}
|
|
677
|
+
if (projectTypes.includes("ci")) {
|
|
678
|
+
sem.ci = "CI config files (.github/**, .gitlab-ci.yml, etc.) define the build and deployment pipeline. Do not modify them unless the task explicitly involves CI/CD changes.";
|
|
679
|
+
}
|
|
680
|
+
return sem;
|
|
681
|
+
}
|
|
682
|
+
function buildEnforcement(surfaces, gitignorePatterns) {
|
|
683
|
+
const allow = dedup([...surfaces.source, ...surfaces.docs, ...surfaces.tests]);
|
|
684
|
+
const approval = [...surfaces.config];
|
|
685
|
+
const deny = dedup([
|
|
686
|
+
...surfaces.secrets,
|
|
687
|
+
...surfaces.generated,
|
|
688
|
+
...surfaces.lockfiles || [],
|
|
689
|
+
...ALWAYS_DENY,
|
|
690
|
+
...gitignorePatterns.filter((p) => p.endsWith("/**")).slice(0, 8)
|
|
691
|
+
]);
|
|
692
|
+
return {
|
|
693
|
+
allow: allow.length > 0 ? allow : ["src/**"],
|
|
694
|
+
approval: approval.length > 0 ? approval : [],
|
|
695
|
+
deny
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
function scanWorkspace(rootDir) {
|
|
699
|
+
const root = path3.resolve(rootDir || process.cwd());
|
|
700
|
+
const projectTypes = detectProjectTypes(root);
|
|
701
|
+
const primary = primaryType(projectTypes);
|
|
702
|
+
const surfaces = detectSurfaces(root, projectTypes);
|
|
703
|
+
const gitignore = readGitignore(root);
|
|
704
|
+
const semantics = buildSemantics(projectTypes, primary, surfaces);
|
|
705
|
+
const enforcement = buildEnforcement(surfaces, gitignore);
|
|
706
|
+
return { projectTypes, primaryType: primary, surfaces, semantics, enforcement };
|
|
707
|
+
}
|
|
708
|
+
function formatSummary(projectTypes, semantics, enforcement) {
|
|
709
|
+
const lines = [];
|
|
710
|
+
const label = Array.isArray(projectTypes) ? projectTypes.join(" + ") : projectTypes;
|
|
711
|
+
lines.push(`Detected: ${label}`);
|
|
712
|
+
lines.push("");
|
|
713
|
+
lines.push("Agent semantics:");
|
|
714
|
+
for (const [, v] of Object.entries(semantics)) {
|
|
715
|
+
lines.push(` - ${v}`);
|
|
716
|
+
}
|
|
717
|
+
lines.push("");
|
|
718
|
+
lines.push("Enforcement:");
|
|
719
|
+
if (enforcement.allow.length) lines.push(` allow: ${enforcement.allow.join(", ")}`);
|
|
720
|
+
if (enforcement.approval.length) lines.push(` approval: ${enforcement.approval.join(", ")}`);
|
|
721
|
+
if (enforcement.deny.length) lines.push(` deny: ${enforcement.deny.slice(0, 6).join(", ")}${enforcement.deny.length > 6 ? " \u2026" : ""}`);
|
|
722
|
+
return lines.join("\n");
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// src/cli/commands/init.js
|
|
726
|
+
function ask(question) {
|
|
727
|
+
if (!process.stdin.isTTY) return Promise.resolve("y");
|
|
728
|
+
return new Promise((resolve) => {
|
|
729
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
730
|
+
rl.question(question, (ans) => {
|
|
731
|
+
rl.close();
|
|
732
|
+
resolve(ans.trim().toLowerCase());
|
|
733
|
+
});
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
function applyStrict(enforcement) {
|
|
737
|
+
return {
|
|
738
|
+
...enforcement,
|
|
739
|
+
deny: [.../* @__PURE__ */ new Set([...enforcement.deny, ...enforcement.approval, "*.json", "config/**"])],
|
|
740
|
+
approval: []
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function applyRelaxed(enforcement) {
|
|
744
|
+
return { ...enforcement, approval: [] };
|
|
745
|
+
}
|
|
746
|
+
function setupCrypto(cwd) {
|
|
747
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
748
|
+
const expiresAt = new Date(Date.now() + 180 * 24 * 60 * 60 * 1e3).toISOString();
|
|
749
|
+
const defaultKeyId = "agent:gpt-v1-2026Q1";
|
|
750
|
+
const signerKeyId = "policy-signer-v1-2026Q1";
|
|
751
|
+
const lbeDir = path4.join(cwd, ".lbe");
|
|
752
|
+
for (const d of ["config", "keys", "data"]) {
|
|
753
|
+
fs4.mkdirSync(path4.join(lbeDir, d), { recursive: true });
|
|
754
|
+
}
|
|
755
|
+
const dataFiles = {
|
|
756
|
+
".lbe/data/nonce.db.json": JSON.stringify({ entries: [] }, null, 2),
|
|
757
|
+
".lbe/data/rate-limit.db.json": JSON.stringify({ entries: [] }, null, 2),
|
|
758
|
+
".lbe/data/policy.state.json": JSON.stringify({ schemaVersion: "1", lastAccepted: null, updatedAt: null }, null, 2),
|
|
759
|
+
".lbe/data/audit.log.jsonl": ""
|
|
760
|
+
};
|
|
761
|
+
for (const [rel, content] of Object.entries(dataFiles)) {
|
|
762
|
+
const p = path4.join(cwd, rel);
|
|
763
|
+
if (!fs4.existsSync(p)) fs4.writeFileSync(p, content);
|
|
764
|
+
}
|
|
765
|
+
const keyDir = path4.join(lbeDir, "keys");
|
|
766
|
+
const pubPath = path4.join(keyDir, "public.key");
|
|
767
|
+
const secPath = path4.join(keyDir, "secret.key");
|
|
768
|
+
let publicKeyB64, secretKeyB64;
|
|
769
|
+
if (fs4.existsSync(pubPath) && fs4.existsSync(secPath)) {
|
|
770
|
+
publicKeyB64 = fs4.readFileSync(pubPath, "utf8").trim();
|
|
771
|
+
secretKeyB64 = fs4.readFileSync(secPath, "utf8").trim();
|
|
772
|
+
} else {
|
|
773
|
+
const kp = generateKeyPair();
|
|
774
|
+
publicKeyB64 = kp.publicKey;
|
|
775
|
+
secretKeyB64 = kp.secretKey;
|
|
776
|
+
fs4.writeFileSync(pubPath, publicKeyB64);
|
|
777
|
+
fs4.writeFileSync(secPath, secretKeyB64, { mode: 384 });
|
|
778
|
+
}
|
|
779
|
+
const keysPath = path4.join(lbeDir, "config/keys.json");
|
|
780
|
+
const keysStore = fs4.existsSync(keysPath) ? JSON.parse(fs4.readFileSync(keysPath, "utf8")) : { schemaVersion: "1", defaultKeyId, trustedKeys: {} };
|
|
781
|
+
for (const keyId of [defaultKeyId, signerKeyId]) {
|
|
782
|
+
if (!keysStore.trustedKeys[keyId]) {
|
|
783
|
+
keysStore.trustedKeys[keyId] = {
|
|
784
|
+
publicKey: publicKeyB64,
|
|
785
|
+
notBefore: nowIso,
|
|
786
|
+
expiresAt,
|
|
787
|
+
validFrom: nowIso,
|
|
788
|
+
validUntil: expiresAt,
|
|
789
|
+
deprecated: false
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
keysStore.defaultKeyId = defaultKeyId;
|
|
794
|
+
fs4.writeFileSync(keysPath, JSON.stringify(keysStore, null, 2));
|
|
795
|
+
const policyPath = path4.join(lbeDir, "config/policy.default.json");
|
|
796
|
+
let policyObj;
|
|
797
|
+
if (fs4.existsSync(policyPath)) {
|
|
798
|
+
policyObj = JSON.parse(fs4.readFileSync(policyPath, "utf8"));
|
|
799
|
+
} else {
|
|
800
|
+
policyObj = {
|
|
801
|
+
default: "DENY",
|
|
802
|
+
version: "1.0.0",
|
|
803
|
+
createdAt: nowIso,
|
|
804
|
+
security: {
|
|
805
|
+
maxClockSkewSec: 600,
|
|
806
|
+
maxPolicyCreatedAtSkewSec: 31536e3,
|
|
807
|
+
defaultRateLimit: { windowSec: 60, maxRequests: 30 }
|
|
808
|
+
},
|
|
809
|
+
requesters: {
|
|
810
|
+
"agent:gpt": {
|
|
811
|
+
allowAdapters: ["noop", "shell"],
|
|
812
|
+
allowCommands: ["RUN_SHELL"],
|
|
813
|
+
rateLimit: { windowSec: 60, maxRequests: 30 },
|
|
814
|
+
filesystem: { roots: [cwd], denyPatterns: ["**/.git/**", "**/secrets/**", "**/*.key"] },
|
|
815
|
+
exec: { allowCmds: ["ls", "node", "python", "echo"], denyCmds: ["rm", "chmod", "chown", "curl", "wget", "su", "sudo"] }
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
fs4.writeFileSync(policyPath, JSON.stringify(policyObj, null, 2));
|
|
820
|
+
}
|
|
821
|
+
const sigResult = createPolicySignatureEnvelope({ policyObj, secretKeyB64, keyId: signerKeyId });
|
|
822
|
+
if (sigResult.ok) {
|
|
823
|
+
fs4.writeFileSync(path4.join(lbeDir, "config/policy.sig.json"), JSON.stringify(sigResult.envelope, null, 2));
|
|
824
|
+
}
|
|
825
|
+
return { defaultKeyId, secretKeyB64, publicKeyB64 };
|
|
826
|
+
}
|
|
827
|
+
function agentContractContent() {
|
|
828
|
+
return `# LBE Governance Contract
|
|
829
|
+
|
|
830
|
+
This project has LetterBlack LBE (Local-first execution Governance) active.
|
|
831
|
+
|
|
832
|
+
## Your role as an agent
|
|
833
|
+
|
|
834
|
+
Every file write, delete, and shell command must go through the LBE executor.
|
|
835
|
+
The executor validates each action against the project policy and audits it.
|
|
836
|
+
|
|
837
|
+
## How to perform actions
|
|
838
|
+
|
|
839
|
+
\`\`\`js
|
|
840
|
+
import { createLocalExecutor } from '@letterblack/lbe-exec';
|
|
841
|
+
const lbe = createLocalExecutor({ rootDir: process.cwd() });
|
|
842
|
+
|
|
843
|
+
await lbe.writeFile('output/report.md', content);
|
|
844
|
+
await lbe.readFile('src/config.json');
|
|
845
|
+
await lbe.patchFile('src/index.js', patch);
|
|
846
|
+
await lbe.deleteFile('tmp/scratch.txt');
|
|
847
|
+
await lbe.runShell('node', ['scripts/build.js']);
|
|
848
|
+
\`\`\`
|
|
849
|
+
|
|
850
|
+
## What you must never do
|
|
851
|
+
|
|
852
|
+
- Call fs.* or child_process.* directly
|
|
853
|
+
- Modify \`.lbe/policy.json\` directly \u2014 propose a rule via \`lbe.policy.proposeRule()\`
|
|
854
|
+
- Attempt to bypass or disable the executor
|
|
855
|
+
|
|
856
|
+
## Proposing a policy rule
|
|
857
|
+
|
|
858
|
+
\`\`\`js
|
|
859
|
+
const proposal = lbe.policy.proposeRule({
|
|
860
|
+
effect: 'deny', type: 'path', pattern: 'secrets/**',
|
|
861
|
+
from: 'agent: these files should never be modified'
|
|
862
|
+
});
|
|
863
|
+
// Return proposal to the user \u2014 never call lbe.policy.addRule() yourself.
|
|
864
|
+
\`\`\`
|
|
865
|
+
|
|
866
|
+
## Result shape
|
|
867
|
+
|
|
868
|
+
\`{ ok: boolean, decision: 'allow' | 'deny' | 'observe', executed: boolean }\`
|
|
869
|
+
|
|
870
|
+
## Files
|
|
871
|
+
|
|
872
|
+
- Policy: \`.lbe/policy.json\`
|
|
873
|
+
- Audit: \`.lbe/audit.jsonl\`
|
|
874
|
+
- Status: \`npx lbe-exec status\`
|
|
875
|
+
`;
|
|
876
|
+
}
|
|
877
|
+
function writeAgentContract(cwd) {
|
|
878
|
+
const lbeDir = path4.join(cwd, ".lbe");
|
|
879
|
+
fs4.mkdirSync(lbeDir, { recursive: true });
|
|
880
|
+
fs4.writeFileSync(path4.join(lbeDir, "AGENT_CONTRACT.md"), agentContractContent());
|
|
881
|
+
}
|
|
882
|
+
function migrateLegacyRootFiles(cwd) {
|
|
883
|
+
const lbeDir = path4.join(cwd, ".lbe");
|
|
884
|
+
fs4.mkdirSync(lbeDir, { recursive: true });
|
|
885
|
+
const migrations = [
|
|
886
|
+
["lbe.policy.json", ".lbe/policy.json"],
|
|
887
|
+
["lbe.workspace.json", ".lbe/workspace.json"]
|
|
888
|
+
];
|
|
889
|
+
const removed = [];
|
|
890
|
+
for (const [src, dest] of migrations) {
|
|
891
|
+
const srcPath = path4.join(cwd, src);
|
|
892
|
+
const destPath = path4.join(cwd, dest);
|
|
893
|
+
if (fs4.existsSync(srcPath) && !fs4.existsSync(destPath)) {
|
|
894
|
+
fs4.renameSync(srcPath, destPath);
|
|
895
|
+
removed.push(src + " \u2192 " + dest);
|
|
896
|
+
} else if (fs4.existsSync(srcPath)) {
|
|
897
|
+
fs4.unlinkSync(srcPath);
|
|
898
|
+
removed.push(src + " (removed \u2014 .lbe/ version exists)");
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
const toDelete = ["CLAUDE.md", path4.join(".github", "copilot-instructions.md")];
|
|
902
|
+
for (const rel of toDelete) {
|
|
903
|
+
const p = path4.join(cwd, rel);
|
|
904
|
+
if (fs4.existsSync(p)) {
|
|
905
|
+
const content = fs4.readFileSync(p, "utf8");
|
|
906
|
+
if (content.includes("lbe-governance") || content.includes("LetterBlack LBE")) {
|
|
907
|
+
fs4.unlinkSync(p);
|
|
908
|
+
removed.push(rel + " (removed \u2014 LBE-generated file)");
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return removed;
|
|
913
|
+
}
|
|
914
|
+
async function initCommand(opts = {}) {
|
|
915
|
+
const cwd = process.cwd();
|
|
916
|
+
const yes = opts.yes || opts.y || !process.stdin.isTTY;
|
|
917
|
+
const lbeDir = path4.join(cwd, ".lbe");
|
|
918
|
+
fs4.mkdirSync(lbeDir, { recursive: true });
|
|
919
|
+
const outPath = path4.join(lbeDir, "workspace.json");
|
|
920
|
+
console.log("\nScanning workspace...\n");
|
|
921
|
+
const { projectTypes, primaryType: primaryType2, semantics, enforcement } = scanWorkspace(cwd);
|
|
922
|
+
console.log(formatSummary(projectTypes, semantics, enforcement));
|
|
923
|
+
console.log("");
|
|
924
|
+
let finalEnforcement = enforcement;
|
|
925
|
+
if (!yes) {
|
|
926
|
+
const answer = await ask("Accept? [Y = accept / s = strict / r = relaxed / n = cancel] ");
|
|
927
|
+
if (answer === "n") {
|
|
928
|
+
console.log("Cancelled.");
|
|
929
|
+
return { success: false };
|
|
930
|
+
}
|
|
931
|
+
if (answer === "s") finalEnforcement = applyStrict(enforcement);
|
|
932
|
+
if (answer === "r") finalEnforcement = applyRelaxed(enforcement);
|
|
933
|
+
}
|
|
934
|
+
const contract = {
|
|
935
|
+
lbe: true,
|
|
936
|
+
version: "0.4.0",
|
|
937
|
+
state: "local",
|
|
938
|
+
projectTypes,
|
|
939
|
+
primaryType: primaryType2,
|
|
940
|
+
semantics,
|
|
941
|
+
enforcement: finalEnforcement
|
|
942
|
+
};
|
|
943
|
+
fs4.writeFileSync(outPath, JSON.stringify(contract, null, 2));
|
|
944
|
+
console.log("\u2713 Wrote .lbe/workspace.json");
|
|
945
|
+
setupCrypto(cwd);
|
|
946
|
+
const localPolicyPath = path4.join(lbeDir, "policy.json");
|
|
947
|
+
if (!fs4.existsSync(localPolicyPath)) {
|
|
948
|
+
fs4.writeFileSync(localPolicyPath, JSON.stringify({ version: 1, mode: "observe", workspace: cwd, rules: [] }, null, 2) + "\n");
|
|
949
|
+
}
|
|
950
|
+
const localAuditPath = path4.join(lbeDir, "audit.jsonl");
|
|
951
|
+
if (!fs4.existsSync(localAuditPath)) fs4.writeFileSync(localAuditPath, "");
|
|
952
|
+
console.log("\u2713 Keys and policy ready (.lbe/)");
|
|
953
|
+
writeAgentContract(cwd);
|
|
954
|
+
console.log("\u2713 Agent contract written \u2192 .lbe/AGENT_CONTRACT.md");
|
|
955
|
+
const migrated = migrateLegacyRootFiles(cwd);
|
|
956
|
+
if (migrated.length) {
|
|
957
|
+
console.log("\n\u2713 Migrated legacy files:");
|
|
958
|
+
for (const m of migrated) console.log(" " + m);
|
|
959
|
+
}
|
|
960
|
+
console.log("\nDone. All LBE state is in .lbe/");
|
|
961
|
+
console.log("Run npx lbe-exec status to verify.\n");
|
|
962
|
+
return { success: true, contract };
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/cli/commands/verify.js
|
|
966
|
+
import fs9 from "fs";
|
|
967
|
+
import path10 from "path";
|
|
968
|
+
|
|
969
|
+
// src/core/policyVersionGuard.js
|
|
970
|
+
import fs6 from "fs";
|
|
971
|
+
import path6 from "path";
|
|
972
|
+
|
|
973
|
+
// src/core/atomicWrite.js
|
|
974
|
+
import fs5 from "fs";
|
|
975
|
+
import path5 from "path";
|
|
976
|
+
import crypto from "crypto";
|
|
977
|
+
var DEFAULT_LOCK_OPTS = {
|
|
978
|
+
timeoutMs: 5e3,
|
|
979
|
+
// total wait before giving up
|
|
980
|
+
pollMs: 15,
|
|
981
|
+
// base poll interval (jittered)
|
|
982
|
+
staleMs: 3e4
|
|
983
|
+
// lock files older than this are presumed orphaned
|
|
984
|
+
};
|
|
985
|
+
function _lockPathFor(targetPath) {
|
|
986
|
+
return targetPath + ".lock";
|
|
987
|
+
}
|
|
988
|
+
function _tryAcquire(lockPath) {
|
|
989
|
+
try {
|
|
990
|
+
const fd = fs5.openSync(lockPath, "wx");
|
|
991
|
+
fs5.writeSync(fd, `pid:${process.pid}:${Date.now()}`);
|
|
992
|
+
fs5.closeSync(fd);
|
|
993
|
+
return true;
|
|
994
|
+
} catch (err) {
|
|
995
|
+
if (err.code === "EEXIST" || err.code === "EPERM" || err.code === "EBUSY" || err.code === "EACCES") {
|
|
996
|
+
return false;
|
|
997
|
+
}
|
|
998
|
+
throw err;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
function _removeIfStale(lockPath, staleMs) {
|
|
1002
|
+
try {
|
|
1003
|
+
const stat = fs5.statSync(lockPath);
|
|
1004
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
1005
|
+
if (ageMs > staleMs) {
|
|
1006
|
+
try {
|
|
1007
|
+
fs5.unlinkSync(lockPath);
|
|
1008
|
+
} catch {
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
} catch {
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
function _sleepSync(ms) {
|
|
1015
|
+
const end = Date.now() + ms;
|
|
1016
|
+
while (Date.now() < end) {
|
|
1017
|
+
try {
|
|
1018
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(1, end - Date.now()));
|
|
1019
|
+
} catch {
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
function withFileLock(targetPath, optsOrFn, maybeFn) {
|
|
1024
|
+
const fn = typeof optsOrFn === "function" ? optsOrFn : maybeFn;
|
|
1025
|
+
const opts = typeof optsOrFn === "function" ? {} : optsOrFn || {};
|
|
1026
|
+
const { timeoutMs, pollMs, staleMs } = { ...DEFAULT_LOCK_OPTS, ...opts };
|
|
1027
|
+
const dir = path5.dirname(targetPath);
|
|
1028
|
+
if (!fs5.existsSync(dir)) {
|
|
1029
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1030
|
+
}
|
|
1031
|
+
const lockPath = _lockPathFor(targetPath);
|
|
1032
|
+
const deadline = Date.now() + timeoutMs;
|
|
1033
|
+
let acquired = false;
|
|
1034
|
+
while (!acquired) {
|
|
1035
|
+
acquired = _tryAcquire(lockPath);
|
|
1036
|
+
if (acquired) break;
|
|
1037
|
+
if (Date.now() >= deadline) {
|
|
1038
|
+
_removeIfStale(lockPath, staleMs);
|
|
1039
|
+
acquired = _tryAcquire(lockPath);
|
|
1040
|
+
if (acquired) break;
|
|
1041
|
+
const err = new Error(`withFileLock: timeout acquiring ${lockPath} after ${timeoutMs}ms`);
|
|
1042
|
+
err.code = "ELOCKTIMEOUT";
|
|
1043
|
+
throw err;
|
|
1044
|
+
}
|
|
1045
|
+
_removeIfStale(lockPath, staleMs);
|
|
1046
|
+
const jitter = Math.floor(Math.random() * pollMs);
|
|
1047
|
+
_sleepSync(pollMs + jitter);
|
|
1048
|
+
}
|
|
1049
|
+
try {
|
|
1050
|
+
return fn();
|
|
1051
|
+
} finally {
|
|
1052
|
+
try {
|
|
1053
|
+
fs5.unlinkSync(lockPath);
|
|
1054
|
+
} catch {
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
function atomicWriteFileSync(filePath, data, options = {}) {
|
|
1059
|
+
const dir = path5.dirname(filePath);
|
|
1060
|
+
if (!fs5.existsSync(dir)) {
|
|
1061
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1062
|
+
}
|
|
1063
|
+
const tempFile = path5.join(dir, `.tmp-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`);
|
|
1064
|
+
try {
|
|
1065
|
+
fs5.writeFileSync(tempFile, data, options);
|
|
1066
|
+
fs5.renameSync(tempFile, filePath);
|
|
1067
|
+
} catch (error) {
|
|
1068
|
+
try {
|
|
1069
|
+
if (fs5.existsSync(tempFile)) {
|
|
1070
|
+
fs5.unlinkSync(tempFile);
|
|
1071
|
+
}
|
|
1072
|
+
} catch (cleanupError) {
|
|
1073
|
+
}
|
|
1074
|
+
throw error;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
function atomicAppendFileSync(filePath, data, options = {}) {
|
|
1078
|
+
const dir = path5.dirname(filePath);
|
|
1079
|
+
if (!fs5.existsSync(dir)) {
|
|
1080
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1081
|
+
}
|
|
1082
|
+
withFileLock(filePath, () => {
|
|
1083
|
+
let existingContent = "";
|
|
1084
|
+
if (fs5.existsSync(filePath)) {
|
|
1085
|
+
existingContent = fs5.readFileSync(filePath, options.encoding || "utf8");
|
|
1086
|
+
}
|
|
1087
|
+
const combinedData = existingContent + data;
|
|
1088
|
+
atomicWriteFileSync(filePath, combinedData, options);
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
function readJSONSafe(filePath) {
|
|
1092
|
+
try {
|
|
1093
|
+
if (!fs5.existsSync(filePath)) {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
const content = fs5.readFileSync(filePath, "utf8");
|
|
1097
|
+
return JSON.parse(content);
|
|
1098
|
+
} catch (e) {
|
|
1099
|
+
console.error(`[atomicWrite] Failed to read JSON from ${filePath}:`, e.message);
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/core/policyVersionGuard.js
|
|
1105
|
+
function parsePolicyVersion(version) {
|
|
1106
|
+
if (typeof version === "number" && Number.isFinite(version)) {
|
|
1107
|
+
return { ok: true, kind: "int", parts: [Math.floor(version)], raw: String(version) };
|
|
1108
|
+
}
|
|
1109
|
+
if (typeof version !== "string" || !version.trim()) {
|
|
1110
|
+
return { ok: false, reason: "POLICY_VERSION_INVALID", message: "Policy version is required" };
|
|
1111
|
+
}
|
|
1112
|
+
const trimmed = version.trim();
|
|
1113
|
+
if (/^\d+$/.test(trimmed)) {
|
|
1114
|
+
return { ok: true, kind: "int", parts: [Number(trimmed)], raw: trimmed };
|
|
1115
|
+
}
|
|
1116
|
+
const semver = trimmed.replace(/^v/i, "");
|
|
1117
|
+
if (/^\d+(\.\d+){0,2}$/.test(semver)) {
|
|
1118
|
+
const parsed = semver.split(".").map((n) => Number(n));
|
|
1119
|
+
while (parsed.length < 3) {
|
|
1120
|
+
parsed.push(0);
|
|
1121
|
+
}
|
|
1122
|
+
return { ok: true, kind: "semver", parts: parsed, raw: trimmed };
|
|
1123
|
+
}
|
|
1124
|
+
return {
|
|
1125
|
+
ok: false,
|
|
1126
|
+
reason: "POLICY_VERSION_INVALID",
|
|
1127
|
+
message: `Unsupported policy version format '${version}' (use integer or semver)`
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
function compareVersions(a, b) {
|
|
1131
|
+
const len = Math.max(a.parts.length, b.parts.length);
|
|
1132
|
+
for (let i = 0; i < len; i++) {
|
|
1133
|
+
const av = a.parts[i] ?? 0;
|
|
1134
|
+
const bv = b.parts[i] ?? 0;
|
|
1135
|
+
if (av > bv) return 1;
|
|
1136
|
+
if (av < bv) return -1;
|
|
1137
|
+
}
|
|
1138
|
+
return 0;
|
|
1139
|
+
}
|
|
1140
|
+
function parseCreatedAt(createdAt) {
|
|
1141
|
+
if (typeof createdAt === "number" && Number.isFinite(createdAt)) {
|
|
1142
|
+
const sec = createdAt > 1e12 ? Math.floor(createdAt / 1e3) : Math.floor(createdAt);
|
|
1143
|
+
return { ok: true, epochSec: sec };
|
|
1144
|
+
}
|
|
1145
|
+
if (typeof createdAt !== "string" || !createdAt.trim()) {
|
|
1146
|
+
return {
|
|
1147
|
+
ok: false,
|
|
1148
|
+
reason: "POLICY_CREATED_AT_INVALID",
|
|
1149
|
+
message: "Policy createdAt is required"
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
const ts = Date.parse(createdAt);
|
|
1153
|
+
if (Number.isNaN(ts)) {
|
|
1154
|
+
return {
|
|
1155
|
+
ok: false,
|
|
1156
|
+
reason: "POLICY_CREATED_AT_INVALID",
|
|
1157
|
+
message: `Invalid policy createdAt '${createdAt}'`
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
return { ok: true, epochSec: Math.floor(ts / 1e3) };
|
|
1161
|
+
}
|
|
1162
|
+
function loadPolicyState(statePath) {
|
|
1163
|
+
if (!fs6.existsSync(statePath)) {
|
|
1164
|
+
return {
|
|
1165
|
+
schemaVersion: "1",
|
|
1166
|
+
lastAccepted: null,
|
|
1167
|
+
updatedAt: null
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
try {
|
|
1171
|
+
const parsed = JSON.parse(fs6.readFileSync(statePath, "utf8"));
|
|
1172
|
+
if (!parsed || typeof parsed !== "object") {
|
|
1173
|
+
throw new Error("Policy state file has invalid structure");
|
|
1174
|
+
}
|
|
1175
|
+
return {
|
|
1176
|
+
schemaVersion: String(parsed.schemaVersion || "1"),
|
|
1177
|
+
lastAccepted: parsed.lastAccepted && typeof parsed.lastAccepted === "object" ? parsed.lastAccepted : null,
|
|
1178
|
+
updatedAt: parsed.updatedAt || null
|
|
1179
|
+
};
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
throw new Error(`Policy state at ${statePath} is corrupt or unreadable: ${err.message}`);
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function savePolicyState(statePath, stateObj) {
|
|
1185
|
+
const payload = JSON.stringify(stateObj, null, 2);
|
|
1186
|
+
atomicWriteFileSync(statePath, payload, { encoding: "utf8" });
|
|
1187
|
+
}
|
|
1188
|
+
function validateAndUpdatePolicyVersionState({
|
|
1189
|
+
policyObj,
|
|
1190
|
+
statePath = path6.resolve(".lbe/data/policy.state.json"),
|
|
1191
|
+
maxCreatedAtSkewSec = 31536e3,
|
|
1192
|
+
nowSec = Math.floor(Date.now() / 1e3),
|
|
1193
|
+
persist = true
|
|
1194
|
+
}) {
|
|
1195
|
+
const version = parsePolicyVersion(policyObj?.version);
|
|
1196
|
+
if (!version.ok) {
|
|
1197
|
+
return {
|
|
1198
|
+
ok: false,
|
|
1199
|
+
reason: version.reason,
|
|
1200
|
+
message: version.message,
|
|
1201
|
+
updated: false
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
const createdAt = parseCreatedAt(policyObj?.createdAt);
|
|
1205
|
+
if (!createdAt.ok) {
|
|
1206
|
+
return {
|
|
1207
|
+
ok: false,
|
|
1208
|
+
reason: createdAt.reason,
|
|
1209
|
+
message: createdAt.message,
|
|
1210
|
+
updated: false
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
const skew = Math.abs(nowSec - createdAt.epochSec);
|
|
1214
|
+
const allowedSkew = Number.isFinite(maxCreatedAtSkewSec) && maxCreatedAtSkewSec > 0 ? Math.floor(maxCreatedAtSkewSec) : 31536e3;
|
|
1215
|
+
if (skew > allowedSkew) {
|
|
1216
|
+
return {
|
|
1217
|
+
ok: false,
|
|
1218
|
+
reason: "POLICY_CREATED_AT_SKEW_EXCEEDED",
|
|
1219
|
+
message: `Policy createdAt skew ${skew}s exceeds allowed ${allowedSkew}s`,
|
|
1220
|
+
updated: false
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
let state;
|
|
1224
|
+
try {
|
|
1225
|
+
state = loadPolicyState(statePath);
|
|
1226
|
+
} catch (err) {
|
|
1227
|
+
return {
|
|
1228
|
+
ok: false,
|
|
1229
|
+
reason: "POLICY_STATE_CORRUPT",
|
|
1230
|
+
message: err.message,
|
|
1231
|
+
updated: false
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
const previous = state.lastAccepted;
|
|
1235
|
+
let previousVersion = null;
|
|
1236
|
+
let previousCreatedAt = null;
|
|
1237
|
+
let versionCompare = 0;
|
|
1238
|
+
if (previous) {
|
|
1239
|
+
previousVersion = parsePolicyVersion(previous.version);
|
|
1240
|
+
previousCreatedAt = parseCreatedAt(previous.createdAt);
|
|
1241
|
+
if (previousVersion.ok && previousCreatedAt.ok) {
|
|
1242
|
+
versionCompare = compareVersions(version, previousVersion);
|
|
1243
|
+
if (versionCompare < 0) {
|
|
1244
|
+
return {
|
|
1245
|
+
ok: false,
|
|
1246
|
+
reason: "POLICY_VERSION_REGRESSION",
|
|
1247
|
+
message: `Policy version regression: current '${version.raw}' < last '${previousVersion.raw}'`,
|
|
1248
|
+
updated: false
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
if (versionCompare === 0 && createdAt.epochSec < previousCreatedAt.epochSec) {
|
|
1252
|
+
return {
|
|
1253
|
+
ok: false,
|
|
1254
|
+
reason: "POLICY_CREATED_AT_REGRESSION",
|
|
1255
|
+
message: `Policy createdAt regression: current '${policyObj.createdAt}' < last '${previous.createdAt}'`,
|
|
1256
|
+
updated: false
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
if (versionCompare > 0 && createdAt.epochSec < previousCreatedAt.epochSec) {
|
|
1260
|
+
return {
|
|
1261
|
+
ok: false,
|
|
1262
|
+
reason: "POLICY_CREATED_AT_REGRESSION",
|
|
1263
|
+
message: `Policy createdAt must be monotonic when version increases`,
|
|
1264
|
+
updated: false
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
const shouldUpdate = !previous || !previousVersion?.ok || !previousCreatedAt?.ok || versionCompare > 0 || versionCompare === 0 && createdAt.epochSec > previousCreatedAt.epochSec;
|
|
1270
|
+
if (persist && shouldUpdate) {
|
|
1271
|
+
const nextState = {
|
|
1272
|
+
schemaVersion: "1",
|
|
1273
|
+
lastAccepted: {
|
|
1274
|
+
version: policyObj.version,
|
|
1275
|
+
createdAt: policyObj.createdAt,
|
|
1276
|
+
environment: policyObj.environment || null
|
|
1277
|
+
},
|
|
1278
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1279
|
+
};
|
|
1280
|
+
savePolicyState(statePath, nextState);
|
|
1281
|
+
}
|
|
1282
|
+
return {
|
|
1283
|
+
ok: true,
|
|
1284
|
+
reason: null,
|
|
1285
|
+
message: "Policy version guard passed",
|
|
1286
|
+
updated: shouldUpdate
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// runtime/engine.js
|
|
1291
|
+
import fs7 from "fs";
|
|
1292
|
+
import path7 from "path";
|
|
1293
|
+
import { fileURLToPath } from "url";
|
|
1294
|
+
var runtimeDir = path7.dirname(fileURLToPath(import.meta.url));
|
|
1295
|
+
var wasmPath = path7.join(runtimeDir, "lbe_engine.wasm");
|
|
1296
|
+
var POLICY_MESSAGES = {
|
|
1297
|
+
0: { allowed: true, reason: null, message: "Policy check passed" },
|
|
1298
|
+
1: { allowed: false, reason: "POLICY_NOT_CONFIGURED", message: "No policy configured" },
|
|
1299
|
+
2: { allowed: false, reason: "REQUESTER_NOT_ALLOWED", message: "Requester not in policy" },
|
|
1300
|
+
3: { allowed: false, reason: "COMMAND_NOT_ALLOWED", message: "Command not allowed for requester" },
|
|
1301
|
+
4: { allowed: false, reason: "ADAPTER_NOT_ALLOWED", message: "Adapter not allowed" },
|
|
1302
|
+
5: { allowed: false, reason: "NO_FILESYSTEM_ROOTS_DEFINED", message: "No filesystem roots defined for requester" },
|
|
1303
|
+
6: { allowed: false, reason: "CWD_OUTSIDE_ALLOWED_ROOT", message: "Path not under allowed roots" },
|
|
1304
|
+
7: { allowed: false, reason: "PATH_DENIED_BY_PATTERN", message: "Path matches deny pattern" },
|
|
1305
|
+
8: { allowed: false, reason: "SHELL_CMD_DENIED", message: "Shell command not allowed" }
|
|
1306
|
+
};
|
|
1307
|
+
var SCHEMA_MESSAGES = {
|
|
1308
|
+
0: { valid: true, error: null },
|
|
1309
|
+
1: { valid: false, error: "Missing required field: id" },
|
|
1310
|
+
2: { valid: false, error: "Missing required field: commandId" },
|
|
1311
|
+
3: { valid: false, error: "Missing required field: requesterId" },
|
|
1312
|
+
4: { valid: false, error: "Missing required field: sessionId" },
|
|
1313
|
+
5: { valid: false, error: "Missing required field: timestamp" },
|
|
1314
|
+
6: { valid: false, error: "Missing required field: nonce" },
|
|
1315
|
+
7: { valid: false, error: "Missing required field: requires" },
|
|
1316
|
+
8: { valid: false, error: "Missing required field: payload" },
|
|
1317
|
+
9: { valid: false, error: "Missing required field: signature" },
|
|
1318
|
+
10: { valid: false, error: "Field 'id' is invalid" },
|
|
1319
|
+
11: { valid: false, error: "Field 'commandId' is invalid" },
|
|
1320
|
+
12: { valid: false, error: "Field 'requesterId' is invalid" },
|
|
1321
|
+
13: { valid: false, error: "Field 'sessionId' is invalid" },
|
|
1322
|
+
14: { valid: false, error: "Field 'timestamp' is invalid" },
|
|
1323
|
+
15: { valid: false, error: "Field 'nonce' is invalid" },
|
|
1324
|
+
16: { valid: false, error: "Field 'requires' is invalid" },
|
|
1325
|
+
17: { valid: false, error: "payload: missing required field: adapter" },
|
|
1326
|
+
18: { valid: false, error: "payload: field 'adapter' is invalid" },
|
|
1327
|
+
19: { valid: false, error: "signature: missing required field: alg" },
|
|
1328
|
+
20: { valid: false, error: "signature: missing required field: keyId" },
|
|
1329
|
+
21: { valid: false, error: "signature: missing required field: sig" },
|
|
1330
|
+
22: { valid: false, error: "signature: field 'alg' must be ed25519" },
|
|
1331
|
+
23: { valid: false, error: "signature: field 'sig' is invalid" },
|
|
1332
|
+
24: { valid: false, error: "Field 'risk' is invalid" }
|
|
1333
|
+
};
|
|
1334
|
+
var KEY_REASONS = {
|
|
1335
|
+
1: "KEY_ID_INVALID",
|
|
1336
|
+
2: "KEY_NOT_TRUSTED",
|
|
1337
|
+
3: "KEY_DEPRECATED",
|
|
1338
|
+
4: "KEY_REQUESTER_MISMATCH",
|
|
1339
|
+
5: "KEY_LIFECYCLE_INVALID",
|
|
1340
|
+
6: "KEY_NOT_YET_VALID",
|
|
1341
|
+
7: "KEY_EXPIRED"
|
|
1342
|
+
};
|
|
1343
|
+
var PIPELINE_STAGES = {
|
|
1344
|
+
0: "schema",
|
|
1345
|
+
1: "timestamp",
|
|
1346
|
+
2: "key",
|
|
1347
|
+
3: "signature",
|
|
1348
|
+
4: "rate_limit",
|
|
1349
|
+
5: "nonce",
|
|
1350
|
+
6: "policy",
|
|
1351
|
+
255: "ok"
|
|
1352
|
+
};
|
|
1353
|
+
var RISK_LABELS = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
1354
|
+
var COMMAND_TYPE = { ECHO: 0, READ_FILE: 1, WRITE_FILE: 2, PATCH_FILE: 3, DELETE_FILE: 4, RUN_SHELL: 5 };
|
|
1355
|
+
var _instance = null;
|
|
1356
|
+
function wasm() {
|
|
1357
|
+
if (_instance) return _instance;
|
|
1358
|
+
if (!fs7.existsSync(wasmPath)) throw new Error(`LBE engine missing: ${wasmPath}`);
|
|
1359
|
+
const bytes = fs7.readFileSync(wasmPath);
|
|
1360
|
+
_instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), {});
|
|
1361
|
+
return _instance;
|
|
1362
|
+
}
|
|
1363
|
+
function memory() {
|
|
1364
|
+
return new Uint8Array(wasm().exports.memory.buffer);
|
|
1365
|
+
}
|
|
1366
|
+
function inPtr() {
|
|
1367
|
+
return wasm().exports.lbe_in_ptr();
|
|
1368
|
+
}
|
|
1369
|
+
function outPtr() {
|
|
1370
|
+
return wasm().exports.lbe_out_ptr();
|
|
1371
|
+
}
|
|
1372
|
+
function bufSize() {
|
|
1373
|
+
return wasm().exports.lbe_buf_size();
|
|
1374
|
+
}
|
|
1375
|
+
function writeIn(str) {
|
|
1376
|
+
const enc = new TextEncoder().encode(str);
|
|
1377
|
+
const mem = memory();
|
|
1378
|
+
const ptr = inPtr();
|
|
1379
|
+
mem.set(enc, ptr);
|
|
1380
|
+
mem[ptr + enc.length] = 0;
|
|
1381
|
+
}
|
|
1382
|
+
function readOut() {
|
|
1383
|
+
const mem = memory();
|
|
1384
|
+
const ptr = outPtr();
|
|
1385
|
+
let end = ptr;
|
|
1386
|
+
while (mem[end] !== 0 && end - ptr < bufSize()) end++;
|
|
1387
|
+
return new TextDecoder().decode(mem.slice(ptr, end));
|
|
1388
|
+
}
|
|
1389
|
+
function writePipelineInput(fields) {
|
|
1390
|
+
const mem = memory();
|
|
1391
|
+
const ptr = inPtr();
|
|
1392
|
+
const view = new DataView(mem.buffer, ptr);
|
|
1393
|
+
fields.forEach((v, i) => view.setUint32(i * 4, v >>> 0, true));
|
|
1394
|
+
}
|
|
1395
|
+
function readPipelineOutput() {
|
|
1396
|
+
const mem = memory();
|
|
1397
|
+
const ptr = outPtr();
|
|
1398
|
+
const view = new DataView(mem.buffer, ptr);
|
|
1399
|
+
return { stage: view.getUint32(0, true), code: view.getUint32(4, true) };
|
|
1400
|
+
}
|
|
1401
|
+
function runValidationPipeline(flags) {
|
|
1402
|
+
writePipelineInput([
|
|
1403
|
+
// Schema flags [0..24]
|
|
1404
|
+
flags.hasId ? 1 : 0,
|
|
1405
|
+
flags.idValid ? 1 : 0,
|
|
1406
|
+
flags.hasCommandId ? 1 : 0,
|
|
1407
|
+
flags.commandIdValid ? 1 : 0,
|
|
1408
|
+
flags.hasRequesterId ? 1 : 0,
|
|
1409
|
+
flags.requesterIdValid ? 1 : 0,
|
|
1410
|
+
flags.hasSessionId ? 1 : 0,
|
|
1411
|
+
flags.sessionIdValid ? 1 : 0,
|
|
1412
|
+
flags.hasTimestamp ? 1 : 0,
|
|
1413
|
+
flags.timestampValid ? 1 : 0,
|
|
1414
|
+
flags.hasNonce ? 1 : 0,
|
|
1415
|
+
flags.nonceValid ? 1 : 0,
|
|
1416
|
+
flags.hasRequires ? 1 : 0,
|
|
1417
|
+
flags.requiresValid ? 1 : 0,
|
|
1418
|
+
flags.hasPayload ? 1 : 0,
|
|
1419
|
+
flags.hasPayloadAdapter ? 1 : 0,
|
|
1420
|
+
flags.payloadAdapterValid ? 1 : 0,
|
|
1421
|
+
flags.hasSignature ? 1 : 0,
|
|
1422
|
+
flags.hasSignatureAlg ? 1 : 0,
|
|
1423
|
+
flags.signatureAlgValid ? 1 : 0,
|
|
1424
|
+
flags.hasSignatureKeyId ? 1 : 0,
|
|
1425
|
+
flags.hasSignatureSig ? 1 : 0,
|
|
1426
|
+
flags.signatureSigValid ? 1 : 0,
|
|
1427
|
+
flags.hasRisk ? 1 : 0,
|
|
1428
|
+
flags.riskValid ? 1 : 0,
|
|
1429
|
+
// Timestamp [25..27]
|
|
1430
|
+
flags.cmdTimestamp >>> 0,
|
|
1431
|
+
flags.nowSec >>> 0,
|
|
1432
|
+
flags.maxClockSkewSec >>> 0,
|
|
1433
|
+
// Key lifecycle [28..34]
|
|
1434
|
+
flags.keyIdFormatValid ? 1 : 0,
|
|
1435
|
+
flags.keyFound ? 1 : 0,
|
|
1436
|
+
flags.keyNotDeprecated ? 1 : 0,
|
|
1437
|
+
flags.keyRequesterMatches ? 1 : 0,
|
|
1438
|
+
flags.keyNotBeforeOk ? 1 : 0,
|
|
1439
|
+
flags.keyNotExpired ? 1 : 0,
|
|
1440
|
+
flags.keyLifecycleFieldsPresent ? 1 : 0,
|
|
1441
|
+
// Signature [35]
|
|
1442
|
+
flags.signatureValid ? 1 : 0,
|
|
1443
|
+
// Rate limit [36..37]
|
|
1444
|
+
flags.rateLimitOk ? 1 : 0,
|
|
1445
|
+
flags.rateLimitRetryAfterSec >>> 0,
|
|
1446
|
+
// Nonce [38]
|
|
1447
|
+
flags.nonceOk ? 1 : 0,
|
|
1448
|
+
// Policy [39..48]
|
|
1449
|
+
flags.policyConfigured ? 1 : 0,
|
|
1450
|
+
flags.requesterConfigured ? 1 : 0,
|
|
1451
|
+
flags.commandAllowed ? 1 : 0,
|
|
1452
|
+
flags.adapterAllowed ? 1 : 0,
|
|
1453
|
+
flags.filesystemRequired ? 1 : 0,
|
|
1454
|
+
flags.filesystemRootsDefined ? 1 : 0,
|
|
1455
|
+
flags.filesystemOk ? 1 : 0,
|
|
1456
|
+
flags.pathDenied ? 1 : 0,
|
|
1457
|
+
flags.shellRequired ? 1 : 0,
|
|
1458
|
+
flags.shellCommandOk ? 1 : 0
|
|
1459
|
+
]);
|
|
1460
|
+
wasm().exports.lbe_validate_pipeline();
|
|
1461
|
+
const { stage, code } = readPipelineOutput();
|
|
1462
|
+
const ok = stage === 255;
|
|
1463
|
+
return {
|
|
1464
|
+
ok,
|
|
1465
|
+
stage,
|
|
1466
|
+
stageLabel: PIPELINE_STAGES[stage] || "unknown",
|
|
1467
|
+
code,
|
|
1468
|
+
schemaError: stage === 0 ? SCHEMA_MESSAGES[code]?.error || "Schema invalid" : null,
|
|
1469
|
+
keyReason: stage === 2 ? KEY_REASONS[code] || "KEY_ERROR" : null,
|
|
1470
|
+
policyResult: stage === 6 ? { ...POLICY_MESSAGES[code] || POLICY_MESSAGES[1], code } : null,
|
|
1471
|
+
retryAfterSec: stage === 4 ? code : 0,
|
|
1472
|
+
skewSec: stage === 1 ? code : 0
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
function checkNonce({ ttlSec, nowSec, newKey, existingEntries }) {
|
|
1476
|
+
const lines = [`${ttlSec}:${nowSec}`, newKey, ...existingEntries].join("\n") + "\n";
|
|
1477
|
+
writeIn(lines);
|
|
1478
|
+
const isReplay = wasm().exports.lbe_nonce_check() !== 0;
|
|
1479
|
+
if (isReplay) return { ok: false, updatedEntriesText: null };
|
|
1480
|
+
const out = readOut();
|
|
1481
|
+
return { ok: true, updatedEntriesText: out.startsWith("OK\n") ? out.slice(3) : out };
|
|
1482
|
+
}
|
|
1483
|
+
function checkRateLimit({ windowSec, maxRequests, nowSec, requesterId, existingEntries }) {
|
|
1484
|
+
const lines = [
|
|
1485
|
+
`${windowSec}:${maxRequests}:${nowSec}`,
|
|
1486
|
+
requesterId,
|
|
1487
|
+
...existingEntries
|
|
1488
|
+
].join("\n") + "\n";
|
|
1489
|
+
writeIn(lines);
|
|
1490
|
+
const exceeded = wasm().exports.lbe_rate_check() !== 0;
|
|
1491
|
+
const out = readOut();
|
|
1492
|
+
if (exceeded) {
|
|
1493
|
+
const retryAfterSec = parseInt(out.match(/^EXCEEDED:(\d+)/)?.[1] ?? "1", 10);
|
|
1494
|
+
const entriesText = out.replace(/^EXCEEDED:\d+\n/, "");
|
|
1495
|
+
return { ok: false, retryAfterSec, updatedEntriesText: entriesText };
|
|
1496
|
+
}
|
|
1497
|
+
return { ok: true, retryAfterSec: 0, updatedEntriesText: out.startsWith("OK\n") ? out.slice(3) : out };
|
|
1498
|
+
}
|
|
1499
|
+
function classifyRisk(commandId, shellCmdIsRm = false) {
|
|
1500
|
+
const typeCode = COMMAND_TYPE[commandId] ?? 0;
|
|
1501
|
+
const code = wasm().exports.lbe_classify_risk(typeCode, shellCmdIsRm ? 1 : 0);
|
|
1502
|
+
return RISK_LABELS[code] ?? "LOW";
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// src/core/validator.js
|
|
1506
|
+
import path8 from "path";
|
|
1507
|
+
function extractSchemaFlags(cmd) {
|
|
1508
|
+
const has = (k) => cmd != null && Object.prototype.hasOwnProperty.call(cmd, k);
|
|
1509
|
+
const isStr = (v) => typeof v === "string";
|
|
1510
|
+
const p = cmd?.payload;
|
|
1511
|
+
const sig = cmd?.signature;
|
|
1512
|
+
return {
|
|
1513
|
+
hasId: has("id"),
|
|
1514
|
+
idValid: isStr(cmd?.id) && /^[A-Z_]+$/.test(cmd.id) && cmd.id.length >= 1 && cmd.id.length <= 50,
|
|
1515
|
+
hasCommandId: has("commandId"),
|
|
1516
|
+
commandIdValid: isStr(cmd?.commandId) && /^[a-f0-9-]+$/.test(cmd.commandId) && cmd.commandId.length === 36,
|
|
1517
|
+
hasRequesterId: has("requesterId"),
|
|
1518
|
+
requesterIdValid: isStr(cmd?.requesterId) && cmd.requesterId.length >= 3 && cmd.requesterId.length <= 100,
|
|
1519
|
+
hasSessionId: has("sessionId"),
|
|
1520
|
+
sessionIdValid: isStr(cmd?.sessionId) && cmd.sessionId.length >= 3,
|
|
1521
|
+
hasTimestamp: has("timestamp"),
|
|
1522
|
+
timestampValid: typeof cmd?.timestamp === "number" && cmd.timestamp >= 1e9,
|
|
1523
|
+
hasNonce: has("nonce"),
|
|
1524
|
+
nonceValid: isStr(cmd?.nonce) && cmd.nonce.length >= 32 && cmd.nonce.length <= 128,
|
|
1525
|
+
hasRequires: has("requires"),
|
|
1526
|
+
requiresValid: Array.isArray(cmd?.requires) && cmd.requires.length >= 1 && cmd.requires.every(isStr),
|
|
1527
|
+
hasPayload: has("payload") && typeof p === "object" && p !== null && !Array.isArray(p),
|
|
1528
|
+
hasPayloadAdapter: p != null && Object.prototype.hasOwnProperty.call(p, "adapter"),
|
|
1529
|
+
payloadAdapterValid: isStr(p?.adapter),
|
|
1530
|
+
hasSignature: has("signature") && typeof sig === "object" && sig !== null && !Array.isArray(sig),
|
|
1531
|
+
hasSignatureAlg: sig != null && Object.prototype.hasOwnProperty.call(sig, "alg"),
|
|
1532
|
+
signatureAlgValid: sig?.alg === "ed25519",
|
|
1533
|
+
hasSignatureKeyId: sig != null && Object.prototype.hasOwnProperty.call(sig, "keyId"),
|
|
1534
|
+
hasSignatureSig: sig != null && Object.prototype.hasOwnProperty.call(sig, "sig"),
|
|
1535
|
+
signatureSigValid: isStr(sig?.sig) && sig.sig.length >= 10,
|
|
1536
|
+
hasRisk: has("risk"),
|
|
1537
|
+
riskValid: ["LOW", "MEDIUM", "HIGH", "CRITICAL"].includes(cmd?.risk)
|
|
1538
|
+
};
|
|
1539
|
+
}
|
|
1540
|
+
function extractPolicyFlags(policy, cmd) {
|
|
1541
|
+
const hasPolicy = !!(policy && policy.default === "DENY" && policy.requesters && typeof policy.requesters === "object");
|
|
1542
|
+
const rp = policy?.requesters?.[cmd.requesterId];
|
|
1543
|
+
const cmdId = cmd.id?.toLowerCase() ?? "";
|
|
1544
|
+
const commandAllowed = !!rp?.allowCommands?.some((c) => c.toLowerCase() === cmdId);
|
|
1545
|
+
const adapterAllowed = !!rp?.allowAdapters?.includes(cmd.payload?.adapter);
|
|
1546
|
+
const filesystemRequired = !!cmd.payload?.cwd;
|
|
1547
|
+
let filesystemRootsDefined = false;
|
|
1548
|
+
let filesystemOk = false;
|
|
1549
|
+
let pathDenied = false;
|
|
1550
|
+
if (filesystemRequired) {
|
|
1551
|
+
const roots = rp?.filesystem?.roots ?? [];
|
|
1552
|
+
filesystemRootsDefined = roots.length > 0;
|
|
1553
|
+
if (filesystemRootsDefined) {
|
|
1554
|
+
const cwd = path8.resolve(cmd.payload.cwd);
|
|
1555
|
+
filesystemOk = roots.some((r) => {
|
|
1556
|
+
const rr = path8.resolve(r);
|
|
1557
|
+
return cwd === rr || cwd.startsWith(rr + path8.sep);
|
|
1558
|
+
});
|
|
1559
|
+
const denyPatterns = rp?.filesystem?.denyPatterns ?? [];
|
|
1560
|
+
pathDenied = denyPatterns.some((pattern) => {
|
|
1561
|
+
const re = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$");
|
|
1562
|
+
return re.test(cwd);
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
let shellRequired = false;
|
|
1567
|
+
let shellCommandOk = true;
|
|
1568
|
+
if (cmd.id === "RUN_SHELL") {
|
|
1569
|
+
shellRequired = true;
|
|
1570
|
+
const allowCmds = rp?.exec?.allowCmds ?? [];
|
|
1571
|
+
const denyCmds = rp?.exec?.denyCmds ?? [];
|
|
1572
|
+
const shellCmd = cmd.payload?.cmd;
|
|
1573
|
+
if (denyCmds.includes(shellCmd)) {
|
|
1574
|
+
shellCommandOk = false;
|
|
1575
|
+
} else {
|
|
1576
|
+
shellCommandOk = allowCmds.length === 0 || allowCmds.includes(shellCmd);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
return {
|
|
1580
|
+
policyConfigured: hasPolicy,
|
|
1581
|
+
requesterConfigured: !!rp,
|
|
1582
|
+
commandAllowed,
|
|
1583
|
+
adapterAllowed,
|
|
1584
|
+
filesystemRequired,
|
|
1585
|
+
filesystemRootsDefined,
|
|
1586
|
+
filesystemOk,
|
|
1587
|
+
pathDenied,
|
|
1588
|
+
shellRequired,
|
|
1589
|
+
shellCommandOk
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
function extractKeyFlags(keyStore, keyId, requesterId, now = /* @__PURE__ */ new Date()) {
|
|
1593
|
+
if (!keyStore || !keyId) {
|
|
1594
|
+
return {
|
|
1595
|
+
keyIdFormatValid: false,
|
|
1596
|
+
keyFound: false,
|
|
1597
|
+
keyNotDeprecated: false,
|
|
1598
|
+
keyRequesterMatches: false,
|
|
1599
|
+
keyNotBeforeOk: false,
|
|
1600
|
+
keyNotExpired: false,
|
|
1601
|
+
keyLifecycleFieldsPresent: false,
|
|
1602
|
+
publicKey: null
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
const KEY_ID_RE = /^[A-Za-z0-9:_-]{3,128}$/;
|
|
1606
|
+
const keyIdFormatValid = KEY_ID_RE.test(keyId) && keyId !== "default";
|
|
1607
|
+
if (!keyIdFormatValid) {
|
|
1608
|
+
return {
|
|
1609
|
+
keyIdFormatValid,
|
|
1610
|
+
keyFound: false,
|
|
1611
|
+
keyNotDeprecated: false,
|
|
1612
|
+
keyRequesterMatches: false,
|
|
1613
|
+
keyNotBeforeOk: false,
|
|
1614
|
+
keyNotExpired: false,
|
|
1615
|
+
keyLifecycleFieldsPresent: false,
|
|
1616
|
+
publicKey: null
|
|
1617
|
+
};
|
|
1618
|
+
}
|
|
1619
|
+
const entry = keyStore.trustedKeys?.[keyId];
|
|
1620
|
+
const keyFound = !!entry;
|
|
1621
|
+
if (!keyFound) {
|
|
1622
|
+
return {
|
|
1623
|
+
keyIdFormatValid,
|
|
1624
|
+
keyFound,
|
|
1625
|
+
keyNotDeprecated: false,
|
|
1626
|
+
keyRequesterMatches: false,
|
|
1627
|
+
keyNotBeforeOk: false,
|
|
1628
|
+
keyNotExpired: false,
|
|
1629
|
+
keyLifecycleFieldsPresent: false,
|
|
1630
|
+
publicKey: null
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
const keyNotDeprecated = !entry.deprecated;
|
|
1634
|
+
const keyRequesterMatches = !entry.requesterId || entry.requesterId === requesterId;
|
|
1635
|
+
const notBefore = entry.notBefore || entry.validFrom;
|
|
1636
|
+
const expiresAt = entry.expiresAt || entry.validUntil;
|
|
1637
|
+
const keyLifecycleFieldsPresent = typeof notBefore === "string" && typeof expiresAt === "string";
|
|
1638
|
+
let keyNotBeforeOk = false;
|
|
1639
|
+
let keyNotExpired = false;
|
|
1640
|
+
if (keyLifecycleFieldsPresent) {
|
|
1641
|
+
const nb = new Date(notBefore);
|
|
1642
|
+
const exp = new Date(expiresAt);
|
|
1643
|
+
if (!isNaN(nb.getTime()) && !isNaN(exp.getTime()) && nb < exp) {
|
|
1644
|
+
keyNotBeforeOk = now >= nb;
|
|
1645
|
+
keyNotExpired = now < exp;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return {
|
|
1649
|
+
keyIdFormatValid,
|
|
1650
|
+
keyFound,
|
|
1651
|
+
keyNotDeprecated,
|
|
1652
|
+
keyRequesterMatches,
|
|
1653
|
+
keyNotBeforeOk,
|
|
1654
|
+
keyNotExpired,
|
|
1655
|
+
keyLifecycleFieldsPresent,
|
|
1656
|
+
publicKey: entry.publicKey ?? null
|
|
1657
|
+
};
|
|
1658
|
+
}
|
|
1659
|
+
function nonceEntriesToText(db) {
|
|
1660
|
+
return (db?.entries ?? []).map((e) => `${e.key}:${e.timestamp}`);
|
|
1661
|
+
}
|
|
1662
|
+
function textToNonceEntries(text) {
|
|
1663
|
+
return text.split("\n").filter(Boolean).map((line) => {
|
|
1664
|
+
const lastColon = line.lastIndexOf(":");
|
|
1665
|
+
return {
|
|
1666
|
+
key: line.slice(0, lastColon),
|
|
1667
|
+
timestamp: parseInt(line.slice(lastColon + 1), 10) || 0
|
|
1668
|
+
};
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
function rateEntriesToText(db) {
|
|
1672
|
+
return (db?.entries ?? []).map((e) => `${e.requesterId}:${e.timestamp}`);
|
|
1673
|
+
}
|
|
1674
|
+
function textToRateEntries(text) {
|
|
1675
|
+
return text.split("\n").filter(Boolean).map((line) => {
|
|
1676
|
+
const lastColon = line.lastIndexOf(":");
|
|
1677
|
+
return {
|
|
1678
|
+
requesterId: line.slice(0, lastColon),
|
|
1679
|
+
timestamp: parseInt(line.slice(lastColon + 1), 10) || 0
|
|
1680
|
+
};
|
|
1681
|
+
});
|
|
1682
|
+
}
|
|
1683
|
+
function validateCommand({
|
|
1684
|
+
commandObj,
|
|
1685
|
+
pubKeyB64,
|
|
1686
|
+
keyStore,
|
|
1687
|
+
nonceDb,
|
|
1688
|
+
policy,
|
|
1689
|
+
rateLimiter,
|
|
1690
|
+
policyStatePath
|
|
1691
|
+
}) {
|
|
1692
|
+
const result = {
|
|
1693
|
+
valid: false,
|
|
1694
|
+
commandId: commandObj?.commandId,
|
|
1695
|
+
checks: {},
|
|
1696
|
+
errors: []
|
|
1697
|
+
};
|
|
1698
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
1699
|
+
const now = /* @__PURE__ */ new Date();
|
|
1700
|
+
const maxClockSkewSec = Number.isFinite(policy?.security?.maxClockSkewSec) ? policy.security.maxClockSkewSec : 600;
|
|
1701
|
+
if (policyStatePath && policy?.version !== void 0) {
|
|
1702
|
+
try {
|
|
1703
|
+
const vCheck = validateAndUpdatePolicyVersionState({ policyObj: policy, statePath: policyStatePath });
|
|
1704
|
+
result.checks.policyVersion = vCheck.ok;
|
|
1705
|
+
if (!vCheck.ok) {
|
|
1706
|
+
result.errors.push({ type: "POLICY_VERSION_INVALID", message: vCheck.message });
|
|
1707
|
+
return result;
|
|
1708
|
+
}
|
|
1709
|
+
} catch {
|
|
1710
|
+
result.checks.policyVersion = true;
|
|
1711
|
+
}
|
|
1712
|
+
} else {
|
|
1713
|
+
result.checks.policyVersion = true;
|
|
1714
|
+
}
|
|
1715
|
+
const schemaFlags = extractSchemaFlags(commandObj);
|
|
1716
|
+
const keyId = commandObj?.signature?.keyId;
|
|
1717
|
+
const keyFlags = extractKeyFlags(keyStore, keyId, commandObj?.requesterId, now);
|
|
1718
|
+
let signatureValid = false;
|
|
1719
|
+
let effectivePubKey = keyFlags.publicKey;
|
|
1720
|
+
if (!effectivePubKey && pubKeyB64) effectivePubKey = pubKeyB64;
|
|
1721
|
+
if (effectivePubKey) {
|
|
1722
|
+
const bodyWithoutSig = { ...commandObj };
|
|
1723
|
+
delete bodyWithoutSig.signature;
|
|
1724
|
+
const sigCheck = verifyEd25519({
|
|
1725
|
+
payloadObj: bodyWithoutSig,
|
|
1726
|
+
sigB64: commandObj?.signature?.sig,
|
|
1727
|
+
pubKeyB64: effectivePubKey
|
|
1728
|
+
});
|
|
1729
|
+
signatureValid = sigCheck.valid;
|
|
1730
|
+
}
|
|
1731
|
+
let rateLimitOk = true;
|
|
1732
|
+
let rateLimitRetryAfterSec = 0;
|
|
1733
|
+
if (signatureValid && rateLimiter && typeof rateLimiter.db !== "undefined") {
|
|
1734
|
+
const rateCfg = policy?.requesters?.[commandObj.requesterId]?.rateLimit || {};
|
|
1735
|
+
const dfltCfg = policy?.security?.defaultRateLimit || {};
|
|
1736
|
+
const windowSec = rateCfg.windowSec ?? dfltCfg.windowSec ?? 60;
|
|
1737
|
+
const maxRequests = rateCfg.maxRequests ?? dfltCfg.maxRequests ?? 30;
|
|
1738
|
+
const rateResult = checkRateLimit({
|
|
1739
|
+
windowSec,
|
|
1740
|
+
maxRequests,
|
|
1741
|
+
nowSec,
|
|
1742
|
+
requesterId: commandObj.requesterId,
|
|
1743
|
+
existingEntries: rateEntriesToText(rateLimiter.db)
|
|
1744
|
+
});
|
|
1745
|
+
rateLimitOk = rateResult.ok;
|
|
1746
|
+
rateLimitRetryAfterSec = rateResult.retryAfterSec;
|
|
1747
|
+
if (rateResult.ok) {
|
|
1748
|
+
rateLimiter.db.entries = textToRateEntries(rateResult.updatedEntriesText);
|
|
1749
|
+
}
|
|
1750
|
+
} else if (signatureValid && rateLimiter && typeof rateLimiter.checkAndRecord === "function") {
|
|
1751
|
+
const rateCfg = policy?.requesters?.[commandObj.requesterId]?.rateLimit || {};
|
|
1752
|
+
const dfltCfg = policy?.security?.defaultRateLimit || {};
|
|
1753
|
+
const rateCheck = rateLimiter.checkAndRecord({
|
|
1754
|
+
requesterId: commandObj.requesterId,
|
|
1755
|
+
nowSec,
|
|
1756
|
+
windowSec: rateCfg.windowSec ?? dfltCfg.windowSec ?? 60,
|
|
1757
|
+
maxRequests: rateCfg.maxRequests ?? dfltCfg.maxRequests ?? 30
|
|
1758
|
+
});
|
|
1759
|
+
rateLimitOk = rateCheck.ok;
|
|
1760
|
+
rateLimitRetryAfterSec = rateCheck.retryAfterSec ?? 0;
|
|
1761
|
+
}
|
|
1762
|
+
let nonceOk = true;
|
|
1763
|
+
const nonceKey = `${commandObj?.requesterId}|${commandObj?.sessionId}|${commandObj?.nonce}`;
|
|
1764
|
+
const ttlSec = 3600;
|
|
1765
|
+
if (signatureValid && rateLimitOk && nonceDb) {
|
|
1766
|
+
if (typeof nonceDb.checkAndRecord === "function") {
|
|
1767
|
+
if (nonceDb.db) {
|
|
1768
|
+
const nonceResult = checkNonce({
|
|
1769
|
+
ttlSec,
|
|
1770
|
+
nowSec,
|
|
1771
|
+
newKey: nonceKey,
|
|
1772
|
+
existingEntries: nonceEntriesToText(nonceDb.db)
|
|
1773
|
+
});
|
|
1774
|
+
nonceOk = nonceResult.ok;
|
|
1775
|
+
if (nonceResult.ok) {
|
|
1776
|
+
nonceDb.db.entries = textToNonceEntries(nonceResult.updatedEntriesText);
|
|
1777
|
+
}
|
|
1778
|
+
} else {
|
|
1779
|
+
const r = nonceDb.checkAndRecord({
|
|
1780
|
+
requesterId: commandObj.requesterId,
|
|
1781
|
+
sessionId: commandObj.sessionId,
|
|
1782
|
+
nonce: commandObj.nonce
|
|
1783
|
+
});
|
|
1784
|
+
nonceOk = r.ok;
|
|
1785
|
+
}
|
|
1786
|
+
} else {
|
|
1787
|
+
const nonceResult = checkNonce({
|
|
1788
|
+
ttlSec,
|
|
1789
|
+
nowSec,
|
|
1790
|
+
newKey: nonceKey,
|
|
1791
|
+
existingEntries: nonceEntriesToText(nonceDb)
|
|
1792
|
+
});
|
|
1793
|
+
nonceOk = nonceResult.ok;
|
|
1794
|
+
if (nonceResult.ok) {
|
|
1795
|
+
nonceDb.entries = textToNonceEntries(nonceResult.updatedEntriesText);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
const policyFlags = extractPolicyFlags(policy, commandObj ?? {});
|
|
1800
|
+
const pipeline = runValidationPipeline({
|
|
1801
|
+
...schemaFlags,
|
|
1802
|
+
cmdTimestamp: commandObj?.timestamp ?? 0,
|
|
1803
|
+
nowSec,
|
|
1804
|
+
maxClockSkewSec,
|
|
1805
|
+
...keyFlags,
|
|
1806
|
+
signatureValid,
|
|
1807
|
+
rateLimitOk,
|
|
1808
|
+
rateLimitRetryAfterSec,
|
|
1809
|
+
nonceOk,
|
|
1810
|
+
...policyFlags
|
|
1811
|
+
});
|
|
1812
|
+
const s = pipeline.stage;
|
|
1813
|
+
result.checks.schema = s !== 0;
|
|
1814
|
+
if (s >= 1) result.checks.timestamp = s !== 1;
|
|
1815
|
+
if (s >= 2) result.checks.keyId = s !== 2;
|
|
1816
|
+
if (s >= 2) result.checks.signature = s !== 2 && s !== 3;
|
|
1817
|
+
if (s >= 4) result.checks.rateLimit = s !== 4;
|
|
1818
|
+
if (s >= 5) result.checks.nonce = s !== 5;
|
|
1819
|
+
if (s >= 6 || pipeline.ok) result.checks.policy = s !== 6;
|
|
1820
|
+
if (!pipeline.ok) {
|
|
1821
|
+
const stage = pipeline.stageLabel;
|
|
1822
|
+
if (stage === "schema") {
|
|
1823
|
+
result.errors.push({ type: "SCHEMA_ERROR", message: pipeline.schemaError || "Schema invalid" });
|
|
1824
|
+
} else if (stage === "timestamp") {
|
|
1825
|
+
result.errors.push({ type: "TIMESTAMP_SKEW_EXCEEDED", message: `Command timestamp skew ${pipeline.skewSec}s exceeds allowed ${maxClockSkewSec}s` });
|
|
1826
|
+
} else if (stage === "key") {
|
|
1827
|
+
const reason = pipeline.keyReason || "KEY_ERROR";
|
|
1828
|
+
const msgs = {
|
|
1829
|
+
KEY_ID_INVALID: `Invalid keyId '${keyId}'`,
|
|
1830
|
+
KEY_NOT_TRUSTED: `Key '${keyId}' is not in trusted key store`,
|
|
1831
|
+
KEY_DEPRECATED: `Key '${keyId}' is deprecated`,
|
|
1832
|
+
KEY_REQUESTER_MISMATCH: `Key '${keyId}' is not authorized for requester '${commandObj?.requesterId}'`,
|
|
1833
|
+
KEY_LIFECYCLE_INVALID: `Key '${keyId}' must define notBefore and expiresAt`,
|
|
1834
|
+
KEY_NOT_YET_VALID: `Key '${keyId}' is not yet valid`,
|
|
1835
|
+
KEY_EXPIRED: `Key '${keyId}' has expired`
|
|
1836
|
+
};
|
|
1837
|
+
result.errors.push({ type: reason, message: msgs[reason] || reason });
|
|
1838
|
+
} else if (stage === "signature") {
|
|
1839
|
+
result.errors.push({ type: "SIGNATURE_INVALID", message: effectivePubKey ? "Signature verification failed" : "No public key available" });
|
|
1840
|
+
} else if (stage === "rate_limit") {
|
|
1841
|
+
result.errors.push({ type: "RATE_LIMIT_EXCEEDED", message: `Rate limit exceeded. Retry after ${pipeline.retryAfterSec}s` });
|
|
1842
|
+
} else if (stage === "nonce") {
|
|
1843
|
+
result.errors.push({ type: "REPLAY_NONCE", message: "Nonce has already been used" });
|
|
1844
|
+
} else if (stage === "policy" && pipeline.policyResult) {
|
|
1845
|
+
result.errors.push({ type: pipeline.policyResult.reason, message: pipeline.policyResult.message });
|
|
1846
|
+
} else {
|
|
1847
|
+
result.errors.push({ type: "VALIDATION_FAILED", message: `Failed at stage: ${stage}` });
|
|
1848
|
+
}
|
|
1849
|
+
return result;
|
|
1850
|
+
}
|
|
1851
|
+
result.valid = true;
|
|
1852
|
+
result.risk = classifyRisk(commandObj.id, commandObj.payload?.cmd === "rm");
|
|
1853
|
+
result.message = "Command validation successful";
|
|
1854
|
+
return result;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
// src/core/nonceStore.js
|
|
1858
|
+
import fs8 from "fs";
|
|
1859
|
+
import path9 from "path";
|
|
1860
|
+
var NonceStore = class {
|
|
1861
|
+
constructor(dbPath, ttlSec = 3600) {
|
|
1862
|
+
this.dbPath = dbPath;
|
|
1863
|
+
this.ttlSec = ttlSec;
|
|
1864
|
+
this.db = { entries: [] };
|
|
1865
|
+
}
|
|
1866
|
+
async load() {
|
|
1867
|
+
if (!fs8.existsSync(this.dbPath)) {
|
|
1868
|
+
this.db = { entries: [] };
|
|
1869
|
+
return;
|
|
1870
|
+
}
|
|
1871
|
+
try {
|
|
1872
|
+
const data = fs8.readFileSync(this.dbPath, "utf8");
|
|
1873
|
+
this.db = JSON.parse(data);
|
|
1874
|
+
this.prune();
|
|
1875
|
+
} catch (err) {
|
|
1876
|
+
throw new Error(`Nonce DB at ${this.dbPath} is corrupt or unreadable: ${err.message}`);
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
async save() {
|
|
1880
|
+
try {
|
|
1881
|
+
const dir = path9.dirname(this.dbPath);
|
|
1882
|
+
if (!fs8.existsSync(dir)) {
|
|
1883
|
+
fs8.mkdirSync(dir, { recursive: true });
|
|
1884
|
+
}
|
|
1885
|
+
atomicWriteFileSync(this.dbPath, JSON.stringify(this.db, null, 2), { encoding: "utf8" });
|
|
1886
|
+
} catch (err) {
|
|
1887
|
+
throw new Error(`Failed to save nonce DB: ${err.message}`);
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
checkAndRecord({ requesterId, sessionId, nonce }) {
|
|
1891
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1892
|
+
this.db.entries = this.db.entries.filter((e) => now - e.timestamp <= this.ttlSec);
|
|
1893
|
+
const key = `${requesterId}|${sessionId}|${nonce}`;
|
|
1894
|
+
if (this.db.entries.some((e) => e.key === key)) {
|
|
1895
|
+
return {
|
|
1896
|
+
ok: false,
|
|
1897
|
+
reason: "REPLAY_NONCE",
|
|
1898
|
+
message: "Nonce has already been used"
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
this.db.entries.push({ key, timestamp: now });
|
|
1902
|
+
return {
|
|
1903
|
+
ok: true,
|
|
1904
|
+
reason: null,
|
|
1905
|
+
message: "Nonce accepted"
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
prune() {
|
|
1909
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1910
|
+
const before = this.db.entries.length;
|
|
1911
|
+
this.db.entries = this.db.entries.filter((e) => now - e.timestamp <= this.ttlSec);
|
|
1912
|
+
const after = this.db.entries.length;
|
|
1913
|
+
return {
|
|
1914
|
+
prunedCount: before - after,
|
|
1915
|
+
remainingCount: after
|
|
1916
|
+
};
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
|
|
1920
|
+
// src/cli/commands/verify.js
|
|
1921
|
+
async function verifyCommand(opts) {
|
|
1922
|
+
const { in: inFile } = opts;
|
|
1923
|
+
const config = opts.config || opts.policy;
|
|
1924
|
+
const pubKey = opts["pub-key"];
|
|
1925
|
+
const keysStorePath = opts["keys-store"] || path10.resolve(".lbe/config/keys.json");
|
|
1926
|
+
const policySigPath = opts["policy-sig"] || path10.resolve(".lbe/config/policy.sig.json");
|
|
1927
|
+
const policyStatePath = opts["policy-state"] || path10.resolve(".lbe/data/policy.state.json");
|
|
1928
|
+
const allowUnsignedPolicy = opts["policy-unsigned-ok"] === true || String(opts["policy-unsigned-ok"]).toLowerCase() === "true";
|
|
1929
|
+
if (!inFile) {
|
|
1930
|
+
console.error("Error: --in <file> is required");
|
|
1931
|
+
process.exit(1);
|
|
1932
|
+
}
|
|
1933
|
+
let proposal;
|
|
1934
|
+
try {
|
|
1935
|
+
const filePath = path10.resolve(inFile);
|
|
1936
|
+
const content = fs9.readFileSync(filePath, "utf-8");
|
|
1937
|
+
proposal = JSON.parse(content);
|
|
1938
|
+
} catch (error) {
|
|
1939
|
+
console.error(JSON.stringify({
|
|
1940
|
+
status: "error",
|
|
1941
|
+
error: "INVALID_PROPOSAL_FILE",
|
|
1942
|
+
message: error.message
|
|
1943
|
+
}));
|
|
1944
|
+
process.exit(5);
|
|
1945
|
+
}
|
|
1946
|
+
let policy;
|
|
1947
|
+
try {
|
|
1948
|
+
const policyPath = config || path10.resolve(".lbe/config/policy.default.json");
|
|
1949
|
+
if (!fs9.existsSync(policyPath)) {
|
|
1950
|
+
console.error(JSON.stringify({
|
|
1951
|
+
status: "error",
|
|
1952
|
+
error: "MISSING_POLICY",
|
|
1953
|
+
message: `Policy file not found: ${policyPath}`
|
|
1954
|
+
}));
|
|
1955
|
+
process.exit(1);
|
|
1956
|
+
}
|
|
1957
|
+
const policyContent = fs9.readFileSync(policyPath, "utf-8");
|
|
1958
|
+
policy = JSON.parse(policyContent);
|
|
1959
|
+
} catch (error) {
|
|
1960
|
+
console.error(JSON.stringify({
|
|
1961
|
+
status: "error",
|
|
1962
|
+
error: "INVALID_POLICY",
|
|
1963
|
+
message: error.message
|
|
1964
|
+
}));
|
|
1965
|
+
process.exit(1);
|
|
1966
|
+
}
|
|
1967
|
+
const keyStoreResult = loadKeysStore(keysStorePath);
|
|
1968
|
+
const keyStore = keyStoreResult.ok ? keyStoreResult.store : null;
|
|
1969
|
+
const policySigCheck = verifyPolicySignature({
|
|
1970
|
+
policyObj: policy,
|
|
1971
|
+
keyStore,
|
|
1972
|
+
policySigPath,
|
|
1973
|
+
allowUnsigned: allowUnsignedPolicy
|
|
1974
|
+
});
|
|
1975
|
+
if (!policySigCheck.ok) {
|
|
1976
|
+
console.error(JSON.stringify({
|
|
1977
|
+
status: "error",
|
|
1978
|
+
error: policySigCheck.reason,
|
|
1979
|
+
message: policySigCheck.message
|
|
1980
|
+
}, null, 2));
|
|
1981
|
+
process.exit(8);
|
|
1982
|
+
}
|
|
1983
|
+
const versionCheck = validateAndUpdatePolicyVersionState({
|
|
1984
|
+
policyObj: policy,
|
|
1985
|
+
statePath: policyStatePath,
|
|
1986
|
+
maxCreatedAtSkewSec: policy?.security?.maxPolicyCreatedAtSkewSec
|
|
1987
|
+
});
|
|
1988
|
+
if (!versionCheck.ok) {
|
|
1989
|
+
console.error(JSON.stringify({
|
|
1990
|
+
status: "error",
|
|
1991
|
+
error: versionCheck.reason,
|
|
1992
|
+
message: versionCheck.message
|
|
1993
|
+
}, null, 2));
|
|
1994
|
+
process.exit(8);
|
|
1995
|
+
}
|
|
1996
|
+
const nonceDb = new NonceStore(path10.resolve(".lbe/data/nonce.db.json"));
|
|
1997
|
+
await nonceDb.load();
|
|
1998
|
+
if (!keyStore && !pubKey) {
|
|
1999
|
+
console.error(JSON.stringify({
|
|
2000
|
+
status: "error",
|
|
2001
|
+
error: "MISSING_KEY_MATERIAL",
|
|
2002
|
+
message: `${keyStoreResult.message}. Provide --pub-key/--pub-key-file or create .lbe/config/keys.json`
|
|
2003
|
+
}));
|
|
2004
|
+
process.exit(1);
|
|
2005
|
+
}
|
|
2006
|
+
const result = validateCommand({
|
|
2007
|
+
commandObj: proposal,
|
|
2008
|
+
pubKeyB64: pubKey,
|
|
2009
|
+
keyStore,
|
|
2010
|
+
nonceDb,
|
|
2011
|
+
policy
|
|
2012
|
+
});
|
|
2013
|
+
const output = {
|
|
2014
|
+
status: result.valid ? "valid" : "invalid",
|
|
2015
|
+
commandId: proposal.commandId || "N/A",
|
|
2016
|
+
checks: result.checks,
|
|
2017
|
+
errors: result.errors || [],
|
|
2018
|
+
risk: result.risk || "UNKNOWN"
|
|
2019
|
+
};
|
|
2020
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2021
|
+
if (!result.valid) {
|
|
2022
|
+
if (result.checks.schema === false) process.exit(5);
|
|
2023
|
+
if (result.checks.signature === false) process.exit(3);
|
|
2024
|
+
if (result.checks.nonce === false) process.exit(4);
|
|
2025
|
+
if (result.checks.timestamp === false) process.exit(6);
|
|
2026
|
+
if (result.checks.rateLimit === false) process.exit(7);
|
|
2027
|
+
if (result.checks.policy === false) process.exit(2);
|
|
2028
|
+
process.exit(9);
|
|
2029
|
+
}
|
|
2030
|
+
process.exit(0);
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// src/cli/commands/dryrun.js
|
|
2034
|
+
import fs13 from "fs";
|
|
2035
|
+
import path14 from "path";
|
|
2036
|
+
|
|
2037
|
+
// src/adapters/noopAdapter.js
|
|
2038
|
+
async function noopAdapter(cmd) {
|
|
2039
|
+
return {
|
|
2040
|
+
adapter: "noop",
|
|
2041
|
+
commandId: cmd.commandId || "unknown",
|
|
2042
|
+
command: cmd.id || "unknown",
|
|
2043
|
+
status: "completed",
|
|
2044
|
+
output: `[NOOP] Would execute: ${cmd.id || "unknown"} on adapter: ${cmd.payload?.adapter || "unknown"}`,
|
|
2045
|
+
exitCode: 0,
|
|
2046
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2047
|
+
};
|
|
2048
|
+
}
|
|
2049
|
+
|
|
2050
|
+
// src/adapters/shellAdapter.js
|
|
2051
|
+
import { spawnSync } from "child_process";
|
|
2052
|
+
import path11 from "path";
|
|
2053
|
+
import fs10 from "fs";
|
|
2054
|
+
function physicalPath(candidate) {
|
|
2055
|
+
try {
|
|
2056
|
+
return fs10.realpathSync(path11.resolve(candidate));
|
|
2057
|
+
} catch {
|
|
2058
|
+
return path11.resolve(candidate);
|
|
2059
|
+
}
|
|
2060
|
+
}
|
|
2061
|
+
function normalizeArgs(args) {
|
|
2062
|
+
if (args === void 0) return { ok: true, args: [] };
|
|
2063
|
+
if (!Array.isArray(args)) {
|
|
2064
|
+
return { ok: false, error: "payload.args must be an array" };
|
|
2065
|
+
}
|
|
2066
|
+
const normalized = [];
|
|
2067
|
+
for (const arg of args) {
|
|
2068
|
+
if (typeof arg !== "string" && typeof arg !== "number" && typeof arg !== "boolean") {
|
|
2069
|
+
return { ok: false, error: "payload.args may only contain string, number, or boolean values" };
|
|
2070
|
+
}
|
|
2071
|
+
normalized.push(String(arg));
|
|
2072
|
+
}
|
|
2073
|
+
return { ok: true, args: normalized };
|
|
2074
|
+
}
|
|
2075
|
+
async function shellAdapter(cmd, policy, requester) {
|
|
2076
|
+
const payload = cmd.payload;
|
|
2077
|
+
const timeout = Math.min(Math.max(Number(payload.timeoutMs) || 3e4, 1), 3e4);
|
|
2078
|
+
const maxOutputSize = Math.min(Math.max(Number(payload.maxOutputBytes) || 1024 * 1024, 1024), 1024 * 1024);
|
|
2079
|
+
if (payload.adapter !== "shell") {
|
|
2080
|
+
return {
|
|
2081
|
+
adapter: "shell",
|
|
2082
|
+
commandId: cmd.commandId,
|
|
2083
|
+
status: "error",
|
|
2084
|
+
error: "Adapter mismatch",
|
|
2085
|
+
exitCode: 1
|
|
2086
|
+
};
|
|
2087
|
+
}
|
|
2088
|
+
const allowedCmds = requester?.exec?.allowCmds || [];
|
|
2089
|
+
const deniedCmds = requester?.exec?.denyCmds || [];
|
|
2090
|
+
if (deniedCmds.includes(payload.cmd)) {
|
|
2091
|
+
return {
|
|
2092
|
+
adapter: "shell",
|
|
2093
|
+
commandId: cmd.commandId,
|
|
2094
|
+
status: "blocked",
|
|
2095
|
+
error: `Command '${payload.cmd}' is denied`,
|
|
2096
|
+
exitCode: 2
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
if (allowedCmds.length > 0 && !allowedCmds.includes(payload.cmd)) {
|
|
2100
|
+
return {
|
|
2101
|
+
adapter: "shell",
|
|
2102
|
+
commandId: cmd.commandId,
|
|
2103
|
+
status: "blocked",
|
|
2104
|
+
error: `Command '${payload.cmd}' not in allowlist`,
|
|
2105
|
+
exitCode: 2
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
const roots = requester?.filesystem?.roots || [];
|
|
2109
|
+
const cwdAllow = roots.some((r) => {
|
|
2110
|
+
const resolvedRoot = physicalPath(r);
|
|
2111
|
+
const norm = physicalPath(payload.cwd);
|
|
2112
|
+
return norm === resolvedRoot || norm.startsWith(resolvedRoot + path11.sep);
|
|
2113
|
+
});
|
|
2114
|
+
if (!cwdAllow) {
|
|
2115
|
+
return {
|
|
2116
|
+
adapter: "shell",
|
|
2117
|
+
commandId: cmd.commandId,
|
|
2118
|
+
status: "blocked",
|
|
2119
|
+
error: `CWD '${payload.cwd}' not authorized`,
|
|
2120
|
+
exitCode: 2
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
const argCheck = normalizeArgs(payload.args);
|
|
2124
|
+
if (!argCheck.ok) {
|
|
2125
|
+
return {
|
|
2126
|
+
adapter: "shell",
|
|
2127
|
+
commandId: cmd.commandId,
|
|
2128
|
+
status: "blocked",
|
|
2129
|
+
error: argCheck.error,
|
|
2130
|
+
exitCode: 2
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
try {
|
|
2134
|
+
const result = spawnSync(payload.cmd, argCheck.args, {
|
|
2135
|
+
cwd: payload.cwd,
|
|
2136
|
+
timeout,
|
|
2137
|
+
encoding: "utf8",
|
|
2138
|
+
maxBuffer: maxOutputSize,
|
|
2139
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2140
|
+
shell: false
|
|
2141
|
+
});
|
|
2142
|
+
if (result.error) {
|
|
2143
|
+
throw result.error;
|
|
2144
|
+
}
|
|
2145
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`;
|
|
2146
|
+
const exitCode = result.status ?? 1;
|
|
2147
|
+
if (exitCode !== 0) {
|
|
2148
|
+
return {
|
|
2149
|
+
adapter: "shell",
|
|
2150
|
+
commandId: cmd.commandId,
|
|
2151
|
+
command: payload.cmd,
|
|
2152
|
+
status: "error",
|
|
2153
|
+
error: output.substring(0, maxOutputSize) || `Command exited with code ${exitCode}`,
|
|
2154
|
+
exitCode,
|
|
2155
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
return {
|
|
2159
|
+
adapter: "shell",
|
|
2160
|
+
commandId: cmd.commandId,
|
|
2161
|
+
command: payload.cmd,
|
|
2162
|
+
status: "completed",
|
|
2163
|
+
output: output.substring(0, maxOutputSize),
|
|
2164
|
+
exitCode: 0,
|
|
2165
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2166
|
+
};
|
|
2167
|
+
} catch (err) {
|
|
2168
|
+
return {
|
|
2169
|
+
adapter: "shell",
|
|
2170
|
+
commandId: cmd.commandId,
|
|
2171
|
+
command: payload.cmd,
|
|
2172
|
+
status: "error",
|
|
2173
|
+
error: err.message,
|
|
2174
|
+
exitCode: err.status || 1,
|
|
2175
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2176
|
+
};
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
// src/adapters/fileAdapter.js
|
|
2181
|
+
import fs12 from "fs";
|
|
2182
|
+
import path13 from "path";
|
|
2183
|
+
|
|
2184
|
+
// src/core/backup.js
|
|
2185
|
+
import fs11 from "fs";
|
|
2186
|
+
import path12 from "path";
|
|
2187
|
+
import crypto2 from "crypto";
|
|
2188
|
+
function createBackup(filePath, backupDir) {
|
|
2189
|
+
const dir = backupDir || path12.resolve(".lbe/data/backups");
|
|
2190
|
+
if (!fs11.existsSync(dir)) {
|
|
2191
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
2192
|
+
}
|
|
2193
|
+
const target = path12.resolve(filePath);
|
|
2194
|
+
const existed = fs11.existsSync(target);
|
|
2195
|
+
let content = null;
|
|
2196
|
+
let hash = null;
|
|
2197
|
+
if (existed) {
|
|
2198
|
+
content = fs11.readFileSync(target);
|
|
2199
|
+
hash = crypto2.createHash("sha256").update(content).digest("hex");
|
|
2200
|
+
}
|
|
2201
|
+
const basename = path12.basename(target).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
2202
|
+
const backupName = `${Date.now()}-${hash ? hash.slice(0, 8) : "new"}-${basename}`;
|
|
2203
|
+
const backupPath = existed ? path12.join(dir, backupName) : null;
|
|
2204
|
+
if (existed && content !== null) {
|
|
2205
|
+
atomicWriteFileSync(backupPath, content);
|
|
2206
|
+
}
|
|
2207
|
+
return {
|
|
2208
|
+
originalPath: target,
|
|
2209
|
+
backupPath,
|
|
2210
|
+
existed,
|
|
2211
|
+
hash,
|
|
2212
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2213
|
+
};
|
|
2214
|
+
}
|
|
2215
|
+
function restoreBackup(backupMeta) {
|
|
2216
|
+
if (!backupMeta) return { restored: false, error: "No backup metadata" };
|
|
2217
|
+
const { originalPath, backupPath, existed } = backupMeta;
|
|
2218
|
+
if (!existed) {
|
|
2219
|
+
try {
|
|
2220
|
+
if (fs11.existsSync(originalPath)) fs11.unlinkSync(originalPath);
|
|
2221
|
+
return { restored: true, action: "deleted" };
|
|
2222
|
+
} catch (e) {
|
|
2223
|
+
return { restored: false, error: e.message };
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (!backupPath || !fs11.existsSync(backupPath)) {
|
|
2227
|
+
return { restored: false, error: "Backup file not found at: " + backupPath };
|
|
2228
|
+
}
|
|
2229
|
+
try {
|
|
2230
|
+
const content = fs11.readFileSync(backupPath);
|
|
2231
|
+
atomicWriteFileSync(originalPath, content);
|
|
2232
|
+
return { restored: true, action: "restored" };
|
|
2233
|
+
} catch (e) {
|
|
2234
|
+
return { restored: false, error: e.message };
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2238
|
+
// src/adapters/fileAdapter.js
|
|
2239
|
+
var MAX_READ_BYTES = 10 * 1024 * 1024;
|
|
2240
|
+
function resolvedTarget(target, cwd) {
|
|
2241
|
+
if (!target) return null;
|
|
2242
|
+
return path13.isAbsolute(target) ? path13.resolve(target) : path13.resolve(cwd || process.cwd(), target);
|
|
2243
|
+
}
|
|
2244
|
+
function isUnderRoot(targetPath, roots) {
|
|
2245
|
+
const norm = resolvePhysicalPath(targetPath);
|
|
2246
|
+
return roots.some((r) => {
|
|
2247
|
+
const root = resolvePhysicalPath(r);
|
|
2248
|
+
return norm === root || norm.startsWith(root + path13.sep);
|
|
2249
|
+
});
|
|
2250
|
+
}
|
|
2251
|
+
function resolvePhysicalPath(candidate) {
|
|
2252
|
+
let current = path13.resolve(candidate);
|
|
2253
|
+
const suffix = [];
|
|
2254
|
+
while (!fs12.existsSync(current)) {
|
|
2255
|
+
const parent = path13.dirname(current);
|
|
2256
|
+
if (parent === current) break;
|
|
2257
|
+
suffix.unshift(path13.basename(current));
|
|
2258
|
+
current = parent;
|
|
2259
|
+
}
|
|
2260
|
+
try {
|
|
2261
|
+
current = fs12.realpathSync(current);
|
|
2262
|
+
} catch {
|
|
2263
|
+
}
|
|
2264
|
+
return path13.join(current, ...suffix);
|
|
2265
|
+
}
|
|
2266
|
+
function matchesDenyPattern(str, patterns) {
|
|
2267
|
+
for (const pattern of patterns || []) {
|
|
2268
|
+
const rx = new RegExp(
|
|
2269
|
+
"^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/\\\\]*") + "$"
|
|
2270
|
+
);
|
|
2271
|
+
if (rx.test(str)) return pattern;
|
|
2272
|
+
}
|
|
2273
|
+
return null;
|
|
2274
|
+
}
|
|
2275
|
+
function blocked(cmd, code, message, exitCode = 2) {
|
|
2276
|
+
return {
|
|
2277
|
+
adapter: "file",
|
|
2278
|
+
commandId: cmd.commandId,
|
|
2279
|
+
status: "blocked",
|
|
2280
|
+
errorCode: code,
|
|
2281
|
+
error: message,
|
|
2282
|
+
exitCode
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
function fail(cmd, code, message, backup = null, exitCode = 1) {
|
|
2286
|
+
return {
|
|
2287
|
+
adapter: "file",
|
|
2288
|
+
commandId: cmd.commandId,
|
|
2289
|
+
status: "error",
|
|
2290
|
+
errorCode: code,
|
|
2291
|
+
error: message,
|
|
2292
|
+
backup: backup ? summariseBackup(backup) : null,
|
|
2293
|
+
exitCode
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
function summariseBackup(b) {
|
|
2297
|
+
return b ? { path: b.backupPath, existed: b.existed, hash: b.hash, createdAt: b.createdAt } : null;
|
|
2298
|
+
}
|
|
2299
|
+
async function fileAdapter(cmd, policy, requester) {
|
|
2300
|
+
const payload = cmd.payload;
|
|
2301
|
+
const action = payload.action;
|
|
2302
|
+
const cwd = payload.cwd || process.cwd();
|
|
2303
|
+
const target = resolvedTarget(payload.target, cwd);
|
|
2304
|
+
if (!action) return blocked(cmd, "FILE_NO_ACTION", "payload.action is required");
|
|
2305
|
+
if (!target && action !== "noop") return blocked(cmd, "FILE_NO_TARGET", "payload.target is required");
|
|
2306
|
+
const roots = requester?.filesystem?.roots || [];
|
|
2307
|
+
if (roots.length === 0) return blocked(cmd, "FILE_NO_ROOTS", "No filesystem roots defined for requester");
|
|
2308
|
+
if (!isUnderRoot(target, roots)) return blocked(cmd, "FILE_OUTSIDE_ROOT", `'${target}' is outside allowed roots`);
|
|
2309
|
+
const denied = matchesDenyPattern(target, requester?.filesystem?.denyPatterns);
|
|
2310
|
+
if (denied) return blocked(cmd, "FILE_PATH_DENIED", `'${target}' matches deny pattern: ${denied}`);
|
|
2311
|
+
switch (action) {
|
|
2312
|
+
case "read":
|
|
2313
|
+
return doRead(cmd, target);
|
|
2314
|
+
case "write":
|
|
2315
|
+
return doWrite(cmd, target, payload);
|
|
2316
|
+
case "patch":
|
|
2317
|
+
return doPatch(cmd, target, payload);
|
|
2318
|
+
case "delete":
|
|
2319
|
+
return doDelete(cmd, target);
|
|
2320
|
+
default:
|
|
2321
|
+
return blocked(cmd, "FILE_UNKNOWN_ACTION", `Unknown action: '${action}'`);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
function doRead(cmd, target) {
|
|
2325
|
+
if (!fs12.existsSync(target)) return fail(cmd, "FILE_NOT_FOUND", `Not found: ${target}`);
|
|
2326
|
+
try {
|
|
2327
|
+
const stat = fs12.statSync(target);
|
|
2328
|
+
if (stat.size > MAX_READ_BYTES) return fail(cmd, "FILE_TOO_LARGE", "File exceeds 10 MB read limit");
|
|
2329
|
+
const content = fs12.readFileSync(target, "utf8");
|
|
2330
|
+
return {
|
|
2331
|
+
adapter: "file",
|
|
2332
|
+
action: "read",
|
|
2333
|
+
commandId: cmd.commandId,
|
|
2334
|
+
status: "completed",
|
|
2335
|
+
target,
|
|
2336
|
+
output: content,
|
|
2337
|
+
bytesRead: stat.size,
|
|
2338
|
+
exitCode: 0
|
|
2339
|
+
};
|
|
2340
|
+
} catch (e) {
|
|
2341
|
+
return fail(cmd, "FILE_READ_ERROR", e.message);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2344
|
+
function doWrite(cmd, target, payload) {
|
|
2345
|
+
const content = payload.content;
|
|
2346
|
+
if (content === void 0 || content === null) {
|
|
2347
|
+
return fail(cmd, "FILE_MISSING_CONTENT", "payload.content is required for write");
|
|
2348
|
+
}
|
|
2349
|
+
const backup = tryBackup(target);
|
|
2350
|
+
try {
|
|
2351
|
+
atomicWriteFileSync(target, content, { encoding: "utf8" });
|
|
2352
|
+
return {
|
|
2353
|
+
adapter: "file",
|
|
2354
|
+
action: "write",
|
|
2355
|
+
commandId: cmd.commandId,
|
|
2356
|
+
status: "completed",
|
|
2357
|
+
target,
|
|
2358
|
+
backup: summariseBackup(backup),
|
|
2359
|
+
output: `Wrote ${Buffer.byteLength(content, "utf8")} bytes to ${target}`,
|
|
2360
|
+
exitCode: 0
|
|
2361
|
+
};
|
|
2362
|
+
} catch (e) {
|
|
2363
|
+
restoreBackup(backup);
|
|
2364
|
+
return fail(cmd, "FILE_WRITE_ERROR", e.message, backup);
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
function doPatch(cmd, target, payload) {
|
|
2368
|
+
const content = payload.content;
|
|
2369
|
+
if (content === void 0 || content === null) {
|
|
2370
|
+
return fail(cmd, "FILE_MISSING_CONTENT", "payload.content is required for patch");
|
|
2371
|
+
}
|
|
2372
|
+
const backup = tryBackup(target);
|
|
2373
|
+
try {
|
|
2374
|
+
atomicWriteFileSync(target, content, { encoding: "utf8" });
|
|
2375
|
+
return {
|
|
2376
|
+
adapter: "file",
|
|
2377
|
+
action: "patch",
|
|
2378
|
+
commandId: cmd.commandId,
|
|
2379
|
+
status: "completed",
|
|
2380
|
+
target,
|
|
2381
|
+
backup: summariseBackup(backup),
|
|
2382
|
+
output: `Patched ${target} (${Buffer.byteLength(content, "utf8")} bytes)`,
|
|
2383
|
+
exitCode: 0
|
|
2384
|
+
};
|
|
2385
|
+
} catch (e) {
|
|
2386
|
+
restoreBackup(backup);
|
|
2387
|
+
return fail(cmd, "FILE_PATCH_ERROR", e.message, backup);
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
function doDelete(cmd, target) {
|
|
2391
|
+
if (!fs12.existsSync(target)) return fail(cmd, "FILE_NOT_FOUND", `Not found: ${target}`);
|
|
2392
|
+
const backup = tryBackup(target);
|
|
2393
|
+
try {
|
|
2394
|
+
fs12.unlinkSync(target);
|
|
2395
|
+
return {
|
|
2396
|
+
adapter: "file",
|
|
2397
|
+
action: "delete",
|
|
2398
|
+
commandId: cmd.commandId,
|
|
2399
|
+
status: "completed",
|
|
2400
|
+
target,
|
|
2401
|
+
backup: summariseBackup(backup),
|
|
2402
|
+
output: `Deleted ${target}`,
|
|
2403
|
+
exitCode: 0
|
|
2404
|
+
};
|
|
2405
|
+
} catch (e) {
|
|
2406
|
+
restoreBackup(backup);
|
|
2407
|
+
return fail(cmd, "FILE_DELETE_ERROR", e.message, backup);
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
function tryBackup(target) {
|
|
2411
|
+
try {
|
|
2412
|
+
return createBackup(target);
|
|
2413
|
+
} catch {
|
|
2414
|
+
return null;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
|
|
2418
|
+
// src/adapters/index.js
|
|
2419
|
+
var ADAPTERS = {
|
|
2420
|
+
noop: noopAdapter,
|
|
2421
|
+
shell: shellAdapter,
|
|
2422
|
+
file: fileAdapter
|
|
2423
|
+
};
|
|
2424
|
+
function getAdapter(name) {
|
|
2425
|
+
return ADAPTERS[name];
|
|
2426
|
+
}
|
|
2427
|
+
async function executeAdapter(adapterName, cmd, policy, requester) {
|
|
2428
|
+
const adapter = getAdapter(adapterName);
|
|
2429
|
+
if (!adapter) {
|
|
2430
|
+
return {
|
|
2431
|
+
adapter: adapterName,
|
|
2432
|
+
commandId: cmd.commandId,
|
|
2433
|
+
status: "error",
|
|
2434
|
+
error: `Adapter '${adapterName}' not found`,
|
|
2435
|
+
exitCode: 1
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
try {
|
|
2439
|
+
return await adapter(cmd, policy, requester);
|
|
2440
|
+
} catch (err) {
|
|
2441
|
+
return {
|
|
2442
|
+
adapter: adapterName,
|
|
2443
|
+
commandId: cmd.commandId,
|
|
2444
|
+
status: "error",
|
|
2445
|
+
error: `Adapter execution failed: ${err.message}`,
|
|
2446
|
+
exitCode: 9
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
var AVAILABLE_ADAPTERS = Object.keys(ADAPTERS);
|
|
2451
|
+
|
|
2452
|
+
// src/cli/commands/dryrun.js
|
|
2453
|
+
async function dryrunCommand(opts) {
|
|
2454
|
+
const { in: inFile } = opts;
|
|
2455
|
+
const config = opts.config || opts.policy;
|
|
2456
|
+
const pubKey = opts["pub-key"];
|
|
2457
|
+
const keysStorePath = opts["keys-store"] || path14.resolve(".lbe/config/keys.json");
|
|
2458
|
+
const policySigPath = opts["policy-sig"] || path14.resolve(".lbe/config/policy.sig.json");
|
|
2459
|
+
const policyStatePath = opts["policy-state"] || path14.resolve(".lbe/data/policy.state.json");
|
|
2460
|
+
const allowUnsignedPolicy = opts["policy-unsigned-ok"] === true || String(opts["policy-unsigned-ok"]).toLowerCase() === "true";
|
|
2461
|
+
if (!inFile) {
|
|
2462
|
+
console.error("Error: --in <file> is required");
|
|
2463
|
+
process.exit(1);
|
|
2464
|
+
}
|
|
2465
|
+
let proposal;
|
|
2466
|
+
try {
|
|
2467
|
+
const filePath = path14.resolve(inFile);
|
|
2468
|
+
const content = fs13.readFileSync(filePath, "utf-8");
|
|
2469
|
+
proposal = JSON.parse(content);
|
|
2470
|
+
} catch (error) {
|
|
2471
|
+
console.error(JSON.stringify({
|
|
2472
|
+
status: "error",
|
|
2473
|
+
error: "INVALID_PROPOSAL_FILE",
|
|
2474
|
+
message: error.message
|
|
2475
|
+
}));
|
|
2476
|
+
process.exit(5);
|
|
2477
|
+
}
|
|
2478
|
+
let policy;
|
|
2479
|
+
try {
|
|
2480
|
+
const policyPath = config || path14.resolve(".lbe/config/policy.default.json");
|
|
2481
|
+
if (!fs13.existsSync(policyPath)) {
|
|
2482
|
+
console.error(JSON.stringify({
|
|
2483
|
+
status: "error",
|
|
2484
|
+
error: "MISSING_POLICY",
|
|
2485
|
+
message: `Policy file not found: ${policyPath}`
|
|
2486
|
+
}));
|
|
2487
|
+
process.exit(1);
|
|
2488
|
+
}
|
|
2489
|
+
const policyContent = fs13.readFileSync(policyPath, "utf-8");
|
|
2490
|
+
policy = JSON.parse(policyContent);
|
|
2491
|
+
} catch (error) {
|
|
2492
|
+
console.error(JSON.stringify({
|
|
2493
|
+
status: "error",
|
|
2494
|
+
error: "INVALID_POLICY",
|
|
2495
|
+
message: error.message
|
|
2496
|
+
}));
|
|
2497
|
+
process.exit(1);
|
|
2498
|
+
}
|
|
2499
|
+
const keyStoreResult = loadKeysStore(keysStorePath);
|
|
2500
|
+
const keyStore = keyStoreResult.ok ? keyStoreResult.store : null;
|
|
2501
|
+
const policySigCheck = verifyPolicySignature({
|
|
2502
|
+
policyObj: policy,
|
|
2503
|
+
keyStore,
|
|
2504
|
+
policySigPath,
|
|
2505
|
+
allowUnsigned: allowUnsignedPolicy
|
|
2506
|
+
});
|
|
2507
|
+
if (!policySigCheck.ok) {
|
|
2508
|
+
console.error(JSON.stringify({
|
|
2509
|
+
status: "error",
|
|
2510
|
+
error: policySigCheck.reason,
|
|
2511
|
+
message: policySigCheck.message
|
|
2512
|
+
}, null, 2));
|
|
2513
|
+
process.exit(8);
|
|
2514
|
+
}
|
|
2515
|
+
const versionCheck = validateAndUpdatePolicyVersionState({
|
|
2516
|
+
policyObj: policy,
|
|
2517
|
+
statePath: policyStatePath,
|
|
2518
|
+
maxCreatedAtSkewSec: policy?.security?.maxPolicyCreatedAtSkewSec
|
|
2519
|
+
});
|
|
2520
|
+
if (!versionCheck.ok) {
|
|
2521
|
+
console.error(JSON.stringify({
|
|
2522
|
+
status: "error",
|
|
2523
|
+
error: versionCheck.reason,
|
|
2524
|
+
message: versionCheck.message
|
|
2525
|
+
}, null, 2));
|
|
2526
|
+
process.exit(8);
|
|
2527
|
+
}
|
|
2528
|
+
const nonceDb = new NonceStore(path14.resolve(".lbe/data/nonce.db.json"));
|
|
2529
|
+
await nonceDb.load();
|
|
2530
|
+
if (!keyStore && !pubKey) {
|
|
2531
|
+
console.error(JSON.stringify({
|
|
2532
|
+
status: "error",
|
|
2533
|
+
error: "MISSING_KEY_MATERIAL",
|
|
2534
|
+
message: `${keyStoreResult.message}. Provide --pub-key/--pub-key-file or create .lbe/config/keys.json`
|
|
2535
|
+
}));
|
|
2536
|
+
process.exit(1);
|
|
2537
|
+
}
|
|
2538
|
+
const validateResult = validateCommand({
|
|
2539
|
+
commandObj: proposal,
|
|
2540
|
+
pubKeyB64: pubKey,
|
|
2541
|
+
keyStore,
|
|
2542
|
+
nonceDb,
|
|
2543
|
+
policy
|
|
2544
|
+
});
|
|
2545
|
+
if (!validateResult.valid) {
|
|
2546
|
+
const output2 = {
|
|
2547
|
+
status: "invalid",
|
|
2548
|
+
commandId: proposal.commandId || "N/A",
|
|
2549
|
+
checks: validateResult.checks,
|
|
2550
|
+
errors: validateResult.errors || [],
|
|
2551
|
+
executionResult: null
|
|
2552
|
+
};
|
|
2553
|
+
console.log(JSON.stringify(output2, null, 2));
|
|
2554
|
+
if (validateResult.checks.schema === false) process.exit(5);
|
|
2555
|
+
if (validateResult.checks.signature === false) process.exit(3);
|
|
2556
|
+
if (validateResult.checks.nonce === false) process.exit(4);
|
|
2557
|
+
if (validateResult.checks.timestamp === false) process.exit(6);
|
|
2558
|
+
if (validateResult.checks.rateLimit === false) process.exit(7);
|
|
2559
|
+
if (validateResult.checks.policy === false) process.exit(2);
|
|
2560
|
+
process.exit(9);
|
|
2561
|
+
}
|
|
2562
|
+
let executionResult;
|
|
2563
|
+
try {
|
|
2564
|
+
const requesterPolicy = policy.requesters?.[proposal.requesterId];
|
|
2565
|
+
executionResult = await executeAdapter(
|
|
2566
|
+
"noop",
|
|
2567
|
+
proposal,
|
|
2568
|
+
policy,
|
|
2569
|
+
requesterPolicy
|
|
2570
|
+
);
|
|
2571
|
+
} catch (error) {
|
|
2572
|
+
executionResult = {
|
|
2573
|
+
adapter: "noop",
|
|
2574
|
+
status: "error",
|
|
2575
|
+
error: error.message
|
|
2576
|
+
};
|
|
2577
|
+
}
|
|
2578
|
+
const output = {
|
|
2579
|
+
status: "valid_simulated",
|
|
2580
|
+
commandId: proposal.commandId || "N/A",
|
|
2581
|
+
checks: validateResult.checks,
|
|
2582
|
+
risk: validateResult.risk || "UNKNOWN",
|
|
2583
|
+
executionResult: {
|
|
2584
|
+
adapter: executionResult.adapter,
|
|
2585
|
+
status: executionResult.status,
|
|
2586
|
+
output: executionResult.output || executionResult.error || "",
|
|
2587
|
+
exitCode: executionResult.exitCode || 0,
|
|
2588
|
+
note: "This is a simulation using noop adapter. No actual execution occurred."
|
|
2589
|
+
}
|
|
2590
|
+
};
|
|
2591
|
+
console.log(JSON.stringify(output, null, 2));
|
|
2592
|
+
process.exit(0);
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
// src/cli/commands/run.js
|
|
2596
|
+
import fs17 from "fs";
|
|
2597
|
+
import path19 from "path";
|
|
2598
|
+
import crypto6 from "crypto";
|
|
2599
|
+
|
|
2600
|
+
// src/core/auditLog.js
|
|
2601
|
+
import fs14 from "fs";
|
|
2602
|
+
import path15 from "path";
|
|
2603
|
+
import crypto3 from "crypto";
|
|
2604
|
+
function sha256(str) {
|
|
2605
|
+
return crypto3.createHash("sha256").update(str).digest("hex");
|
|
2606
|
+
}
|
|
2607
|
+
function getLastHash(logPath) {
|
|
2608
|
+
try {
|
|
2609
|
+
if (!fs14.existsSync(logPath)) return "GENESIS";
|
|
2610
|
+
const content = fs14.readFileSync(logPath, "utf8").trim();
|
|
2611
|
+
if (!content) return "GENESIS";
|
|
2612
|
+
const lines = content.split("\n");
|
|
2613
|
+
const lastLine = lines[lines.length - 1];
|
|
2614
|
+
try {
|
|
2615
|
+
const lastEntry = JSON.parse(lastLine);
|
|
2616
|
+
return lastEntry.hash || "GENESIS";
|
|
2617
|
+
} catch (err) {
|
|
2618
|
+
return "GENESIS";
|
|
2619
|
+
}
|
|
2620
|
+
} catch (err) {
|
|
2621
|
+
return "GENESIS";
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
function appendAudit(logPath, entry) {
|
|
2625
|
+
const dir = path15.dirname(logPath);
|
|
2626
|
+
if (!fs14.existsSync(dir)) {
|
|
2627
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
2628
|
+
}
|
|
2629
|
+
let result;
|
|
2630
|
+
withFileLock(logPath, () => {
|
|
2631
|
+
const prevHash = getLastHash(logPath);
|
|
2632
|
+
const record = {
|
|
2633
|
+
...entry,
|
|
2634
|
+
prevHash,
|
|
2635
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
2636
|
+
};
|
|
2637
|
+
delete record.hash;
|
|
2638
|
+
const recordStr = JSON.stringify(record);
|
|
2639
|
+
const hash = sha256(recordStr);
|
|
2640
|
+
const final = JSON.stringify({ ...record, hash });
|
|
2641
|
+
let existingContent = "";
|
|
2642
|
+
if (fs14.existsSync(logPath)) {
|
|
2643
|
+
existingContent = fs14.readFileSync(logPath, "utf8");
|
|
2644
|
+
}
|
|
2645
|
+
try {
|
|
2646
|
+
atomicWriteFileSync(logPath, existingContent + final + "\n", { encoding: "utf8" });
|
|
2647
|
+
} catch (err) {
|
|
2648
|
+
throw new Error(`Audit log write failed: ${err.message}`);
|
|
2649
|
+
}
|
|
2650
|
+
result = {
|
|
2651
|
+
success: true,
|
|
2652
|
+
hash,
|
|
2653
|
+
prevHash,
|
|
2654
|
+
message: "Audit entry appended"
|
|
2655
|
+
};
|
|
2656
|
+
});
|
|
2657
|
+
return result;
|
|
2658
|
+
}
|
|
2659
|
+
function verifyAuditLogIntegrity(logPath, options = {}) {
|
|
2660
|
+
const failFast = options.failFast !== false;
|
|
2661
|
+
const maxEntries = Number.isFinite(options.maxEntries) && options.maxEntries > 0 ? Math.floor(options.maxEntries) : null;
|
|
2662
|
+
const response = {
|
|
2663
|
+
ok: true,
|
|
2664
|
+
file: path15.resolve(logPath),
|
|
2665
|
+
entries: 0,
|
|
2666
|
+
valid: true,
|
|
2667
|
+
firstInvalidIndex: null,
|
|
2668
|
+
reason: null,
|
|
2669
|
+
errors: [],
|
|
2670
|
+
message: "Audit log verified"
|
|
2671
|
+
};
|
|
2672
|
+
try {
|
|
2673
|
+
if (!fs14.existsSync(logPath)) {
|
|
2674
|
+
response.message = "Audit log file not found (treated as empty)";
|
|
2675
|
+
return response;
|
|
2676
|
+
}
|
|
2677
|
+
const raw = fs14.readFileSync(logPath, "utf8").trim();
|
|
2678
|
+
if (!raw) {
|
|
2679
|
+
response.message = "Empty audit log";
|
|
2680
|
+
return response;
|
|
2681
|
+
}
|
|
2682
|
+
const allLines = raw.split("\n");
|
|
2683
|
+
const lines = maxEntries ? allLines.slice(0, maxEntries) : allLines;
|
|
2684
|
+
response.entries = lines.length;
|
|
2685
|
+
let expectedPrevHash = "GENESIS";
|
|
2686
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2687
|
+
let entry;
|
|
2688
|
+
try {
|
|
2689
|
+
entry = JSON.parse(lines[i]);
|
|
2690
|
+
} catch {
|
|
2691
|
+
const errObj = {
|
|
2692
|
+
index: i,
|
|
2693
|
+
reason: "INVALID_JSON_LINE",
|
|
2694
|
+
message: `Line ${i} is not valid JSON`
|
|
2695
|
+
};
|
|
2696
|
+
response.valid = false;
|
|
2697
|
+
response.ok = false;
|
|
2698
|
+
response.firstInvalidIndex ??= i;
|
|
2699
|
+
response.reason ??= errObj.reason;
|
|
2700
|
+
response.errors.push(errObj);
|
|
2701
|
+
if (failFast) break;
|
|
2702
|
+
continue;
|
|
2703
|
+
}
|
|
2704
|
+
if (entry.prevHash !== expectedPrevHash) {
|
|
2705
|
+
const errObj = {
|
|
2706
|
+
index: i,
|
|
2707
|
+
reason: "PREV_HASH_MISMATCH",
|
|
2708
|
+
message: `Expected prevHash '${expectedPrevHash}', got '${entry.prevHash}'`
|
|
2709
|
+
};
|
|
2710
|
+
response.valid = false;
|
|
2711
|
+
response.ok = false;
|
|
2712
|
+
response.firstInvalidIndex ??= i;
|
|
2713
|
+
response.reason ??= errObj.reason;
|
|
2714
|
+
response.errors.push(errObj);
|
|
2715
|
+
if (failFast) break;
|
|
2716
|
+
}
|
|
2717
|
+
const recordCopy = { ...entry };
|
|
2718
|
+
const recordHash = recordCopy.hash;
|
|
2719
|
+
delete recordCopy.hash;
|
|
2720
|
+
const expectedHash = sha256(JSON.stringify(recordCopy));
|
|
2721
|
+
if (recordHash !== expectedHash) {
|
|
2722
|
+
const errObj = {
|
|
2723
|
+
index: i,
|
|
2724
|
+
reason: "HASH_MISMATCH",
|
|
2725
|
+
message: `Expected hash '${expectedHash}', got '${recordHash}'`
|
|
2726
|
+
};
|
|
2727
|
+
response.valid = false;
|
|
2728
|
+
response.ok = false;
|
|
2729
|
+
response.firstInvalidIndex ??= i;
|
|
2730
|
+
response.reason ??= errObj.reason;
|
|
2731
|
+
response.errors.push(errObj);
|
|
2732
|
+
if (failFast) break;
|
|
2733
|
+
}
|
|
2734
|
+
expectedPrevHash = recordHash;
|
|
2735
|
+
}
|
|
2736
|
+
response.message = response.valid ? `Audit log verified: ${response.entries} entries` : `Audit log integrity failed at index ${response.firstInvalidIndex}`;
|
|
2737
|
+
return response;
|
|
2738
|
+
} catch (err) {
|
|
2739
|
+
return {
|
|
2740
|
+
ok: false,
|
|
2741
|
+
file: path15.resolve(logPath),
|
|
2742
|
+
entries: 0,
|
|
2743
|
+
valid: false,
|
|
2744
|
+
firstInvalidIndex: null,
|
|
2745
|
+
reason: "AUDIT_VERIFY_ERROR",
|
|
2746
|
+
errors: [{ index: null, reason: "AUDIT_VERIFY_ERROR", message: err.message }],
|
|
2747
|
+
message: `Integrity check failed: ${err.message}`
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
// src/core/requestRateLimiter.js
|
|
2753
|
+
import fs15 from "fs";
|
|
2754
|
+
import path16 from "path";
|
|
2755
|
+
var RequestRateLimiter = class {
|
|
2756
|
+
constructor(dbPath) {
|
|
2757
|
+
this.dbPath = dbPath;
|
|
2758
|
+
this.db = { entries: [] };
|
|
2759
|
+
}
|
|
2760
|
+
async load() {
|
|
2761
|
+
try {
|
|
2762
|
+
if (!fs15.existsSync(this.dbPath)) {
|
|
2763
|
+
this.db = { entries: [] };
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
const data = fs15.readFileSync(this.dbPath, "utf8");
|
|
2767
|
+
this.db = JSON.parse(data);
|
|
2768
|
+
if (!Array.isArray(this.db.entries)) {
|
|
2769
|
+
this.db = { entries: [] };
|
|
2770
|
+
}
|
|
2771
|
+
} catch {
|
|
2772
|
+
this.db = { entries: [] };
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
async save() {
|
|
2776
|
+
const dir = path16.dirname(this.dbPath);
|
|
2777
|
+
if (!fs15.existsSync(dir)) {
|
|
2778
|
+
fs15.mkdirSync(dir, { recursive: true });
|
|
2779
|
+
}
|
|
2780
|
+
atomicWriteFileSync(this.dbPath, JSON.stringify(this.db, null, 2), { encoding: "utf8" });
|
|
2781
|
+
}
|
|
2782
|
+
checkAndRecord({ requesterId, nowSec, windowSec, maxRequests }) {
|
|
2783
|
+
const now = Number.isFinite(nowSec) ? nowSec : Math.floor(Date.now() / 1e3);
|
|
2784
|
+
const window = Number.isFinite(windowSec) && windowSec > 0 ? windowSec : 60;
|
|
2785
|
+
const limit = Number.isFinite(maxRequests) && maxRequests > 0 ? maxRequests : 30;
|
|
2786
|
+
const cutoff = now - window;
|
|
2787
|
+
this.db.entries = this.db.entries.filter((entry) => entry.timestamp >= cutoff);
|
|
2788
|
+
const requesterEntries = this.db.entries.filter((entry) => entry.requesterId === requesterId);
|
|
2789
|
+
if (requesterEntries.length >= limit) {
|
|
2790
|
+
const oldest = requesterEntries.sort((a, b) => a.timestamp - b.timestamp)[0];
|
|
2791
|
+
const retryAfterSec = Math.max(1, window - (now - oldest.timestamp));
|
|
2792
|
+
return {
|
|
2793
|
+
ok: false,
|
|
2794
|
+
reason: "RATE_LIMIT_EXCEEDED",
|
|
2795
|
+
message: `Rate limit exceeded for '${requesterId}' (${limit}/${window}s)`,
|
|
2796
|
+
retryAfterSec
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
this.db.entries.push({ requesterId, timestamp: now });
|
|
2800
|
+
return {
|
|
2801
|
+
ok: true,
|
|
2802
|
+
reason: null,
|
|
2803
|
+
message: "Rate limit check passed",
|
|
2804
|
+
retryAfterSec: 0
|
|
2805
|
+
};
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2808
|
+
|
|
2809
|
+
// src/core/approval-token.js
|
|
2810
|
+
import crypto4 from "crypto";
|
|
2811
|
+
|
|
2812
|
+
// src/core/checkpoint-store.js
|
|
2813
|
+
import path17 from "path";
|
|
2814
|
+
var CheckpointStore = class {
|
|
2815
|
+
constructor(dbPath) {
|
|
2816
|
+
this.dbPath = dbPath || path17.resolve(".lbe/data/checkpoints.db.json");
|
|
2817
|
+
this.store = { checkpoints: {}, tokens: {} };
|
|
2818
|
+
this._load();
|
|
2819
|
+
}
|
|
2820
|
+
_load() {
|
|
2821
|
+
const data = readJSONSafe(this.dbPath);
|
|
2822
|
+
if (data) {
|
|
2823
|
+
this.store = data;
|
|
2824
|
+
this.store.checkpoints = this.store.checkpoints || {};
|
|
2825
|
+
this.store.tokens = this.store.tokens || {};
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
_save() {
|
|
2829
|
+
const jsonStr = JSON.stringify(this.store, null, 2);
|
|
2830
|
+
atomicWriteFileSync(this.dbPath, jsonStr, { encoding: "utf8" });
|
|
2831
|
+
}
|
|
2832
|
+
// --- Checkpoint Management ---
|
|
2833
|
+
saveCheckpoint(jobId, state) {
|
|
2834
|
+
this.store.checkpoints[jobId] = {
|
|
2835
|
+
jobId,
|
|
2836
|
+
...state,
|
|
2837
|
+
updatedAt: Date.now()
|
|
2838
|
+
};
|
|
2839
|
+
this._save();
|
|
2840
|
+
}
|
|
2841
|
+
getCheckpoint(jobId) {
|
|
2842
|
+
return this.store.checkpoints[jobId] || null;
|
|
2843
|
+
}
|
|
2844
|
+
getAllCheckpoints() {
|
|
2845
|
+
return Object.values(this.store.checkpoints);
|
|
2846
|
+
}
|
|
2847
|
+
removeCheckpoint(jobId) {
|
|
2848
|
+
if (this.store.checkpoints[jobId]) {
|
|
2849
|
+
delete this.store.checkpoints[jobId];
|
|
2850
|
+
this._save();
|
|
2851
|
+
return true;
|
|
2852
|
+
}
|
|
2853
|
+
return false;
|
|
2854
|
+
}
|
|
2855
|
+
// --- Approval Token Management ---
|
|
2856
|
+
saveToken(tokenId, tokenData) {
|
|
2857
|
+
this.store.tokens[tokenId] = {
|
|
2858
|
+
tokenId,
|
|
2859
|
+
...tokenData,
|
|
2860
|
+
createdAt: Date.now()
|
|
2861
|
+
};
|
|
2862
|
+
this._save();
|
|
2863
|
+
}
|
|
2864
|
+
getToken(tokenId) {
|
|
2865
|
+
return this.store.tokens[tokenId] || null;
|
|
2866
|
+
}
|
|
2867
|
+
getAllTokens() {
|
|
2868
|
+
return Object.values(this.store.tokens);
|
|
2869
|
+
}
|
|
2870
|
+
removeToken(tokenId) {
|
|
2871
|
+
if (this.store.tokens[tokenId]) {
|
|
2872
|
+
delete this.store.tokens[tokenId];
|
|
2873
|
+
this._save();
|
|
2874
|
+
return true;
|
|
2875
|
+
}
|
|
2876
|
+
return false;
|
|
2877
|
+
}
|
|
2878
|
+
};
|
|
2879
|
+
var instance = null;
|
|
2880
|
+
function getCheckpointStore(dbPath) {
|
|
2881
|
+
if (!instance) {
|
|
2882
|
+
instance = new CheckpointStore(dbPath);
|
|
2883
|
+
}
|
|
2884
|
+
return instance;
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// src/core/approval-token.js
|
|
2888
|
+
var ApprovalManager = class {
|
|
2889
|
+
constructor(storePath) {
|
|
2890
|
+
this.store = getCheckpointStore(storePath);
|
|
2891
|
+
this._pendingResolvers = /* @__PURE__ */ new Map();
|
|
2892
|
+
}
|
|
2893
|
+
/**
|
|
2894
|
+
* Create a new approval token and persist it
|
|
2895
|
+
* @param {string} jobId The workflow or job ID awaiting approval
|
|
2896
|
+
* @param {object} context Contextual data for the approver
|
|
2897
|
+
*/
|
|
2898
|
+
createToken(jobId, context = {}) {
|
|
2899
|
+
const tokenId = crypto4.randomBytes(16).toString("hex");
|
|
2900
|
+
const tokenData = {
|
|
2901
|
+
jobId,
|
|
2902
|
+
context,
|
|
2903
|
+
status: "pending",
|
|
2904
|
+
expiresAt: Date.now() + 24 * 60 * 60 * 1e3
|
|
2905
|
+
// 24 hours
|
|
2906
|
+
};
|
|
2907
|
+
this.store.saveToken(tokenId, tokenData);
|
|
2908
|
+
return tokenId;
|
|
2909
|
+
}
|
|
2910
|
+
/**
|
|
2911
|
+
* Rehydrate an approval wait into memory.
|
|
2912
|
+
* This allows a resumed workflow to await a previously created token.
|
|
2913
|
+
* @param {string} tokenId The token to await
|
|
2914
|
+
* @returns {Promise} Resolves when approved, rejects when denied or expired
|
|
2915
|
+
*/
|
|
2916
|
+
awaitApproval(tokenId) {
|
|
2917
|
+
const token = this.store.getToken(tokenId);
|
|
2918
|
+
if (!token) {
|
|
2919
|
+
return Promise.reject(new Error(`Approval token ${tokenId} not found`));
|
|
2920
|
+
}
|
|
2921
|
+
if (token.status !== "pending") {
|
|
2922
|
+
return Promise.reject(new Error(`Approval token ${tokenId} is no longer pending (status: ${token.status})`));
|
|
2923
|
+
}
|
|
2924
|
+
if (Date.now() > token.expiresAt) {
|
|
2925
|
+
this.store.removeToken(tokenId);
|
|
2926
|
+
return Promise.reject(new Error(`Approval token ${tokenId} expired`));
|
|
2927
|
+
}
|
|
2928
|
+
return new Promise((resolve, reject) => {
|
|
2929
|
+
this._pendingResolvers.set(tokenId, { resolve, reject });
|
|
2930
|
+
});
|
|
2931
|
+
}
|
|
2932
|
+
/**
|
|
2933
|
+
* Approve a pending token
|
|
2934
|
+
*/
|
|
2935
|
+
approve(tokenId, approverData = {}) {
|
|
2936
|
+
const token = this.store.getToken(tokenId);
|
|
2937
|
+
if (!token) throw new Error("Token not found");
|
|
2938
|
+
if (token.status !== "pending") throw new Error("Token not pending");
|
|
2939
|
+
this.store.saveToken(tokenId, { ...token, status: "approved", approverData, resolvedAt: Date.now() });
|
|
2940
|
+
const resolver = this._pendingResolvers.get(tokenId);
|
|
2941
|
+
if (resolver) {
|
|
2942
|
+
resolver.resolve({ approved: true, approverData });
|
|
2943
|
+
this._pendingResolvers.delete(tokenId);
|
|
2944
|
+
}
|
|
2945
|
+
return true;
|
|
2946
|
+
}
|
|
2947
|
+
/**
|
|
2948
|
+
* Deny a pending token
|
|
2949
|
+
*/
|
|
2950
|
+
deny(tokenId, reason = "Manually denied") {
|
|
2951
|
+
const token = this.store.getToken(tokenId);
|
|
2952
|
+
if (!token) throw new Error("Token not found");
|
|
2953
|
+
if (token.status !== "pending") throw new Error("Token not pending");
|
|
2954
|
+
this.store.saveToken(tokenId, { ...token, status: "denied", reason, resolvedAt: Date.now() });
|
|
2955
|
+
const resolver = this._pendingResolvers.get(tokenId);
|
|
2956
|
+
if (resolver) {
|
|
2957
|
+
resolver.reject(new Error(`Approval denied: ${reason}`));
|
|
2958
|
+
this._pendingResolvers.delete(tokenId);
|
|
2959
|
+
}
|
|
2960
|
+
return true;
|
|
2961
|
+
}
|
|
2962
|
+
};
|
|
2963
|
+
var instance2 = null;
|
|
2964
|
+
function getApprovalManager(storePath) {
|
|
2965
|
+
if (!instance2) {
|
|
2966
|
+
instance2 = new ApprovalManager(storePath);
|
|
2967
|
+
}
|
|
2968
|
+
return instance2;
|
|
2969
|
+
}
|
|
2970
|
+
|
|
2971
|
+
// src/core/localPolicy.js
|
|
2972
|
+
import fs16 from "fs";
|
|
2973
|
+
import path18 from "path";
|
|
2974
|
+
import crypto5 from "crypto";
|
|
2975
|
+
var POLICY_FILE = ".lbe/policy.json";
|
|
2976
|
+
var AUDIT_FILE = ".lbe/audit.jsonl";
|
|
2977
|
+
function glob(pattern) {
|
|
2978
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
2979
|
+
return new RegExp("^" + escaped.replace(/\*\*\//g, "(?:.*/)?").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$");
|
|
2980
|
+
}
|
|
2981
|
+
function relative(root, value) {
|
|
2982
|
+
const rel = path18.relative(root, path18.resolve(value));
|
|
2983
|
+
return rel.split(path18.sep).join("/");
|
|
2984
|
+
}
|
|
2985
|
+
function localPolicyPaths(rootDir) {
|
|
2986
|
+
const root = path18.resolve(rootDir || process.cwd());
|
|
2987
|
+
return { root, policyPath: path18.join(root, POLICY_FILE), auditPath: path18.join(root, AUDIT_FILE) };
|
|
2988
|
+
}
|
|
2989
|
+
function loadLocalPolicy(rootDir, mode = "observe") {
|
|
2990
|
+
const paths = localPolicyPaths(rootDir);
|
|
2991
|
+
if (!fs16.existsSync(paths.policyPath)) {
|
|
2992
|
+
return { ...paths, policy: { version: 1, mode, workspace: paths.root, rules: [] } };
|
|
2993
|
+
}
|
|
2994
|
+
const policy = JSON.parse(fs16.readFileSync(paths.policyPath, "utf8"));
|
|
2995
|
+
if (policy?.version !== 1 || !["observe", "enforce"].includes(policy.mode) || !Array.isArray(policy.rules)) {
|
|
2996
|
+
throw new Error(`Invalid ${POLICY_FILE}`);
|
|
2997
|
+
}
|
|
2998
|
+
return { ...paths, policy };
|
|
2999
|
+
}
|
|
3000
|
+
function writeLocalPolicy(rootDir, policy) {
|
|
3001
|
+
const { policyPath, root } = localPolicyPaths(rootDir);
|
|
3002
|
+
const next = { ...policy, version: 1, workspace: root, rules: Array.isArray(policy.rules) ? policy.rules : [] };
|
|
3003
|
+
atomicWriteFileSync(policyPath, JSON.stringify(next, null, 2) + "\n", { encoding: "utf8" });
|
|
3004
|
+
return next;
|
|
3005
|
+
}
|
|
3006
|
+
function addLocalPolicyRule(rootDir, rule, mode) {
|
|
3007
|
+
if (!rule || !["allow", "deny"].includes(rule.effect) || !["path", "command"].includes(rule.type) || typeof rule.pattern !== "string" || !rule.pattern || typeof rule.from !== "string" || !rule.from) {
|
|
3008
|
+
throw new Error("Rule requires effect, type, pattern, and from");
|
|
3009
|
+
}
|
|
3010
|
+
const loaded = loadLocalPolicy(rootDir, mode);
|
|
3011
|
+
const entry = {
|
|
3012
|
+
id: rule.id || crypto5.randomUUID(),
|
|
3013
|
+
effect: rule.effect,
|
|
3014
|
+
type: rule.type,
|
|
3015
|
+
pattern: rule.pattern,
|
|
3016
|
+
from: rule.from,
|
|
3017
|
+
at: rule.at || (/* @__PURE__ */ new Date()).toISOString()
|
|
3018
|
+
};
|
|
3019
|
+
writeLocalPolicy(loaded.root, { ...loaded.policy, mode: mode || loaded.policy.mode, rules: [...loaded.policy.rules, entry] });
|
|
3020
|
+
return { id: entry.id, added: true, rule: entry };
|
|
3021
|
+
}
|
|
3022
|
+
function evaluateLocalPolicy(policy, rootDir, { target, command } = {}) {
|
|
3023
|
+
const root = path18.resolve(rootDir);
|
|
3024
|
+
const candidates = [];
|
|
3025
|
+
if (target) candidates.push({ type: "path", value: relative(root, target) });
|
|
3026
|
+
if (command) candidates.push({ type: "command", value: command });
|
|
3027
|
+
const matched = policy.rules.filter((rule) => candidates.some((c) => c.type === rule.type && glob(rule.pattern).test(c.value)));
|
|
3028
|
+
const denied = matched.filter((rule) => rule.effect === "deny");
|
|
3029
|
+
return { allowed: denied.length === 0, matched, winningRules: denied.length ? denied : matched.filter((r) => r.effect === "allow"), reason: denied.length ? "LOCAL_POLICY_DENY" : null };
|
|
3030
|
+
}
|
|
3031
|
+
function auditLocalPolicy(rootDir, entry) {
|
|
3032
|
+
const { auditPath } = localPolicyPaths(rootDir);
|
|
3033
|
+
appendAudit(auditPath, { kind: "local_policy", timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
// src/cli/commands/run.js
|
|
3037
|
+
function sha2562(obj) {
|
|
3038
|
+
return crypto6.createHash("sha256").update(JSON.stringify(obj)).digest("hex");
|
|
3039
|
+
}
|
|
3040
|
+
async function runCommand(opts) {
|
|
3041
|
+
const { in: inFile } = opts;
|
|
3042
|
+
const config = opts.config || opts.policy;
|
|
3043
|
+
const pubKey = opts["pub-key"];
|
|
3044
|
+
const keysStorePath = opts["keys-store"] || path19.resolve(".lbe/config/keys.json");
|
|
3045
|
+
const policySigPath = opts["policy-sig"] || path19.resolve(".lbe/config/policy.sig.json");
|
|
3046
|
+
const policyStatePath = opts["policy-state"] || path19.resolve(".lbe/data/policy.state.json");
|
|
3047
|
+
const allowUnsignedPolicy = opts["policy-unsigned-ok"] === true || String(opts["policy-unsigned-ok"]).toLowerCase() === "true";
|
|
3048
|
+
if (!inFile) {
|
|
3049
|
+
console.error("Error: --in <file> is required");
|
|
3050
|
+
process.exit(1);
|
|
3051
|
+
}
|
|
3052
|
+
let proposal;
|
|
3053
|
+
try {
|
|
3054
|
+
const filePath = path19.resolve(inFile);
|
|
3055
|
+
const content = fs17.readFileSync(filePath, "utf-8");
|
|
3056
|
+
proposal = JSON.parse(content);
|
|
3057
|
+
} catch (error) {
|
|
3058
|
+
console.error(JSON.stringify({
|
|
3059
|
+
status: "error",
|
|
3060
|
+
error: "INVALID_PROPOSAL_FILE",
|
|
3061
|
+
message: error.message
|
|
3062
|
+
}));
|
|
3063
|
+
process.exit(5);
|
|
3064
|
+
}
|
|
3065
|
+
let policy;
|
|
3066
|
+
try {
|
|
3067
|
+
const policyPath = config || path19.resolve(".lbe/config/policy.default.json");
|
|
3068
|
+
if (!fs17.existsSync(policyPath)) {
|
|
3069
|
+
console.error(JSON.stringify({
|
|
3070
|
+
status: "error",
|
|
3071
|
+
error: "MISSING_POLICY",
|
|
3072
|
+
message: `Policy file not found: ${policyPath}`
|
|
3073
|
+
}));
|
|
3074
|
+
process.exit(1);
|
|
3075
|
+
}
|
|
3076
|
+
const policyContent = fs17.readFileSync(policyPath, "utf-8");
|
|
3077
|
+
policy = JSON.parse(policyContent);
|
|
3078
|
+
} catch (error) {
|
|
3079
|
+
console.error(JSON.stringify({
|
|
3080
|
+
status: "error",
|
|
3081
|
+
error: "INVALID_POLICY",
|
|
3082
|
+
message: error.message
|
|
3083
|
+
}));
|
|
3084
|
+
process.exit(1);
|
|
3085
|
+
}
|
|
3086
|
+
const rootDir = process.cwd();
|
|
3087
|
+
let localPolicy;
|
|
3088
|
+
try {
|
|
3089
|
+
localPolicy = loadLocalPolicy(rootDir);
|
|
3090
|
+
} catch (error) {
|
|
3091
|
+
console.error(JSON.stringify({ status: "error", error: "LOCAL_POLICY_INVALID", message: error.message }));
|
|
3092
|
+
process.exit(1);
|
|
3093
|
+
}
|
|
3094
|
+
const localDecision = evaluateLocalPolicy(localPolicy.policy, rootDir, {
|
|
3095
|
+
target: proposal.payload?.target,
|
|
3096
|
+
command: proposal.payload?.cmd
|
|
3097
|
+
});
|
|
3098
|
+
const localBlocked = localPolicy.policy.mode === "enforce" && !localDecision.allowed;
|
|
3099
|
+
auditLocalPolicy(rootDir, {
|
|
3100
|
+
commandId: proposal.commandId || "N/A",
|
|
3101
|
+
requesterId: proposal.requesterId || "unknown",
|
|
3102
|
+
mode: localPolicy.policy.mode,
|
|
3103
|
+
decision: localBlocked ? "deny" : "allow",
|
|
3104
|
+
wouldDeny: !localDecision.allowed,
|
|
3105
|
+
ruleIds: localDecision.winningRules.map((rule) => rule.id)
|
|
3106
|
+
});
|
|
3107
|
+
if (localBlocked) {
|
|
3108
|
+
console.error(JSON.stringify({ status: "blocked", error: "LOCAL_POLICY_DENY", ruleIds: localDecision.winningRules.map((rule) => rule.id) }, null, 2));
|
|
3109
|
+
process.exit(2);
|
|
3110
|
+
}
|
|
3111
|
+
const keyStoreResult = loadKeysStore(keysStorePath);
|
|
3112
|
+
const keyStore = keyStoreResult.ok ? keyStoreResult.store : null;
|
|
3113
|
+
const policySigCheck = verifyPolicySignature({
|
|
3114
|
+
policyObj: policy,
|
|
3115
|
+
keyStore,
|
|
3116
|
+
policySigPath,
|
|
3117
|
+
allowUnsigned: allowUnsignedPolicy
|
|
3118
|
+
});
|
|
3119
|
+
if (!policySigCheck.ok) {
|
|
3120
|
+
console.error(JSON.stringify({
|
|
3121
|
+
status: "error",
|
|
3122
|
+
error: policySigCheck.reason,
|
|
3123
|
+
message: policySigCheck.message
|
|
3124
|
+
}, null, 2));
|
|
3125
|
+
process.exit(8);
|
|
3126
|
+
}
|
|
3127
|
+
const versionCheck = validateAndUpdatePolicyVersionState({
|
|
3128
|
+
policyObj: policy,
|
|
3129
|
+
statePath: policyStatePath,
|
|
3130
|
+
maxCreatedAtSkewSec: policy?.security?.maxPolicyCreatedAtSkewSec
|
|
3131
|
+
});
|
|
3132
|
+
if (!versionCheck.ok) {
|
|
3133
|
+
console.error(JSON.stringify({
|
|
3134
|
+
status: "error",
|
|
3135
|
+
error: versionCheck.reason,
|
|
3136
|
+
message: versionCheck.message
|
|
3137
|
+
}, null, 2));
|
|
3138
|
+
process.exit(8);
|
|
3139
|
+
}
|
|
3140
|
+
const nonceDb = new NonceStore(path19.resolve(".lbe/data/nonce.db.json"));
|
|
3141
|
+
await nonceDb.load();
|
|
3142
|
+
if (!keyStore && !pubKey) {
|
|
3143
|
+
console.error(JSON.stringify({
|
|
3144
|
+
status: "error",
|
|
3145
|
+
error: "MISSING_KEY_MATERIAL",
|
|
3146
|
+
message: `${keyStoreResult.message}. Provide --pub-key/--pub-key-file or create .lbe/config/keys.json`
|
|
3147
|
+
}));
|
|
3148
|
+
process.exit(1);
|
|
3149
|
+
}
|
|
3150
|
+
const rateLimiter = new RequestRateLimiter(path19.resolve(".lbe/data/rate-limit.db.json"));
|
|
3151
|
+
await rateLimiter.load();
|
|
3152
|
+
const validateResult = validateCommand({
|
|
3153
|
+
commandObj: proposal,
|
|
3154
|
+
pubKeyB64: pubKey,
|
|
3155
|
+
keyStore,
|
|
3156
|
+
nonceDb,
|
|
3157
|
+
policy,
|
|
3158
|
+
rateLimiter
|
|
3159
|
+
});
|
|
3160
|
+
if (!validateResult.valid) {
|
|
3161
|
+
try {
|
|
3162
|
+
await nonceDb.save();
|
|
3163
|
+
await rateLimiter.save();
|
|
3164
|
+
} catch {
|
|
3165
|
+
}
|
|
3166
|
+
const output2 = {
|
|
3167
|
+
status: "invalid",
|
|
3168
|
+
commandId: proposal.commandId || "N/A",
|
|
3169
|
+
checks: validateResult.checks,
|
|
3170
|
+
errors: validateResult.errors || [],
|
|
3171
|
+
executionResult: null
|
|
3172
|
+
};
|
|
3173
|
+
console.error(JSON.stringify(output2, null, 2));
|
|
3174
|
+
const auditPath2 = path19.resolve(".lbe/data/audit.log.jsonl");
|
|
3175
|
+
try {
|
|
3176
|
+
appendAudit(auditPath2, {
|
|
3177
|
+
commandId: proposal.commandId || "N/A",
|
|
3178
|
+
status: "rejected",
|
|
3179
|
+
requesterId: proposal.requesterId || "unknown",
|
|
3180
|
+
payloadHash: sha2562(proposal),
|
|
3181
|
+
reason: validateResult.checks,
|
|
3182
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3183
|
+
});
|
|
3184
|
+
} catch (auditErr) {
|
|
3185
|
+
console.error(JSON.stringify({
|
|
3186
|
+
status: "error",
|
|
3187
|
+
error: "AUDIT_WRITE_FAILED",
|
|
3188
|
+
message: auditErr.message
|
|
3189
|
+
}));
|
|
3190
|
+
process.exit(10);
|
|
3191
|
+
}
|
|
3192
|
+
if (validateResult.checks.schema === false) process.exit(5);
|
|
3193
|
+
if (validateResult.checks.signature === false) process.exit(3);
|
|
3194
|
+
if (validateResult.checks.nonce === false) process.exit(4);
|
|
3195
|
+
if (validateResult.checks.timestamp === false) process.exit(6);
|
|
3196
|
+
if (validateResult.checks.rateLimit === false) process.exit(7);
|
|
3197
|
+
if (validateResult.checks.policy === false) process.exit(2);
|
|
3198
|
+
process.exit(9);
|
|
3199
|
+
}
|
|
3200
|
+
const risk = validateResult.risk || "LOW";
|
|
3201
|
+
const adapterName = proposal.payload.adapter || "shell";
|
|
3202
|
+
const requesterPolicy = policy.requesters?.[proposal.requesterId];
|
|
3203
|
+
const approvalRule = requesterPolicy?.requireApproval;
|
|
3204
|
+
const approvalRequired = approvalRule === true || Array.isArray(approvalRule) && (approvalRule.includes(risk) || approvalRule.includes("*") || ["HIGH", "CRITICAL"].includes(risk) && approvalRule.includes("HIGH+"));
|
|
3205
|
+
if (approvalRequired) {
|
|
3206
|
+
const mgr = getApprovalManager();
|
|
3207
|
+
const tokenId = mgr.createToken(proposal.commandId, {
|
|
3208
|
+
requesterId: proposal.requesterId,
|
|
3209
|
+
adapter: adapterName,
|
|
3210
|
+
risk
|
|
3211
|
+
});
|
|
3212
|
+
await nonceDb.save().catch(() => {
|
|
3213
|
+
});
|
|
3214
|
+
await rateLimiter.save().catch(() => {
|
|
3215
|
+
});
|
|
3216
|
+
console.log(JSON.stringify({
|
|
3217
|
+
status: "approval_required",
|
|
3218
|
+
commandId: proposal.commandId || "N/A",
|
|
3219
|
+
risk,
|
|
3220
|
+
approvalToken: tokenId,
|
|
3221
|
+
message: `${risk} risk operation requires operator approval. Approve with: lbe approve --token ${tokenId}`
|
|
3222
|
+
}, null, 2));
|
|
3223
|
+
process.exit(11);
|
|
3224
|
+
}
|
|
3225
|
+
let backup = null;
|
|
3226
|
+
const shouldBackup = opts.backup === true || adapterName === "file";
|
|
3227
|
+
if (shouldBackup && proposal.payload.target) {
|
|
3228
|
+
try {
|
|
3229
|
+
backup = createBackup(path19.resolve(proposal.payload.target));
|
|
3230
|
+
} catch {
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
let executionResult;
|
|
3234
|
+
try {
|
|
3235
|
+
executionResult = await executeAdapter(adapterName, proposal, policy, requesterPolicy);
|
|
3236
|
+
} catch (error) {
|
|
3237
|
+
executionResult = {
|
|
3238
|
+
adapter: adapterName,
|
|
3239
|
+
status: "error",
|
|
3240
|
+
error: error.message,
|
|
3241
|
+
exitCode: 1
|
|
3242
|
+
};
|
|
3243
|
+
}
|
|
3244
|
+
const executionFailed = executionResult.status === "error" || executionResult.exitCode !== 0;
|
|
3245
|
+
let rollbackResult = null;
|
|
3246
|
+
if (executionFailed && backup && opts["rollback-on-failure"] !== false) {
|
|
3247
|
+
try {
|
|
3248
|
+
rollbackResult = restoreBackup(backup);
|
|
3249
|
+
} catch (e) {
|
|
3250
|
+
rollbackResult = { restored: false, error: e.message };
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
let postValidation = null;
|
|
3254
|
+
if (!executionFailed && proposal.payload.target) {
|
|
3255
|
+
const writeActions = ["write", "patch"];
|
|
3256
|
+
if (writeActions.includes(proposal.payload.action)) {
|
|
3257
|
+
const exists2 = fs17.existsSync(path19.resolve(proposal.payload.target));
|
|
3258
|
+
postValidation = { ok: exists2, check: "target_exists", target: proposal.payload.target };
|
|
3259
|
+
if (!exists2 && backup) {
|
|
3260
|
+
rollbackResult = restoreBackup(backup);
|
|
3261
|
+
executionResult.status = "error";
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
const auditPath = path19.resolve(".lbe/data/audit.log.jsonl");
|
|
3266
|
+
try {
|
|
3267
|
+
appendAudit(auditPath, {
|
|
3268
|
+
commandId: proposal.commandId || "N/A",
|
|
3269
|
+
status: rollbackResult?.restored ? "rolled_back" : executionResult.status || "completed",
|
|
3270
|
+
requesterId: proposal.requesterId || "unknown",
|
|
3271
|
+
payloadHash: sha2562(proposal),
|
|
3272
|
+
executionHash: sha2562(executionResult),
|
|
3273
|
+
adapter: executionResult.adapter,
|
|
3274
|
+
riskLevel: risk,
|
|
3275
|
+
exitCode: executionResult.exitCode || 0,
|
|
3276
|
+
rolledBack: rollbackResult?.restored || false,
|
|
3277
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
3278
|
+
});
|
|
3279
|
+
} catch (auditErr) {
|
|
3280
|
+
console.error(JSON.stringify({
|
|
3281
|
+
status: "error",
|
|
3282
|
+
error: "AUDIT_WRITE_FAILED",
|
|
3283
|
+
message: auditErr.message
|
|
3284
|
+
}));
|
|
3285
|
+
process.exit(10);
|
|
3286
|
+
}
|
|
3287
|
+
await nonceDb.save();
|
|
3288
|
+
await rateLimiter.save();
|
|
3289
|
+
const output = {
|
|
3290
|
+
status: executionFailed || postValidation && !postValidation.ok ? "failed" : "executed",
|
|
3291
|
+
commandId: proposal.commandId || "N/A",
|
|
3292
|
+
risk,
|
|
3293
|
+
checks: validateResult.checks,
|
|
3294
|
+
executionResult: {
|
|
3295
|
+
adapter: executionResult.adapter,
|
|
3296
|
+
status: executionResult.status || "completed",
|
|
3297
|
+
output: executionResult.output || executionResult.error || "",
|
|
3298
|
+
exitCode: executionResult.exitCode || 0
|
|
3299
|
+
},
|
|
3300
|
+
backup: backup ? { path: backup.backupPath, existed: backup.existed, hash: backup.hash } : null,
|
|
3301
|
+
rollback: rollbackResult,
|
|
3302
|
+
postValidation
|
|
3303
|
+
};
|
|
3304
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3305
|
+
process.exit(executionResult.exitCode || 0);
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
// src/cli/commands/auditVerify.js
|
|
3309
|
+
import path20 from "path";
|
|
3310
|
+
function toBoolean(value, defaultValue) {
|
|
3311
|
+
if (value === void 0) return defaultValue;
|
|
3312
|
+
if (value === true || value === false) return value;
|
|
3313
|
+
const normalized = String(value).trim().toLowerCase();
|
|
3314
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes") return true;
|
|
3315
|
+
if (normalized === "false" || normalized === "0" || normalized === "no") return false;
|
|
3316
|
+
return defaultValue;
|
|
3317
|
+
}
|
|
3318
|
+
async function auditVerifyCommand(opts) {
|
|
3319
|
+
const auditPath = opts.audit ? path20.resolve(opts.audit) : path20.resolve(".lbe/data/audit.log.jsonl");
|
|
3320
|
+
const failFast = toBoolean(opts["fail-fast"], true);
|
|
3321
|
+
const jsonOutput = toBoolean(opts.json, true);
|
|
3322
|
+
const maxEntries = Number.isFinite(Number(opts.max)) ? Number(opts.max) : void 0;
|
|
3323
|
+
const result = verifyAuditLogIntegrity(auditPath, { failFast, maxEntries });
|
|
3324
|
+
if (jsonOutput) {
|
|
3325
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3326
|
+
} else if (result.valid) {
|
|
3327
|
+
console.log(`OK: ${result.file}`);
|
|
3328
|
+
console.log(`Entries: ${result.entries}`);
|
|
3329
|
+
} else {
|
|
3330
|
+
console.log(`FAIL: ${result.file}`);
|
|
3331
|
+
console.log(`First invalid index: ${result.firstInvalidIndex}`);
|
|
3332
|
+
console.log(`Reason: ${result.reason}`);
|
|
3333
|
+
}
|
|
3334
|
+
process.exit(result.valid ? 0 : 8);
|
|
3335
|
+
}
|
|
3336
|
+
|
|
3337
|
+
// src/cli/commands/integrityCheck.js
|
|
3338
|
+
import path22 from "path";
|
|
3339
|
+
|
|
3340
|
+
// src/core/integrity.js
|
|
3341
|
+
import fs18 from "fs";
|
|
3342
|
+
import path21 from "path";
|
|
3343
|
+
import crypto7 from "crypto";
|
|
3344
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3345
|
+
var __dirname = path21.dirname(fileURLToPath2(import.meta.url));
|
|
3346
|
+
function sha256File(filePath) {
|
|
3347
|
+
try {
|
|
3348
|
+
const content = fs18.readFileSync(filePath);
|
|
3349
|
+
return crypto7.createHash("sha256").update(content).digest("hex");
|
|
3350
|
+
} catch (err) {
|
|
3351
|
+
return null;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
function generateIntegrityManifest(rootDir = path21.join(__dirname, "../..")) {
|
|
3355
|
+
const manifest = {};
|
|
3356
|
+
const criticalFiles = [
|
|
3357
|
+
"src/core/signature.js",
|
|
3358
|
+
"src/core/validator.js",
|
|
3359
|
+
"src/core/policyEngine.js",
|
|
3360
|
+
"src/core/nonceStore.js",
|
|
3361
|
+
"src/core/auditLog.js",
|
|
3362
|
+
"src/core/schema.js",
|
|
3363
|
+
"bin/lbe.js"
|
|
3364
|
+
];
|
|
3365
|
+
for (const file of criticalFiles) {
|
|
3366
|
+
const filePath = path21.join(rootDir, file);
|
|
3367
|
+
const hash = sha256File(filePath);
|
|
3368
|
+
if (hash) {
|
|
3369
|
+
manifest[file] = hash;
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
return manifest;
|
|
3373
|
+
}
|
|
3374
|
+
function verifyIntegrity(manifest, rootDir = path21.join(__dirname, "../..")) {
|
|
3375
|
+
const results = {
|
|
3376
|
+
valid: true,
|
|
3377
|
+
mismatches: [],
|
|
3378
|
+
missing: [],
|
|
3379
|
+
checkedFiles: 0
|
|
3380
|
+
};
|
|
3381
|
+
for (const [file, expectedHash] of Object.entries(manifest)) {
|
|
3382
|
+
const filePath = path21.join(rootDir, file);
|
|
3383
|
+
if (!fs18.existsSync(filePath)) {
|
|
3384
|
+
results.valid = false;
|
|
3385
|
+
results.missing.push(file);
|
|
3386
|
+
continue;
|
|
3387
|
+
}
|
|
3388
|
+
const actualHash = sha256File(filePath);
|
|
3389
|
+
results.checkedFiles++;
|
|
3390
|
+
if (actualHash !== expectedHash) {
|
|
3391
|
+
results.valid = false;
|
|
3392
|
+
results.mismatches.push({
|
|
3393
|
+
file,
|
|
3394
|
+
expected: expectedHash,
|
|
3395
|
+
actual: actualHash
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
return results;
|
|
3400
|
+
}
|
|
3401
|
+
async function performIntegrityCheck(options = {}) {
|
|
3402
|
+
const opts = typeof options === "string" || options === null ? { manifestPath: options } : options;
|
|
3403
|
+
const rootDir = opts.rootDir || path21.join(__dirname, "../..");
|
|
3404
|
+
const strict = opts.strict === true;
|
|
3405
|
+
const manifestPath = opts.manifestPath || path21.join(rootDir, ".lbe/config/integrity.manifest.json");
|
|
3406
|
+
if (!fs18.existsSync(manifestPath)) {
|
|
3407
|
+
if (strict) {
|
|
3408
|
+
return {
|
|
3409
|
+
valid: false,
|
|
3410
|
+
skipped: false,
|
|
3411
|
+
reason: "INTEGRITY_MANIFEST_MISSING",
|
|
3412
|
+
message: `Integrity manifest not found: ${manifestPath}`,
|
|
3413
|
+
checkedFiles: 0,
|
|
3414
|
+
mismatches: [],
|
|
3415
|
+
missing: []
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
return {
|
|
3419
|
+
valid: true,
|
|
3420
|
+
skipped: true,
|
|
3421
|
+
reason: null,
|
|
3422
|
+
message: "Integrity manifest not found - check skipped",
|
|
3423
|
+
checkedFiles: 0,
|
|
3424
|
+
mismatches: [],
|
|
3425
|
+
missing: []
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
try {
|
|
3429
|
+
const manifestContent = fs18.readFileSync(manifestPath, "utf8");
|
|
3430
|
+
const manifest = JSON.parse(manifestContent);
|
|
3431
|
+
const result = verifyIntegrity(manifest, rootDir);
|
|
3432
|
+
return {
|
|
3433
|
+
...result,
|
|
3434
|
+
skipped: false,
|
|
3435
|
+
reason: result.valid ? null : "INTEGRITY_CHECK_FAILED",
|
|
3436
|
+
message: result.valid ? `Integrity check passed (${result.checkedFiles} files verified)` : "Runtime integrity check failed - system may be tampered"
|
|
3437
|
+
};
|
|
3438
|
+
} catch (err) {
|
|
3439
|
+
return {
|
|
3440
|
+
valid: false,
|
|
3441
|
+
skipped: false,
|
|
3442
|
+
reason: "INTEGRITY_CHECK_ERROR",
|
|
3443
|
+
message: `Integrity check error: ${err.message}`,
|
|
3444
|
+
checkedFiles: 0,
|
|
3445
|
+
mismatches: [],
|
|
3446
|
+
missing: []
|
|
3447
|
+
};
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
function writeIntegrityManifest({
|
|
3451
|
+
outputPath = path21.join(__dirname, "../../.lbe/config/integrity.manifest.json"),
|
|
3452
|
+
rootDir = path21.join(__dirname, "../..")
|
|
3453
|
+
} = {}) {
|
|
3454
|
+
const manifest = generateIntegrityManifest(rootDir);
|
|
3455
|
+
const output = JSON.stringify(manifest, null, 2);
|
|
3456
|
+
const outDir = path21.dirname(outputPath);
|
|
3457
|
+
if (!fs18.existsSync(outDir)) {
|
|
3458
|
+
fs18.mkdirSync(outDir, { recursive: true });
|
|
3459
|
+
}
|
|
3460
|
+
fs18.writeFileSync(outputPath, output);
|
|
3461
|
+
return {
|
|
3462
|
+
outputPath,
|
|
3463
|
+
fileCount: Object.keys(manifest).length,
|
|
3464
|
+
manifest
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
|
|
3468
|
+
// src/cli/commands/integrityCheck.js
|
|
3469
|
+
function toBoolean2(value, defaultValue = false) {
|
|
3470
|
+
if (value === void 0) return defaultValue;
|
|
3471
|
+
if (value === true || value === false) return value;
|
|
3472
|
+
const normalized = String(value).trim().toLowerCase();
|
|
3473
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes") return true;
|
|
3474
|
+
if (normalized === "false" || normalized === "0" || normalized === "no") return false;
|
|
3475
|
+
return defaultValue;
|
|
3476
|
+
}
|
|
3477
|
+
async function integrityCheckCommand(opts) {
|
|
3478
|
+
const strict = toBoolean2(opts.strict, false) || toBoolean2(opts["integrity-strict"], false);
|
|
3479
|
+
const manifestPath = opts.manifest ? path22.resolve(opts.manifest) : path22.resolve(opts["integrity-manifest"] || ".lbe/config/integrity.manifest.json");
|
|
3480
|
+
const jsonOutput = toBoolean2(opts.json, true);
|
|
3481
|
+
const result = await performIntegrityCheck({
|
|
3482
|
+
manifestPath,
|
|
3483
|
+
strict
|
|
3484
|
+
});
|
|
3485
|
+
if (jsonOutput) {
|
|
3486
|
+
console.log(JSON.stringify({
|
|
3487
|
+
ok: result.valid,
|
|
3488
|
+
valid: result.valid,
|
|
3489
|
+
skipped: result.skipped === true,
|
|
3490
|
+
strict,
|
|
3491
|
+
manifestPath,
|
|
3492
|
+
checkedFiles: result.checkedFiles,
|
|
3493
|
+
mismatches: result.mismatches || [],
|
|
3494
|
+
missing: result.missing || [],
|
|
3495
|
+
reason: result.reason || null,
|
|
3496
|
+
message: result.message
|
|
3497
|
+
}, null, 2));
|
|
3498
|
+
} else {
|
|
3499
|
+
console.log(result.message);
|
|
3500
|
+
}
|
|
3501
|
+
process.exit(result.valid ? 0 : 8);
|
|
3502
|
+
}
|
|
3503
|
+
async function integrityGenerateCommand(opts) {
|
|
3504
|
+
const outputPath = path22.resolve(opts.out || opts.output || opts.manifest || ".lbe/config/integrity.manifest.json");
|
|
3505
|
+
const result = writeIntegrityManifest({ outputPath });
|
|
3506
|
+
console.log(JSON.stringify({
|
|
3507
|
+
ok: true,
|
|
3508
|
+
outputPath: result.outputPath,
|
|
3509
|
+
fileCount: result.fileCount
|
|
3510
|
+
}, null, 2));
|
|
3511
|
+
process.exit(0);
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
// src/cli/commands/policySign.js
|
|
3515
|
+
import fs19 from "fs";
|
|
3516
|
+
import path23 from "path";
|
|
3517
|
+
async function policySignCommand(opts) {
|
|
3518
|
+
const policyPath = path23.resolve(opts.config || opts.policy || ".lbe/config/policy.default.json");
|
|
3519
|
+
const sigPath = path23.resolve(opts["policy-sig"] || ".lbe/config/policy.sig.json");
|
|
3520
|
+
const secretKeyPath = path23.resolve(opts["secret-key-file"] || ".lbe/keys/secret.key");
|
|
3521
|
+
const keyId = String(opts["policy-key-id"] || "policy-signer-v1-2026Q1");
|
|
3522
|
+
if (!fs19.existsSync(policyPath)) {
|
|
3523
|
+
console.error(JSON.stringify({
|
|
3524
|
+
status: "error",
|
|
3525
|
+
error: "POLICY_FILE_MISSING",
|
|
3526
|
+
message: `Policy file not found: ${policyPath}`
|
|
3527
|
+
}, null, 2));
|
|
3528
|
+
process.exit(1);
|
|
3529
|
+
}
|
|
3530
|
+
if (!fs19.existsSync(secretKeyPath)) {
|
|
3531
|
+
console.error(JSON.stringify({
|
|
3532
|
+
status: "error",
|
|
3533
|
+
error: "SECRET_KEY_MISSING",
|
|
3534
|
+
message: `Secret key file not found: ${secretKeyPath}`
|
|
3535
|
+
}, null, 2));
|
|
3536
|
+
process.exit(1);
|
|
3537
|
+
}
|
|
3538
|
+
const policyObj = JSON.parse(fs19.readFileSync(policyPath, "utf8"));
|
|
3539
|
+
if (typeof policyObj.version === "undefined" || typeof policyObj.createdAt === "undefined") {
|
|
3540
|
+
console.error(JSON.stringify({
|
|
3541
|
+
status: "error",
|
|
3542
|
+
error: "POLICY_VERSION_METADATA_MISSING",
|
|
3543
|
+
message: "Policy must include version and createdAt before signing"
|
|
3544
|
+
}, null, 2));
|
|
3545
|
+
process.exit(8);
|
|
3546
|
+
}
|
|
3547
|
+
const secretKeyB64 = fs19.readFileSync(secretKeyPath, "utf8").trim();
|
|
3548
|
+
const signResult = createPolicySignatureEnvelope({
|
|
3549
|
+
policyObj,
|
|
3550
|
+
secretKeyB64,
|
|
3551
|
+
keyId
|
|
3552
|
+
});
|
|
3553
|
+
if (!signResult.ok) {
|
|
3554
|
+
console.error(JSON.stringify({
|
|
3555
|
+
status: "error",
|
|
3556
|
+
error: signResult.reason || "POLICY_SIGN_FAILED",
|
|
3557
|
+
message: signResult.message
|
|
3558
|
+
}, null, 2));
|
|
3559
|
+
process.exit(8);
|
|
3560
|
+
}
|
|
3561
|
+
const outDir = path23.dirname(sigPath);
|
|
3562
|
+
if (!fs19.existsSync(outDir)) {
|
|
3563
|
+
fs19.mkdirSync(outDir, { recursive: true });
|
|
3564
|
+
}
|
|
3565
|
+
fs19.writeFileSync(sigPath, JSON.stringify(signResult.envelope, null, 2));
|
|
3566
|
+
console.log(JSON.stringify({
|
|
3567
|
+
status: "ok",
|
|
3568
|
+
message: "Policy signature written",
|
|
3569
|
+
policy: policyPath,
|
|
3570
|
+
policySig: sigPath,
|
|
3571
|
+
keyId
|
|
3572
|
+
}, null, 2));
|
|
3573
|
+
process.exit(0);
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
// src/cli/commands/health.js
|
|
3577
|
+
import fs20 from "fs";
|
|
3578
|
+
import path24 from "path";
|
|
3579
|
+
function toBoolean3(value, defaultValue) {
|
|
3580
|
+
if (value === void 0) return defaultValue;
|
|
3581
|
+
if (value === true || value === false) return value;
|
|
3582
|
+
const normalized = String(value).trim().toLowerCase();
|
|
3583
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes") return true;
|
|
3584
|
+
if (normalized === "false" || normalized === "0" || normalized === "no") return false;
|
|
3585
|
+
return defaultValue;
|
|
3586
|
+
}
|
|
3587
|
+
function addCheck(checks, name, ok, message) {
|
|
3588
|
+
checks[name] = { ok, message };
|
|
3589
|
+
}
|
|
3590
|
+
function canReadFile(filePath) {
|
|
3591
|
+
try {
|
|
3592
|
+
fs20.accessSync(filePath, fs20.constants.R_OK);
|
|
3593
|
+
return true;
|
|
3594
|
+
} catch {
|
|
3595
|
+
return false;
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
function checkDataWritable(dataDir) {
|
|
3599
|
+
const marker = path24.join(dataDir, `.healthcheck-${Date.now()}`);
|
|
3600
|
+
try {
|
|
3601
|
+
fs20.mkdirSync(dataDir, { recursive: true });
|
|
3602
|
+
fs20.writeFileSync(marker, "ok", "utf8");
|
|
3603
|
+
fs20.unlinkSync(marker);
|
|
3604
|
+
return { ok: true, message: "Data directory writable" };
|
|
3605
|
+
} catch (error) {
|
|
3606
|
+
return { ok: false, message: `Data directory not writable: ${error.message}` };
|
|
3607
|
+
}
|
|
3608
|
+
}
|
|
3609
|
+
async function healthCommand(opts) {
|
|
3610
|
+
const jsonOutput = toBoolean3(opts.json, true);
|
|
3611
|
+
const policyPath = path24.resolve(opts.config || opts.policy || ".lbe/config/policy.default.json");
|
|
3612
|
+
const policySigPath = path24.resolve(opts["policy-sig"] || ".lbe/config/policy.sig.json");
|
|
3613
|
+
const keysStorePath = path24.resolve(opts["keys-store"] || ".lbe/config/keys.json");
|
|
3614
|
+
const dataDir = path24.resolve(opts["data-dir"] || ".lbe/data");
|
|
3615
|
+
const auditPath = path24.resolve(opts.audit || path24.join(dataDir, "audit.log.jsonl"));
|
|
3616
|
+
const noncePath = path24.resolve(opts["nonce-db"] || path24.join(dataDir, "nonce.db.json"));
|
|
3617
|
+
const ratePath = path24.resolve(opts["rate-db"] || path24.join(dataDir, "rate-limit.db.json"));
|
|
3618
|
+
const policyStatePath = path24.resolve(opts["policy-state"] || path24.join(dataDir, "policy.state.json"));
|
|
3619
|
+
const integrityStrict = toBoolean3(opts["integrity-strict"], false);
|
|
3620
|
+
const integrityManifestPath = path24.resolve(opts["integrity-manifest"] || ".lbe/config/integrity.manifest.json");
|
|
3621
|
+
const checks = {};
|
|
3622
|
+
addCheck(
|
|
3623
|
+
checks,
|
|
3624
|
+
"policy",
|
|
3625
|
+
fs20.existsSync(policyPath) && canReadFile(policyPath),
|
|
3626
|
+
fs20.existsSync(policyPath) ? `Policy file readable: ${policyPath}` : `Policy file missing: ${policyPath}`
|
|
3627
|
+
);
|
|
3628
|
+
addCheck(
|
|
3629
|
+
checks,
|
|
3630
|
+
"policySignature",
|
|
3631
|
+
fs20.existsSync(policySigPath) && canReadFile(policySigPath),
|
|
3632
|
+
fs20.existsSync(policySigPath) ? `Policy signature readable: ${policySigPath}` : `Policy signature missing: ${policySigPath}`
|
|
3633
|
+
);
|
|
3634
|
+
addCheck(
|
|
3635
|
+
checks,
|
|
3636
|
+
"trustedKeys",
|
|
3637
|
+
fs20.existsSync(keysStorePath) && canReadFile(keysStorePath),
|
|
3638
|
+
fs20.existsSync(keysStorePath) ? `Trusted keys readable: ${keysStorePath}` : `Trusted keys missing: ${keysStorePath}`
|
|
3639
|
+
);
|
|
3640
|
+
addCheck(
|
|
3641
|
+
checks,
|
|
3642
|
+
"auditLog",
|
|
3643
|
+
fs20.existsSync(auditPath) && canReadFile(auditPath),
|
|
3644
|
+
fs20.existsSync(auditPath) ? `Audit log readable: ${auditPath}` : `Audit log missing: ${auditPath}`
|
|
3645
|
+
);
|
|
3646
|
+
addCheck(
|
|
3647
|
+
checks,
|
|
3648
|
+
"nonceDb",
|
|
3649
|
+
fs20.existsSync(noncePath) && canReadFile(noncePath),
|
|
3650
|
+
fs20.existsSync(noncePath) ? `Nonce DB readable: ${noncePath}` : `Nonce DB missing: ${noncePath}`
|
|
3651
|
+
);
|
|
3652
|
+
addCheck(
|
|
3653
|
+
checks,
|
|
3654
|
+
"rateLimitDb",
|
|
3655
|
+
fs20.existsSync(ratePath) && canReadFile(ratePath),
|
|
3656
|
+
fs20.existsSync(ratePath) ? `Rate-limit DB readable: ${ratePath}` : `Rate-limit DB missing: ${ratePath}`
|
|
3657
|
+
);
|
|
3658
|
+
addCheck(
|
|
3659
|
+
checks,
|
|
3660
|
+
"policyState",
|
|
3661
|
+
fs20.existsSync(policyStatePath) && canReadFile(policyStatePath),
|
|
3662
|
+
fs20.existsSync(policyStatePath) ? `Policy state readable: ${policyStatePath}` : `Policy state missing: ${policyStatePath}`
|
|
3663
|
+
);
|
|
3664
|
+
const writable = checkDataWritable(dataDir);
|
|
3665
|
+
addCheck(checks, "dataWritable", writable.ok, writable.message);
|
|
3666
|
+
if (integrityStrict) {
|
|
3667
|
+
const integrity = await performIntegrityCheck({
|
|
3668
|
+
strict: true,
|
|
3669
|
+
manifestPath: integrityManifestPath
|
|
3670
|
+
});
|
|
3671
|
+
addCheck(
|
|
3672
|
+
checks,
|
|
3673
|
+
"integrity",
|
|
3674
|
+
integrity.valid,
|
|
3675
|
+
integrity.valid ? integrity.message : `${integrity.reason}: ${integrity.message}`
|
|
3676
|
+
);
|
|
3677
|
+
}
|
|
3678
|
+
const allOk = Object.values(checks).every((c) => c.ok === true);
|
|
3679
|
+
const output = {
|
|
3680
|
+
ok: allOk,
|
|
3681
|
+
status: allOk ? "healthy" : "unhealthy",
|
|
3682
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3683
|
+
checks
|
|
3684
|
+
};
|
|
3685
|
+
if (jsonOutput) {
|
|
3686
|
+
console.log(JSON.stringify(output, null, 2));
|
|
3687
|
+
} else {
|
|
3688
|
+
console.log(`${output.status.toUpperCase()}: ${Object.keys(checks).length} checks`);
|
|
3689
|
+
}
|
|
3690
|
+
process.exit(allOk ? 0 : 8);
|
|
3691
|
+
}
|
|
3692
|
+
|
|
3693
|
+
// src/cli/commands/policyAdd.js
|
|
3694
|
+
async function policyAddCommand(opts = {}) {
|
|
3695
|
+
const result = addLocalPolicyRule(opts.root || process.cwd(), {
|
|
3696
|
+
effect: opts.effect,
|
|
3697
|
+
type: opts.type,
|
|
3698
|
+
pattern: opts.pattern,
|
|
3699
|
+
from: opts.from
|
|
3700
|
+
}, opts.mode);
|
|
3701
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
// src/cli/commands/policyMode.js
|
|
3705
|
+
async function policyModeCommand(mode, opts = {}) {
|
|
3706
|
+
const loaded = loadLocalPolicy(opts.root || process.cwd(), mode);
|
|
3707
|
+
writeLocalPolicy(loaded.root, { ...loaded.policy, mode });
|
|
3708
|
+
console.log(JSON.stringify({ mode, policy: loaded.policyPath }, null, 2));
|
|
3709
|
+
}
|
|
3710
|
+
|
|
3711
|
+
// src/cli/commands/status.js
|
|
3712
|
+
import fs25 from "node:fs";
|
|
3713
|
+
import path30 from "node:path";
|
|
3714
|
+
|
|
3715
|
+
// src/state/index.js
|
|
3716
|
+
import fs24 from "node:fs";
|
|
3717
|
+
import path29 from "node:path";
|
|
3718
|
+
|
|
3719
|
+
// src/state/workspaceId.js
|
|
3720
|
+
import crypto8 from "node:crypto";
|
|
3721
|
+
import fs21 from "node:fs";
|
|
3722
|
+
import path25 from "node:path";
|
|
3723
|
+
function canonicalWorkspacePath(workspaceRoot) {
|
|
3724
|
+
let resolved;
|
|
3725
|
+
try {
|
|
3726
|
+
resolved = fs21.realpathSync.native(workspaceRoot);
|
|
3727
|
+
} catch (_) {
|
|
3728
|
+
resolved = path25.resolve(workspaceRoot);
|
|
3729
|
+
}
|
|
3730
|
+
const normalised = path25.normalize(resolved);
|
|
3731
|
+
return process.platform === "win32" ? normalised.toLowerCase() : normalised;
|
|
3732
|
+
}
|
|
3733
|
+
function workspaceId(workspaceRoot) {
|
|
3734
|
+
return crypto8.createHash("sha256").update(canonicalWorkspacePath(workspaceRoot)).digest("hex");
|
|
3735
|
+
}
|
|
3736
|
+
function workspaceStateDir(stateRoot2, id) {
|
|
3737
|
+
return path25.join(stateRoot2, "workspaces", id.slice(0, 2), id.slice(2, 4), id.slice(4, 6), id);
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
// src/state/stateRoot.js
|
|
3741
|
+
import path26 from "node:path";
|
|
3742
|
+
import os from "node:os";
|
|
3743
|
+
function stateRoot() {
|
|
3744
|
+
const home = os.homedir();
|
|
3745
|
+
if (process.platform === "win32") {
|
|
3746
|
+
const localAppData = process.env.LOCALAPPDATA || path26.join(home, "AppData", "Local");
|
|
3747
|
+
return path26.join(localAppData, "LetterBlack", "Sentinel");
|
|
3748
|
+
}
|
|
3749
|
+
if (process.platform === "darwin") {
|
|
3750
|
+
const appSupport = process.env.HOME ? path26.join(process.env.HOME, "Library", "Application Support") : path26.join(home, "Library", "Application Support");
|
|
3751
|
+
return path26.join(appSupport, "LetterBlack", "Sentinel");
|
|
3752
|
+
}
|
|
3753
|
+
const xdgData = process.env.XDG_DATA_HOME || path26.join(home, ".local", "share");
|
|
3754
|
+
return path26.join(xdgData, "LetterBlack", "Sentinel");
|
|
3755
|
+
}
|
|
3756
|
+
|
|
3757
|
+
// src/state/workspaceRegistry.js
|
|
3758
|
+
import fs22 from "node:fs";
|
|
3759
|
+
import path27 from "node:path";
|
|
3760
|
+
var FORMAT = 1;
|
|
3761
|
+
function emptyRegistry() {
|
|
3762
|
+
return { format: FORMAT, workspaces: {} };
|
|
3763
|
+
}
|
|
3764
|
+
function readRegistry(registryPath) {
|
|
3765
|
+
if (!fs22.existsSync(registryPath)) return { registry: emptyRegistry(), readable: true };
|
|
3766
|
+
try {
|
|
3767
|
+
const registry = JSON.parse(fs22.readFileSync(registryPath, "utf8"));
|
|
3768
|
+
if (!registry || typeof registry !== "object" || Array.isArray(registry) || registry.format !== FORMAT || !registry.workspaces || typeof registry.workspaces !== "object" || Array.isArray(registry.workspaces)) {
|
|
3769
|
+
return { registry: null, readable: false };
|
|
3770
|
+
}
|
|
3771
|
+
return { registry, readable: true };
|
|
3772
|
+
} catch (_) {
|
|
3773
|
+
return { registry: null, readable: false };
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
function registerWorkspace(registryPath, workspaceId2, workspacePath) {
|
|
3777
|
+
return withFileLock(registryPath, () => {
|
|
3778
|
+
const { registry, readable } = readRegistry(registryPath);
|
|
3779
|
+
if (!readable) return null;
|
|
3780
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3781
|
+
const existing = registry.workspaces[workspaceId2];
|
|
3782
|
+
registry.workspaces[workspaceId2] = {
|
|
3783
|
+
path: workspacePath,
|
|
3784
|
+
alias: existing?.alias || path27.basename(workspacePath),
|
|
3785
|
+
first_seen: existing?.first_seen || now,
|
|
3786
|
+
last_active: now
|
|
3787
|
+
};
|
|
3788
|
+
atomicWriteFileSync(registryPath, JSON.stringify(registry, null, 2) + "\n", "utf8");
|
|
3789
|
+
return registry.workspaces[workspaceId2];
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
function listWorkspaces(registryPath) {
|
|
3793
|
+
const { registry, readable } = readRegistry(registryPath);
|
|
3794
|
+
if (!readable) return [];
|
|
3795
|
+
return Object.entries(registry.workspaces).map(([workspaceId2, workspace]) => ({
|
|
3796
|
+
workspaceId: workspaceId2,
|
|
3797
|
+
...workspace
|
|
3798
|
+
}));
|
|
3799
|
+
}
|
|
3800
|
+
function isWorkspaceRegistryReadable(registryPath) {
|
|
3801
|
+
return readRegistry(registryPath).readable;
|
|
3802
|
+
}
|
|
3803
|
+
|
|
3804
|
+
// src/state/migration.js
|
|
3805
|
+
import crypto9 from "node:crypto";
|
|
3806
|
+
import fs23 from "node:fs";
|
|
3807
|
+
import path28 from "node:path";
|
|
3808
|
+
var SOURCE = ".lbe/events.jsonl";
|
|
3809
|
+
function sha2563(data) {
|
|
3810
|
+
return crypto9.createHash("sha256").update(data).digest("hex");
|
|
3811
|
+
}
|
|
3812
|
+
function readMarker(markerPath) {
|
|
3813
|
+
try {
|
|
3814
|
+
return JSON.parse(fs23.readFileSync(markerPath, "utf8"));
|
|
3815
|
+
} catch (_) {
|
|
3816
|
+
return null;
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
function readCentralLines(eventsPath) {
|
|
3820
|
+
try {
|
|
3821
|
+
return new Set(fs23.readFileSync(eventsPath, "utf8").split(/\r?\n/).filter(Boolean));
|
|
3822
|
+
} catch (_) {
|
|
3823
|
+
return /* @__PURE__ */ new Set();
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
function migrateLegacyEvents(workspaceRoot, stateDir) {
|
|
3827
|
+
const sourcePath = path28.join(workspaceRoot, ".lbe", "events.jsonl");
|
|
3828
|
+
const migrationDir = path28.join(stateDir, "migration");
|
|
3829
|
+
const markerPath = path28.join(migrationDir, "events-v1.json");
|
|
3830
|
+
const invalidPath = path28.join(migrationDir, "migration-invalid.jsonl");
|
|
3831
|
+
const result = {
|
|
3832
|
+
attempted: false,
|
|
3833
|
+
imported_count: 0,
|
|
3834
|
+
skipped_duplicate_count: 0,
|
|
3835
|
+
invalid_count: 0,
|
|
3836
|
+
markerPath,
|
|
3837
|
+
invalidPath
|
|
3838
|
+
};
|
|
3839
|
+
let source;
|
|
3840
|
+
try {
|
|
3841
|
+
if (!fs23.existsSync(sourcePath)) return result;
|
|
3842
|
+
source = fs23.readFileSync(sourcePath, "utf8");
|
|
3843
|
+
} catch (_) {
|
|
3844
|
+
return result;
|
|
3845
|
+
}
|
|
3846
|
+
result.attempted = true;
|
|
3847
|
+
const sourceSha256 = sha2563(source);
|
|
3848
|
+
const marker = readMarker(markerPath);
|
|
3849
|
+
if (marker?.format === 1 && marker.source_sha256 === sourceSha256) return result;
|
|
3850
|
+
const validLines = [];
|
|
3851
|
+
const invalidLines = [];
|
|
3852
|
+
for (const [index, rawLine] of source.split(/\r?\n/).entries()) {
|
|
3853
|
+
const line = rawLine.trim();
|
|
3854
|
+
if (!line) continue;
|
|
3855
|
+
try {
|
|
3856
|
+
JSON.parse(line);
|
|
3857
|
+
validLines.push(line);
|
|
3858
|
+
} catch (_) {
|
|
3859
|
+
invalidLines.push({ source: SOURCE, line_number: index + 1, line, source_sha256: sourceSha256 });
|
|
3860
|
+
}
|
|
3861
|
+
}
|
|
3862
|
+
const eventsPath = path28.join(stateDir, "lbe-events.jsonl");
|
|
3863
|
+
try {
|
|
3864
|
+
withFileLock(eventsPath, () => {
|
|
3865
|
+
const centralLines = readCentralLines(eventsPath);
|
|
3866
|
+
const imported = [];
|
|
3867
|
+
for (const line of validLines) {
|
|
3868
|
+
if (centralLines.has(line)) {
|
|
3869
|
+
result.skipped_duplicate_count++;
|
|
3870
|
+
continue;
|
|
3871
|
+
}
|
|
3872
|
+
centralLines.add(line);
|
|
3873
|
+
imported.push(line);
|
|
3874
|
+
result.imported_count++;
|
|
3875
|
+
}
|
|
3876
|
+
if (imported.length > 0) {
|
|
3877
|
+
const existing = fs23.existsSync(eventsPath) ? fs23.readFileSync(eventsPath, "utf8") : "";
|
|
3878
|
+
atomicWriteFileSync(eventsPath, existing + (existing && !existing.endsWith("\n") ? "\n" : "") + imported.join("\n") + "\n", "utf8");
|
|
3879
|
+
}
|
|
3880
|
+
});
|
|
3881
|
+
if (invalidLines.length > 0) {
|
|
3882
|
+
atomicAppendFileSync(invalidPath, invalidLines.map((line) => JSON.stringify(line) + "\n").join(""), { encoding: "utf8" });
|
|
3883
|
+
result.invalid_count = invalidLines.length;
|
|
3884
|
+
}
|
|
3885
|
+
fs23.mkdirSync(migrationDir, { recursive: true });
|
|
3886
|
+
atomicWriteFileSync(markerPath, JSON.stringify({
|
|
3887
|
+
format: 1,
|
|
3888
|
+
source: SOURCE,
|
|
3889
|
+
source_sha256: sourceSha256,
|
|
3890
|
+
migrated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3891
|
+
imported_count: result.imported_count,
|
|
3892
|
+
skipped_duplicate_count: result.skipped_duplicate_count,
|
|
3893
|
+
invalid_count: result.invalid_count
|
|
3894
|
+
}, null, 2) + "\n", "utf8");
|
|
3895
|
+
} catch (_) {
|
|
3896
|
+
}
|
|
3897
|
+
return result;
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
// src/state/index.js
|
|
3901
|
+
function resolveWorkspaceState(workspaceRoot) {
|
|
3902
|
+
const root = stateRoot();
|
|
3903
|
+
const id = workspaceId(workspaceRoot);
|
|
3904
|
+
const dir = workspaceStateDir(root, id);
|
|
3905
|
+
fs24.mkdirSync(dir, { recursive: true });
|
|
3906
|
+
fs24.mkdirSync(path29.join(dir, "file-index"), { recursive: true });
|
|
3907
|
+
fs24.mkdirSync(path29.join(dir, "proof"), { recursive: true });
|
|
3908
|
+
registerWorkspace(path29.join(root, "registry.json"), id, workspaceRoot);
|
|
3909
|
+
migrateLegacyEvents(workspaceRoot, dir);
|
|
3910
|
+
return {
|
|
3911
|
+
stateDir: dir,
|
|
3912
|
+
workspaceId: id,
|
|
3913
|
+
paths: buildPaths(dir)
|
|
3914
|
+
};
|
|
3915
|
+
}
|
|
3916
|
+
function buildPaths(dir) {
|
|
3917
|
+
return {
|
|
3918
|
+
workspace: path29.join(dir, "workspace.json"),
|
|
3919
|
+
events: path29.join(dir, "lbe-events.jsonl"),
|
|
3920
|
+
intent: path29.join(dir, "intent.jsonl"),
|
|
3921
|
+
targetRegistry: path29.join(dir, "target_registry.jsonl"),
|
|
3922
|
+
fileIndexDir: path29.join(dir, "file-index"),
|
|
3923
|
+
fileIndexBefore: path29.join(dir, "file-index", "before.json"),
|
|
3924
|
+
fileIndexAfter: path29.join(dir, "file-index", "after.json"),
|
|
3925
|
+
proofDir: path29.join(dir, "proof"),
|
|
3926
|
+
proofLatest: path29.join(dir, "proof", "latest.json")
|
|
3927
|
+
};
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
// src/cli/commands/status.js
|
|
3931
|
+
async function statusCommand(opts) {
|
|
3932
|
+
if (opts.all) {
|
|
3933
|
+
const registryPath = opts.registryPath || path30.join(stateRoot(), "registry.json");
|
|
3934
|
+
if (!isWorkspaceRegistryReadable(registryPath)) {
|
|
3935
|
+
console.log("Workspace registry unreadable");
|
|
3936
|
+
return { workspaces: [], registryReadable: false };
|
|
3937
|
+
}
|
|
3938
|
+
const workspaces = listWorkspaces(registryPath);
|
|
3939
|
+
if (workspaces.length === 0) {
|
|
3940
|
+
console.log("No known workspaces yet");
|
|
3941
|
+
return { workspaces, registryReadable: true };
|
|
3942
|
+
}
|
|
3943
|
+
console.log("\nKnown LBE workspaces");
|
|
3944
|
+
for (const workspace of workspaces) {
|
|
3945
|
+
console.log(` ${workspace.alias}`);
|
|
3946
|
+
console.log(` workspace_id ${workspace.workspaceId}`);
|
|
3947
|
+
console.log(` path ${workspace.path}`);
|
|
3948
|
+
console.log(` last_active ${workspace.last_active}`);
|
|
3949
|
+
}
|
|
3950
|
+
console.log("");
|
|
3951
|
+
return { workspaces, registryReadable: true };
|
|
3952
|
+
}
|
|
3953
|
+
const workspaceRoot = path30.resolve(opts.root || process.cwd());
|
|
3954
|
+
const { stateDir, workspaceId: workspaceId2, paths } = resolveWorkspaceState(workspaceRoot);
|
|
3955
|
+
const policyPath = path30.join(workspaceRoot, ".lbe", "policy.json");
|
|
3956
|
+
let policySource = "not found";
|
|
3957
|
+
let policyMode = "unknown";
|
|
3958
|
+
if (fs25.existsSync(policyPath)) {
|
|
3959
|
+
try {
|
|
3960
|
+
const policy = JSON.parse(fs25.readFileSync(policyPath, "utf8"));
|
|
3961
|
+
policyMode = policy.mode || "unknown";
|
|
3962
|
+
policySource = policyPath;
|
|
3963
|
+
} catch (_) {
|
|
3964
|
+
policySource = policyPath + " (unreadable)";
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
const hasProof = fs25.existsSync(paths.proofLatest);
|
|
3968
|
+
const hasEvents = fs25.existsSync(paths.events);
|
|
3969
|
+
console.log(`
|
|
3970
|
+
LBE Central State \u2014 ${workspaceRoot}`);
|
|
3971
|
+
console.log(` workspace_id ${workspaceId2}`);
|
|
3972
|
+
console.log(` state_dir ${stateDir}`);
|
|
3973
|
+
console.log(` policy_source ${policySource}`);
|
|
3974
|
+
console.log(` policy_mode ${policyMode}`);
|
|
3975
|
+
console.log(` central_proof ${hasProof ? paths.proofLatest : "No central proof yet"}`);
|
|
3976
|
+
console.log(` central_logs ${hasEvents ? paths.events : "No central logs yet. Hook dual-write not enabled."}`);
|
|
3977
|
+
console.log("");
|
|
3978
|
+
return { workspaceId: workspaceId2, stateDir, policySource, policyMode, hasProof, hasEvents };
|
|
3979
|
+
}
|
|
3980
|
+
|
|
3981
|
+
// src/cli/commands/logs.js
|
|
3982
|
+
import fs26 from "node:fs";
|
|
3983
|
+
import path31 from "node:path";
|
|
3984
|
+
var DEFAULT_LIMIT = 20;
|
|
3985
|
+
async function logsCommand(opts) {
|
|
3986
|
+
const workspaceRoot = path31.resolve(opts.root || process.cwd());
|
|
3987
|
+
const { paths } = resolveWorkspaceState(workspaceRoot);
|
|
3988
|
+
const limit = opts.limit ? parseInt(opts.limit, 10) : DEFAULT_LIMIT;
|
|
3989
|
+
if (!fs26.existsSync(paths.events)) {
|
|
3990
|
+
console.log("\nLBE Central Logs");
|
|
3991
|
+
console.log(" No central logs yet. Hook dual-write not enabled.");
|
|
3992
|
+
console.log(` Expected at: ${paths.events}`);
|
|
3993
|
+
console.log("");
|
|
3994
|
+
return { eventsPath: paths.events, count: 0, entries: [], missing: true };
|
|
3995
|
+
}
|
|
3996
|
+
const raw = fs26.readFileSync(paths.events, "utf8").trim();
|
|
3997
|
+
const lines = raw ? raw.split("\n") : [];
|
|
3998
|
+
const entries = lines.map((line) => {
|
|
3999
|
+
try {
|
|
4000
|
+
return JSON.parse(line);
|
|
4001
|
+
} catch (_) {
|
|
4002
|
+
return null;
|
|
4003
|
+
}
|
|
4004
|
+
}).filter(Boolean);
|
|
4005
|
+
const tail = entries.slice(-limit);
|
|
4006
|
+
console.log(`
|
|
4007
|
+
LBE Central Logs \u2014 last ${tail.length} of ${entries.length} entries`);
|
|
4008
|
+
console.log(` source: ${paths.events}
|
|
4009
|
+
`);
|
|
4010
|
+
for (const entry of tail) {
|
|
4011
|
+
const ts = entry.ts ? new Date(entry.ts * 1e3).toISOString() : "?";
|
|
4012
|
+
const action = entry.action || "?";
|
|
4013
|
+
const dec = entry.decision || "?";
|
|
4014
|
+
const target = entry.path || entry.cmd || "";
|
|
4015
|
+
console.log(` [${ts}] ${dec.toUpperCase().padEnd(5)} ${action} ${target}`);
|
|
4016
|
+
}
|
|
4017
|
+
console.log("");
|
|
4018
|
+
return { eventsPath: paths.events, count: entries.length, entries: tail, missing: false };
|
|
4019
|
+
}
|
|
4020
|
+
|
|
4021
|
+
// src/cli/commands/openState.js
|
|
4022
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
4023
|
+
import path32 from "node:path";
|
|
4024
|
+
async function openStateCommand(opts) {
|
|
4025
|
+
const workspaceRoot = path32.resolve(opts.root || process.cwd());
|
|
4026
|
+
const { stateDir } = resolveWorkspaceState(workspaceRoot);
|
|
4027
|
+
console.log(`
|
|
4028
|
+
LBE Central State Directory`);
|
|
4029
|
+
console.log(` ${stateDir}
|
|
4030
|
+
`);
|
|
4031
|
+
if (process.env.LBE_NO_OPEN === "1") {
|
|
4032
|
+
return { stateDir, opened: false };
|
|
4033
|
+
}
|
|
4034
|
+
let opened = false;
|
|
4035
|
+
try {
|
|
4036
|
+
if (process.platform === "win32") {
|
|
4037
|
+
spawnSync2("explorer.exe", [stateDir], { detached: true, stdio: "ignore" });
|
|
4038
|
+
opened = true;
|
|
4039
|
+
} else if (process.platform === "darwin") {
|
|
4040
|
+
spawnSync2("open", [stateDir], { detached: true, stdio: "ignore" });
|
|
4041
|
+
opened = true;
|
|
4042
|
+
} else {
|
|
4043
|
+
spawnSync2("xdg-open", [stateDir], { detached: true, stdio: "ignore" });
|
|
4044
|
+
opened = true;
|
|
4045
|
+
}
|
|
4046
|
+
} catch (_) {
|
|
4047
|
+
}
|
|
4048
|
+
return { stateDir, opened };
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
// src/cli/commands/proof.js
|
|
4052
|
+
import fs28 from "node:fs";
|
|
4053
|
+
import path34 from "node:path";
|
|
4054
|
+
import os2 from "node:os";
|
|
4055
|
+
|
|
4056
|
+
// src/state/proofRunner.js
|
|
4057
|
+
import fs27 from "node:fs";
|
|
4058
|
+
import path33 from "node:path";
|
|
4059
|
+
function loadJson(filePath) {
|
|
4060
|
+
if (!filePath || !fs27.existsSync(filePath)) return null;
|
|
4061
|
+
try {
|
|
4062
|
+
return JSON.parse(fs27.readFileSync(filePath, "utf8"));
|
|
4063
|
+
} catch (_) {
|
|
4064
|
+
return null;
|
|
4065
|
+
}
|
|
4066
|
+
}
|
|
4067
|
+
function loadLatestProof(stateDir) {
|
|
4068
|
+
return loadJson(path33.join(stateDir, "proof", "latest.json"));
|
|
4069
|
+
}
|
|
4070
|
+
|
|
4071
|
+
// src/cli/commands/proof.js
|
|
4072
|
+
function buildPublicProof(proof, targets) {
|
|
4073
|
+
const lastTarget = Array.isArray(targets) && targets.length > 0 ? targets[targets.length - 1] : null;
|
|
4074
|
+
const pub = {
|
|
4075
|
+
result: proof.result,
|
|
4076
|
+
profile: proof.profile,
|
|
4077
|
+
checks: proof.checks_run || [],
|
|
4078
|
+
allow_deny: proof.failures && proof.failures.length > 0 ? "deny" : "allow"
|
|
4079
|
+
};
|
|
4080
|
+
if (lastTarget) {
|
|
4081
|
+
pub.target_type = lastTarget.kind || null;
|
|
4082
|
+
pub.target_label = lastTarget.label || null;
|
|
4083
|
+
pub.target_file = lastTarget.component_file || null;
|
|
4084
|
+
}
|
|
4085
|
+
if (proof.failures && proof.failures.length > 0) {
|
|
4086
|
+
pub.failure_reasons = proof.failures.map((f) => ({ check: f.check, reason: f.reason }));
|
|
4087
|
+
}
|
|
4088
|
+
return pub;
|
|
4089
|
+
}
|
|
4090
|
+
async function proofCommand(opts) {
|
|
4091
|
+
const workspaceRoot = path34.resolve(opts.root || process.cwd());
|
|
4092
|
+
const { stateDir, workspaceId: workspaceId2, paths } = resolveWorkspaceState(workspaceRoot);
|
|
4093
|
+
const proof = loadLatestProof(stateDir);
|
|
4094
|
+
const isPublic = opts.public === true || opts.public === "true";
|
|
4095
|
+
const isJson = opts.json === true || opts.json === "true" || isPublic;
|
|
4096
|
+
if (!proof) {
|
|
4097
|
+
if (isJson) {
|
|
4098
|
+
console.log(JSON.stringify({ found: false, message: "No proof record found. Run lbe proof after using the hook." }, null, 2));
|
|
4099
|
+
} else {
|
|
4100
|
+
console.log("\nNo proof record found.");
|
|
4101
|
+
console.log("Use the hook-protected workflow and then run: lbe proof\n");
|
|
4102
|
+
}
|
|
4103
|
+
return { found: false };
|
|
4104
|
+
}
|
|
4105
|
+
let targets = [];
|
|
4106
|
+
if (isPublic) {
|
|
4107
|
+
const targetPath = path34.join(stateDir, "target_registry.jsonl");
|
|
4108
|
+
if (fs28.existsSync(targetPath)) {
|
|
4109
|
+
const raw = fs28.readFileSync(targetPath, "utf8").trim();
|
|
4110
|
+
targets = raw ? raw.split("\n").reduce((acc, l) => {
|
|
4111
|
+
try {
|
|
4112
|
+
acc.push(JSON.parse(l));
|
|
4113
|
+
} catch (_) {
|
|
4114
|
+
}
|
|
4115
|
+
return acc;
|
|
4116
|
+
}, []) : [];
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
4119
|
+
if (isPublic) {
|
|
4120
|
+
const pub = buildPublicProof(proof, targets);
|
|
4121
|
+
console.log(JSON.stringify(pub, null, 2));
|
|
4122
|
+
return { found: true, result: proof.result, profile: proof.profile, public: true };
|
|
4123
|
+
}
|
|
4124
|
+
if (isJson) {
|
|
4125
|
+
console.log(JSON.stringify(proof, null, 2));
|
|
4126
|
+
return { found: true, result: proof.result, profile: proof.profile };
|
|
4127
|
+
}
|
|
4128
|
+
const resultMark = proof.result === "PASS" ? "\u2713" : proof.result === "WEAK_PROOF" ? "\u26A0" : "\u2717";
|
|
4129
|
+
const workspace = path34.basename(workspaceRoot);
|
|
4130
|
+
console.log(`
|
|
4131
|
+
LBE Proof \u2014 ${workspace}`);
|
|
4132
|
+
console.log(` Result ${resultMark} ${proof.result}`);
|
|
4133
|
+
console.log(` Profile ${proof.profile}`);
|
|
4134
|
+
console.log(` Changed files ${(proof.files_changed || []).length}`);
|
|
4135
|
+
console.log(` Checks run ${(proof.checks_run || []).join(", ")}`);
|
|
4136
|
+
if (proof.failures && proof.failures.length > 0) {
|
|
4137
|
+
console.log(` Failures ${proof.failures.length}`);
|
|
4138
|
+
for (const f of proof.failures) {
|
|
4139
|
+
console.log(` \u2022 [${f.check}] ${f.reason}${f.file ? ": " + f.file : ""}`);
|
|
4140
|
+
}
|
|
4141
|
+
}
|
|
4142
|
+
console.log(` Recorded at ${proof.ts}`);
|
|
4143
|
+
console.log("");
|
|
4144
|
+
return { found: true, result: proof.result, profile: proof.profile };
|
|
4145
|
+
}
|
|
4146
|
+
|
|
4147
|
+
// src/cli/commands/assertConsumer.js
|
|
4148
|
+
import fs29 from "fs";
|
|
4149
|
+
import path35 from "path";
|
|
4150
|
+
import { createRequire } from "module";
|
|
4151
|
+
var PACKAGE_NAME = "@letterblack/lbe-core";
|
|
4152
|
+
var DEP_SECTIONS = [
|
|
4153
|
+
"dependencies",
|
|
4154
|
+
"devDependencies",
|
|
4155
|
+
"optionalDependencies",
|
|
4156
|
+
"peerDependencies"
|
|
4157
|
+
];
|
|
4158
|
+
function readJson(filePath) {
|
|
4159
|
+
try {
|
|
4160
|
+
return JSON.parse(fs29.readFileSync(filePath, "utf-8"));
|
|
4161
|
+
} catch {
|
|
4162
|
+
return null;
|
|
4163
|
+
}
|
|
4164
|
+
}
|
|
4165
|
+
function addCheck2(checks, name, ok, details = {}) {
|
|
4166
|
+
checks.push({ name, ok, ...details });
|
|
4167
|
+
return ok;
|
|
4168
|
+
}
|
|
4169
|
+
function findDependencySpec(packageJson2) {
|
|
4170
|
+
if (!packageJson2 || typeof packageJson2 !== "object") return null;
|
|
4171
|
+
for (const section of DEP_SECTIONS) {
|
|
4172
|
+
const value = packageJson2[section]?.[PACKAGE_NAME];
|
|
4173
|
+
if (value !== void 0) {
|
|
4174
|
+
return { section, spec: String(value) };
|
|
4175
|
+
}
|
|
4176
|
+
}
|
|
4177
|
+
return null;
|
|
4178
|
+
}
|
|
4179
|
+
function classifyDependencySpec(spec) {
|
|
4180
|
+
const raw = String(spec || "").trim();
|
|
4181
|
+
const lower = raw.toLowerCase();
|
|
4182
|
+
if (!raw) {
|
|
4183
|
+
return { ok: false, reason: "EMPTY_DEPENDENCY_SPEC" };
|
|
4184
|
+
}
|
|
4185
|
+
const blockedPrefixes = [
|
|
4186
|
+
"file:",
|
|
4187
|
+
"link:",
|
|
4188
|
+
"workspace:",
|
|
4189
|
+
"git+",
|
|
4190
|
+
"github:",
|
|
4191
|
+
"git://",
|
|
4192
|
+
"ssh://"
|
|
4193
|
+
];
|
|
4194
|
+
for (const prefix of blockedPrefixes) {
|
|
4195
|
+
if (lower.startsWith(prefix)) {
|
|
4196
|
+
return { ok: false, reason: "LOCAL_OR_GIT_DEPENDENCY_SPEC", prefix };
|
|
4197
|
+
}
|
|
4198
|
+
}
|
|
4199
|
+
if (/^[a-z]:[\\/]/i.test(raw) || raw.startsWith("./") || raw.startsWith("../")) {
|
|
4200
|
+
return { ok: false, reason: "PATH_DEPENDENCY_SPEC" };
|
|
4201
|
+
}
|
|
4202
|
+
return { ok: true };
|
|
4203
|
+
}
|
|
4204
|
+
function findPackageRoot(startFile) {
|
|
4205
|
+
let dir = fs29.statSync(startFile).isDirectory() ? startFile : path35.dirname(startFile);
|
|
4206
|
+
while (true) {
|
|
4207
|
+
const candidate = path35.join(dir, "package.json");
|
|
4208
|
+
const pkg = readJson(candidate);
|
|
4209
|
+
if (pkg?.name === PACKAGE_NAME) return dir;
|
|
4210
|
+
const parent = path35.dirname(dir);
|
|
4211
|
+
if (parent === dir) return null;
|
|
4212
|
+
dir = parent;
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
function inspectPackageLock(root) {
|
|
4216
|
+
const lockPath = path35.join(root, "package-lock.json");
|
|
4217
|
+
const lock = readJson(lockPath);
|
|
4218
|
+
if (!lock) return { present: false };
|
|
4219
|
+
const packageEntry = lock.packages?.[`node_modules/${PACKAGE_NAME}`];
|
|
4220
|
+
if (!packageEntry) return { present: true, packageEntryPresent: false };
|
|
4221
|
+
const resolved = String(packageEntry.resolved || "");
|
|
4222
|
+
const blockedResolved = packageEntry.link === true || resolved.startsWith("file:") || resolved.startsWith("git+") || resolved.startsWith("github:") || resolved.startsWith("ssh://") || /^[a-z]:[\\/]/i.test(resolved);
|
|
4223
|
+
return {
|
|
4224
|
+
present: true,
|
|
4225
|
+
packageEntryPresent: true,
|
|
4226
|
+
link: packageEntry.link === true,
|
|
4227
|
+
resolved: resolved || null,
|
|
4228
|
+
blockedResolved
|
|
4229
|
+
};
|
|
4230
|
+
}
|
|
4231
|
+
function getInstalledPackagePath(root) {
|
|
4232
|
+
return path35.join(root, "node_modules", ...PACKAGE_NAME.split("/"));
|
|
4233
|
+
}
|
|
4234
|
+
async function assertConsumerCommand(opts = {}) {
|
|
4235
|
+
const root = path35.resolve(opts.root || process.cwd());
|
|
4236
|
+
const packageJsonPath2 = path35.join(root, "package.json");
|
|
4237
|
+
const packageJson2 = readJson(packageJsonPath2);
|
|
4238
|
+
const checks = [];
|
|
4239
|
+
addCheck2(checks, "project-package-json-present", Boolean(packageJson2), { path: packageJsonPath2 });
|
|
4240
|
+
const dependency = findDependencySpec(packageJson2);
|
|
4241
|
+
addCheck2(checks, "declares-lbe-package-dependency", Boolean(dependency), dependency || {});
|
|
4242
|
+
const specCheck = dependency ? classifyDependencySpec(dependency.spec) : { ok: false, reason: "DEPENDENCY_NOT_DECLARED" };
|
|
4243
|
+
addCheck2(checks, "dependency-spec-is-registry-package", specCheck.ok, {
|
|
4244
|
+
spec: dependency?.spec || null,
|
|
4245
|
+
reason: specCheck.reason || null,
|
|
4246
|
+
blockedPrefix: specCheck.prefix || null
|
|
4247
|
+
});
|
|
4248
|
+
let resolvedEntry = null;
|
|
4249
|
+
let resolvedRoot = null;
|
|
4250
|
+
try {
|
|
4251
|
+
const req = createRequire(path35.join(root, "package.json"));
|
|
4252
|
+
resolvedEntry = req.resolve(PACKAGE_NAME);
|
|
4253
|
+
resolvedRoot = findPackageRoot(resolvedEntry);
|
|
4254
|
+
} catch (error) {
|
|
4255
|
+
addCheck2(checks, "lbe-package-resolves-from-project", false, { message: error.message });
|
|
4256
|
+
}
|
|
4257
|
+
if (resolvedEntry) {
|
|
4258
|
+
addCheck2(checks, "lbe-package-resolves-from-project", true, { resolvedEntry });
|
|
4259
|
+
}
|
|
4260
|
+
if (resolvedRoot) {
|
|
4261
|
+
const stat = fs29.lstatSync(resolvedRoot);
|
|
4262
|
+
addCheck2(checks, "lbe-package-is-not-symlink", !stat.isSymbolicLink(), { resolvedRoot });
|
|
4263
|
+
} else {
|
|
4264
|
+
addCheck2(checks, "lbe-package-is-not-symlink", false, { reason: "PACKAGE_ROOT_NOT_FOUND" });
|
|
4265
|
+
}
|
|
4266
|
+
const installedPackagePath = getInstalledPackagePath(root);
|
|
4267
|
+
if (fs29.existsSync(installedPackagePath)) {
|
|
4268
|
+
const installedStat = fs29.lstatSync(installedPackagePath);
|
|
4269
|
+
addCheck2(checks, "installed-node_modules-package-is-not-symlink", !installedStat.isSymbolicLink(), {
|
|
4270
|
+
installedPackagePath
|
|
4271
|
+
});
|
|
4272
|
+
} else {
|
|
4273
|
+
addCheck2(checks, "installed-node_modules-package-is-not-symlink", false, {
|
|
4274
|
+
installedPackagePath,
|
|
4275
|
+
reason: "PACKAGE_NOT_INSTALLED_LOCALLY"
|
|
4276
|
+
});
|
|
4277
|
+
}
|
|
4278
|
+
const lockInfo = inspectPackageLock(root);
|
|
4279
|
+
if (lockInfo.present && lockInfo.packageEntryPresent) {
|
|
4280
|
+
addCheck2(checks, "package-lock-does-not-link-local-lbe", !lockInfo.blockedResolved, lockInfo);
|
|
4281
|
+
} else {
|
|
4282
|
+
addCheck2(checks, "package-lock-does-not-link-local-lbe", true, lockInfo);
|
|
4283
|
+
}
|
|
4284
|
+
const ok = checks.every((check) => check.ok);
|
|
4285
|
+
const result = {
|
|
4286
|
+
ok,
|
|
4287
|
+
command: "assert-consumer",
|
|
4288
|
+
package: PACKAGE_NAME,
|
|
4289
|
+
root,
|
|
4290
|
+
classification: ok ? "consumer-project-using-installed-registry-dependency" : "not-proven-consumer-installed-dependency",
|
|
4291
|
+
releaseClaimsAllowed: false,
|
|
4292
|
+
message: ok ? "This project consumes LBE as an installed package dependency. This does not certify LBE release safety." : "This project is not proven to consume LBE only as an installed registry dependency.",
|
|
4293
|
+
checks
|
|
4294
|
+
};
|
|
4295
|
+
console.log(JSON.stringify(result, null, 2));
|
|
4296
|
+
if (!ok) {
|
|
4297
|
+
process.exitCode = 7;
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4300
|
+
|
|
4301
|
+
// src/cli/main.js
|
|
4302
|
+
function toBoolean4(value, defaultValue = false) {
|
|
4303
|
+
if (value === void 0) return defaultValue;
|
|
4304
|
+
if (value === true || value === false) return value;
|
|
4305
|
+
const normalized = String(value).trim().toLowerCase();
|
|
4306
|
+
if (normalized === "true" || normalized === "1" || normalized === "yes") return true;
|
|
4307
|
+
if (normalized === "false" || normalized === "0" || normalized === "no") return false;
|
|
4308
|
+
return defaultValue;
|
|
4309
|
+
}
|
|
4310
|
+
var __dirname2 = path36.dirname(fileURLToPath3(import.meta.url));
|
|
4311
|
+
var packageJsonPath = path36.join(__dirname2, "../../package.json");
|
|
4312
|
+
var packageJson = JSON.parse(fs30.readFileSync(packageJsonPath, "utf-8"));
|
|
4313
|
+
async function main() {
|
|
4314
|
+
const argv = process.argv.slice(2);
|
|
4315
|
+
if (argv.includes("--version")) {
|
|
4316
|
+
console.log(`LetterBlack Sentinel v${packageJson.version}`);
|
|
4317
|
+
process.exit(0);
|
|
4318
|
+
}
|
|
4319
|
+
if (argv.length === 0 || argv.includes("--help") || argv.includes("-h")) {
|
|
4320
|
+
printHelp(packageJson.version);
|
|
4321
|
+
process.exit(0);
|
|
4322
|
+
}
|
|
4323
|
+
const { command, opts } = parseArgs(argv);
|
|
4324
|
+
if (opts.version) {
|
|
4325
|
+
console.log(`LetterBlack Sentinel v${packageJson.version}`);
|
|
4326
|
+
process.exit(0);
|
|
4327
|
+
}
|
|
4328
|
+
if (opts.help || !command || command === "help") {
|
|
4329
|
+
printHelp(packageJson.version);
|
|
4330
|
+
process.exit(0);
|
|
4331
|
+
}
|
|
4332
|
+
try {
|
|
4333
|
+
if (opts["pub-key-file"]) {
|
|
4334
|
+
try {
|
|
4335
|
+
opts["pub-key"] = fs30.readFileSync(path36.resolve(opts["pub-key-file"]), "utf-8").trim();
|
|
4336
|
+
} catch (error) {
|
|
4337
|
+
console.error(`Error reading public key file: ${error.message}`);
|
|
4338
|
+
process.exit(1);
|
|
4339
|
+
}
|
|
4340
|
+
}
|
|
4341
|
+
if (["verify", "dryrun", "run"].includes(command)) {
|
|
4342
|
+
const integrityStrict = toBoolean4(opts["integrity-strict"], false);
|
|
4343
|
+
const integrityManifestPath = path36.resolve(opts["integrity-manifest"] || ".lbe/config/integrity.manifest.json");
|
|
4344
|
+
const integrityResult = await performIntegrityCheck({
|
|
4345
|
+
strict: integrityStrict,
|
|
4346
|
+
manifestPath: integrityManifestPath
|
|
4347
|
+
});
|
|
4348
|
+
if (!integrityResult.valid) {
|
|
4349
|
+
console.error(JSON.stringify({
|
|
4350
|
+
status: "error",
|
|
4351
|
+
error: integrityResult.reason || "INTEGRITY_CHECK_FAILED",
|
|
4352
|
+
message: integrityResult.message
|
|
4353
|
+
}, null, 2));
|
|
4354
|
+
process.exit(8);
|
|
4355
|
+
}
|
|
4356
|
+
}
|
|
4357
|
+
switch (command) {
|
|
4358
|
+
case "init":
|
|
4359
|
+
await initCommand(opts);
|
|
4360
|
+
break;
|
|
4361
|
+
case "verify":
|
|
4362
|
+
await verifyCommand(opts);
|
|
4363
|
+
break;
|
|
4364
|
+
case "dryrun":
|
|
4365
|
+
await dryrunCommand(opts);
|
|
4366
|
+
break;
|
|
4367
|
+
case "run":
|
|
4368
|
+
await runCommand(opts);
|
|
4369
|
+
break;
|
|
4370
|
+
case "audit-verify":
|
|
4371
|
+
await auditVerifyCommand(opts);
|
|
4372
|
+
break;
|
|
4373
|
+
case "integrity-check":
|
|
4374
|
+
await integrityCheckCommand(opts);
|
|
4375
|
+
break;
|
|
4376
|
+
case "integrity-generate":
|
|
4377
|
+
await integrityGenerateCommand(opts);
|
|
4378
|
+
break;
|
|
4379
|
+
case "policy-sign":
|
|
4380
|
+
await policySignCommand(opts);
|
|
4381
|
+
break;
|
|
4382
|
+
case "health":
|
|
4383
|
+
await healthCommand(opts);
|
|
4384
|
+
break;
|
|
4385
|
+
case "policy-add":
|
|
4386
|
+
await policyAddCommand(opts);
|
|
4387
|
+
break;
|
|
4388
|
+
case "observe":
|
|
4389
|
+
case "enforce":
|
|
4390
|
+
await policyModeCommand(command, opts);
|
|
4391
|
+
break;
|
|
4392
|
+
case "status":
|
|
4393
|
+
await statusCommand(opts);
|
|
4394
|
+
break;
|
|
4395
|
+
case "logs":
|
|
4396
|
+
await logsCommand(opts);
|
|
4397
|
+
break;
|
|
4398
|
+
case "open-state":
|
|
4399
|
+
await openStateCommand(opts);
|
|
4400
|
+
break;
|
|
4401
|
+
case "proof":
|
|
4402
|
+
await proofCommand(opts);
|
|
4403
|
+
break;
|
|
4404
|
+
case "assert-consumer":
|
|
4405
|
+
await assertConsumerCommand(opts);
|
|
4406
|
+
break;
|
|
4407
|
+
default:
|
|
4408
|
+
console.error(`Unknown command: ${command}`);
|
|
4409
|
+
printHelp(packageJson.version);
|
|
4410
|
+
process.exit(1);
|
|
4411
|
+
}
|
|
4412
|
+
} catch (error) {
|
|
4413
|
+
console.error(JSON.stringify({
|
|
4414
|
+
status: "error",
|
|
4415
|
+
error: "INTERNAL_ERROR",
|
|
4416
|
+
message: error.message,
|
|
4417
|
+
stack: process.env.DEBUG ? error.stack : void 0
|
|
4418
|
+
}));
|
|
4419
|
+
process.exit(9);
|
|
4420
|
+
}
|
|
4421
|
+
}
|
|
4422
|
+
main().catch((error) => {
|
|
4423
|
+
console.error(JSON.stringify({
|
|
4424
|
+
status: "error",
|
|
4425
|
+
error: "FATAL_ERROR",
|
|
4426
|
+
message: error.message
|
|
4427
|
+
}));
|
|
4428
|
+
process.exit(9);
|
|
4429
|
+
});
|
|
4430
|
+
export {
|
|
4431
|
+
main
|
|
4432
|
+
};
|