@letterblack/lbe-exec 1.2.2

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/dist/index.js ADDED
@@ -0,0 +1,1762 @@
1
+ // src/exec/localExecutor.js
2
+ import crypto5 from "crypto";
3
+ import fs9 from "fs";
4
+ import path10 from "path";
5
+
6
+ // src/core/signature.js
7
+ import nacl from "tweetnacl";
8
+ import { canonicalize } from "json-canonicalize";
9
+ function bytesFromBase64(b64) {
10
+ return Buffer.from(b64, "base64");
11
+ }
12
+ function toBase64(bytes) {
13
+ return Buffer.from(bytes).toString("base64");
14
+ }
15
+ function verifyEd25519({ payloadObj, sigB64, pubKeyB64 }) {
16
+ try {
17
+ const msg = Buffer.from(canonicalize(payloadObj), "utf8");
18
+ const sig = bytesFromBase64(sigB64);
19
+ const pub = bytesFromBase64(pubKeyB64);
20
+ const isValid = nacl.sign.detached.verify(
21
+ new Uint8Array(msg),
22
+ new Uint8Array(sig),
23
+ new Uint8Array(pub)
24
+ );
25
+ return {
26
+ valid: isValid,
27
+ message: isValid ? "Signature verified" : "Signature verification failed"
28
+ };
29
+ } catch (err) {
30
+ return {
31
+ valid: false,
32
+ message: `Signature verification error: ${err.message}`
33
+ };
34
+ }
35
+ }
36
+ function generateKeyPair() {
37
+ const keyPair = nacl.sign.keyPair();
38
+ return {
39
+ publicKey: toBase64(keyPair.publicKey),
40
+ secretKey: toBase64(keyPair.secretKey)
41
+ };
42
+ }
43
+ function signEd25519({ payloadObj, secretKeyB64 }) {
44
+ try {
45
+ const msg = Buffer.from(canonicalize(payloadObj), "utf8");
46
+ const secretKey = bytesFromBase64(secretKeyB64);
47
+ const sig = nacl.sign.detached(new Uint8Array(msg), new Uint8Array(secretKey));
48
+ return {
49
+ signature: toBase64(sig),
50
+ error: null
51
+ };
52
+ } catch (err) {
53
+ return {
54
+ signature: null,
55
+ error: `Signing failed: ${err.message}`
56
+ };
57
+ }
58
+ }
59
+
60
+ // src/core/policyVersionGuard.js
61
+ import fs2 from "fs";
62
+ import path2 from "path";
63
+
64
+ // src/core/atomicWrite.js
65
+ import fs from "fs";
66
+ import path from "path";
67
+ import crypto from "crypto";
68
+ var DEFAULT_LOCK_OPTS = {
69
+ timeoutMs: 5e3,
70
+ // total wait before giving up
71
+ pollMs: 15,
72
+ // base poll interval (jittered)
73
+ staleMs: 3e4
74
+ // lock files older than this are presumed orphaned
75
+ };
76
+ function _lockPathFor(targetPath) {
77
+ return targetPath + ".lock";
78
+ }
79
+ function _tryAcquire(lockPath) {
80
+ try {
81
+ const fd = fs.openSync(lockPath, "wx");
82
+ fs.writeSync(fd, `pid:${process.pid}:${Date.now()}`);
83
+ fs.closeSync(fd);
84
+ return true;
85
+ } catch (err) {
86
+ if (err.code === "EEXIST" || err.code === "EPERM" || err.code === "EBUSY" || err.code === "EACCES") {
87
+ return false;
88
+ }
89
+ throw err;
90
+ }
91
+ }
92
+ function _removeIfStale(lockPath, staleMs) {
93
+ try {
94
+ const stat = fs.statSync(lockPath);
95
+ const ageMs = Date.now() - stat.mtimeMs;
96
+ if (ageMs > staleMs) {
97
+ try {
98
+ fs.unlinkSync(lockPath);
99
+ } catch {
100
+ }
101
+ }
102
+ } catch {
103
+ }
104
+ }
105
+ function _sleepSync(ms) {
106
+ const end = Date.now() + ms;
107
+ while (Date.now() < end) {
108
+ try {
109
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(1, end - Date.now()));
110
+ } catch {
111
+ }
112
+ }
113
+ }
114
+ function withFileLock(targetPath, optsOrFn, maybeFn) {
115
+ const fn = typeof optsOrFn === "function" ? optsOrFn : maybeFn;
116
+ const opts = typeof optsOrFn === "function" ? {} : optsOrFn || {};
117
+ const { timeoutMs, pollMs, staleMs } = { ...DEFAULT_LOCK_OPTS, ...opts };
118
+ const dir = path.dirname(targetPath);
119
+ if (!fs.existsSync(dir)) {
120
+ fs.mkdirSync(dir, { recursive: true });
121
+ }
122
+ const lockPath = _lockPathFor(targetPath);
123
+ const deadline = Date.now() + timeoutMs;
124
+ let acquired = false;
125
+ while (!acquired) {
126
+ acquired = _tryAcquire(lockPath);
127
+ if (acquired) break;
128
+ if (Date.now() >= deadline) {
129
+ _removeIfStale(lockPath, staleMs);
130
+ acquired = _tryAcquire(lockPath);
131
+ if (acquired) break;
132
+ const err = new Error(`withFileLock: timeout acquiring ${lockPath} after ${timeoutMs}ms`);
133
+ err.code = "ELOCKTIMEOUT";
134
+ throw err;
135
+ }
136
+ _removeIfStale(lockPath, staleMs);
137
+ const jitter = Math.floor(Math.random() * pollMs);
138
+ _sleepSync(pollMs + jitter);
139
+ }
140
+ try {
141
+ return fn();
142
+ } finally {
143
+ try {
144
+ fs.unlinkSync(lockPath);
145
+ } catch {
146
+ }
147
+ }
148
+ }
149
+ function atomicWriteFileSync(filePath, data, options = {}) {
150
+ const dir = path.dirname(filePath);
151
+ if (!fs.existsSync(dir)) {
152
+ fs.mkdirSync(dir, { recursive: true });
153
+ }
154
+ const tempFile = path.join(dir, `.tmp-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`);
155
+ try {
156
+ fs.writeFileSync(tempFile, data, options);
157
+ fs.renameSync(tempFile, filePath);
158
+ } catch (error2) {
159
+ try {
160
+ if (fs.existsSync(tempFile)) {
161
+ fs.unlinkSync(tempFile);
162
+ }
163
+ } catch (cleanupError) {
164
+ }
165
+ throw error2;
166
+ }
167
+ }
168
+
169
+ // src/core/policyVersionGuard.js
170
+ function parsePolicyVersion(version) {
171
+ if (typeof version === "number" && Number.isFinite(version)) {
172
+ return { ok: true, kind: "int", parts: [Math.floor(version)], raw: String(version) };
173
+ }
174
+ if (typeof version !== "string" || !version.trim()) {
175
+ return { ok: false, reason: "POLICY_VERSION_INVALID", message: "Policy version is required" };
176
+ }
177
+ const trimmed = version.trim();
178
+ if (/^\d+$/.test(trimmed)) {
179
+ return { ok: true, kind: "int", parts: [Number(trimmed)], raw: trimmed };
180
+ }
181
+ const semver = trimmed.replace(/^v/i, "");
182
+ if (/^\d+(\.\d+){0,2}$/.test(semver)) {
183
+ const parsed = semver.split(".").map((n) => Number(n));
184
+ while (parsed.length < 3) {
185
+ parsed.push(0);
186
+ }
187
+ return { ok: true, kind: "semver", parts: parsed, raw: trimmed };
188
+ }
189
+ return {
190
+ ok: false,
191
+ reason: "POLICY_VERSION_INVALID",
192
+ message: `Unsupported policy version format '${version}' (use integer or semver)`
193
+ };
194
+ }
195
+ function compareVersions(a, b) {
196
+ const len = Math.max(a.parts.length, b.parts.length);
197
+ for (let i = 0; i < len; i++) {
198
+ const av = a.parts[i] ?? 0;
199
+ const bv = b.parts[i] ?? 0;
200
+ if (av > bv) return 1;
201
+ if (av < bv) return -1;
202
+ }
203
+ return 0;
204
+ }
205
+ function parseCreatedAt(createdAt) {
206
+ if (typeof createdAt === "number" && Number.isFinite(createdAt)) {
207
+ const sec = createdAt > 1e12 ? Math.floor(createdAt / 1e3) : Math.floor(createdAt);
208
+ return { ok: true, epochSec: sec };
209
+ }
210
+ if (typeof createdAt !== "string" || !createdAt.trim()) {
211
+ return {
212
+ ok: false,
213
+ reason: "POLICY_CREATED_AT_INVALID",
214
+ message: "Policy createdAt is required"
215
+ };
216
+ }
217
+ const ts = Date.parse(createdAt);
218
+ if (Number.isNaN(ts)) {
219
+ return {
220
+ ok: false,
221
+ reason: "POLICY_CREATED_AT_INVALID",
222
+ message: `Invalid policy createdAt '${createdAt}'`
223
+ };
224
+ }
225
+ return { ok: true, epochSec: Math.floor(ts / 1e3) };
226
+ }
227
+ function loadPolicyState(statePath) {
228
+ if (!fs2.existsSync(statePath)) {
229
+ return {
230
+ schemaVersion: "1",
231
+ lastAccepted: null,
232
+ updatedAt: null
233
+ };
234
+ }
235
+ try {
236
+ const parsed = JSON.parse(fs2.readFileSync(statePath, "utf8"));
237
+ if (!parsed || typeof parsed !== "object") {
238
+ throw new Error("Policy state file has invalid structure");
239
+ }
240
+ return {
241
+ schemaVersion: String(parsed.schemaVersion || "1"),
242
+ lastAccepted: parsed.lastAccepted && typeof parsed.lastAccepted === "object" ? parsed.lastAccepted : null,
243
+ updatedAt: parsed.updatedAt || null
244
+ };
245
+ } catch (err) {
246
+ throw new Error(`Policy state at ${statePath} is corrupt or unreadable: ${err.message}`);
247
+ }
248
+ }
249
+ function savePolicyState(statePath, stateObj) {
250
+ const payload = JSON.stringify(stateObj, null, 2);
251
+ atomicWriteFileSync(statePath, payload, { encoding: "utf8" });
252
+ }
253
+ function validateAndUpdatePolicyVersionState({
254
+ policyObj,
255
+ statePath = path2.resolve("data/policy.state.json"),
256
+ maxCreatedAtSkewSec = 31536e3,
257
+ nowSec = Math.floor(Date.now() / 1e3),
258
+ persist = true
259
+ }) {
260
+ const version = parsePolicyVersion(policyObj?.version);
261
+ if (!version.ok) {
262
+ return {
263
+ ok: false,
264
+ reason: version.reason,
265
+ message: version.message,
266
+ updated: false
267
+ };
268
+ }
269
+ const createdAt = parseCreatedAt(policyObj?.createdAt);
270
+ if (!createdAt.ok) {
271
+ return {
272
+ ok: false,
273
+ reason: createdAt.reason,
274
+ message: createdAt.message,
275
+ updated: false
276
+ };
277
+ }
278
+ const skew = Math.abs(nowSec - createdAt.epochSec);
279
+ const allowedSkew = Number.isFinite(maxCreatedAtSkewSec) && maxCreatedAtSkewSec > 0 ? Math.floor(maxCreatedAtSkewSec) : 31536e3;
280
+ if (skew > allowedSkew) {
281
+ return {
282
+ ok: false,
283
+ reason: "POLICY_CREATED_AT_SKEW_EXCEEDED",
284
+ message: `Policy createdAt skew ${skew}s exceeds allowed ${allowedSkew}s`,
285
+ updated: false
286
+ };
287
+ }
288
+ let state;
289
+ try {
290
+ state = loadPolicyState(statePath);
291
+ } catch (err) {
292
+ return {
293
+ ok: false,
294
+ reason: "POLICY_STATE_CORRUPT",
295
+ message: err.message,
296
+ updated: false
297
+ };
298
+ }
299
+ const previous = state.lastAccepted;
300
+ let previousVersion = null;
301
+ let previousCreatedAt = null;
302
+ let versionCompare = 0;
303
+ if (previous) {
304
+ previousVersion = parsePolicyVersion(previous.version);
305
+ previousCreatedAt = parseCreatedAt(previous.createdAt);
306
+ if (previousVersion.ok && previousCreatedAt.ok) {
307
+ versionCompare = compareVersions(version, previousVersion);
308
+ if (versionCompare < 0) {
309
+ return {
310
+ ok: false,
311
+ reason: "POLICY_VERSION_REGRESSION",
312
+ message: `Policy version regression: current '${version.raw}' < last '${previousVersion.raw}'`,
313
+ updated: false
314
+ };
315
+ }
316
+ if (versionCompare === 0 && createdAt.epochSec < previousCreatedAt.epochSec) {
317
+ return {
318
+ ok: false,
319
+ reason: "POLICY_CREATED_AT_REGRESSION",
320
+ message: `Policy createdAt regression: current '${policyObj.createdAt}' < last '${previous.createdAt}'`,
321
+ updated: false
322
+ };
323
+ }
324
+ if (versionCompare > 0 && createdAt.epochSec < previousCreatedAt.epochSec) {
325
+ return {
326
+ ok: false,
327
+ reason: "POLICY_CREATED_AT_REGRESSION",
328
+ message: `Policy createdAt must be monotonic when version increases`,
329
+ updated: false
330
+ };
331
+ }
332
+ }
333
+ }
334
+ const shouldUpdate = !previous || !previousVersion?.ok || !previousCreatedAt?.ok || versionCompare > 0 || versionCompare === 0 && createdAt.epochSec > previousCreatedAt.epochSec;
335
+ if (persist && shouldUpdate) {
336
+ const nextState = {
337
+ schemaVersion: "1",
338
+ lastAccepted: {
339
+ version: policyObj.version,
340
+ createdAt: policyObj.createdAt,
341
+ environment: policyObj.environment || null
342
+ },
343
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
344
+ };
345
+ savePolicyState(statePath, nextState);
346
+ }
347
+ return {
348
+ ok: true,
349
+ reason: null,
350
+ message: "Policy version guard passed",
351
+ updated: shouldUpdate
352
+ };
353
+ }
354
+
355
+ // runtime/engine.js
356
+ import fs3 from "fs";
357
+ import path3 from "path";
358
+ import { fileURLToPath } from "url";
359
+ var runtimeDir = path3.dirname(fileURLToPath(import.meta.url));
360
+ var wasmPath = path3.join(runtimeDir, "lbe_engine.wasm");
361
+ var POLICY_MESSAGES = {
362
+ 0: { allowed: true, reason: null, message: "Policy check passed" },
363
+ 1: { allowed: false, reason: "POLICY_NOT_CONFIGURED", message: "No policy configured" },
364
+ 2: { allowed: false, reason: "REQUESTER_NOT_ALLOWED", message: "Requester not in policy" },
365
+ 3: { allowed: false, reason: "COMMAND_NOT_ALLOWED", message: "Command not allowed for requester" },
366
+ 4: { allowed: false, reason: "ADAPTER_NOT_ALLOWED", message: "Adapter not allowed" },
367
+ 5: { allowed: false, reason: "NO_FILESYSTEM_ROOTS_DEFINED", message: "No filesystem roots defined for requester" },
368
+ 6: { allowed: false, reason: "CWD_OUTSIDE_ALLOWED_ROOT", message: "Path not under allowed roots" },
369
+ 7: { allowed: false, reason: "PATH_DENIED_BY_PATTERN", message: "Path matches deny pattern" },
370
+ 8: { allowed: false, reason: "SHELL_CMD_DENIED", message: "Shell command not allowed" }
371
+ };
372
+ var SCHEMA_MESSAGES = {
373
+ 0: { valid: true, error: null },
374
+ 1: { valid: false, error: "Missing required field: id" },
375
+ 2: { valid: false, error: "Missing required field: commandId" },
376
+ 3: { valid: false, error: "Missing required field: requesterId" },
377
+ 4: { valid: false, error: "Missing required field: sessionId" },
378
+ 5: { valid: false, error: "Missing required field: timestamp" },
379
+ 6: { valid: false, error: "Missing required field: nonce" },
380
+ 7: { valid: false, error: "Missing required field: requires" },
381
+ 8: { valid: false, error: "Missing required field: payload" },
382
+ 9: { valid: false, error: "Missing required field: signature" },
383
+ 10: { valid: false, error: "Field 'id' is invalid" },
384
+ 11: { valid: false, error: "Field 'commandId' is invalid" },
385
+ 12: { valid: false, error: "Field 'requesterId' is invalid" },
386
+ 13: { valid: false, error: "Field 'sessionId' is invalid" },
387
+ 14: { valid: false, error: "Field 'timestamp' is invalid" },
388
+ 15: { valid: false, error: "Field 'nonce' is invalid" },
389
+ 16: { valid: false, error: "Field 'requires' is invalid" },
390
+ 17: { valid: false, error: "payload: missing required field: adapter" },
391
+ 18: { valid: false, error: "payload: field 'adapter' is invalid" },
392
+ 19: { valid: false, error: "signature: missing required field: alg" },
393
+ 20: { valid: false, error: "signature: missing required field: keyId" },
394
+ 21: { valid: false, error: "signature: missing required field: sig" },
395
+ 22: { valid: false, error: "signature: field 'alg' must be ed25519" },
396
+ 23: { valid: false, error: "signature: field 'sig' is invalid" },
397
+ 24: { valid: false, error: "Field 'risk' is invalid" }
398
+ };
399
+ var KEY_REASONS = {
400
+ 1: "KEY_ID_INVALID",
401
+ 2: "KEY_NOT_TRUSTED",
402
+ 3: "KEY_DEPRECATED",
403
+ 4: "KEY_REQUESTER_MISMATCH",
404
+ 5: "KEY_LIFECYCLE_INVALID",
405
+ 6: "KEY_NOT_YET_VALID",
406
+ 7: "KEY_EXPIRED"
407
+ };
408
+ var PIPELINE_STAGES = {
409
+ 0: "schema",
410
+ 1: "timestamp",
411
+ 2: "key",
412
+ 3: "signature",
413
+ 4: "rate_limit",
414
+ 5: "nonce",
415
+ 6: "policy",
416
+ 255: "ok"
417
+ };
418
+ var RISK_LABELS = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
419
+ var COMMAND_TYPE = { ECHO: 0, READ_FILE: 1, WRITE_FILE: 2, PATCH_FILE: 3, DELETE_FILE: 4, RUN_SHELL: 5 };
420
+ var _instance = null;
421
+ function wasm() {
422
+ if (_instance) return _instance;
423
+ if (!fs3.existsSync(wasmPath)) throw new Error(`LBE engine missing: ${wasmPath}`);
424
+ const bytes = fs3.readFileSync(wasmPath);
425
+ _instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), {});
426
+ return _instance;
427
+ }
428
+ function memory() {
429
+ return new Uint8Array(wasm().exports.memory.buffer);
430
+ }
431
+ function inPtr() {
432
+ return wasm().exports.lbe_in_ptr();
433
+ }
434
+ function outPtr() {
435
+ return wasm().exports.lbe_out_ptr();
436
+ }
437
+ function bufSize() {
438
+ return wasm().exports.lbe_buf_size();
439
+ }
440
+ function writeIn(str) {
441
+ const enc = new TextEncoder().encode(str);
442
+ const mem = memory();
443
+ const ptr = inPtr();
444
+ mem.set(enc, ptr);
445
+ mem[ptr + enc.length] = 0;
446
+ }
447
+ function readOut() {
448
+ const mem = memory();
449
+ const ptr = outPtr();
450
+ let end = ptr;
451
+ while (mem[end] !== 0 && end - ptr < bufSize()) end++;
452
+ return new TextDecoder().decode(mem.slice(ptr, end));
453
+ }
454
+ function writePipelineInput(fields) {
455
+ const mem = memory();
456
+ const ptr = inPtr();
457
+ const view = new DataView(mem.buffer, ptr);
458
+ fields.forEach((v, i) => view.setUint32(i * 4, v >>> 0, true));
459
+ }
460
+ function readPipelineOutput() {
461
+ const mem = memory();
462
+ const ptr = outPtr();
463
+ const view = new DataView(mem.buffer, ptr);
464
+ return { stage: view.getUint32(0, true), code: view.getUint32(4, true) };
465
+ }
466
+ function runValidationPipeline(flags) {
467
+ writePipelineInput([
468
+ // Schema flags [0..24]
469
+ flags.hasId ? 1 : 0,
470
+ flags.idValid ? 1 : 0,
471
+ flags.hasCommandId ? 1 : 0,
472
+ flags.commandIdValid ? 1 : 0,
473
+ flags.hasRequesterId ? 1 : 0,
474
+ flags.requesterIdValid ? 1 : 0,
475
+ flags.hasSessionId ? 1 : 0,
476
+ flags.sessionIdValid ? 1 : 0,
477
+ flags.hasTimestamp ? 1 : 0,
478
+ flags.timestampValid ? 1 : 0,
479
+ flags.hasNonce ? 1 : 0,
480
+ flags.nonceValid ? 1 : 0,
481
+ flags.hasRequires ? 1 : 0,
482
+ flags.requiresValid ? 1 : 0,
483
+ flags.hasPayload ? 1 : 0,
484
+ flags.hasPayloadAdapter ? 1 : 0,
485
+ flags.payloadAdapterValid ? 1 : 0,
486
+ flags.hasSignature ? 1 : 0,
487
+ flags.hasSignatureAlg ? 1 : 0,
488
+ flags.signatureAlgValid ? 1 : 0,
489
+ flags.hasSignatureKeyId ? 1 : 0,
490
+ flags.hasSignatureSig ? 1 : 0,
491
+ flags.signatureSigValid ? 1 : 0,
492
+ flags.hasRisk ? 1 : 0,
493
+ flags.riskValid ? 1 : 0,
494
+ // Timestamp [25..27]
495
+ flags.cmdTimestamp >>> 0,
496
+ flags.nowSec >>> 0,
497
+ flags.maxClockSkewSec >>> 0,
498
+ // Key lifecycle [28..34]
499
+ flags.keyIdFormatValid ? 1 : 0,
500
+ flags.keyFound ? 1 : 0,
501
+ flags.keyNotDeprecated ? 1 : 0,
502
+ flags.keyRequesterMatches ? 1 : 0,
503
+ flags.keyNotBeforeOk ? 1 : 0,
504
+ flags.keyNotExpired ? 1 : 0,
505
+ flags.keyLifecycleFieldsPresent ? 1 : 0,
506
+ // Signature [35]
507
+ flags.signatureValid ? 1 : 0,
508
+ // Rate limit [36..37]
509
+ flags.rateLimitOk ? 1 : 0,
510
+ flags.rateLimitRetryAfterSec >>> 0,
511
+ // Nonce [38]
512
+ flags.nonceOk ? 1 : 0,
513
+ // Policy [39..48]
514
+ flags.policyConfigured ? 1 : 0,
515
+ flags.requesterConfigured ? 1 : 0,
516
+ flags.commandAllowed ? 1 : 0,
517
+ flags.adapterAllowed ? 1 : 0,
518
+ flags.filesystemRequired ? 1 : 0,
519
+ flags.filesystemRootsDefined ? 1 : 0,
520
+ flags.filesystemOk ? 1 : 0,
521
+ flags.pathDenied ? 1 : 0,
522
+ flags.shellRequired ? 1 : 0,
523
+ flags.shellCommandOk ? 1 : 0
524
+ ]);
525
+ wasm().exports.lbe_validate_pipeline();
526
+ const { stage, code } = readPipelineOutput();
527
+ const ok = stage === 255;
528
+ return {
529
+ ok,
530
+ stage,
531
+ stageLabel: PIPELINE_STAGES[stage] || "unknown",
532
+ code,
533
+ schemaError: stage === 0 ? SCHEMA_MESSAGES[code]?.error || "Schema invalid" : null,
534
+ keyReason: stage === 2 ? KEY_REASONS[code] || "KEY_ERROR" : null,
535
+ policyResult: stage === 6 ? { ...POLICY_MESSAGES[code] || POLICY_MESSAGES[1], code } : null,
536
+ retryAfterSec: stage === 4 ? code : 0,
537
+ skewSec: stage === 1 ? code : 0
538
+ };
539
+ }
540
+ function checkNonce({ ttlSec, nowSec, newKey, existingEntries }) {
541
+ const lines = [`${ttlSec}:${nowSec}`, newKey, ...existingEntries].join("\n") + "\n";
542
+ writeIn(lines);
543
+ const isReplay = wasm().exports.lbe_nonce_check() !== 0;
544
+ if (isReplay) return { ok: false, updatedEntriesText: null };
545
+ const out = readOut();
546
+ return { ok: true, updatedEntriesText: out.startsWith("OK\n") ? out.slice(3) : out };
547
+ }
548
+ function checkRateLimit({ windowSec, maxRequests, nowSec, requesterId, existingEntries }) {
549
+ const lines = [
550
+ `${windowSec}:${maxRequests}:${nowSec}`,
551
+ requesterId,
552
+ ...existingEntries
553
+ ].join("\n") + "\n";
554
+ writeIn(lines);
555
+ const exceeded = wasm().exports.lbe_rate_check() !== 0;
556
+ const out = readOut();
557
+ if (exceeded) {
558
+ const retryAfterSec = parseInt(out.match(/^EXCEEDED:(\d+)/)?.[1] ?? "1", 10);
559
+ const entriesText = out.replace(/^EXCEEDED:\d+\n/, "");
560
+ return { ok: false, retryAfterSec, updatedEntriesText: entriesText };
561
+ }
562
+ return { ok: true, retryAfterSec: 0, updatedEntriesText: out.startsWith("OK\n") ? out.slice(3) : out };
563
+ }
564
+ function classifyRisk(commandId, shellCmdIsRm = false) {
565
+ const typeCode = COMMAND_TYPE[commandId] ?? 0;
566
+ const code = wasm().exports.lbe_classify_risk(typeCode, shellCmdIsRm ? 1 : 0);
567
+ return RISK_LABELS[code] ?? "LOW";
568
+ }
569
+
570
+ // src/core/validator.js
571
+ import path4 from "path";
572
+ function extractSchemaFlags(cmd) {
573
+ const has = (k) => cmd != null && Object.prototype.hasOwnProperty.call(cmd, k);
574
+ const isStr = (v) => typeof v === "string";
575
+ const p = cmd?.payload;
576
+ const sig = cmd?.signature;
577
+ return {
578
+ hasId: has("id"),
579
+ idValid: isStr(cmd?.id) && /^[A-Z_]+$/.test(cmd.id) && cmd.id.length >= 1 && cmd.id.length <= 50,
580
+ hasCommandId: has("commandId"),
581
+ commandIdValid: isStr(cmd?.commandId) && /^[a-f0-9-]+$/.test(cmd.commandId) && cmd.commandId.length === 36,
582
+ hasRequesterId: has("requesterId"),
583
+ requesterIdValid: isStr(cmd?.requesterId) && cmd.requesterId.length >= 3 && cmd.requesterId.length <= 100,
584
+ hasSessionId: has("sessionId"),
585
+ sessionIdValid: isStr(cmd?.sessionId) && cmd.sessionId.length >= 3,
586
+ hasTimestamp: has("timestamp"),
587
+ timestampValid: typeof cmd?.timestamp === "number" && cmd.timestamp >= 1e9,
588
+ hasNonce: has("nonce"),
589
+ nonceValid: isStr(cmd?.nonce) && cmd.nonce.length >= 32 && cmd.nonce.length <= 128,
590
+ hasRequires: has("requires"),
591
+ requiresValid: Array.isArray(cmd?.requires) && cmd.requires.length >= 1 && cmd.requires.every(isStr),
592
+ hasPayload: has("payload") && typeof p === "object" && p !== null && !Array.isArray(p),
593
+ hasPayloadAdapter: p != null && Object.prototype.hasOwnProperty.call(p, "adapter"),
594
+ payloadAdapterValid: isStr(p?.adapter),
595
+ hasSignature: has("signature") && typeof sig === "object" && sig !== null && !Array.isArray(sig),
596
+ hasSignatureAlg: sig != null && Object.prototype.hasOwnProperty.call(sig, "alg"),
597
+ signatureAlgValid: sig?.alg === "ed25519",
598
+ hasSignatureKeyId: sig != null && Object.prototype.hasOwnProperty.call(sig, "keyId"),
599
+ hasSignatureSig: sig != null && Object.prototype.hasOwnProperty.call(sig, "sig"),
600
+ signatureSigValid: isStr(sig?.sig) && sig.sig.length >= 10,
601
+ hasRisk: has("risk"),
602
+ riskValid: ["LOW", "MEDIUM", "HIGH", "CRITICAL"].includes(cmd?.risk)
603
+ };
604
+ }
605
+ function extractPolicyFlags(policy, cmd) {
606
+ const hasPolicy = !!(policy && policy.default === "DENY" && policy.requesters && typeof policy.requesters === "object");
607
+ const rp = policy?.requesters?.[cmd.requesterId];
608
+ const cmdId = cmd.id?.toLowerCase() ?? "";
609
+ const commandAllowed = !!rp?.allowCommands?.some((c) => c.toLowerCase() === cmdId);
610
+ const adapterAllowed = !!rp?.allowAdapters?.includes(cmd.payload?.adapter);
611
+ const filesystemRequired = !!cmd.payload?.cwd;
612
+ let filesystemRootsDefined = false;
613
+ let filesystemOk = false;
614
+ let pathDenied = false;
615
+ if (filesystemRequired) {
616
+ const roots = rp?.filesystem?.roots ?? [];
617
+ filesystemRootsDefined = roots.length > 0;
618
+ if (filesystemRootsDefined) {
619
+ const cwd = path4.resolve(cmd.payload.cwd);
620
+ filesystemOk = roots.some((r) => {
621
+ const rr = path4.resolve(r);
622
+ return cwd === rr || cwd.startsWith(rr + path4.sep);
623
+ });
624
+ const denyPatterns = rp?.filesystem?.denyPatterns ?? [];
625
+ pathDenied = denyPatterns.some((pattern) => {
626
+ const re = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$");
627
+ return re.test(cwd);
628
+ });
629
+ }
630
+ }
631
+ let shellRequired = false;
632
+ let shellCommandOk = true;
633
+ if (cmd.id === "RUN_SHELL") {
634
+ shellRequired = true;
635
+ const allowCmds = rp?.exec?.allowCmds ?? [];
636
+ const denyCmds = rp?.exec?.denyCmds ?? [];
637
+ const shellCmd = cmd.payload?.cmd;
638
+ if (denyCmds.includes(shellCmd)) {
639
+ shellCommandOk = false;
640
+ } else {
641
+ shellCommandOk = allowCmds.length === 0 || allowCmds.includes(shellCmd);
642
+ }
643
+ }
644
+ return {
645
+ policyConfigured: hasPolicy,
646
+ requesterConfigured: !!rp,
647
+ commandAllowed,
648
+ adapterAllowed,
649
+ filesystemRequired,
650
+ filesystemRootsDefined,
651
+ filesystemOk,
652
+ pathDenied,
653
+ shellRequired,
654
+ shellCommandOk
655
+ };
656
+ }
657
+ function extractKeyFlags(keyStore, keyId, requesterId, now = /* @__PURE__ */ new Date()) {
658
+ if (!keyStore || !keyId) {
659
+ return {
660
+ keyIdFormatValid: false,
661
+ keyFound: false,
662
+ keyNotDeprecated: false,
663
+ keyRequesterMatches: false,
664
+ keyNotBeforeOk: false,
665
+ keyNotExpired: false,
666
+ keyLifecycleFieldsPresent: false,
667
+ publicKey: null
668
+ };
669
+ }
670
+ const KEY_ID_RE = /^[A-Za-z0-9:_-]{3,128}$/;
671
+ const keyIdFormatValid = KEY_ID_RE.test(keyId) && keyId !== "default";
672
+ if (!keyIdFormatValid) {
673
+ return {
674
+ keyIdFormatValid,
675
+ keyFound: false,
676
+ keyNotDeprecated: false,
677
+ keyRequesterMatches: false,
678
+ keyNotBeforeOk: false,
679
+ keyNotExpired: false,
680
+ keyLifecycleFieldsPresent: false,
681
+ publicKey: null
682
+ };
683
+ }
684
+ const entry = keyStore.trustedKeys?.[keyId];
685
+ const keyFound = !!entry;
686
+ if (!keyFound) {
687
+ return {
688
+ keyIdFormatValid,
689
+ keyFound,
690
+ keyNotDeprecated: false,
691
+ keyRequesterMatches: false,
692
+ keyNotBeforeOk: false,
693
+ keyNotExpired: false,
694
+ keyLifecycleFieldsPresent: false,
695
+ publicKey: null
696
+ };
697
+ }
698
+ const keyNotDeprecated = !entry.deprecated;
699
+ const keyRequesterMatches = !entry.requesterId || entry.requesterId === requesterId;
700
+ const notBefore = entry.notBefore || entry.validFrom;
701
+ const expiresAt = entry.expiresAt || entry.validUntil;
702
+ const keyLifecycleFieldsPresent = typeof notBefore === "string" && typeof expiresAt === "string";
703
+ let keyNotBeforeOk = false;
704
+ let keyNotExpired = false;
705
+ if (keyLifecycleFieldsPresent) {
706
+ const nb = new Date(notBefore);
707
+ const exp = new Date(expiresAt);
708
+ if (!isNaN(nb.getTime()) && !isNaN(exp.getTime()) && nb < exp) {
709
+ keyNotBeforeOk = now >= nb;
710
+ keyNotExpired = now < exp;
711
+ }
712
+ }
713
+ return {
714
+ keyIdFormatValid,
715
+ keyFound,
716
+ keyNotDeprecated,
717
+ keyRequesterMatches,
718
+ keyNotBeforeOk,
719
+ keyNotExpired,
720
+ keyLifecycleFieldsPresent,
721
+ publicKey: entry.publicKey ?? null
722
+ };
723
+ }
724
+ function nonceEntriesToText(db) {
725
+ return (db?.entries ?? []).map((e) => `${e.key}:${e.timestamp}`);
726
+ }
727
+ function textToNonceEntries(text) {
728
+ return text.split("\n").filter(Boolean).map((line) => {
729
+ const lastColon = line.lastIndexOf(":");
730
+ return {
731
+ key: line.slice(0, lastColon),
732
+ timestamp: parseInt(line.slice(lastColon + 1), 10) || 0
733
+ };
734
+ });
735
+ }
736
+ function rateEntriesToText(db) {
737
+ return (db?.entries ?? []).map((e) => `${e.requesterId}:${e.timestamp}`);
738
+ }
739
+ function textToRateEntries(text) {
740
+ return text.split("\n").filter(Boolean).map((line) => {
741
+ const lastColon = line.lastIndexOf(":");
742
+ return {
743
+ requesterId: line.slice(0, lastColon),
744
+ timestamp: parseInt(line.slice(lastColon + 1), 10) || 0
745
+ };
746
+ });
747
+ }
748
+ function validateCommand({
749
+ commandObj,
750
+ pubKeyB64,
751
+ keyStore,
752
+ nonceDb,
753
+ policy,
754
+ rateLimiter,
755
+ policyStatePath
756
+ }) {
757
+ const result = {
758
+ valid: false,
759
+ commandId: commandObj?.commandId,
760
+ checks: {},
761
+ errors: []
762
+ };
763
+ const nowSec = Math.floor(Date.now() / 1e3);
764
+ const now = /* @__PURE__ */ new Date();
765
+ const maxClockSkewSec = Number.isFinite(policy?.security?.maxClockSkewSec) ? policy.security.maxClockSkewSec : 600;
766
+ if (policyStatePath && policy?.version !== void 0) {
767
+ try {
768
+ const vCheck = validateAndUpdatePolicyVersionState({ policyObj: policy, statePath: policyStatePath });
769
+ result.checks.policyVersion = vCheck.ok;
770
+ if (!vCheck.ok) {
771
+ result.errors.push({ type: "POLICY_VERSION_INVALID", message: vCheck.message });
772
+ return result;
773
+ }
774
+ } catch {
775
+ result.checks.policyVersion = true;
776
+ }
777
+ } else {
778
+ result.checks.policyVersion = true;
779
+ }
780
+ const schemaFlags = extractSchemaFlags(commandObj);
781
+ const keyId = commandObj?.signature?.keyId;
782
+ const keyFlags = extractKeyFlags(keyStore, keyId, commandObj?.requesterId, now);
783
+ let signatureValid = false;
784
+ let effectivePubKey = keyFlags.publicKey;
785
+ if (!effectivePubKey && pubKeyB64) effectivePubKey = pubKeyB64;
786
+ if (effectivePubKey) {
787
+ const bodyWithoutSig = { ...commandObj };
788
+ delete bodyWithoutSig.signature;
789
+ const sigCheck = verifyEd25519({
790
+ payloadObj: bodyWithoutSig,
791
+ sigB64: commandObj?.signature?.sig,
792
+ pubKeyB64: effectivePubKey
793
+ });
794
+ signatureValid = sigCheck.valid;
795
+ }
796
+ let rateLimitOk = true;
797
+ let rateLimitRetryAfterSec = 0;
798
+ if (signatureValid && rateLimiter && typeof rateLimiter.db !== "undefined") {
799
+ const rateCfg = policy?.requesters?.[commandObj.requesterId]?.rateLimit || {};
800
+ const dfltCfg = policy?.security?.defaultRateLimit || {};
801
+ const windowSec = rateCfg.windowSec ?? dfltCfg.windowSec ?? 60;
802
+ const maxRequests = rateCfg.maxRequests ?? dfltCfg.maxRequests ?? 30;
803
+ const rateResult = checkRateLimit({
804
+ windowSec,
805
+ maxRequests,
806
+ nowSec,
807
+ requesterId: commandObj.requesterId,
808
+ existingEntries: rateEntriesToText(rateLimiter.db)
809
+ });
810
+ rateLimitOk = rateResult.ok;
811
+ rateLimitRetryAfterSec = rateResult.retryAfterSec;
812
+ if (rateResult.ok) {
813
+ rateLimiter.db.entries = textToRateEntries(rateResult.updatedEntriesText);
814
+ }
815
+ } else if (signatureValid && rateLimiter && typeof rateLimiter.checkAndRecord === "function") {
816
+ const rateCfg = policy?.requesters?.[commandObj.requesterId]?.rateLimit || {};
817
+ const dfltCfg = policy?.security?.defaultRateLimit || {};
818
+ const rateCheck = rateLimiter.checkAndRecord({
819
+ requesterId: commandObj.requesterId,
820
+ nowSec,
821
+ windowSec: rateCfg.windowSec ?? dfltCfg.windowSec ?? 60,
822
+ maxRequests: rateCfg.maxRequests ?? dfltCfg.maxRequests ?? 30
823
+ });
824
+ rateLimitOk = rateCheck.ok;
825
+ rateLimitRetryAfterSec = rateCheck.retryAfterSec ?? 0;
826
+ }
827
+ let nonceOk = true;
828
+ const nonceKey = `${commandObj?.requesterId}|${commandObj?.sessionId}|${commandObj?.nonce}`;
829
+ const ttlSec = 3600;
830
+ if (signatureValid && rateLimitOk && nonceDb) {
831
+ if (typeof nonceDb.checkAndRecord === "function") {
832
+ if (nonceDb.db) {
833
+ const nonceResult = checkNonce({
834
+ ttlSec,
835
+ nowSec,
836
+ newKey: nonceKey,
837
+ existingEntries: nonceEntriesToText(nonceDb.db)
838
+ });
839
+ nonceOk = nonceResult.ok;
840
+ if (nonceResult.ok) {
841
+ nonceDb.db.entries = textToNonceEntries(nonceResult.updatedEntriesText);
842
+ }
843
+ } else {
844
+ const r = nonceDb.checkAndRecord({
845
+ requesterId: commandObj.requesterId,
846
+ sessionId: commandObj.sessionId,
847
+ nonce: commandObj.nonce
848
+ });
849
+ nonceOk = r.ok;
850
+ }
851
+ } else {
852
+ const nonceResult = checkNonce({
853
+ ttlSec,
854
+ nowSec,
855
+ newKey: nonceKey,
856
+ existingEntries: nonceEntriesToText(nonceDb)
857
+ });
858
+ nonceOk = nonceResult.ok;
859
+ if (nonceResult.ok) {
860
+ nonceDb.entries = textToNonceEntries(nonceResult.updatedEntriesText);
861
+ }
862
+ }
863
+ }
864
+ const policyFlags = extractPolicyFlags(policy, commandObj ?? {});
865
+ const pipeline = runValidationPipeline({
866
+ ...schemaFlags,
867
+ cmdTimestamp: commandObj?.timestamp ?? 0,
868
+ nowSec,
869
+ maxClockSkewSec,
870
+ ...keyFlags,
871
+ signatureValid,
872
+ rateLimitOk,
873
+ rateLimitRetryAfterSec,
874
+ nonceOk,
875
+ ...policyFlags
876
+ });
877
+ const s = pipeline.stage;
878
+ result.checks.schema = s !== 0;
879
+ if (s >= 1) result.checks.timestamp = s !== 1;
880
+ if (s >= 2) result.checks.keyId = s !== 2;
881
+ if (s >= 2) result.checks.signature = s !== 2 && s !== 3;
882
+ if (s >= 4) result.checks.rateLimit = s !== 4;
883
+ if (s >= 5) result.checks.nonce = s !== 5;
884
+ if (s >= 6 || pipeline.ok) result.checks.policy = s !== 6;
885
+ if (!pipeline.ok) {
886
+ const stage = pipeline.stageLabel;
887
+ if (stage === "schema") {
888
+ result.errors.push({ type: "SCHEMA_ERROR", message: pipeline.schemaError || "Schema invalid" });
889
+ } else if (stage === "timestamp") {
890
+ result.errors.push({ type: "TIMESTAMP_SKEW_EXCEEDED", message: `Command timestamp skew ${pipeline.skewSec}s exceeds allowed ${maxClockSkewSec}s` });
891
+ } else if (stage === "key") {
892
+ const reason = pipeline.keyReason || "KEY_ERROR";
893
+ const msgs = {
894
+ KEY_ID_INVALID: `Invalid keyId '${keyId}'`,
895
+ KEY_NOT_TRUSTED: `Key '${keyId}' is not in trusted key store`,
896
+ KEY_DEPRECATED: `Key '${keyId}' is deprecated`,
897
+ KEY_REQUESTER_MISMATCH: `Key '${keyId}' is not authorized for requester '${commandObj?.requesterId}'`,
898
+ KEY_LIFECYCLE_INVALID: `Key '${keyId}' must define notBefore and expiresAt`,
899
+ KEY_NOT_YET_VALID: `Key '${keyId}' is not yet valid`,
900
+ KEY_EXPIRED: `Key '${keyId}' has expired`
901
+ };
902
+ result.errors.push({ type: reason, message: msgs[reason] || reason });
903
+ } else if (stage === "signature") {
904
+ result.errors.push({ type: "SIGNATURE_INVALID", message: effectivePubKey ? "Signature verification failed" : "No public key available" });
905
+ } else if (stage === "rate_limit") {
906
+ result.errors.push({ type: "RATE_LIMIT_EXCEEDED", message: `Rate limit exceeded. Retry after ${pipeline.retryAfterSec}s` });
907
+ } else if (stage === "nonce") {
908
+ result.errors.push({ type: "REPLAY_NONCE", message: "Nonce has already been used" });
909
+ } else if (stage === "policy" && pipeline.policyResult) {
910
+ result.errors.push({ type: pipeline.policyResult.reason, message: pipeline.policyResult.message });
911
+ } else {
912
+ result.errors.push({ type: "VALIDATION_FAILED", message: `Failed at stage: ${stage}` });
913
+ }
914
+ return result;
915
+ }
916
+ result.valid = true;
917
+ result.risk = classifyRisk(commandObj.id, commandObj.payload?.cmd === "rm");
918
+ result.message = "Command validation successful";
919
+ return result;
920
+ }
921
+
922
+ // src/adapters/noopAdapter.js
923
+ async function noopAdapter(cmd) {
924
+ return {
925
+ adapter: "noop",
926
+ commandId: cmd.commandId || "unknown",
927
+ command: cmd.id || "unknown",
928
+ status: "completed",
929
+ output: `[NOOP] Would execute: ${cmd.id || "unknown"} on adapter: ${cmd.payload?.adapter || "unknown"}`,
930
+ exitCode: 0,
931
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
932
+ };
933
+ }
934
+
935
+ // src/adapters/shellAdapter.js
936
+ import { spawnSync } from "child_process";
937
+ import path5 from "path";
938
+ import fs4 from "fs";
939
+ function physicalPath(candidate) {
940
+ try {
941
+ return fs4.realpathSync(path5.resolve(candidate));
942
+ } catch {
943
+ return path5.resolve(candidate);
944
+ }
945
+ }
946
+ function normalizeArgs(args) {
947
+ if (args === void 0) return { ok: true, args: [] };
948
+ if (!Array.isArray(args)) {
949
+ return { ok: false, error: "payload.args must be an array" };
950
+ }
951
+ const normalized = [];
952
+ for (const arg of args) {
953
+ if (typeof arg !== "string" && typeof arg !== "number" && typeof arg !== "boolean") {
954
+ return { ok: false, error: "payload.args may only contain string, number, or boolean values" };
955
+ }
956
+ normalized.push(String(arg));
957
+ }
958
+ return { ok: true, args: normalized };
959
+ }
960
+ async function shellAdapter(cmd, policy, requester) {
961
+ const payload = cmd.payload;
962
+ const timeout = Math.min(Math.max(Number(payload.timeoutMs) || 3e4, 1), 3e4);
963
+ const maxOutputSize = Math.min(Math.max(Number(payload.maxOutputBytes) || 1024 * 1024, 1024), 1024 * 1024);
964
+ if (payload.adapter !== "shell") {
965
+ return {
966
+ adapter: "shell",
967
+ commandId: cmd.commandId,
968
+ status: "error",
969
+ error: "Adapter mismatch",
970
+ exitCode: 1
971
+ };
972
+ }
973
+ const allowedCmds = requester?.exec?.allowCmds || [];
974
+ const deniedCmds = requester?.exec?.denyCmds || [];
975
+ if (deniedCmds.includes(payload.cmd)) {
976
+ return {
977
+ adapter: "shell",
978
+ commandId: cmd.commandId,
979
+ status: "blocked",
980
+ error: `Command '${payload.cmd}' is denied`,
981
+ exitCode: 2
982
+ };
983
+ }
984
+ if (allowedCmds.length > 0 && !allowedCmds.includes(payload.cmd)) {
985
+ return {
986
+ adapter: "shell",
987
+ commandId: cmd.commandId,
988
+ status: "blocked",
989
+ error: `Command '${payload.cmd}' not in allowlist`,
990
+ exitCode: 2
991
+ };
992
+ }
993
+ const roots = requester?.filesystem?.roots || [];
994
+ const cwdAllow = roots.some((r) => {
995
+ const resolvedRoot = physicalPath(r);
996
+ const norm = physicalPath(payload.cwd);
997
+ return norm === resolvedRoot || norm.startsWith(resolvedRoot + path5.sep);
998
+ });
999
+ if (!cwdAllow) {
1000
+ return {
1001
+ adapter: "shell",
1002
+ commandId: cmd.commandId,
1003
+ status: "blocked",
1004
+ error: `CWD '${payload.cwd}' not authorized`,
1005
+ exitCode: 2
1006
+ };
1007
+ }
1008
+ const argCheck = normalizeArgs(payload.args);
1009
+ if (!argCheck.ok) {
1010
+ return {
1011
+ adapter: "shell",
1012
+ commandId: cmd.commandId,
1013
+ status: "blocked",
1014
+ error: argCheck.error,
1015
+ exitCode: 2
1016
+ };
1017
+ }
1018
+ try {
1019
+ const result = spawnSync(payload.cmd, argCheck.args, {
1020
+ cwd: payload.cwd,
1021
+ timeout,
1022
+ encoding: "utf8",
1023
+ maxBuffer: maxOutputSize,
1024
+ stdio: ["pipe", "pipe", "pipe"],
1025
+ shell: false
1026
+ });
1027
+ if (result.error) {
1028
+ throw result.error;
1029
+ }
1030
+ const output = `${result.stdout || ""}${result.stderr || ""}`;
1031
+ const exitCode = result.status ?? 1;
1032
+ if (exitCode !== 0) {
1033
+ return {
1034
+ adapter: "shell",
1035
+ commandId: cmd.commandId,
1036
+ command: payload.cmd,
1037
+ status: "error",
1038
+ error: output.substring(0, maxOutputSize) || `Command exited with code ${exitCode}`,
1039
+ exitCode,
1040
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1041
+ };
1042
+ }
1043
+ return {
1044
+ adapter: "shell",
1045
+ commandId: cmd.commandId,
1046
+ command: payload.cmd,
1047
+ status: "completed",
1048
+ output: output.substring(0, maxOutputSize),
1049
+ exitCode: 0,
1050
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1051
+ };
1052
+ } catch (err) {
1053
+ return {
1054
+ adapter: "shell",
1055
+ commandId: cmd.commandId,
1056
+ command: payload.cmd,
1057
+ status: "error",
1058
+ error: err.message,
1059
+ exitCode: err.status || 1,
1060
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1061
+ };
1062
+ }
1063
+ }
1064
+
1065
+ // src/adapters/fileAdapter.js
1066
+ import fs6 from "fs";
1067
+ import path7 from "path";
1068
+
1069
+ // src/core/backup.js
1070
+ import fs5 from "fs";
1071
+ import path6 from "path";
1072
+ import crypto2 from "crypto";
1073
+ function createBackup(filePath, backupDir) {
1074
+ const dir = backupDir || path6.resolve("data/backups");
1075
+ if (!fs5.existsSync(dir)) {
1076
+ fs5.mkdirSync(dir, { recursive: true });
1077
+ }
1078
+ const target = path6.resolve(filePath);
1079
+ const existed = fs5.existsSync(target);
1080
+ let content = null;
1081
+ let hash = null;
1082
+ if (existed) {
1083
+ content = fs5.readFileSync(target);
1084
+ hash = crypto2.createHash("sha256").update(content).digest("hex");
1085
+ }
1086
+ const basename = path6.basename(target).replace(/[^a-zA-Z0-9._-]/g, "_");
1087
+ const backupName = `${Date.now()}-${hash ? hash.slice(0, 8) : "new"}-${basename}`;
1088
+ const backupPath = existed ? path6.join(dir, backupName) : null;
1089
+ if (existed && content !== null) {
1090
+ atomicWriteFileSync(backupPath, content);
1091
+ }
1092
+ return {
1093
+ originalPath: target,
1094
+ backupPath,
1095
+ existed,
1096
+ hash,
1097
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1098
+ };
1099
+ }
1100
+ function restoreBackup(backupMeta) {
1101
+ if (!backupMeta) return { restored: false, error: "No backup metadata" };
1102
+ const { originalPath, backupPath, existed } = backupMeta;
1103
+ if (!existed) {
1104
+ try {
1105
+ if (fs5.existsSync(originalPath)) fs5.unlinkSync(originalPath);
1106
+ return { restored: true, action: "deleted" };
1107
+ } catch (e) {
1108
+ return { restored: false, error: e.message };
1109
+ }
1110
+ }
1111
+ if (!backupPath || !fs5.existsSync(backupPath)) {
1112
+ return { restored: false, error: "Backup file not found at: " + backupPath };
1113
+ }
1114
+ try {
1115
+ const content = fs5.readFileSync(backupPath);
1116
+ atomicWriteFileSync(originalPath, content);
1117
+ return { restored: true, action: "restored" };
1118
+ } catch (e) {
1119
+ return { restored: false, error: e.message };
1120
+ }
1121
+ }
1122
+
1123
+ // src/adapters/fileAdapter.js
1124
+ var MAX_READ_BYTES = 10 * 1024 * 1024;
1125
+ function resolvedTarget(target, cwd) {
1126
+ if (!target) return null;
1127
+ return path7.isAbsolute(target) ? path7.resolve(target) : path7.resolve(cwd || process.cwd(), target);
1128
+ }
1129
+ function isUnderRoot(targetPath, roots) {
1130
+ const norm = resolvePhysicalPath(targetPath);
1131
+ return roots.some((r) => {
1132
+ const root = resolvePhysicalPath(r);
1133
+ return norm === root || norm.startsWith(root + path7.sep);
1134
+ });
1135
+ }
1136
+ function resolvePhysicalPath(candidate) {
1137
+ let current = path7.resolve(candidate);
1138
+ const suffix = [];
1139
+ while (!fs6.existsSync(current)) {
1140
+ const parent = path7.dirname(current);
1141
+ if (parent === current) break;
1142
+ suffix.unshift(path7.basename(current));
1143
+ current = parent;
1144
+ }
1145
+ try {
1146
+ current = fs6.realpathSync(current);
1147
+ } catch {
1148
+ }
1149
+ return path7.join(current, ...suffix);
1150
+ }
1151
+ function matchesDenyPattern(str, patterns) {
1152
+ for (const pattern of patterns || []) {
1153
+ const rx = new RegExp(
1154
+ "^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/\\\\]*") + "$"
1155
+ );
1156
+ if (rx.test(str)) return pattern;
1157
+ }
1158
+ return null;
1159
+ }
1160
+ function blocked(cmd, code, message, exitCode = 2) {
1161
+ return {
1162
+ adapter: "file",
1163
+ commandId: cmd.commandId,
1164
+ status: "blocked",
1165
+ errorCode: code,
1166
+ error: message,
1167
+ exitCode
1168
+ };
1169
+ }
1170
+ function fail(cmd, code, message, backup = null, exitCode = 1) {
1171
+ return {
1172
+ adapter: "file",
1173
+ commandId: cmd.commandId,
1174
+ status: "error",
1175
+ errorCode: code,
1176
+ error: message,
1177
+ backup: backup ? summariseBackup(backup) : null,
1178
+ exitCode
1179
+ };
1180
+ }
1181
+ function summariseBackup(b) {
1182
+ return b ? { path: b.backupPath, existed: b.existed, hash: b.hash, createdAt: b.createdAt } : null;
1183
+ }
1184
+ async function fileAdapter(cmd, policy, requester) {
1185
+ const payload = cmd.payload;
1186
+ const action = payload.action;
1187
+ const cwd = payload.cwd || process.cwd();
1188
+ const target = resolvedTarget(payload.target, cwd);
1189
+ if (!action) return blocked(cmd, "FILE_NO_ACTION", "payload.action is required");
1190
+ if (!target && action !== "noop") return blocked(cmd, "FILE_NO_TARGET", "payload.target is required");
1191
+ const roots = requester?.filesystem?.roots || [];
1192
+ if (roots.length === 0) return blocked(cmd, "FILE_NO_ROOTS", "No filesystem roots defined for requester");
1193
+ if (!isUnderRoot(target, roots)) return blocked(cmd, "FILE_OUTSIDE_ROOT", `'${target}' is outside allowed roots`);
1194
+ const denied = matchesDenyPattern(target, requester?.filesystem?.denyPatterns);
1195
+ if (denied) return blocked(cmd, "FILE_PATH_DENIED", `'${target}' matches deny pattern: ${denied}`);
1196
+ switch (action) {
1197
+ case "read":
1198
+ return doRead(cmd, target);
1199
+ case "write":
1200
+ return doWrite(cmd, target, payload);
1201
+ case "patch":
1202
+ return doPatch(cmd, target, payload);
1203
+ case "delete":
1204
+ return doDelete(cmd, target);
1205
+ default:
1206
+ return blocked(cmd, "FILE_UNKNOWN_ACTION", `Unknown action: '${action}'`);
1207
+ }
1208
+ }
1209
+ function doRead(cmd, target) {
1210
+ if (!fs6.existsSync(target)) return fail(cmd, "FILE_NOT_FOUND", `Not found: ${target}`);
1211
+ try {
1212
+ const stat = fs6.statSync(target);
1213
+ if (stat.size > MAX_READ_BYTES) return fail(cmd, "FILE_TOO_LARGE", "File exceeds 10 MB read limit");
1214
+ const content = fs6.readFileSync(target, "utf8");
1215
+ return {
1216
+ adapter: "file",
1217
+ action: "read",
1218
+ commandId: cmd.commandId,
1219
+ status: "completed",
1220
+ target,
1221
+ output: content,
1222
+ bytesRead: stat.size,
1223
+ exitCode: 0
1224
+ };
1225
+ } catch (e) {
1226
+ return fail(cmd, "FILE_READ_ERROR", e.message);
1227
+ }
1228
+ }
1229
+ function doWrite(cmd, target, payload) {
1230
+ const content = payload.content;
1231
+ if (content === void 0 || content === null) {
1232
+ return fail(cmd, "FILE_MISSING_CONTENT", "payload.content is required for write");
1233
+ }
1234
+ const backup = tryBackup(target);
1235
+ try {
1236
+ atomicWriteFileSync(target, content, { encoding: "utf8" });
1237
+ return {
1238
+ adapter: "file",
1239
+ action: "write",
1240
+ commandId: cmd.commandId,
1241
+ status: "completed",
1242
+ target,
1243
+ backup: summariseBackup(backup),
1244
+ output: `Wrote ${Buffer.byteLength(content, "utf8")} bytes to ${target}`,
1245
+ exitCode: 0
1246
+ };
1247
+ } catch (e) {
1248
+ restoreBackup(backup);
1249
+ return fail(cmd, "FILE_WRITE_ERROR", e.message, backup);
1250
+ }
1251
+ }
1252
+ function doPatch(cmd, target, payload) {
1253
+ const content = payload.content;
1254
+ if (content === void 0 || content === null) {
1255
+ return fail(cmd, "FILE_MISSING_CONTENT", "payload.content is required for patch");
1256
+ }
1257
+ const backup = tryBackup(target);
1258
+ try {
1259
+ atomicWriteFileSync(target, content, { encoding: "utf8" });
1260
+ return {
1261
+ adapter: "file",
1262
+ action: "patch",
1263
+ commandId: cmd.commandId,
1264
+ status: "completed",
1265
+ target,
1266
+ backup: summariseBackup(backup),
1267
+ output: `Patched ${target} (${Buffer.byteLength(content, "utf8")} bytes)`,
1268
+ exitCode: 0
1269
+ };
1270
+ } catch (e) {
1271
+ restoreBackup(backup);
1272
+ return fail(cmd, "FILE_PATCH_ERROR", e.message, backup);
1273
+ }
1274
+ }
1275
+ function doDelete(cmd, target) {
1276
+ if (!fs6.existsSync(target)) return fail(cmd, "FILE_NOT_FOUND", `Not found: ${target}`);
1277
+ const backup = tryBackup(target);
1278
+ try {
1279
+ fs6.unlinkSync(target);
1280
+ return {
1281
+ adapter: "file",
1282
+ action: "delete",
1283
+ commandId: cmd.commandId,
1284
+ status: "completed",
1285
+ target,
1286
+ backup: summariseBackup(backup),
1287
+ output: `Deleted ${target}`,
1288
+ exitCode: 0
1289
+ };
1290
+ } catch (e) {
1291
+ restoreBackup(backup);
1292
+ return fail(cmd, "FILE_DELETE_ERROR", e.message, backup);
1293
+ }
1294
+ }
1295
+ function tryBackup(target) {
1296
+ try {
1297
+ return createBackup(target);
1298
+ } catch {
1299
+ return null;
1300
+ }
1301
+ }
1302
+
1303
+ // src/adapters/index.js
1304
+ var ADAPTERS = {
1305
+ noop: noopAdapter,
1306
+ shell: shellAdapter,
1307
+ file: fileAdapter
1308
+ };
1309
+ function getAdapter(name) {
1310
+ return ADAPTERS[name];
1311
+ }
1312
+ async function executeAdapter(adapterName, cmd, policy, requester) {
1313
+ const adapter = getAdapter(adapterName);
1314
+ if (!adapter) {
1315
+ return {
1316
+ adapter: adapterName,
1317
+ commandId: cmd.commandId,
1318
+ status: "error",
1319
+ error: `Adapter '${adapterName}' not found`,
1320
+ exitCode: 1
1321
+ };
1322
+ }
1323
+ try {
1324
+ return await adapter(cmd, policy, requester);
1325
+ } catch (err) {
1326
+ return {
1327
+ adapter: adapterName,
1328
+ commandId: cmd.commandId,
1329
+ status: "error",
1330
+ error: `Adapter execution failed: ${err.message}`,
1331
+ exitCode: 9
1332
+ };
1333
+ }
1334
+ }
1335
+ var AVAILABLE_ADAPTERS = Object.keys(ADAPTERS);
1336
+
1337
+ // src/core/auditLog.js
1338
+ import fs7 from "fs";
1339
+ import path8 from "path";
1340
+ import crypto3 from "crypto";
1341
+ function sha256(str) {
1342
+ return crypto3.createHash("sha256").update(str).digest("hex");
1343
+ }
1344
+ function getLastHash(logPath) {
1345
+ try {
1346
+ if (!fs7.existsSync(logPath)) return "GENESIS";
1347
+ const content = fs7.readFileSync(logPath, "utf8").trim();
1348
+ if (!content) return "GENESIS";
1349
+ const lines = content.split("\n");
1350
+ const lastLine = lines[lines.length - 1];
1351
+ try {
1352
+ const lastEntry = JSON.parse(lastLine);
1353
+ return lastEntry.hash || "GENESIS";
1354
+ } catch (err) {
1355
+ return "GENESIS";
1356
+ }
1357
+ } catch (err) {
1358
+ return "GENESIS";
1359
+ }
1360
+ }
1361
+ function appendAudit(logPath, entry) {
1362
+ const dir = path8.dirname(logPath);
1363
+ if (!fs7.existsSync(dir)) {
1364
+ fs7.mkdirSync(dir, { recursive: true });
1365
+ }
1366
+ let result;
1367
+ withFileLock(logPath, () => {
1368
+ const prevHash = getLastHash(logPath);
1369
+ const record = {
1370
+ ...entry,
1371
+ prevHash,
1372
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1373
+ };
1374
+ delete record.hash;
1375
+ const recordStr = JSON.stringify(record);
1376
+ const hash = sha256(recordStr);
1377
+ const final = JSON.stringify({ ...record, hash });
1378
+ let existingContent = "";
1379
+ if (fs7.existsSync(logPath)) {
1380
+ existingContent = fs7.readFileSync(logPath, "utf8");
1381
+ }
1382
+ try {
1383
+ atomicWriteFileSync(logPath, existingContent + final + "\n", { encoding: "utf8" });
1384
+ } catch (err) {
1385
+ throw new Error(`Audit log write failed: ${err.message}`);
1386
+ }
1387
+ result = {
1388
+ success: true,
1389
+ hash,
1390
+ prevHash,
1391
+ message: "Audit entry appended"
1392
+ };
1393
+ });
1394
+ return result;
1395
+ }
1396
+ function verifyAuditLogIntegrity(logPath, options = {}) {
1397
+ const failFast = options.failFast !== false;
1398
+ const maxEntries = Number.isFinite(options.maxEntries) && options.maxEntries > 0 ? Math.floor(options.maxEntries) : null;
1399
+ const response = {
1400
+ ok: true,
1401
+ file: path8.resolve(logPath),
1402
+ entries: 0,
1403
+ valid: true,
1404
+ firstInvalidIndex: null,
1405
+ reason: null,
1406
+ errors: [],
1407
+ message: "Audit log verified"
1408
+ };
1409
+ try {
1410
+ if (!fs7.existsSync(logPath)) {
1411
+ response.message = "Audit log file not found (treated as empty)";
1412
+ return response;
1413
+ }
1414
+ const raw = fs7.readFileSync(logPath, "utf8").trim();
1415
+ if (!raw) {
1416
+ response.message = "Empty audit log";
1417
+ return response;
1418
+ }
1419
+ const allLines = raw.split("\n");
1420
+ const lines = maxEntries ? allLines.slice(0, maxEntries) : allLines;
1421
+ response.entries = lines.length;
1422
+ let expectedPrevHash = "GENESIS";
1423
+ for (let i = 0; i < lines.length; i++) {
1424
+ let entry;
1425
+ try {
1426
+ entry = JSON.parse(lines[i]);
1427
+ } catch {
1428
+ const errObj = {
1429
+ index: i,
1430
+ reason: "INVALID_JSON_LINE",
1431
+ message: `Line ${i} is not valid JSON`
1432
+ };
1433
+ response.valid = false;
1434
+ response.ok = false;
1435
+ response.firstInvalidIndex ??= i;
1436
+ response.reason ??= errObj.reason;
1437
+ response.errors.push(errObj);
1438
+ if (failFast) break;
1439
+ continue;
1440
+ }
1441
+ if (entry.prevHash !== expectedPrevHash) {
1442
+ const errObj = {
1443
+ index: i,
1444
+ reason: "PREV_HASH_MISMATCH",
1445
+ message: `Expected prevHash '${expectedPrevHash}', got '${entry.prevHash}'`
1446
+ };
1447
+ response.valid = false;
1448
+ response.ok = false;
1449
+ response.firstInvalidIndex ??= i;
1450
+ response.reason ??= errObj.reason;
1451
+ response.errors.push(errObj);
1452
+ if (failFast) break;
1453
+ }
1454
+ const recordCopy = { ...entry };
1455
+ const recordHash = recordCopy.hash;
1456
+ delete recordCopy.hash;
1457
+ const expectedHash = sha256(JSON.stringify(recordCopy));
1458
+ if (recordHash !== expectedHash) {
1459
+ const errObj = {
1460
+ index: i,
1461
+ reason: "HASH_MISMATCH",
1462
+ message: `Expected hash '${expectedHash}', got '${recordHash}'`
1463
+ };
1464
+ response.valid = false;
1465
+ response.ok = false;
1466
+ response.firstInvalidIndex ??= i;
1467
+ response.reason ??= errObj.reason;
1468
+ response.errors.push(errObj);
1469
+ if (failFast) break;
1470
+ }
1471
+ expectedPrevHash = recordHash;
1472
+ }
1473
+ response.message = response.valid ? `Audit log verified: ${response.entries} entries` : `Audit log integrity failed at index ${response.firstInvalidIndex}`;
1474
+ return response;
1475
+ } catch (err) {
1476
+ return {
1477
+ ok: false,
1478
+ file: path8.resolve(logPath),
1479
+ entries: 0,
1480
+ valid: false,
1481
+ firstInvalidIndex: null,
1482
+ reason: "AUDIT_VERIFY_ERROR",
1483
+ errors: [{ index: null, reason: "AUDIT_VERIFY_ERROR", message: err.message }],
1484
+ message: `Integrity check failed: ${err.message}`
1485
+ };
1486
+ }
1487
+ }
1488
+
1489
+ // src/core/localPolicy.js
1490
+ import fs8 from "fs";
1491
+ import path9 from "path";
1492
+ import crypto4 from "crypto";
1493
+ var POLICY_FILE = "lbe.policy.json";
1494
+ var AUDIT_FILE = "lbe.audit.jsonl";
1495
+ function glob(pattern) {
1496
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
1497
+ return new RegExp("^" + escaped.replace(/\*\*\//g, "(?:.*/)?").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$");
1498
+ }
1499
+ function relative(root, value) {
1500
+ const rel = path9.relative(root, path9.resolve(value));
1501
+ return rel.split(path9.sep).join("/");
1502
+ }
1503
+ function localPolicyPaths(rootDir) {
1504
+ const root = path9.resolve(rootDir || process.cwd());
1505
+ return { root, policyPath: path9.join(root, POLICY_FILE), auditPath: path9.join(root, AUDIT_FILE) };
1506
+ }
1507
+ function loadLocalPolicy(rootDir, mode = "observe") {
1508
+ const paths = localPolicyPaths(rootDir);
1509
+ if (!fs8.existsSync(paths.policyPath)) {
1510
+ return { ...paths, policy: { version: 1, mode, workspace: paths.root, rules: [] } };
1511
+ }
1512
+ const policy = JSON.parse(fs8.readFileSync(paths.policyPath, "utf8"));
1513
+ if (policy?.version !== 1 || !["observe", "enforce"].includes(policy.mode) || !Array.isArray(policy.rules)) {
1514
+ throw new Error(`Invalid ${POLICY_FILE}`);
1515
+ }
1516
+ return { ...paths, policy };
1517
+ }
1518
+ function writeLocalPolicy(rootDir, policy) {
1519
+ const { policyPath, root } = localPolicyPaths(rootDir);
1520
+ const next = { ...policy, version: 1, workspace: root, rules: Array.isArray(policy.rules) ? policy.rules : [] };
1521
+ atomicWriteFileSync(policyPath, JSON.stringify(next, null, 2) + "\n", { encoding: "utf8" });
1522
+ return next;
1523
+ }
1524
+ function addLocalPolicyRule(rootDir, rule, mode) {
1525
+ if (!rule || !["allow", "deny"].includes(rule.effect) || !["path", "command"].includes(rule.type) || typeof rule.pattern !== "string" || !rule.pattern || typeof rule.from !== "string" || !rule.from) {
1526
+ throw new Error("Rule requires effect, type, pattern, and from");
1527
+ }
1528
+ const loaded = loadLocalPolicy(rootDir, mode);
1529
+ const entry = {
1530
+ id: rule.id || crypto4.randomUUID(),
1531
+ effect: rule.effect,
1532
+ type: rule.type,
1533
+ pattern: rule.pattern,
1534
+ from: rule.from,
1535
+ at: rule.at || (/* @__PURE__ */ new Date()).toISOString()
1536
+ };
1537
+ writeLocalPolicy(loaded.root, { ...loaded.policy, mode: mode || loaded.policy.mode, rules: [...loaded.policy.rules, entry] });
1538
+ return { id: entry.id, added: true, rule: entry };
1539
+ }
1540
+ function proposePolicyRule(rule) {
1541
+ return { ...rule, proposed: true, at: (/* @__PURE__ */ new Date()).toISOString() };
1542
+ }
1543
+ function evaluateLocalPolicy(policy, rootDir, { target, command } = {}) {
1544
+ const root = path9.resolve(rootDir);
1545
+ const candidates = [];
1546
+ if (target) candidates.push({ type: "path", value: relative(root, target) });
1547
+ if (command) candidates.push({ type: "command", value: command });
1548
+ const matched = policy.rules.filter((rule) => candidates.some((c) => c.type === rule.type && glob(rule.pattern).test(c.value)));
1549
+ const denied = matched.filter((rule) => rule.effect === "deny");
1550
+ return { allowed: denied.length === 0, matched, winningRules: denied.length ? denied : matched.filter((r) => r.effect === "allow"), reason: denied.length ? "LOCAL_POLICY_DENY" : null };
1551
+ }
1552
+ function auditLocalPolicy(rootDir, entry) {
1553
+ const { auditPath } = localPolicyPaths(rootDir);
1554
+ appendAudit(auditPath, { kind: "local_policy", timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
1555
+ }
1556
+
1557
+ // src/exec/localExecutor.js
1558
+ var INTENTS = {
1559
+ read_file: { id: "READ_FILE", adapter: "file", action: "read" },
1560
+ write_file: { id: "WRITE_FILE", adapter: "file", action: "write" },
1561
+ patch_file: { id: "PATCH_FILE", adapter: "file", action: "patch" },
1562
+ delete_file: { id: "DELETE_FILE", adapter: "file", action: "delete" },
1563
+ run_shell: { id: "RUN_SHELL", adapter: "shell", action: "run" }
1564
+ };
1565
+ var MUTATIONS = /* @__PURE__ */ new Set(["write_file", "patch_file", "delete_file"]);
1566
+ function error(code, message, recoverable = false) {
1567
+ return { ok: false, decision: "deny", executed: false, dryRun: false, error: { code, message, recoverable } };
1568
+ }
1569
+ function commandPolicy(rootDir, actor, shell = {}) {
1570
+ const now = /* @__PURE__ */ new Date();
1571
+ const expires = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1e3);
1572
+ return {
1573
+ version: 1,
1574
+ default: "DENY",
1575
+ requesters: {
1576
+ [actor]: {
1577
+ allowCommands: Object.values(INTENTS).map((item) => item.id),
1578
+ allowAdapters: ["file", "shell"],
1579
+ filesystem: { roots: [rootDir], denyPatterns: [] },
1580
+ exec: { allowCmds: shell.allowCommands || [], denyCmds: shell.denyCommands || [] },
1581
+ rateLimit: { windowSec: 60, maxRequests: shell.maxRequests || 60 }
1582
+ }
1583
+ },
1584
+ security: { maxClockSkewSec: 600, defaultRateLimit: { windowSec: 60, maxRequests: 60 } },
1585
+ _keyWindow: { notBefore: now.toISOString(), expiresAt: expires.toISOString() }
1586
+ };
1587
+ }
1588
+ function physicalPath2(candidate) {
1589
+ let current = path10.resolve(candidate);
1590
+ const suffix = [];
1591
+ while (!fs9.existsSync(current)) {
1592
+ const parent = path10.dirname(current);
1593
+ if (parent === current) break;
1594
+ suffix.unshift(path10.basename(current));
1595
+ current = parent;
1596
+ }
1597
+ try {
1598
+ current = fs9.realpathSync(current);
1599
+ } catch {
1600
+ }
1601
+ return path10.join(current, ...suffix);
1602
+ }
1603
+ function underRoot(candidate, root) {
1604
+ const target = physicalPath2(candidate);
1605
+ const resolvedRoot = physicalPath2(root);
1606
+ return target === resolvedRoot || target.startsWith(resolvedRoot + path10.sep);
1607
+ }
1608
+ function normalize(rootDir, request, shell = {}) {
1609
+ if (!request || typeof request !== "object") return { error: error("REQUEST_INVALID", "request must be an object") };
1610
+ const detail = INTENTS[request.intent];
1611
+ if (!detail) return { error: error("INTENT_UNSUPPORTED", `Unsupported intent '${request.intent}'`) };
1612
+ const actor = typeof request.actor === "string" && request.actor ? request.actor : "agent:local";
1613
+ let target = null;
1614
+ if (detail.adapter === "file") {
1615
+ if (typeof request.target !== "string" || !request.target) return { error: error("TARGET_REQUIRED", "target is required for file intents") };
1616
+ target = path10.resolve(rootDir, request.target);
1617
+ if (!underRoot(target, rootDir)) return { error: error("PATH_OUTSIDE_ROOT", "target is outside project root") };
1618
+ if (["write_file", "patch_file"].includes(request.intent) && typeof request.content !== "string") {
1619
+ return { error: error("CONTENT_REQUIRED", "content is required for write and patch") };
1620
+ }
1621
+ }
1622
+ let command = null;
1623
+ if (detail.adapter === "shell") {
1624
+ command = request.command;
1625
+ if (!command || typeof command.cmd !== "string" || !Array.isArray(command.args) || command.args.some((arg) => typeof arg !== "string")) {
1626
+ return { error: error("COMMAND_INVALID", "command requires cmd and string args") };
1627
+ }
1628
+ const cwd = path10.resolve(rootDir, command.cwd || ".");
1629
+ if (!underRoot(cwd, rootDir)) return { error: error("CWD_OUTSIDE_ROOT", "command cwd is outside project root") };
1630
+ if (!Array.isArray(shell.allowCommands) || !shell.allowCommands.includes(command.cmd)) {
1631
+ return { error: error("SHELL_NOT_ALLOWLISTED", `command '${command.cmd}' is not explicitly allowlisted`) };
1632
+ }
1633
+ if (shell.denyCommands?.includes(command.cmd)) return { error: error("SHELL_DENIED", `command '${command.cmd}' is denied`) };
1634
+ command = { ...command, cwd, timeoutMs: Math.min(Math.max(command.timeoutMs || 3e4, 1), 3e4), maxOutputBytes: Math.min(Math.max(command.maxOutputBytes || 1024 * 1024, 1024), 1024 * 1024) };
1635
+ }
1636
+ return { actor, detail, target, command, request };
1637
+ }
1638
+ function envelope(normalized, keyId, secretKey) {
1639
+ const { actor, detail, target, command, request } = normalized;
1640
+ const body = {
1641
+ id: detail.id,
1642
+ risk: MUTATIONS.has(request.intent) ? "MEDIUM" : "LOW",
1643
+ commandId: crypto5.randomUUID(),
1644
+ requesterId: actor,
1645
+ sessionId: "local-host",
1646
+ timestamp: Math.floor(Date.now() / 1e3),
1647
+ nonce: crypto5.randomBytes(32).toString("hex"),
1648
+ requires: ["policy", "signature"],
1649
+ payload: {
1650
+ adapter: detail.adapter,
1651
+ action: detail.action,
1652
+ target,
1653
+ content: request.content,
1654
+ cmd: command?.cmd,
1655
+ args: command?.args,
1656
+ timeoutMs: command?.timeoutMs,
1657
+ maxOutputBytes: command?.maxOutputBytes,
1658
+ cwd: command?.cwd || (target ? path10.dirname(target) : process.cwd())
1659
+ }
1660
+ };
1661
+ const signed = signEd25519({ payloadObj: body, secretKeyB64: secretKey });
1662
+ if (signed.error) throw new Error(signed.error);
1663
+ return { ...body, signature: { alg: "ed25519", keyId, sig: signed.signature } };
1664
+ }
1665
+ function createLocalExecutor(options = {}) {
1666
+ const rootDir = path10.resolve(options.rootDir || process.cwd());
1667
+ const keyId = options.keyId || "host:local-exec";
1668
+ const keyPair = options.keyPair || generateKeyPair();
1669
+ const shell = options.shell || {};
1670
+ function prepare(request, { recordNonce = false } = {}) {
1671
+ const normalized = normalize(rootDir, request, shell);
1672
+ if (normalized.error) return normalized;
1673
+ const local = loadLocalPolicy(rootDir, options.mode || "enforce");
1674
+ const localDecision = evaluateLocalPolicy(local.policy, rootDir, { target: normalized.target, command: normalized.command?.cmd });
1675
+ const localBlocked = local.policy.mode === "enforce" && !localDecision.allowed;
1676
+ if (localBlocked) return { error: error("LOCAL_POLICY_DENY", `Blocked by rule(s): ${localDecision.winningRules.map((rule) => rule.id).join(", ")}`), local, localDecision, normalized };
1677
+ const policy = commandPolicy(rootDir, normalized.actor, shell);
1678
+ const keyStore = { defaultKeyId: keyId, trustedKeys: { [keyId]: { publicKey: keyPair.publicKey, notBefore: policy._keyWindow.notBefore, expiresAt: policy._keyWindow.expiresAt, deprecated: false } } };
1679
+ delete policy._keyWindow;
1680
+ const proposal = envelope(normalized, keyId, keyPair.secretKey);
1681
+ const nonceDb = { entries: [] };
1682
+ const validation = validateCommand({ commandObj: proposal, keyStore, nonceDb: recordNonce ? nonceDb : { entries: [] }, policy });
1683
+ if (!validation.valid) return { error: error(validation.errors[0]?.type || "VALIDATION_FAILED", validation.errors[0]?.message || "Validation failed"), local, localDecision, normalized, proposal, policy, validation };
1684
+ return { local, localDecision, normalized, proposal, policy, validation };
1685
+ }
1686
+ async function dryRun(request) {
1687
+ const prepared = prepare(request);
1688
+ if (prepared.error) return { ...prepared.error, dryRun: true };
1689
+ return {
1690
+ ok: true,
1691
+ decision: prepared.local.policy.mode === "observe" ? "observe" : "allow",
1692
+ executed: false,
1693
+ dryRun: true,
1694
+ matchedRules: prepared.localDecision.winningRules.map((rule) => rule.id),
1695
+ rollback: { available: MUTATIONS.has(prepared.normalized.request.intent), performed: false }
1696
+ };
1697
+ }
1698
+ async function execute(request) {
1699
+ const prepared = prepare(request, { recordNonce: true });
1700
+ if (prepared.error) {
1701
+ auditLocalPolicy(rootDir, { action: request?.intent, actor: request?.actor || "agent:local", decision: "deny", error: prepared.error.error.code });
1702
+ return prepared.error;
1703
+ }
1704
+ if (prepared.local.policy.mode === "observe") {
1705
+ appendAudit(path10.join(rootDir, "lbe.audit.jsonl"), {
1706
+ kind: "local_execution",
1707
+ commandId: prepared.proposal.commandId,
1708
+ requesterId: prepared.normalized.actor,
1709
+ intent: prepared.normalized.request.intent,
1710
+ decision: "observe",
1711
+ status: "observed"
1712
+ });
1713
+ return {
1714
+ ok: true,
1715
+ decision: "observe",
1716
+ executed: false,
1717
+ dryRun: false,
1718
+ matchedRules: prepared.localDecision.winningRules.map((r) => r.id),
1719
+ rollback: { available: false, performed: false }
1720
+ };
1721
+ }
1722
+ const requester = prepared.policy.requesters[prepared.normalized.actor];
1723
+ const adapterResult = await executeAdapter(prepared.normalized.detail.adapter, prepared.proposal, prepared.policy, requester);
1724
+ const ok = adapterResult.status === "completed";
1725
+ const audit = appendAudit(path10.join(rootDir, "lbe.audit.jsonl"), {
1726
+ kind: "local_execution",
1727
+ commandId: prepared.proposal.commandId,
1728
+ requesterId: prepared.normalized.actor,
1729
+ intent: prepared.normalized.request.intent,
1730
+ decision: ok ? "allow" : "deny",
1731
+ status: adapterResult.status
1732
+ });
1733
+ return {
1734
+ ok,
1735
+ decision: ok ? "allow" : "deny",
1736
+ executed: ok,
1737
+ dryRun: false,
1738
+ matchedRules: prepared.localDecision.winningRules.map((rule) => rule.id),
1739
+ auditId: audit.hash,
1740
+ rollback: { available: MUTATIONS.has(prepared.normalized.request.intent), performed: false, backupId: adapterResult.backup?.hash },
1741
+ ...ok ? {} : { error: { code: adapterResult.errorCode || "EXECUTION_FAILED", message: adapterResult.error || "Execution failed", recoverable: true } }
1742
+ };
1743
+ }
1744
+ return {
1745
+ rootDir,
1746
+ validate: async (request) => {
1747
+ const preview = await dryRun(request);
1748
+ return { ...preview, dryRun: false, executed: false };
1749
+ },
1750
+ dryRun,
1751
+ execute,
1752
+ policy: {
1753
+ read: () => loadLocalPolicy(rootDir, options.mode || "enforce").policy,
1754
+ proposeRule: proposePolicyRule,
1755
+ addRule: (rule) => addLocalPolicyRule(rootDir, rule, options.mode || "enforce")
1756
+ },
1757
+ audit: { verify: () => verifyAuditLogIntegrity(path10.join(rootDir, "lbe.audit.jsonl")) }
1758
+ };
1759
+ }
1760
+ export {
1761
+ createLocalExecutor
1762
+ };