@letterblack/lbe-exec 1.2.17 → 1.2.19
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/TRUST.md +90 -0
- package/dist/cli.js +41 -2845
- package/dist/index.js +14 -1835
- package/hooks/register.cjs +5 -476
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,1835 +1,14 @@
|
|
|
1
|
-
// src/exec/localExecutor.js
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
function bytesFromBase64(b64) {
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
function
|
|
13
|
-
|
|
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(".lbe/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(".lbe/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
|
-
var FORBIDDEN_CONTENT = [
|
|
1609
|
-
/\beval\s*\(/i,
|
|
1610
|
-
/\bFunction\s*\(/i,
|
|
1611
|
-
/\bexec\s*\(/i,
|
|
1612
|
-
/\brequire\s*\(/,
|
|
1613
|
-
/\bimport\s*\(/,
|
|
1614
|
-
/\bchild_process\b/,
|
|
1615
|
-
/\b__proto__\b/,
|
|
1616
|
-
/\bconstructor\s*\[/,
|
|
1617
|
-
/evalScript/i
|
|
1618
|
-
];
|
|
1619
|
-
function scanContent(value, fieldName) {
|
|
1620
|
-
if (typeof value !== "string") return null;
|
|
1621
|
-
for (const pattern of FORBIDDEN_CONTENT) {
|
|
1622
|
-
if (pattern.test(value)) {
|
|
1623
|
-
return error("PAYLOAD_CONTENT_REJECTED", `Forbidden pattern in ${fieldName}: ${pattern}`);
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
return null;
|
|
1627
|
-
}
|
|
1628
|
-
function normalize(rootDir, request, shell = {}) {
|
|
1629
|
-
if (!request || typeof request !== "object") return { error: error("REQUEST_INVALID", "request must be an object") };
|
|
1630
|
-
const detail = INTENTS[request.intent];
|
|
1631
|
-
if (!detail) return { error: error("INTENT_UNSUPPORTED", `Unsupported intent '${request.intent}'`) };
|
|
1632
|
-
const actor = typeof request.actor === "string" && request.actor ? request.actor : "agent:local";
|
|
1633
|
-
let target = null;
|
|
1634
|
-
if (detail.adapter === "file") {
|
|
1635
|
-
if (typeof request.target !== "string" || !request.target) return { error: error("TARGET_REQUIRED", "target is required for file intents") };
|
|
1636
|
-
target = path10.resolve(rootDir, request.target);
|
|
1637
|
-
if (!underRoot(target, rootDir)) return { error: error("PATH_OUTSIDE_ROOT", "target is outside project root") };
|
|
1638
|
-
if (["write_file", "patch_file"].includes(request.intent) && typeof request.content !== "string") {
|
|
1639
|
-
return { error: error("CONTENT_REQUIRED", "content is required for write and patch") };
|
|
1640
|
-
}
|
|
1641
|
-
const contentScan = scanContent(request.content, "content");
|
|
1642
|
-
if (contentScan) return { error: contentScan };
|
|
1643
|
-
}
|
|
1644
|
-
let command = null;
|
|
1645
|
-
if (detail.adapter === "shell") {
|
|
1646
|
-
command = request.command;
|
|
1647
|
-
if (!command || typeof command.cmd !== "string" || !Array.isArray(command.args) || command.args.some((arg) => typeof arg !== "string")) {
|
|
1648
|
-
return { error: error("COMMAND_INVALID", "command requires cmd and string args") };
|
|
1649
|
-
}
|
|
1650
|
-
const cwd = path10.resolve(rootDir, command.cwd || ".");
|
|
1651
|
-
if (!underRoot(cwd, rootDir)) return { error: error("CWD_OUTSIDE_ROOT", "command cwd is outside project root") };
|
|
1652
|
-
if (!Array.isArray(shell.allowCommands) || !shell.allowCommands.includes(command.cmd)) {
|
|
1653
|
-
return { error: error("SHELL_NOT_ALLOWLISTED", `command '${command.cmd}' is not explicitly allowlisted`) };
|
|
1654
|
-
}
|
|
1655
|
-
if (shell.denyCommands?.includes(command.cmd)) return { error: error("SHELL_DENIED", `command '${command.cmd}' is denied`) };
|
|
1656
|
-
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) };
|
|
1657
|
-
}
|
|
1658
|
-
return { actor, detail, target, command, request };
|
|
1659
|
-
}
|
|
1660
|
-
function envelope(normalized, keyId, secretKey) {
|
|
1661
|
-
const { actor, detail, target, command, request } = normalized;
|
|
1662
|
-
const body = {
|
|
1663
|
-
id: detail.id,
|
|
1664
|
-
risk: MUTATIONS.has(request.intent) ? "MEDIUM" : "LOW",
|
|
1665
|
-
commandId: crypto5.randomUUID(),
|
|
1666
|
-
requesterId: actor,
|
|
1667
|
-
sessionId: "local-host",
|
|
1668
|
-
timestamp: Math.floor(Date.now() / 1e3),
|
|
1669
|
-
nonce: crypto5.randomBytes(32).toString("hex"),
|
|
1670
|
-
requires: ["policy", "signature"],
|
|
1671
|
-
payload: {
|
|
1672
|
-
adapter: detail.adapter,
|
|
1673
|
-
action: detail.action,
|
|
1674
|
-
target,
|
|
1675
|
-
content: request.content,
|
|
1676
|
-
cmd: command?.cmd,
|
|
1677
|
-
args: command?.args,
|
|
1678
|
-
timeoutMs: command?.timeoutMs,
|
|
1679
|
-
maxOutputBytes: command?.maxOutputBytes,
|
|
1680
|
-
cwd: command?.cwd || (target ? path10.dirname(target) : process.cwd())
|
|
1681
|
-
}
|
|
1682
|
-
};
|
|
1683
|
-
const signed = signEd25519({ payloadObj: body, secretKeyB64: secretKey });
|
|
1684
|
-
if (signed.error) throw new Error(signed.error);
|
|
1685
|
-
return { ...body, signature: { alg: "ed25519", keyId, sig: signed.signature } };
|
|
1686
|
-
}
|
|
1687
|
-
function createLocalExecutor(options = {}) {
|
|
1688
|
-
const rootDir = path10.resolve(options.rootDir || process.cwd());
|
|
1689
|
-
const keyId = options.keyId || "host:local-exec";
|
|
1690
|
-
const keyPair = options.keyPair || generateKeyPair();
|
|
1691
|
-
const shell = options.shell || {};
|
|
1692
|
-
function prepare(request, { recordNonce = false } = {}) {
|
|
1693
|
-
const normalized = normalize(rootDir, request, shell);
|
|
1694
|
-
if (normalized.error) return normalized;
|
|
1695
|
-
const local = loadLocalPolicy(rootDir, options.mode || "enforce");
|
|
1696
|
-
const localDecision = evaluateLocalPolicy(local.policy, rootDir, { target: normalized.target, command: normalized.command?.cmd });
|
|
1697
|
-
const localBlocked = local.policy.mode === "enforce" && !localDecision.allowed;
|
|
1698
|
-
if (localBlocked) return { error: error("LOCAL_POLICY_DENY", `Blocked by rule(s): ${localDecision.winningRules.map((rule) => rule.id).join(", ")}`), local, localDecision, normalized };
|
|
1699
|
-
const policy = commandPolicy(rootDir, normalized.actor, shell);
|
|
1700
|
-
const keyStore = { defaultKeyId: keyId, trustedKeys: { [keyId]: { publicKey: keyPair.publicKey, notBefore: policy._keyWindow.notBefore, expiresAt: policy._keyWindow.expiresAt, deprecated: false } } };
|
|
1701
|
-
delete policy._keyWindow;
|
|
1702
|
-
const proposal = envelope(normalized, keyId, keyPair.secretKey);
|
|
1703
|
-
const nonceDb = { entries: [] };
|
|
1704
|
-
const validation = validateCommand({ commandObj: proposal, keyStore, nonceDb: recordNonce ? nonceDb : { entries: [] }, policy });
|
|
1705
|
-
if (!validation.valid) return { error: error(validation.errors[0]?.type || "VALIDATION_FAILED", validation.errors[0]?.message || "Validation failed"), local, localDecision, normalized, proposal, policy, validation };
|
|
1706
|
-
return { local, localDecision, normalized, proposal, policy, validation };
|
|
1707
|
-
}
|
|
1708
|
-
function evaluateSync(action) {
|
|
1709
|
-
const local = loadLocalPolicy(rootDir, options.mode || "observe");
|
|
1710
|
-
const mode = local.policy.mode;
|
|
1711
|
-
let target = null;
|
|
1712
|
-
let command = null;
|
|
1713
|
-
if (action.path) {
|
|
1714
|
-
try {
|
|
1715
|
-
target = path10.resolve(rootDir, action.path);
|
|
1716
|
-
if (!underRoot(target, rootDir)) {
|
|
1717
|
-
return { decision: "deny", deny: true, matchedRules: ["path:outside_root"], mode, enforced: mode === "enforce", reason: "PATH_OUTSIDE_ROOT" };
|
|
1718
|
-
}
|
|
1719
|
-
} catch (e) {
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
if (action.cmd) command = action.cmd;
|
|
1723
|
-
const localDecision = evaluateLocalPolicy(local.policy, rootDir, { target, command });
|
|
1724
|
-
const isDeny = !localDecision.allowed;
|
|
1725
|
-
return {
|
|
1726
|
-
decision: isDeny ? "deny" : "allow",
|
|
1727
|
-
deny: isDeny,
|
|
1728
|
-
matchedRules: localDecision.winningRules.map((r) => r.id),
|
|
1729
|
-
mode,
|
|
1730
|
-
enforced: mode === "enforce"
|
|
1731
|
-
};
|
|
1732
|
-
}
|
|
1733
|
-
function auditSync(entry) {
|
|
1734
|
-
const eventsPath = path10.join(rootDir, ".lbe", "events.jsonl");
|
|
1735
|
-
const dir = path10.dirname(eventsPath);
|
|
1736
|
-
if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
|
|
1737
|
-
const line = JSON.stringify({ ts: Math.floor(Date.now() / 1e3), ...entry }) + "\n";
|
|
1738
|
-
const fd = fs9.openSync(eventsPath, "a");
|
|
1739
|
-
try {
|
|
1740
|
-
fs9.writeSync(fd, line);
|
|
1741
|
-
} finally {
|
|
1742
|
-
fs9.closeSync(fd);
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
async function dryRun(request) {
|
|
1746
|
-
const prepared = prepare(request);
|
|
1747
|
-
if (prepared.error) return { ...prepared.error, dryRun: true };
|
|
1748
|
-
return {
|
|
1749
|
-
ok: true,
|
|
1750
|
-
decision: prepared.local.policy.mode === "observe" ? "observe" : "allow",
|
|
1751
|
-
executed: false,
|
|
1752
|
-
dryRun: true,
|
|
1753
|
-
matchedRules: prepared.localDecision.winningRules.map((rule) => rule.id),
|
|
1754
|
-
rollback: { available: MUTATIONS.has(prepared.normalized.request.intent), performed: false }
|
|
1755
|
-
};
|
|
1756
|
-
}
|
|
1757
|
-
async function execute(request) {
|
|
1758
|
-
const prepared = prepare(request, { recordNonce: true });
|
|
1759
|
-
if (prepared.error) {
|
|
1760
|
-
auditLocalPolicy(rootDir, { action: request?.intent, actor: request?.actor || "agent:local", decision: "deny", error: prepared.error.error.code });
|
|
1761
|
-
return prepared.error;
|
|
1762
|
-
}
|
|
1763
|
-
if (prepared.local.policy.mode === "observe") {
|
|
1764
|
-
appendAudit(path10.join(rootDir, ".lbe/audit.jsonl"), {
|
|
1765
|
-
kind: "local_execution",
|
|
1766
|
-
commandId: prepared.proposal.commandId,
|
|
1767
|
-
requesterId: prepared.normalized.actor,
|
|
1768
|
-
intent: prepared.normalized.request.intent,
|
|
1769
|
-
decision: "observe",
|
|
1770
|
-
status: "observed"
|
|
1771
|
-
});
|
|
1772
|
-
return {
|
|
1773
|
-
ok: true,
|
|
1774
|
-
decision: "observe",
|
|
1775
|
-
executed: false,
|
|
1776
|
-
dryRun: false,
|
|
1777
|
-
matchedRules: prepared.localDecision.winningRules.map((r) => r.id),
|
|
1778
|
-
rollback: { available: false, performed: false }
|
|
1779
|
-
};
|
|
1780
|
-
}
|
|
1781
|
-
const requester = prepared.policy.requesters[prepared.normalized.actor];
|
|
1782
|
-
const adapterResult = await executeAdapter(prepared.normalized.detail.adapter, prepared.proposal, prepared.policy, requester);
|
|
1783
|
-
const ok = adapterResult.status === "completed";
|
|
1784
|
-
const audit = appendAudit(path10.join(rootDir, ".lbe/audit.jsonl"), {
|
|
1785
|
-
kind: "local_execution",
|
|
1786
|
-
commandId: prepared.proposal.commandId,
|
|
1787
|
-
requesterId: prepared.normalized.actor,
|
|
1788
|
-
intent: prepared.normalized.request.intent,
|
|
1789
|
-
decision: ok ? "allow" : "deny",
|
|
1790
|
-
status: adapterResult.status
|
|
1791
|
-
});
|
|
1792
|
-
return {
|
|
1793
|
-
ok,
|
|
1794
|
-
decision: ok ? "allow" : "deny",
|
|
1795
|
-
executed: ok,
|
|
1796
|
-
dryRun: false,
|
|
1797
|
-
matchedRules: prepared.localDecision.winningRules.map((rule) => rule.id),
|
|
1798
|
-
auditId: audit.hash,
|
|
1799
|
-
rollback: { available: MUTATIONS.has(prepared.normalized.request.intent), performed: false, backupId: adapterResult.backup?.hash },
|
|
1800
|
-
...ok ? {} : { error: { code: adapterResult.errorCode || "EXECUTION_FAILED", message: adapterResult.error || "Execution failed", recoverable: true } }
|
|
1801
|
-
};
|
|
1802
|
-
}
|
|
1803
|
-
const writeFile = (target, content) => execute({ intent: "write_file", target, content });
|
|
1804
|
-
const readFile = (target) => execute({ intent: "read_file", target });
|
|
1805
|
-
const patchFile = (target, content) => execute({ intent: "patch_file", target, content });
|
|
1806
|
-
const deleteFile = (target) => execute({ intent: "delete_file", target });
|
|
1807
|
-
const runShell = (cmd, args = [], opts = {}) => execute({ intent: "run_shell", command: { cmd, args, ...opts } });
|
|
1808
|
-
return {
|
|
1809
|
-
rootDir,
|
|
1810
|
-
// High-level API — use these
|
|
1811
|
-
writeFile,
|
|
1812
|
-
readFile,
|
|
1813
|
-
patchFile,
|
|
1814
|
-
deleteFile,
|
|
1815
|
-
runShell,
|
|
1816
|
-
// Low-level API — for advanced use
|
|
1817
|
-
validate: async (request) => {
|
|
1818
|
-
const preview = await dryRun(request);
|
|
1819
|
-
return { ...preview, dryRun: false, executed: false };
|
|
1820
|
-
},
|
|
1821
|
-
dryRun,
|
|
1822
|
-
execute,
|
|
1823
|
-
policy: {
|
|
1824
|
-
read: () => loadLocalPolicy(rootDir, options.mode || "enforce").policy,
|
|
1825
|
-
proposeRule: proposePolicyRule,
|
|
1826
|
-
addRule: (rule) => addLocalPolicyRule(rootDir, rule, options.mode || "enforce")
|
|
1827
|
-
},
|
|
1828
|
-
audit: { verify: () => verifyAuditLogIntegrity(path10.join(rootDir, ".lbe/audit.jsonl")) },
|
|
1829
|
-
evaluateSync,
|
|
1830
|
-
auditSync
|
|
1831
|
-
};
|
|
1832
|
-
}
|
|
1833
|
-
export {
|
|
1834
|
-
createLocalExecutor
|
|
1835
|
-
};
|
|
1
|
+
import je from"crypto";import q from"fs";import A from"path";import J from"tweetnacl";import{canonicalize as ce}from"json-canonicalize";function z(e){return Buffer.from(e,"base64")}function G(e){return Buffer.from(e).toString("base64")}function le({payloadObj:e,sigB64:t,pubKeyB64:r}){try{let n=Buffer.from(ce(e),"utf8"),o=z(t),i=z(r),s=J.sign.detached.verify(new Uint8Array(n),new Uint8Array(o),new Uint8Array(i));return{valid:s,message:s?"Signature verified":"Signature verification failed"}}catch(n){return{valid:!1,message:`Signature verification error: ${n.message}`}}}function de(){let e=J.sign.keyPair();return{publicKey:G(e.publicKey),secretKey:G(e.secretKey)}}function ue({payloadObj:e,secretKeyB64:t}){try{let r=Buffer.from(ce(e),"utf8"),n=z(t),o=J.sign.detached(new Uint8Array(r),new Uint8Array(n));return{signature:G(o),error:null}}catch(r){return{signature:null,error:`Signing failed: ${r.message}`}}}import ye from"fs";import tt from"path";import R from"fs";import X from"path";import Xe from"crypto";var Qe={timeoutMs:5e3,pollMs:15,staleMs:3e4};function Ze(e){return e+".lock"}function fe(e){try{let t=R.openSync(e,"wx");return R.writeSync(t,`pid:${process.pid}:${Date.now()}`),R.closeSync(t),!0}catch(t){if(t.code==="EEXIST"||t.code==="EPERM"||t.code==="EBUSY"||t.code==="EACCES")return!1;throw t}}function pe(e,t){try{let r=R.statSync(e);if(Date.now()-r.mtimeMs>t)try{R.unlinkSync(e)}catch{}}catch{}}function et(e){let t=Date.now()+e;for(;Date.now()<t;)try{Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,Math.max(1,t-Date.now()))}catch{}}function me(e,t,r){let n=typeof t=="function"?t:r,o=typeof t=="function"?{}:t||{},{timeoutMs:i,pollMs:s,staleMs:a}={...Qe,...o},d=X.dirname(e);R.existsSync(d)||R.mkdirSync(d,{recursive:!0});let l=Ze(e),I=Date.now()+i,f=!1;for(;!f&&(f=fe(l),!f);){if(Date.now()>=I){if(pe(l,a),f=fe(l),f)break;let m=new Error(`withFileLock: timeout acquiring ${l} after ${i}ms`);throw m.code="ELOCKTIMEOUT",m}pe(l,a);let p=Math.floor(Math.random()*s);et(s+p)}try{return n()}finally{try{R.unlinkSync(l)}catch{}}}function v(e,t,r={}){let n=X.dirname(e);R.existsSync(n)||R.mkdirSync(n,{recursive:!0});let o=X.join(n,`.tmp-${Date.now()}-${Xe.randomBytes(4).toString("hex")}`);try{R.writeFileSync(o,t,r),R.renameSync(o,e)}catch(i){try{R.existsSync(o)&&R.unlinkSync(o)}catch{}throw i}}function he(e){if(typeof e=="number"&&Number.isFinite(e))return{ok:!0,kind:"int",parts:[Math.floor(e)],raw:String(e)};if(typeof e!="string"||!e.trim())return{ok:!1,reason:"POLICY_VERSION_INVALID",message:"Policy version is required"};let t=e.trim();if(/^\d+$/.test(t))return{ok:!0,kind:"int",parts:[Number(t)],raw:t};let r=t.replace(/^v/i,"");if(/^\d+(\.\d+){0,2}$/.test(r)){let n=r.split(".").map(o=>Number(o));for(;n.length<3;)n.push(0);return{ok:!0,kind:"semver",parts:n,raw:t}}return{ok:!1,reason:"POLICY_VERSION_INVALID",message:`Unsupported policy version format '${e}' (use integer or semver)`}}function rt(e,t){let r=Math.max(e.parts.length,t.parts.length);for(let n=0;n<r;n++){let o=e.parts[n]??0,i=t.parts[n]??0;if(o>i)return 1;if(o<i)return-1}return 0}function Ie(e){if(typeof e=="number"&&Number.isFinite(e))return{ok:!0,epochSec:e>1e12?Math.floor(e/1e3):Math.floor(e)};if(typeof e!="string"||!e.trim())return{ok:!1,reason:"POLICY_CREATED_AT_INVALID",message:"Policy createdAt is required"};let t=Date.parse(e);return Number.isNaN(t)?{ok:!1,reason:"POLICY_CREATED_AT_INVALID",message:`Invalid policy createdAt '${e}'`}:{ok:!0,epochSec:Math.floor(t/1e3)}}function nt(e){if(!ye.existsSync(e))return{schemaVersion:"1",lastAccepted:null,updatedAt:null};try{let t=JSON.parse(ye.readFileSync(e,"utf8"));if(!t||typeof t!="object")throw new Error("Policy state file has invalid structure");return{schemaVersion:String(t.schemaVersion||"1"),lastAccepted:t.lastAccepted&&typeof t.lastAccepted=="object"?t.lastAccepted:null,updatedAt:t.updatedAt||null}}catch(t){throw new Error(`Policy state at ${e} is corrupt or unreadable: ${t.message}`)}}function ot(e,t){let r=JSON.stringify(t,null,2);v(e,r,{encoding:"utf8"})}function Ee({policyObj:e,statePath:t=tt.resolve(".lbe/data/policy.state.json"),maxCreatedAtSkewSec:r=31536e3,nowSec:n=Math.floor(Date.now()/1e3),persist:o=!0}){let i=he(e?.version);if(!i.ok)return{ok:!1,reason:i.reason,message:i.message,updated:!1};let s=Ie(e?.createdAt);if(!s.ok)return{ok:!1,reason:s.reason,message:s.message,updated:!1};let a=Math.abs(n-s.epochSec),d=Number.isFinite(r)&&r>0?Math.floor(r):31536e3;if(a>d)return{ok:!1,reason:"POLICY_CREATED_AT_SKEW_EXCEEDED",message:`Policy createdAt skew ${a}s exceeds allowed ${d}s`,updated:!1};let l;try{l=nt(t)}catch(u){return{ok:!1,reason:"POLICY_STATE_CORRUPT",message:u.message,updated:!1}}let I=l.lastAccepted,f=null,p=null,m=0;if(I&&(f=he(I.version),p=Ie(I.createdAt),f.ok&&p.ok)){if(m=rt(i,f),m<0)return{ok:!1,reason:"POLICY_VERSION_REGRESSION",message:`Policy version regression: current '${i.raw}' < last '${f.raw}'`,updated:!1};if(m===0&&s.epochSec<p.epochSec)return{ok:!1,reason:"POLICY_CREATED_AT_REGRESSION",message:`Policy createdAt regression: current '${e.createdAt}' < last '${I.createdAt}'`,updated:!1};if(m>0&&s.epochSec<p.epochSec)return{ok:!1,reason:"POLICY_CREATED_AT_REGRESSION",message:"Policy createdAt must be monotonic when version increases",updated:!1}}let E=!I||!f?.ok||!p?.ok||m>0||m===0&&s.epochSec>p.epochSec;if(o&&E){let u={schemaVersion:"1",lastAccepted:{version:e.version,createdAt:e.createdAt,environment:e.environment||null},updatedAt:new Date().toISOString()};ot(t,u)}return{ok:!0,reason:null,message:"Policy version guard passed",updated:E}}import ge from"fs";import ke from"path";import{fileURLToPath as st}from"url";var it=ke.dirname(st(import.meta.url)),Q=ke.join(it,"lbe_engine.wasm"),Se={0:{allowed:!0,reason:null,message:"Policy check passed"},1:{allowed:!1,reason:"POLICY_NOT_CONFIGURED",message:"No policy configured"},2:{allowed:!1,reason:"REQUESTER_NOT_ALLOWED",message:"Requester not in policy"},3:{allowed:!1,reason:"COMMAND_NOT_ALLOWED",message:"Command not allowed for requester"},4:{allowed:!1,reason:"ADAPTER_NOT_ALLOWED",message:"Adapter not allowed"},5:{allowed:!1,reason:"NO_FILESYSTEM_ROOTS_DEFINED",message:"No filesystem roots defined for requester"},6:{allowed:!1,reason:"CWD_OUTSIDE_ALLOWED_ROOT",message:"Path not under allowed roots"},7:{allowed:!1,reason:"PATH_DENIED_BY_PATTERN",message:"Path matches deny pattern"},8:{allowed:!1,reason:"SHELL_CMD_DENIED",message:"Shell command not allowed"}},at={0:{valid:!0,error:null},1:{valid:!1,error:"Missing required field: id"},2:{valid:!1,error:"Missing required field: commandId"},3:{valid:!1,error:"Missing required field: requesterId"},4:{valid:!1,error:"Missing required field: sessionId"},5:{valid:!1,error:"Missing required field: timestamp"},6:{valid:!1,error:"Missing required field: nonce"},7:{valid:!1,error:"Missing required field: requires"},8:{valid:!1,error:"Missing required field: payload"},9:{valid:!1,error:"Missing required field: signature"},10:{valid:!1,error:"Field 'id' is invalid"},11:{valid:!1,error:"Field 'commandId' is invalid"},12:{valid:!1,error:"Field 'requesterId' is invalid"},13:{valid:!1,error:"Field 'sessionId' is invalid"},14:{valid:!1,error:"Field 'timestamp' is invalid"},15:{valid:!1,error:"Field 'nonce' is invalid"},16:{valid:!1,error:"Field 'requires' is invalid"},17:{valid:!1,error:"payload: missing required field: adapter"},18:{valid:!1,error:"payload: field 'adapter' is invalid"},19:{valid:!1,error:"signature: missing required field: alg"},20:{valid:!1,error:"signature: missing required field: keyId"},21:{valid:!1,error:"signature: missing required field: sig"},22:{valid:!1,error:"signature: field 'alg' must be ed25519"},23:{valid:!1,error:"signature: field 'sig' is invalid"},24:{valid:!1,error:"Field 'risk' is invalid"}},ct={1:"KEY_ID_INVALID",2:"KEY_NOT_TRUSTED",3:"KEY_DEPRECATED",4:"KEY_REQUESTER_MISMATCH",5:"KEY_LIFECYCLE_INVALID",6:"KEY_NOT_YET_VALID",7:"KEY_EXPIRED"},lt={0:"schema",1:"timestamp",2:"key",3:"signature",4:"rate_limit",5:"nonce",6:"policy",255:"ok"},dt=["LOW","MEDIUM","HIGH","CRITICAL"],ut={ECHO:0,READ_FILE:1,WRITE_FILE:2,PATCH_FILE:3,DELETE_FILE:4,RUN_SHELL:5},B=null;function C(){if(B)return B;if(!ge.existsSync(Q))throw new Error(`LBE engine missing: ${Q}`);let e=ge.readFileSync(Q);return B=new WebAssembly.Instance(new WebAssembly.Module(e),{}),B}function Y(){return new Uint8Array(C().exports.memory.buffer)}function we(){return C().exports.lbe_in_ptr()}function _e(){return C().exports.lbe_out_ptr()}function ft(){return C().exports.lbe_buf_size()}function xe(e){let t=new TextEncoder().encode(e),r=Y(),n=we();r.set(t,n),r[n+t.length]=0}function Ae(){let e=Y(),t=_e(),r=t;for(;e[r]!==0&&r-t<ft();)r++;return new TextDecoder().decode(e.slice(t,r))}function pt(e){let t=Y(),r=we(),n=new DataView(t.buffer,r);e.forEach((o,i)=>n.setUint32(i*4,o>>>0,!0))}function mt(){let e=Y(),t=_e(),r=new DataView(e.buffer,t);return{stage:r.getUint32(0,!0),code:r.getUint32(4,!0)}}function Re(e){pt([e.hasId?1:0,e.idValid?1:0,e.hasCommandId?1:0,e.commandIdValid?1:0,e.hasRequesterId?1:0,e.requesterIdValid?1:0,e.hasSessionId?1:0,e.sessionIdValid?1:0,e.hasTimestamp?1:0,e.timestampValid?1:0,e.hasNonce?1:0,e.nonceValid?1:0,e.hasRequires?1:0,e.requiresValid?1:0,e.hasPayload?1:0,e.hasPayloadAdapter?1:0,e.payloadAdapterValid?1:0,e.hasSignature?1:0,e.hasSignatureAlg?1:0,e.signatureAlgValid?1:0,e.hasSignatureKeyId?1:0,e.hasSignatureSig?1:0,e.signatureSigValid?1:0,e.hasRisk?1:0,e.riskValid?1:0,e.cmdTimestamp>>>0,e.nowSec>>>0,e.maxClockSkewSec>>>0,e.keyIdFormatValid?1:0,e.keyFound?1:0,e.keyNotDeprecated?1:0,e.keyRequesterMatches?1:0,e.keyNotBeforeOk?1:0,e.keyNotExpired?1:0,e.keyLifecycleFieldsPresent?1:0,e.signatureValid?1:0,e.rateLimitOk?1:0,e.rateLimitRetryAfterSec>>>0,e.nonceOk?1:0,e.policyConfigured?1:0,e.requesterConfigured?1:0,e.commandAllowed?1:0,e.adapterAllowed?1:0,e.filesystemRequired?1:0,e.filesystemRootsDefined?1:0,e.filesystemOk?1:0,e.pathDenied?1:0,e.shellRequired?1:0,e.shellCommandOk?1:0]),C().exports.lbe_validate_pipeline();let{stage:t,code:r}=mt();return{ok:t===255,stage:t,stageLabel:lt[t]||"unknown",code:r,schemaError:t===0?at[r]?.error||"Schema invalid":null,keyReason:t===2?ct[r]||"KEY_ERROR":null,policyResult:t===6?{...Se[r]||Se[1],code:r}:null,retryAfterSec:t===4?r:0,skewSec:t===1?r:0}}function Z({ttlSec:e,nowSec:t,newKey:r,existingEntries:n}){let o=[`${e}:${t}`,r,...n].join(`
|
|
2
|
+
`)+`
|
|
3
|
+
`;if(xe(o),C().exports.lbe_nonce_check()!==0)return{ok:!1,updatedEntriesText:null};let s=Ae();return{ok:!0,updatedEntriesText:s.startsWith(`OK
|
|
4
|
+
`)?s.slice(3):s}}function ve({windowSec:e,maxRequests:t,nowSec:r,requesterId:n,existingEntries:o}){let i=[`${e}:${t}:${r}`,n,...o].join(`
|
|
5
|
+
`)+`
|
|
6
|
+
`;xe(i);let s=C().exports.lbe_rate_check()!==0,a=Ae();if(s){let d=parseInt(a.match(/^EXCEEDED:(\d+)/)?.[1]??"1",10),l=a.replace(/^EXCEEDED:\d+\n/,"");return{ok:!1,retryAfterSec:d,updatedEntriesText:l}}return{ok:!0,retryAfterSec:0,updatedEntriesText:a.startsWith(`OK
|
|
7
|
+
`)?a.slice(3):a}}function De(e,t=!1){let r=ut[e]??0,n=C().exports.lbe_classify_risk(r,t?1:0);return dt[n]??"LOW"}import ee from"path";function yt(e){let t=i=>e!=null&&Object.prototype.hasOwnProperty.call(e,i),r=i=>typeof i=="string",n=e?.payload,o=e?.signature;return{hasId:t("id"),idValid:r(e?.id)&&/^[A-Z_]+$/.test(e.id)&&e.id.length>=1&&e.id.length<=50,hasCommandId:t("commandId"),commandIdValid:r(e?.commandId)&&/^[a-f0-9-]+$/.test(e.commandId)&&e.commandId.length===36,hasRequesterId:t("requesterId"),requesterIdValid:r(e?.requesterId)&&e.requesterId.length>=3&&e.requesterId.length<=100,hasSessionId:t("sessionId"),sessionIdValid:r(e?.sessionId)&&e.sessionId.length>=3,hasTimestamp:t("timestamp"),timestampValid:typeof e?.timestamp=="number"&&e.timestamp>=1e9,hasNonce:t("nonce"),nonceValid:r(e?.nonce)&&e.nonce.length>=32&&e.nonce.length<=128,hasRequires:t("requires"),requiresValid:Array.isArray(e?.requires)&&e.requires.length>=1&&e.requires.every(r),hasPayload:t("payload")&&typeof n=="object"&&n!==null&&!Array.isArray(n),hasPayloadAdapter:n!=null&&Object.prototype.hasOwnProperty.call(n,"adapter"),payloadAdapterValid:r(n?.adapter),hasSignature:t("signature")&&typeof o=="object"&&o!==null&&!Array.isArray(o),hasSignatureAlg:o!=null&&Object.prototype.hasOwnProperty.call(o,"alg"),signatureAlgValid:o?.alg==="ed25519",hasSignatureKeyId:o!=null&&Object.prototype.hasOwnProperty.call(o,"keyId"),hasSignatureSig:o!=null&&Object.prototype.hasOwnProperty.call(o,"sig"),signatureSigValid:r(o?.sig)&&o.sig.length>=10,hasRisk:t("risk"),riskValid:["LOW","MEDIUM","HIGH","CRITICAL"].includes(e?.risk)}}function ht(e,t){let r=!!(e&&e.default==="DENY"&&e.requesters&&typeof e.requesters=="object"),n=e?.requesters?.[t.requesterId],o=t.id?.toLowerCase()??"",i=!!n?.allowCommands?.some(m=>m.toLowerCase()===o),s=!!n?.allowAdapters?.includes(t.payload?.adapter),a=!!t.payload?.cwd,d=!1,l=!1,I=!1;if(a){let m=n?.filesystem?.roots??[];if(d=m.length>0,d){let E=ee.resolve(t.payload.cwd);l=m.some(c=>{let y=ee.resolve(c);return E===y||E.startsWith(y+ee.sep)}),I=(n?.filesystem?.denyPatterns??[]).some(c=>new RegExp("^"+c.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/]*")+"$").test(E))}}let f=!1,p=!0;if(t.id==="RUN_SHELL"){f=!0;let m=n?.exec?.allowCmds??[],E=n?.exec?.denyCmds??[],u=t.payload?.cmd;E.includes(u)?p=!1:p=m.length===0||m.includes(u)}return{policyConfigured:r,requesterConfigured:!!n,commandAllowed:i,adapterAllowed:s,filesystemRequired:a,filesystemRootsDefined:d,filesystemOk:l,pathDenied:I,shellRequired:f,shellCommandOk:p}}function It(e,t,r,n=new Date){if(!e||!t)return{keyIdFormatValid:!1,keyFound:!1,keyNotDeprecated:!1,keyRequesterMatches:!1,keyNotBeforeOk:!1,keyNotExpired:!1,keyLifecycleFieldsPresent:!1,publicKey:null};let i=/^[A-Za-z0-9:_-]{3,128}$/.test(t)&&t!=="default";if(!i)return{keyIdFormatValid:i,keyFound:!1,keyNotDeprecated:!1,keyRequesterMatches:!1,keyNotBeforeOk:!1,keyNotExpired:!1,keyLifecycleFieldsPresent:!1,publicKey:null};let s=e.trustedKeys?.[t],a=!!s;if(!a)return{keyIdFormatValid:i,keyFound:a,keyNotDeprecated:!1,keyRequesterMatches:!1,keyNotBeforeOk:!1,keyNotExpired:!1,keyLifecycleFieldsPresent:!1,publicKey:null};let d=!s.deprecated,l=!s.requesterId||s.requesterId===r,I=s.notBefore||s.validFrom,f=s.expiresAt||s.validUntil,p=typeof I=="string"&&typeof f=="string",m=!1,E=!1;if(p){let u=new Date(I),c=new Date(f);!isNaN(u.getTime())&&!isNaN(c.getTime())&&u<c&&(m=n>=u,E=n<c)}return{keyIdFormatValid:i,keyFound:a,keyNotDeprecated:d,keyRequesterMatches:l,keyNotBeforeOk:m,keyNotExpired:E,keyLifecycleFieldsPresent:p,publicKey:s.publicKey??null}}function Oe(e){return(e?.entries??[]).map(t=>`${t.key}:${t.timestamp}`)}function Ne(e){return e.split(`
|
|
8
|
+
`).filter(Boolean).map(t=>{let r=t.lastIndexOf(":");return{key:t.slice(0,r),timestamp:parseInt(t.slice(r+1),10)||0}})}function Et(e){return(e?.entries??[]).map(t=>`${t.requesterId}:${t.timestamp}`)}function gt(e){return e.split(`
|
|
9
|
+
`).filter(Boolean).map(t=>{let r=t.lastIndexOf(":");return{requesterId:t.slice(0,r),timestamp:parseInt(t.slice(r+1),10)||0}})}function Le({commandObj:e,pubKeyB64:t,keyStore:r,nonceDb:n,policy:o,rateLimiter:i,policyStatePath:s}){let a={valid:!1,commandId:e?.commandId,checks:{},errors:[]},d=Math.floor(Date.now()/1e3),l=new Date,I=Number.isFinite(o?.security?.maxClockSkewSec)?o.security.maxClockSkewSec:600;if(s&&o?.version!==void 0)try{let h=Ee({policyObj:o,statePath:s});if(a.checks.policyVersion=h.ok,!h.ok)return a.errors.push({type:"POLICY_VERSION_INVALID",message:h.message}),a}catch{a.checks.policyVersion=!0}else a.checks.policyVersion=!0;let f=yt(e),p=e?.signature?.keyId,m=It(r,p,e?.requesterId,l),E=!1,u=m.publicKey;if(!u&&t&&(u=t),u){let h={...e};delete h.signature,E=le({payloadObj:h,sigB64:e?.signature?.sig,pubKeyB64:u}).valid}let c=!0,y=0;if(E&&i&&typeof i.db<"u"){let h=o?.requesters?.[e.requesterId]?.rateLimit||{},_=o?.security?.defaultRateLimit||{},L=h.windowSec??_.windowSec??60,Je=h.maxRequests??_.maxRequests??30,U=ve({windowSec:L,maxRequests:Je,nowSec:d,requesterId:e.requesterId,existingEntries:Et(i.db)});c=U.ok,y=U.retryAfterSec,U.ok&&(i.db.entries=gt(U.updatedEntriesText))}else if(E&&i&&typeof i.checkAndRecord=="function"){let h=o?.requesters?.[e.requesterId]?.rateLimit||{},_=o?.security?.defaultRateLimit||{},L=i.checkAndRecord({requesterId:e.requesterId,nowSec:d,windowSec:h.windowSec??_.windowSec??60,maxRequests:h.maxRequests??_.maxRequests??30});c=L.ok,y=L.retryAfterSec??0}let g=!0,S=`${e?.requesterId}|${e?.sessionId}|${e?.nonce}`,N=3600;if(E&&c&&n)if(typeof n.checkAndRecord=="function")if(n.db){let h=Z({ttlSec:N,nowSec:d,newKey:S,existingEntries:Oe(n.db)});g=h.ok,h.ok&&(n.db.entries=Ne(h.updatedEntriesText))}else g=n.checkAndRecord({requesterId:e.requesterId,sessionId:e.sessionId,nonce:e.nonce}).ok;else{let h=Z({ttlSec:N,nowSec:d,newKey:S,existingEntries:Oe(n)});g=h.ok,h.ok&&(n.entries=Ne(h.updatedEntriesText))}let x=ht(o,e??{}),w=Re({...f,cmdTimestamp:e?.timestamp??0,nowSec:d,maxClockSkewSec:I,...m,signatureValid:E,rateLimitOk:c,rateLimitRetryAfterSec:y,nonceOk:g,...x}),k=w.stage;if(a.checks.schema=k!==0,k>=1&&(a.checks.timestamp=k!==1),k>=2&&(a.checks.keyId=k!==2),k>=2&&(a.checks.signature=k!==2&&k!==3),k>=4&&(a.checks.rateLimit=k!==4),k>=5&&(a.checks.nonce=k!==5),(k>=6||w.ok)&&(a.checks.policy=k!==6),!w.ok){let h=w.stageLabel;if(h==="schema")a.errors.push({type:"SCHEMA_ERROR",message:w.schemaError||"Schema invalid"});else if(h==="timestamp")a.errors.push({type:"TIMESTAMP_SKEW_EXCEEDED",message:`Command timestamp skew ${w.skewSec}s exceeds allowed ${I}s`});else if(h==="key"){let _=w.keyReason||"KEY_ERROR",L={KEY_ID_INVALID:`Invalid keyId '${p}'`,KEY_NOT_TRUSTED:`Key '${p}' is not in trusted key store`,KEY_DEPRECATED:`Key '${p}' is deprecated`,KEY_REQUESTER_MISMATCH:`Key '${p}' is not authorized for requester '${e?.requesterId}'`,KEY_LIFECYCLE_INVALID:`Key '${p}' must define notBefore and expiresAt`,KEY_NOT_YET_VALID:`Key '${p}' is not yet valid`,KEY_EXPIRED:`Key '${p}' has expired`};a.errors.push({type:_,message:L[_]||_})}else h==="signature"?a.errors.push({type:"SIGNATURE_INVALID",message:u?"Signature verification failed":"No public key available"}):h==="rate_limit"?a.errors.push({type:"RATE_LIMIT_EXCEEDED",message:`Rate limit exceeded. Retry after ${w.retryAfterSec}s`}):h==="nonce"?a.errors.push({type:"REPLAY_NONCE",message:"Nonce has already been used"}):h==="policy"&&w.policyResult?a.errors.push({type:w.policyResult.reason,message:w.policyResult.message}):a.errors.push({type:"VALIDATION_FAILED",message:`Failed at stage: ${h}`});return a}return a.valid=!0,a.risk=De(e.id,e.payload?.cmd==="rm"),a.message="Command validation successful",a}async function Ce(e){return{adapter:"noop",commandId:e.commandId||"unknown",command:e.id||"unknown",status:"completed",output:`[NOOP] Would execute: ${e.id||"unknown"} on adapter: ${e.payload?.adapter||"unknown"}`,exitCode:0,timestamp:new Date().toISOString()}}import{spawnSync as St}from"child_process";import te from"path";import kt from"fs";function Te(e){try{return kt.realpathSync(te.resolve(e))}catch{return te.resolve(e)}}function wt(e){if(e===void 0)return{ok:!0,args:[]};if(!Array.isArray(e))return{ok:!1,error:"payload.args must be an array"};let t=[];for(let r of e){if(typeof r!="string"&&typeof r!="number"&&typeof r!="boolean")return{ok:!1,error:"payload.args may only contain string, number, or boolean values"};t.push(String(r))}return{ok:!0,args:t}}async function be(e,t,r){let n=e.payload,o=Math.min(Math.max(Number(n.timeoutMs)||3e4,1),3e4),i=Math.min(Math.max(Number(n.maxOutputBytes)||1024*1024,1024),1024*1024);if(n.adapter!=="shell")return{adapter:"shell",commandId:e.commandId,status:"error",error:"Adapter mismatch",exitCode:1};let s=r?.exec?.allowCmds||[];if((r?.exec?.denyCmds||[]).includes(n.cmd))return{adapter:"shell",commandId:e.commandId,status:"blocked",error:`Command '${n.cmd}' is denied`,exitCode:2};if(s.length>0&&!s.includes(n.cmd))return{adapter:"shell",commandId:e.commandId,status:"blocked",error:`Command '${n.cmd}' not in allowlist`,exitCode:2};if(!(r?.filesystem?.roots||[]).some(f=>{let p=Te(f),m=Te(n.cwd);return m===p||m.startsWith(p+te.sep)}))return{adapter:"shell",commandId:e.commandId,status:"blocked",error:`CWD '${n.cwd}' not authorized`,exitCode:2};let I=wt(n.args);if(!I.ok)return{adapter:"shell",commandId:e.commandId,status:"blocked",error:I.error,exitCode:2};try{let f=St(n.cmd,I.args,{cwd:n.cwd,timeout:o,encoding:"utf8",maxBuffer:i,stdio:["pipe","pipe","pipe"],shell:!1});if(f.error)throw f.error;let p=`${f.stdout||""}${f.stderr||""}`,m=f.status??1;return m!==0?{adapter:"shell",commandId:e.commandId,command:n.cmd,status:"error",error:p.substring(0,i)||`Command exited with code ${m}`,exitCode:m,timestamp:new Date().toISOString()}:{adapter:"shell",commandId:e.commandId,command:n.cmd,status:"completed",output:p.substring(0,i),exitCode:0,timestamp:new Date().toISOString()}}catch(f){return{adapter:"shell",commandId:e.commandId,command:n.cmd,status:"error",error:f.message,exitCode:f.status||1,timestamp:new Date().toISOString()}}}import F from"fs";import b from"path";import T from"fs";import W from"path";import _t from"crypto";function Pe(e,t){let r=t||W.resolve(".lbe/data/backups");T.existsSync(r)||T.mkdirSync(r,{recursive:!0});let n=W.resolve(e),o=T.existsSync(n),i=null,s=null;o&&(i=T.readFileSync(n),s=_t.createHash("sha256").update(i).digest("hex"));let a=W.basename(n).replace(/[^a-zA-Z0-9._-]/g,"_"),d=`${Date.now()}-${s?s.slice(0,8):"new"}-${a}`,l=o?W.join(r,d):null;return o&&i!==null&&v(l,i),{originalPath:n,backupPath:l,existed:o,hash:s,createdAt:new Date().toISOString()}}function H(e){if(!e)return{restored:!1,error:"No backup metadata"};let{originalPath:t,backupPath:r,existed:n}=e;if(!n)try{return T.existsSync(t)&&T.unlinkSync(t),{restored:!0,action:"deleted"}}catch(o){return{restored:!1,error:o.message}}if(!r||!T.existsSync(r))return{restored:!1,error:"Backup file not found at: "+r};try{let o=T.readFileSync(r);return v(t,o),{restored:!0,action:"restored"}}catch(o){return{restored:!1,error:o.message}}}var xt=10*1024*1024;function At(e,t){return e?b.isAbsolute(e)?b.resolve(e):b.resolve(t||process.cwd(),e):null}function Rt(e,t){let r=Fe(e);return t.some(n=>{let o=Fe(n);return r===o||r.startsWith(o+b.sep)})}function Fe(e){let t=b.resolve(e),r=[];for(;!F.existsSync(t);){let n=b.dirname(t);if(n===t)break;r.unshift(b.basename(t)),t=n}try{t=F.realpathSync(t)}catch{}return b.join(t,...r)}function vt(e,t){for(let r of t||[])if(new RegExp("^"+r.replace(/\./g,"\\.").replace(/\*\*/g,".*").replace(/\*/g,"[^/\\\\]*")+"$").test(e))return r;return null}function M(e,t,r,n=2){return{adapter:"file",commandId:e.commandId,status:"blocked",errorCode:t,error:r,exitCode:n}}function O(e,t,r,n=null,o=1){return{adapter:"file",commandId:e.commandId,status:"error",errorCode:t,error:r,backup:n?j(n):null,exitCode:o}}function j(e){return e?{path:e.backupPath,existed:e.existed,hash:e.hash,createdAt:e.createdAt}:null}async function $e(e,t,r){let n=e.payload,o=n.action,i=n.cwd||process.cwd(),s=At(n.target,i);if(!o)return M(e,"FILE_NO_ACTION","payload.action is required");if(!s&&o!=="noop")return M(e,"FILE_NO_TARGET","payload.target is required");let a=r?.filesystem?.roots||[];if(a.length===0)return M(e,"FILE_NO_ROOTS","No filesystem roots defined for requester");if(!Rt(s,a))return M(e,"FILE_OUTSIDE_ROOT",`'${s}' is outside allowed roots`);let d=vt(s,r?.filesystem?.denyPatterns);if(d)return M(e,"FILE_PATH_DENIED",`'${s}' matches deny pattern: ${d}`);switch(o){case"read":return Dt(e,s);case"write":return Ot(e,s,n);case"patch":return Nt(e,s,n);case"delete":return Lt(e,s);default:return M(e,"FILE_UNKNOWN_ACTION",`Unknown action: '${o}'`)}}function Dt(e,t){if(!F.existsSync(t))return O(e,"FILE_NOT_FOUND",`Not found: ${t}`);try{let r=F.statSync(t);if(r.size>xt)return O(e,"FILE_TOO_LARGE","File exceeds 10 MB read limit");let n=F.readFileSync(t,"utf8");return{adapter:"file",action:"read",commandId:e.commandId,status:"completed",target:t,output:n,bytesRead:r.size,exitCode:0}}catch(r){return O(e,"FILE_READ_ERROR",r.message)}}function Ot(e,t,r){let n=r.content;if(n==null)return O(e,"FILE_MISSING_CONTENT","payload.content is required for write");let o=re(t);try{return v(t,n,{encoding:"utf8"}),{adapter:"file",action:"write",commandId:e.commandId,status:"completed",target:t,backup:j(o),output:`Wrote ${Buffer.byteLength(n,"utf8")} bytes to ${t}`,exitCode:0}}catch(i){return H(o),O(e,"FILE_WRITE_ERROR",i.message,o)}}function Nt(e,t,r){let n=r.content;if(n==null)return O(e,"FILE_MISSING_CONTENT","payload.content is required for patch");let o=re(t);try{return v(t,n,{encoding:"utf8"}),{adapter:"file",action:"patch",commandId:e.commandId,status:"completed",target:t,backup:j(o),output:`Patched ${t} (${Buffer.byteLength(n,"utf8")} bytes)`,exitCode:0}}catch(i){return H(o),O(e,"FILE_PATCH_ERROR",i.message,o)}}function Lt(e,t){if(!F.existsSync(t))return O(e,"FILE_NOT_FOUND",`Not found: ${t}`);let r=re(t);try{return F.unlinkSync(t),{adapter:"file",action:"delete",commandId:e.commandId,status:"completed",target:t,backup:j(r),output:`Deleted ${t}`,exitCode:0}}catch(n){return H(r),O(e,"FILE_DELETE_ERROR",n.message,r)}}function re(e){try{return Pe(e)}catch{return null}}var qe={noop:Ce,shell:be,file:$e};function Ct(e){return qe[e]}async function Me(e,t,r,n){let o=Ct(e);if(!o)return{adapter:e,commandId:t.commandId,status:"error",error:`Adapter '${e}' not found`,exitCode:1};try{return await o(t,r,n)}catch(i){return{adapter:e,commandId:t.commandId,status:"error",error:`Adapter execution failed: ${i.message}`,exitCode:9}}}var Nr=Object.keys(qe);import P from"fs";import ne from"path";import Tt from"crypto";function Ve(e){return Tt.createHash("sha256").update(e).digest("hex")}function bt(e){try{if(!P.existsSync(e))return"GENESIS";let t=P.readFileSync(e,"utf8").trim();if(!t)return"GENESIS";let r=t.split(`
|
|
10
|
+
`),n=r[r.length-1];try{return JSON.parse(n).hash||"GENESIS"}catch{return"GENESIS"}}catch{return"GENESIS"}}function V(e,t){let r=ne.dirname(e);P.existsSync(r)||P.mkdirSync(r,{recursive:!0});let n;return me(e,()=>{let o=bt(e),i={...t,prevHash:o,timestamp:new Date().toISOString()};delete i.hash;let s=JSON.stringify(i),a=Ve(s),d=JSON.stringify({...i,hash:a}),l="";P.existsSync(e)&&(l=P.readFileSync(e,"utf8"));try{v(e,l+d+`
|
|
11
|
+
`,{encoding:"utf8"})}catch(I){throw new Error(`Audit log write failed: ${I.message}`)}n={success:!0,hash:a,prevHash:o,message:"Audit entry appended"}}),n}function Ke(e,t={}){let r=t.failFast!==!1,n=Number.isFinite(t.maxEntries)&&t.maxEntries>0?Math.floor(t.maxEntries):null,o={ok:!0,file:ne.resolve(e),entries:0,valid:!0,firstInvalidIndex:null,reason:null,errors:[],message:"Audit log verified"};try{if(!P.existsSync(e))return o.message="Audit log file not found (treated as empty)",o;let i=P.readFileSync(e,"utf8").trim();if(!i)return o.message="Empty audit log",o;let s=i.split(`
|
|
12
|
+
`),a=n?s.slice(0,n):s;o.entries=a.length;let d="GENESIS";for(let l=0;l<a.length;l++){let I;try{I=JSON.parse(a[l])}catch{let E={index:l,reason:"INVALID_JSON_LINE",message:`Line ${l} is not valid JSON`};if(o.valid=!1,o.ok=!1,o.firstInvalidIndex??=l,o.reason??=E.reason,o.errors.push(E),r)break;continue}if(I.prevHash!==d){let E={index:l,reason:"PREV_HASH_MISMATCH",message:`Expected prevHash '${d}', got '${I.prevHash}'`};if(o.valid=!1,o.ok=!1,o.firstInvalidIndex??=l,o.reason??=E.reason,o.errors.push(E),r)break}let f={...I},p=f.hash;delete f.hash;let m=Ve(JSON.stringify(f));if(p!==m){let E={index:l,reason:"HASH_MISMATCH",message:`Expected hash '${m}', got '${p}'`};if(o.valid=!1,o.ok=!1,o.firstInvalidIndex??=l,o.reason??=E.reason,o.errors.push(E),r)break}d=p}return o.message=o.valid?`Audit log verified: ${o.entries} entries`:`Audit log integrity failed at index ${o.firstInvalidIndex}`,o}catch(i){return{ok:!1,file:ne.resolve(e),entries:0,valid:!1,firstInvalidIndex:null,reason:"AUDIT_VERIFY_ERROR",errors:[{index:null,reason:"AUDIT_VERIFY_ERROR",message:i.message}],message:`Integrity check failed: ${i.message}`}}}import Ue from"fs";import $ from"path";import Pt from"crypto";var Be=".lbe/policy.json",Ft=".lbe/audit.jsonl";function $t(e){let t=e.replace(/[.+^${}()|[\]\\]/g,"\\$&");return new RegExp("^"+t.replace(/\*\*\//g,"(?:.*/)?").replace(/\*\*/g,".*").replace(/\*/g,"[^/]*")+"$")}function qt(e,t){return $.relative(e,$.resolve(t)).split($.sep).join("/")}function oe(e){let t=$.resolve(e||process.cwd());return{root:t,policyPath:$.join(t,Be),auditPath:$.join(t,Ft)}}function K(e,t="observe"){let r=oe(e);if(!Ue.existsSync(r.policyPath))return{...r,policy:{version:1,mode:t,workspace:r.root,rules:[]}};let n=JSON.parse(Ue.readFileSync(r.policyPath,"utf8"));if(n?.version!==1||!["observe","enforce"].includes(n.mode)||!Array.isArray(n.rules))throw new Error(`Invalid ${Be}`);return{...r,policy:n}}function Mt(e,t){let{policyPath:r,root:n}=oe(e),o={...t,version:1,workspace:n,rules:Array.isArray(t.rules)?t.rules:[]};return v(r,JSON.stringify(o,null,2)+`
|
|
13
|
+
`,{encoding:"utf8"}),o}function Ye(e,t,r){if(!t||!["allow","deny"].includes(t.effect)||!["path","command"].includes(t.type)||typeof t.pattern!="string"||!t.pattern||typeof t.from!="string"||!t.from)throw new Error("Rule requires effect, type, pattern, and from");let n=K(e,r),o={id:t.id||Pt.randomUUID(),effect:t.effect,type:t.type,pattern:t.pattern,from:t.from,at:t.at||new Date().toISOString()};return Mt(n.root,{...n.policy,mode:r||n.policy.mode,rules:[...n.policy.rules,o]}),{id:o.id,added:!0,rule:o}}function We(e){return{...e,proposed:!0,at:new Date().toISOString()}}function se(e,t,{target:r,command:n}={}){let o=$.resolve(t),i=[];r&&i.push({type:"path",value:qt(o,r)}),n&&i.push({type:"command",value:n});let s=e.rules.filter(d=>i.some(l=>l.type===d.type&&$t(d.pattern).test(l.value))),a=s.filter(d=>d.effect==="deny");return{allowed:a.length===0,matched:s,winningRules:a.length?a:s.filter(d=>d.effect==="allow"),reason:a.length?"LOCAL_POLICY_DENY":null}}function He(e,t){let{auditPath:r}=oe(e);V(r,{kind:"local_policy",timestamp:new Date().toISOString(),...t})}var Ge={read_file:{id:"READ_FILE",adapter:"file",action:"read"},write_file:{id:"WRITE_FILE",adapter:"file",action:"write"},patch_file:{id:"PATCH_FILE",adapter:"file",action:"patch"},delete_file:{id:"DELETE_FILE",adapter:"file",action:"delete"},run_shell:{id:"RUN_SHELL",adapter:"shell",action:"run"}},ie=new Set(["write_file","patch_file","delete_file"]);function D(e,t,r=!1){return{ok:!1,decision:"deny",executed:!1,dryRun:!1,error:{code:e,message:t,recoverable:r}}}function Vt(e,t,r={}){let n=new Date,o=new Date(n.getTime()+365*24*60*60*1e3);return{version:1,default:"DENY",requesters:{[t]:{allowCommands:Object.values(Ge).map(i=>i.id),allowAdapters:["file","shell"],filesystem:{roots:[e],denyPatterns:[]},exec:{allowCmds:r.allowCommands||[],denyCmds:r.denyCommands||[]},rateLimit:{windowSec:60,maxRequests:r.maxRequests||60}}},security:{maxClockSkewSec:600,defaultRateLimit:{windowSec:60,maxRequests:60}},_keyWindow:{notBefore:n.toISOString(),expiresAt:o.toISOString()}}}function ze(e){let t=A.resolve(e),r=[];for(;!q.existsSync(t);){let n=A.dirname(t);if(n===t)break;r.unshift(A.basename(t)),t=n}try{t=q.realpathSync(t)}catch{}return A.join(t,...r)}function ae(e,t){let r=ze(e),n=ze(t);return r===n||r.startsWith(n+A.sep)}var Kt=[/\beval\s*\(/i,/\bFunction\s*\(/i,/\bexec\s*\(/i,/\brequire\s*\(/,/\bimport\s*\(/,/\bchild_process\b/,/\b__proto__\b/,/\bconstructor\s*\[/,/evalScript/i];function Ut(e,t){if(typeof e!="string")return null;for(let r of Kt)if(r.test(e))return D("PAYLOAD_CONTENT_REJECTED",`Forbidden pattern in ${t}: ${r}`);return null}function Bt(e,t,r={}){if(!t||typeof t!="object")return{error:D("REQUEST_INVALID","request must be an object")};let n=Ge[t.intent];if(!n)return{error:D("INTENT_UNSUPPORTED",`Unsupported intent '${t.intent}'`)};let o=typeof t.actor=="string"&&t.actor?t.actor:"agent:local",i=null;if(n.adapter==="file"){if(typeof t.target!="string"||!t.target)return{error:D("TARGET_REQUIRED","target is required for file intents")};if(i=A.resolve(e,t.target),!ae(i,e))return{error:D("PATH_OUTSIDE_ROOT","target is outside project root")};if(["write_file","patch_file"].includes(t.intent)&&typeof t.content!="string")return{error:D("CONTENT_REQUIRED","content is required for write and patch")};let a=Ut(t.content,"content");if(a)return{error:a}}let s=null;if(n.adapter==="shell"){if(s=t.command,!s||typeof s.cmd!="string"||!Array.isArray(s.args)||s.args.some(d=>typeof d!="string"))return{error:D("COMMAND_INVALID","command requires cmd and string args")};let a=A.resolve(e,s.cwd||".");if(!ae(a,e))return{error:D("CWD_OUTSIDE_ROOT","command cwd is outside project root")};if(!Array.isArray(r.allowCommands)||!r.allowCommands.includes(s.cmd))return{error:D("SHELL_NOT_ALLOWLISTED",`command '${s.cmd}' is not explicitly allowlisted`)};if(r.denyCommands?.includes(s.cmd))return{error:D("SHELL_DENIED",`command '${s.cmd}' is denied`)};s={...s,cwd:a,timeoutMs:Math.min(Math.max(s.timeoutMs||3e4,1),3e4),maxOutputBytes:Math.min(Math.max(s.maxOutputBytes||1024*1024,1024),1024*1024)}}return{actor:o,detail:n,target:i,command:s,request:t}}function Yt(e,t,r){let{actor:n,detail:o,target:i,command:s,request:a}=e,d={id:o.id,risk:ie.has(a.intent)?"MEDIUM":"LOW",commandId:je.randomUUID(),requesterId:n,sessionId:"local-host",timestamp:Math.floor(Date.now()/1e3),nonce:je.randomBytes(32).toString("hex"),requires:["policy","signature"],payload:{adapter:o.adapter,action:o.action,target:i,content:a.content,cmd:s?.cmd,args:s?.args,timeoutMs:s?.timeoutMs,maxOutputBytes:s?.maxOutputBytes,cwd:s?.cwd||(i?A.dirname(i):process.cwd())}},l=ue({payloadObj:d,secretKeyB64:r});if(l.error)throw new Error(l.error);return{...d,signature:{alg:"ed25519",keyId:t,sig:l.signature}}}function Wt(e={}){let t=A.resolve(e.rootDir||process.cwd()),r=e.keyId||"host:local-exec",n=e.keyPair||de(),o=e.shell||{};function i(u,{recordNonce:c=!1}={}){let y=Bt(t,u,o);if(y.error)return y;let g=K(t,e.mode||"enforce"),S=se(g.policy,t,{target:y.target,command:y.command?.cmd});if(g.policy.mode==="enforce"&&!S.allowed)return{error:D("LOCAL_POLICY_DENY",`Blocked by rule(s): ${S.winningRules.map(L=>L.id).join(", ")}`),local:g,localDecision:S,normalized:y};let x=Vt(t,y.actor,o),w={defaultKeyId:r,trustedKeys:{[r]:{publicKey:n.publicKey,notBefore:x._keyWindow.notBefore,expiresAt:x._keyWindow.expiresAt,deprecated:!1}}};delete x._keyWindow;let k=Yt(y,r,n.secretKey),_=Le({commandObj:k,keyStore:w,nonceDb:c?{entries:[]}:{entries:[]},policy:x});return _.valid?{local:g,localDecision:S,normalized:y,proposal:k,policy:x,validation:_}:{error:D(_.errors[0]?.type||"VALIDATION_FAILED",_.errors[0]?.message||"Validation failed"),local:g,localDecision:S,normalized:y,proposal:k,policy:x,validation:_}}function s(u){let c=K(t,e.mode||"observe"),y=c.policy.mode,g=null,S=null;if(u.path)try{if(g=A.resolve(t,u.path),!ae(g,t))return{decision:"deny",deny:!0,matchedRules:["path:outside_root"],mode:y,enforced:y==="enforce",reason:"PATH_OUTSIDE_ROOT"}}catch{}u.cmd&&(S=u.cmd);let N=se(c.policy,t,{target:g,command:S}),x=!N.allowed;return{decision:x?"deny":"allow",deny:x,matchedRules:N.winningRules.map(w=>w.id),mode:y,enforced:y==="enforce"}}function a(u){let c=A.join(t,".lbe","events.jsonl"),y=A.dirname(c);q.existsSync(y)||q.mkdirSync(y,{recursive:!0});let g=JSON.stringify({ts:Math.floor(Date.now()/1e3),...u})+`
|
|
14
|
+
`,S=q.openSync(c,"a");try{q.writeSync(S,g)}finally{q.closeSync(S)}}async function d(u){let c=i(u);return c.error?{...c.error,dryRun:!0}:{ok:!0,decision:c.local.policy.mode==="observe"?"observe":"allow",executed:!1,dryRun:!0,matchedRules:c.localDecision.winningRules.map(y=>y.id),rollback:{available:ie.has(c.normalized.request.intent),performed:!1}}}async function l(u){let c=i(u,{recordNonce:!0});if(c.error)return He(t,{action:u?.intent,actor:u?.actor||"agent:local",decision:"deny",error:c.error.error.code}),c.error;if(c.local.policy.mode==="observe")return V(A.join(t,".lbe/audit.jsonl"),{kind:"local_execution",commandId:c.proposal.commandId,requesterId:c.normalized.actor,intent:c.normalized.request.intent,decision:"observe",status:"observed"}),{ok:!0,decision:"observe",executed:!1,dryRun:!1,matchedRules:c.localDecision.winningRules.map(x=>x.id),rollback:{available:!1,performed:!1}};let y=c.policy.requesters[c.normalized.actor],g=await Me(c.normalized.detail.adapter,c.proposal,c.policy,y),S=g.status==="completed",N=V(A.join(t,".lbe/audit.jsonl"),{kind:"local_execution",commandId:c.proposal.commandId,requesterId:c.normalized.actor,intent:c.normalized.request.intent,decision:S?"allow":"deny",status:g.status});return{ok:S,decision:S?"allow":"deny",executed:S,dryRun:!1,matchedRules:c.localDecision.winningRules.map(x=>x.id),auditId:N.hash,rollback:{available:ie.has(c.normalized.request.intent),performed:!1,backupId:g.backup?.hash},...S?{}:{error:{code:g.errorCode||"EXECUTION_FAILED",message:g.error||"Execution failed",recoverable:!0}}}}return{rootDir:t,writeFile:(u,c)=>l({intent:"write_file",target:u,content:c}),readFile:u=>l({intent:"read_file",target:u}),patchFile:(u,c)=>l({intent:"patch_file",target:u,content:c}),deleteFile:u=>l({intent:"delete_file",target:u}),runShell:(u,c=[],y={})=>l({intent:"run_shell",command:{cmd:u,args:c,...y}}),validate:async u=>({...await d(u),dryRun:!1,executed:!1}),dryRun:d,execute:l,policy:{read:()=>K(t,e.mode||"enforce").policy,proposeRule:We,addRule:u=>Ye(t,u,e.mode||"enforce")},audit:{verify:()=>Ke(A.join(t,".lbe/audit.jsonl"))},evaluateSync:s,auditSync:a}}export{Wt as createLocalExecutor};
|