@letterblack/lbe-exec 1.2.2 → 1.2.3
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/README.md +164 -14
- package/assets/lbe-gates.png +0 -0
- package/assets/runtime-boundary.svg +36 -0
- package/assets/story-allow.png +0 -0
- package/assets/story-deny.png +0 -0
- package/dist/cli.js +2614 -0
- package/dist/index.js +40 -6
- package/package.json +5 -1
- package/types.d.ts +43 -12
package/dist/cli.js
ADDED
|
@@ -0,0 +1,2614 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
#!/usr/bin/env node
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __esm = (fn, res) => function __init() {
|
|
6
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
7
|
+
};
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/core/signature.js
|
|
14
|
+
import nacl from "tweetnacl";
|
|
15
|
+
import { canonicalize } from "json-canonicalize";
|
|
16
|
+
function bytesFromBase64(b64) {
|
|
17
|
+
return Buffer.from(b64, "base64");
|
|
18
|
+
}
|
|
19
|
+
function toBase64(bytes) {
|
|
20
|
+
return Buffer.from(bytes).toString("base64");
|
|
21
|
+
}
|
|
22
|
+
function verifyEd25519({ payloadObj, sigB64, pubKeyB64 }) {
|
|
23
|
+
try {
|
|
24
|
+
const msg = Buffer.from(canonicalize(payloadObj), "utf8");
|
|
25
|
+
const sig = bytesFromBase64(sigB64);
|
|
26
|
+
const pub = bytesFromBase64(pubKeyB64);
|
|
27
|
+
const isValid = nacl.sign.detached.verify(
|
|
28
|
+
new Uint8Array(msg),
|
|
29
|
+
new Uint8Array(sig),
|
|
30
|
+
new Uint8Array(pub)
|
|
31
|
+
);
|
|
32
|
+
return {
|
|
33
|
+
valid: isValid,
|
|
34
|
+
message: isValid ? "Signature verified" : "Signature verification failed"
|
|
35
|
+
};
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return {
|
|
38
|
+
valid: false,
|
|
39
|
+
message: `Signature verification error: ${err.message}`
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function generateKeyPair() {
|
|
44
|
+
const keyPair = nacl.sign.keyPair();
|
|
45
|
+
return {
|
|
46
|
+
publicKey: toBase64(keyPair.publicKey),
|
|
47
|
+
secretKey: toBase64(keyPair.secretKey)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function signEd25519({ payloadObj, secretKeyB64 }) {
|
|
51
|
+
try {
|
|
52
|
+
const msg = Buffer.from(canonicalize(payloadObj), "utf8");
|
|
53
|
+
const secretKey = bytesFromBase64(secretKeyB64);
|
|
54
|
+
const sig = nacl.sign.detached(new Uint8Array(msg), new Uint8Array(secretKey));
|
|
55
|
+
return {
|
|
56
|
+
signature: toBase64(sig),
|
|
57
|
+
error: null
|
|
58
|
+
};
|
|
59
|
+
} catch (err) {
|
|
60
|
+
return {
|
|
61
|
+
signature: null,
|
|
62
|
+
error: `Signing failed: ${err.message}`
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
var init_signature = __esm({
|
|
67
|
+
"src/core/signature.js"() {
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// src/core/atomicWrite.js
|
|
72
|
+
import fs5 from "fs";
|
|
73
|
+
import path5 from "path";
|
|
74
|
+
import crypto from "crypto";
|
|
75
|
+
function _lockPathFor(targetPath) {
|
|
76
|
+
return targetPath + ".lock";
|
|
77
|
+
}
|
|
78
|
+
function _tryAcquire(lockPath) {
|
|
79
|
+
try {
|
|
80
|
+
const fd = fs5.openSync(lockPath, "wx");
|
|
81
|
+
fs5.writeSync(fd, `pid:${process.pid}:${Date.now()}`);
|
|
82
|
+
fs5.closeSync(fd);
|
|
83
|
+
return true;
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err.code === "EEXIST" || err.code === "EPERM" || err.code === "EBUSY" || err.code === "EACCES") {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function _removeIfStale(lockPath, staleMs) {
|
|
92
|
+
try {
|
|
93
|
+
const stat = fs5.statSync(lockPath);
|
|
94
|
+
const ageMs = Date.now() - stat.mtimeMs;
|
|
95
|
+
if (ageMs > staleMs) {
|
|
96
|
+
try {
|
|
97
|
+
fs5.unlinkSync(lockPath);
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function _sleepSync(ms) {
|
|
105
|
+
const end = Date.now() + ms;
|
|
106
|
+
while (Date.now() < end) {
|
|
107
|
+
try {
|
|
108
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, Math.max(1, end - Date.now()));
|
|
109
|
+
} catch {
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function withFileLock(targetPath, optsOrFn, maybeFn) {
|
|
114
|
+
const fn = typeof optsOrFn === "function" ? optsOrFn : maybeFn;
|
|
115
|
+
const opts2 = typeof optsOrFn === "function" ? {} : optsOrFn || {};
|
|
116
|
+
const { timeoutMs, pollMs, staleMs } = { ...DEFAULT_LOCK_OPTS, ...opts2 };
|
|
117
|
+
const dir = path5.dirname(targetPath);
|
|
118
|
+
if (!fs5.existsSync(dir)) {
|
|
119
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
120
|
+
}
|
|
121
|
+
const lockPath = _lockPathFor(targetPath);
|
|
122
|
+
const deadline = Date.now() + timeoutMs;
|
|
123
|
+
let acquired = false;
|
|
124
|
+
while (!acquired) {
|
|
125
|
+
acquired = _tryAcquire(lockPath);
|
|
126
|
+
if (acquired) break;
|
|
127
|
+
if (Date.now() >= deadline) {
|
|
128
|
+
_removeIfStale(lockPath, staleMs);
|
|
129
|
+
acquired = _tryAcquire(lockPath);
|
|
130
|
+
if (acquired) break;
|
|
131
|
+
const err = new Error(`withFileLock: timeout acquiring ${lockPath} after ${timeoutMs}ms`);
|
|
132
|
+
err.code = "ELOCKTIMEOUT";
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
_removeIfStale(lockPath, staleMs);
|
|
136
|
+
const jitter = Math.floor(Math.random() * pollMs);
|
|
137
|
+
_sleepSync(pollMs + jitter);
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return fn();
|
|
141
|
+
} finally {
|
|
142
|
+
try {
|
|
143
|
+
fs5.unlinkSync(lockPath);
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function atomicWriteFileSync(filePath, data, options = {}) {
|
|
149
|
+
const dir = path5.dirname(filePath);
|
|
150
|
+
if (!fs5.existsSync(dir)) {
|
|
151
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
const tempFile = path5.join(dir, `.tmp-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`);
|
|
154
|
+
try {
|
|
155
|
+
fs5.writeFileSync(tempFile, data, options);
|
|
156
|
+
fs5.renameSync(tempFile, filePath);
|
|
157
|
+
} catch (error2) {
|
|
158
|
+
try {
|
|
159
|
+
if (fs5.existsSync(tempFile)) {
|
|
160
|
+
fs5.unlinkSync(tempFile);
|
|
161
|
+
}
|
|
162
|
+
} catch (cleanupError) {
|
|
163
|
+
}
|
|
164
|
+
throw error2;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
var DEFAULT_LOCK_OPTS;
|
|
168
|
+
var init_atomicWrite = __esm({
|
|
169
|
+
"src/core/atomicWrite.js"() {
|
|
170
|
+
DEFAULT_LOCK_OPTS = {
|
|
171
|
+
timeoutMs: 5e3,
|
|
172
|
+
// total wait before giving up
|
|
173
|
+
pollMs: 15,
|
|
174
|
+
// base poll interval (jittered)
|
|
175
|
+
staleMs: 3e4
|
|
176
|
+
// lock files older than this are presumed orphaned
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// src/core/auditLog.js
|
|
182
|
+
import fs6 from "fs";
|
|
183
|
+
import path6 from "path";
|
|
184
|
+
import crypto2 from "crypto";
|
|
185
|
+
function sha256(str) {
|
|
186
|
+
return crypto2.createHash("sha256").update(str).digest("hex");
|
|
187
|
+
}
|
|
188
|
+
function getLastHash(logPath) {
|
|
189
|
+
try {
|
|
190
|
+
if (!fs6.existsSync(logPath)) return "GENESIS";
|
|
191
|
+
const content = fs6.readFileSync(logPath, "utf8").trim();
|
|
192
|
+
if (!content) return "GENESIS";
|
|
193
|
+
const lines = content.split("\n");
|
|
194
|
+
const lastLine = lines[lines.length - 1];
|
|
195
|
+
try {
|
|
196
|
+
const lastEntry = JSON.parse(lastLine);
|
|
197
|
+
return lastEntry.hash || "GENESIS";
|
|
198
|
+
} catch (err) {
|
|
199
|
+
return "GENESIS";
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
return "GENESIS";
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function appendAudit(logPath, entry) {
|
|
206
|
+
const dir = path6.dirname(logPath);
|
|
207
|
+
if (!fs6.existsSync(dir)) {
|
|
208
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
209
|
+
}
|
|
210
|
+
let result;
|
|
211
|
+
withFileLock(logPath, () => {
|
|
212
|
+
const prevHash = getLastHash(logPath);
|
|
213
|
+
const record = {
|
|
214
|
+
...entry,
|
|
215
|
+
prevHash,
|
|
216
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
217
|
+
};
|
|
218
|
+
delete record.hash;
|
|
219
|
+
const recordStr = JSON.stringify(record);
|
|
220
|
+
const hash = sha256(recordStr);
|
|
221
|
+
const final = JSON.stringify({ ...record, hash });
|
|
222
|
+
let existingContent = "";
|
|
223
|
+
if (fs6.existsSync(logPath)) {
|
|
224
|
+
existingContent = fs6.readFileSync(logPath, "utf8");
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
atomicWriteFileSync(logPath, existingContent + final + "\n", { encoding: "utf8" });
|
|
228
|
+
} catch (err) {
|
|
229
|
+
throw new Error(`Audit log write failed: ${err.message}`);
|
|
230
|
+
}
|
|
231
|
+
result = {
|
|
232
|
+
success: true,
|
|
233
|
+
hash,
|
|
234
|
+
prevHash,
|
|
235
|
+
message: "Audit entry appended"
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
return result;
|
|
239
|
+
}
|
|
240
|
+
function verifyAuditLogIntegrity(logPath, options = {}) {
|
|
241
|
+
const failFast = options.failFast !== false;
|
|
242
|
+
const maxEntries = Number.isFinite(options.maxEntries) && options.maxEntries > 0 ? Math.floor(options.maxEntries) : null;
|
|
243
|
+
const response = {
|
|
244
|
+
ok: true,
|
|
245
|
+
file: path6.resolve(logPath),
|
|
246
|
+
entries: 0,
|
|
247
|
+
valid: true,
|
|
248
|
+
firstInvalidIndex: null,
|
|
249
|
+
reason: null,
|
|
250
|
+
errors: [],
|
|
251
|
+
message: "Audit log verified"
|
|
252
|
+
};
|
|
253
|
+
try {
|
|
254
|
+
if (!fs6.existsSync(logPath)) {
|
|
255
|
+
response.message = "Audit log file not found (treated as empty)";
|
|
256
|
+
return response;
|
|
257
|
+
}
|
|
258
|
+
const raw = fs6.readFileSync(logPath, "utf8").trim();
|
|
259
|
+
if (!raw) {
|
|
260
|
+
response.message = "Empty audit log";
|
|
261
|
+
return response;
|
|
262
|
+
}
|
|
263
|
+
const allLines = raw.split("\n");
|
|
264
|
+
const lines = maxEntries ? allLines.slice(0, maxEntries) : allLines;
|
|
265
|
+
response.entries = lines.length;
|
|
266
|
+
let expectedPrevHash = "GENESIS";
|
|
267
|
+
for (let i = 0; i < lines.length; i++) {
|
|
268
|
+
let entry;
|
|
269
|
+
try {
|
|
270
|
+
entry = JSON.parse(lines[i]);
|
|
271
|
+
} catch {
|
|
272
|
+
const errObj = {
|
|
273
|
+
index: i,
|
|
274
|
+
reason: "INVALID_JSON_LINE",
|
|
275
|
+
message: `Line ${i} is not valid JSON`
|
|
276
|
+
};
|
|
277
|
+
response.valid = false;
|
|
278
|
+
response.ok = false;
|
|
279
|
+
response.firstInvalidIndex ??= i;
|
|
280
|
+
response.reason ??= errObj.reason;
|
|
281
|
+
response.errors.push(errObj);
|
|
282
|
+
if (failFast) break;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (entry.prevHash !== expectedPrevHash) {
|
|
286
|
+
const errObj = {
|
|
287
|
+
index: i,
|
|
288
|
+
reason: "PREV_HASH_MISMATCH",
|
|
289
|
+
message: `Expected prevHash '${expectedPrevHash}', got '${entry.prevHash}'`
|
|
290
|
+
};
|
|
291
|
+
response.valid = false;
|
|
292
|
+
response.ok = false;
|
|
293
|
+
response.firstInvalidIndex ??= i;
|
|
294
|
+
response.reason ??= errObj.reason;
|
|
295
|
+
response.errors.push(errObj);
|
|
296
|
+
if (failFast) break;
|
|
297
|
+
}
|
|
298
|
+
const recordCopy = { ...entry };
|
|
299
|
+
const recordHash = recordCopy.hash;
|
|
300
|
+
delete recordCopy.hash;
|
|
301
|
+
const expectedHash = sha256(JSON.stringify(recordCopy));
|
|
302
|
+
if (recordHash !== expectedHash) {
|
|
303
|
+
const errObj = {
|
|
304
|
+
index: i,
|
|
305
|
+
reason: "HASH_MISMATCH",
|
|
306
|
+
message: `Expected hash '${expectedHash}', got '${recordHash}'`
|
|
307
|
+
};
|
|
308
|
+
response.valid = false;
|
|
309
|
+
response.ok = false;
|
|
310
|
+
response.firstInvalidIndex ??= i;
|
|
311
|
+
response.reason ??= errObj.reason;
|
|
312
|
+
response.errors.push(errObj);
|
|
313
|
+
if (failFast) break;
|
|
314
|
+
}
|
|
315
|
+
expectedPrevHash = recordHash;
|
|
316
|
+
}
|
|
317
|
+
response.message = response.valid ? `Audit log verified: ${response.entries} entries` : `Audit log integrity failed at index ${response.firstInvalidIndex}`;
|
|
318
|
+
return response;
|
|
319
|
+
} catch (err) {
|
|
320
|
+
return {
|
|
321
|
+
ok: false,
|
|
322
|
+
file: path6.resolve(logPath),
|
|
323
|
+
entries: 0,
|
|
324
|
+
valid: false,
|
|
325
|
+
firstInvalidIndex: null,
|
|
326
|
+
reason: "AUDIT_VERIFY_ERROR",
|
|
327
|
+
errors: [{ index: null, reason: "AUDIT_VERIFY_ERROR", message: err.message }],
|
|
328
|
+
message: `Integrity check failed: ${err.message}`
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
var init_auditLog = __esm({
|
|
333
|
+
"src/core/auditLog.js"() {
|
|
334
|
+
init_atomicWrite();
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// src/core/localPolicy.js
|
|
339
|
+
import fs7 from "fs";
|
|
340
|
+
import path7 from "path";
|
|
341
|
+
import crypto3 from "crypto";
|
|
342
|
+
function glob(pattern) {
|
|
343
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
344
|
+
return new RegExp("^" + escaped.replace(/\*\*\//g, "(?:.*/)?").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$");
|
|
345
|
+
}
|
|
346
|
+
function relative(root, value) {
|
|
347
|
+
const rel = path7.relative(root, path7.resolve(value));
|
|
348
|
+
return rel.split(path7.sep).join("/");
|
|
349
|
+
}
|
|
350
|
+
function localPolicyPaths(rootDir) {
|
|
351
|
+
const root = path7.resolve(rootDir || process.cwd());
|
|
352
|
+
return { root, policyPath: path7.join(root, POLICY_FILE), auditPath: path7.join(root, AUDIT_FILE) };
|
|
353
|
+
}
|
|
354
|
+
function loadLocalPolicy(rootDir, mode = "observe") {
|
|
355
|
+
const paths = localPolicyPaths(rootDir);
|
|
356
|
+
if (!fs7.existsSync(paths.policyPath)) {
|
|
357
|
+
return { ...paths, policy: { version: 1, mode, workspace: paths.root, rules: [] } };
|
|
358
|
+
}
|
|
359
|
+
const policy = JSON.parse(fs7.readFileSync(paths.policyPath, "utf8"));
|
|
360
|
+
if (policy?.version !== 1 || !["observe", "enforce"].includes(policy.mode) || !Array.isArray(policy.rules)) {
|
|
361
|
+
throw new Error(`Invalid ${POLICY_FILE}`);
|
|
362
|
+
}
|
|
363
|
+
return { ...paths, policy };
|
|
364
|
+
}
|
|
365
|
+
function writeLocalPolicy(rootDir, policy) {
|
|
366
|
+
const { policyPath, root } = localPolicyPaths(rootDir);
|
|
367
|
+
const next = { ...policy, version: 1, workspace: root, rules: Array.isArray(policy.rules) ? policy.rules : [] };
|
|
368
|
+
atomicWriteFileSync(policyPath, JSON.stringify(next, null, 2) + "\n", { encoding: "utf8" });
|
|
369
|
+
return next;
|
|
370
|
+
}
|
|
371
|
+
function addLocalPolicyRule(rootDir, rule, mode) {
|
|
372
|
+
if (!rule || !["allow", "deny"].includes(rule.effect) || !["path", "command"].includes(rule.type) || typeof rule.pattern !== "string" || !rule.pattern || typeof rule.from !== "string" || !rule.from) {
|
|
373
|
+
throw new Error("Rule requires effect, type, pattern, and from");
|
|
374
|
+
}
|
|
375
|
+
const loaded = loadLocalPolicy(rootDir, mode);
|
|
376
|
+
const entry = {
|
|
377
|
+
id: rule.id || crypto3.randomUUID(),
|
|
378
|
+
effect: rule.effect,
|
|
379
|
+
type: rule.type,
|
|
380
|
+
pattern: rule.pattern,
|
|
381
|
+
from: rule.from,
|
|
382
|
+
at: rule.at || (/* @__PURE__ */ new Date()).toISOString()
|
|
383
|
+
};
|
|
384
|
+
writeLocalPolicy(loaded.root, { ...loaded.policy, mode: mode || loaded.policy.mode, rules: [...loaded.policy.rules, entry] });
|
|
385
|
+
return { id: entry.id, added: true, rule: entry };
|
|
386
|
+
}
|
|
387
|
+
function proposePolicyRule(rule) {
|
|
388
|
+
return { ...rule, proposed: true, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
389
|
+
}
|
|
390
|
+
function evaluateLocalPolicy(policy, rootDir, { target, command } = {}) {
|
|
391
|
+
const root = path7.resolve(rootDir);
|
|
392
|
+
const candidates = [];
|
|
393
|
+
if (target) candidates.push({ type: "path", value: relative(root, target) });
|
|
394
|
+
if (command) candidates.push({ type: "command", value: command });
|
|
395
|
+
const matched = policy.rules.filter((rule) => candidates.some((c) => c.type === rule.type && glob(rule.pattern).test(c.value)));
|
|
396
|
+
const denied = matched.filter((rule) => rule.effect === "deny");
|
|
397
|
+
return { allowed: denied.length === 0, matched, winningRules: denied.length ? denied : matched.filter((r) => r.effect === "allow"), reason: denied.length ? "LOCAL_POLICY_DENY" : null };
|
|
398
|
+
}
|
|
399
|
+
function auditLocalPolicy(rootDir, entry) {
|
|
400
|
+
const { auditPath } = localPolicyPaths(rootDir);
|
|
401
|
+
appendAudit(auditPath, { kind: "local_policy", timestamp: (/* @__PURE__ */ new Date()).toISOString(), ...entry });
|
|
402
|
+
}
|
|
403
|
+
var POLICY_FILE, AUDIT_FILE;
|
|
404
|
+
var init_localPolicy = __esm({
|
|
405
|
+
"src/core/localPolicy.js"() {
|
|
406
|
+
init_auditLog();
|
|
407
|
+
init_atomicWrite();
|
|
408
|
+
POLICY_FILE = "lbe.policy.json";
|
|
409
|
+
AUDIT_FILE = ".lbe/audit.jsonl";
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// src/core/policyVersionGuard.js
|
|
414
|
+
import fs8 from "fs";
|
|
415
|
+
import path8 from "path";
|
|
416
|
+
function parsePolicyVersion(version) {
|
|
417
|
+
if (typeof version === "number" && Number.isFinite(version)) {
|
|
418
|
+
return { ok: true, kind: "int", parts: [Math.floor(version)], raw: String(version) };
|
|
419
|
+
}
|
|
420
|
+
if (typeof version !== "string" || !version.trim()) {
|
|
421
|
+
return { ok: false, reason: "POLICY_VERSION_INVALID", message: "Policy version is required" };
|
|
422
|
+
}
|
|
423
|
+
const trimmed = version.trim();
|
|
424
|
+
if (/^\d+$/.test(trimmed)) {
|
|
425
|
+
return { ok: true, kind: "int", parts: [Number(trimmed)], raw: trimmed };
|
|
426
|
+
}
|
|
427
|
+
const semver = trimmed.replace(/^v/i, "");
|
|
428
|
+
if (/^\d+(\.\d+){0,2}$/.test(semver)) {
|
|
429
|
+
const parsed = semver.split(".").map((n) => Number(n));
|
|
430
|
+
while (parsed.length < 3) {
|
|
431
|
+
parsed.push(0);
|
|
432
|
+
}
|
|
433
|
+
return { ok: true, kind: "semver", parts: parsed, raw: trimmed };
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
reason: "POLICY_VERSION_INVALID",
|
|
438
|
+
message: `Unsupported policy version format '${version}' (use integer or semver)`
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function compareVersions(a, b) {
|
|
442
|
+
const len = Math.max(a.parts.length, b.parts.length);
|
|
443
|
+
for (let i = 0; i < len; i++) {
|
|
444
|
+
const av = a.parts[i] ?? 0;
|
|
445
|
+
const bv = b.parts[i] ?? 0;
|
|
446
|
+
if (av > bv) return 1;
|
|
447
|
+
if (av < bv) return -1;
|
|
448
|
+
}
|
|
449
|
+
return 0;
|
|
450
|
+
}
|
|
451
|
+
function parseCreatedAt(createdAt) {
|
|
452
|
+
if (typeof createdAt === "number" && Number.isFinite(createdAt)) {
|
|
453
|
+
const sec = createdAt > 1e12 ? Math.floor(createdAt / 1e3) : Math.floor(createdAt);
|
|
454
|
+
return { ok: true, epochSec: sec };
|
|
455
|
+
}
|
|
456
|
+
if (typeof createdAt !== "string" || !createdAt.trim()) {
|
|
457
|
+
return {
|
|
458
|
+
ok: false,
|
|
459
|
+
reason: "POLICY_CREATED_AT_INVALID",
|
|
460
|
+
message: "Policy createdAt is required"
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const ts = Date.parse(createdAt);
|
|
464
|
+
if (Number.isNaN(ts)) {
|
|
465
|
+
return {
|
|
466
|
+
ok: false,
|
|
467
|
+
reason: "POLICY_CREATED_AT_INVALID",
|
|
468
|
+
message: `Invalid policy createdAt '${createdAt}'`
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
return { ok: true, epochSec: Math.floor(ts / 1e3) };
|
|
472
|
+
}
|
|
473
|
+
function loadPolicyState(statePath) {
|
|
474
|
+
if (!fs8.existsSync(statePath)) {
|
|
475
|
+
return {
|
|
476
|
+
schemaVersion: "1",
|
|
477
|
+
lastAccepted: null,
|
|
478
|
+
updatedAt: null
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
const parsed = JSON.parse(fs8.readFileSync(statePath, "utf8"));
|
|
483
|
+
if (!parsed || typeof parsed !== "object") {
|
|
484
|
+
throw new Error("Policy state file has invalid structure");
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
schemaVersion: String(parsed.schemaVersion || "1"),
|
|
488
|
+
lastAccepted: parsed.lastAccepted && typeof parsed.lastAccepted === "object" ? parsed.lastAccepted : null,
|
|
489
|
+
updatedAt: parsed.updatedAt || null
|
|
490
|
+
};
|
|
491
|
+
} catch (err) {
|
|
492
|
+
throw new Error(`Policy state at ${statePath} is corrupt or unreadable: ${err.message}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
function savePolicyState(statePath, stateObj) {
|
|
496
|
+
const payload = JSON.stringify(stateObj, null, 2);
|
|
497
|
+
atomicWriteFileSync(statePath, payload, { encoding: "utf8" });
|
|
498
|
+
}
|
|
499
|
+
function validateAndUpdatePolicyVersionState({
|
|
500
|
+
policyObj,
|
|
501
|
+
statePath = path8.resolve(".lbe/data/policy.state.json"),
|
|
502
|
+
maxCreatedAtSkewSec = 31536e3,
|
|
503
|
+
nowSec = Math.floor(Date.now() / 1e3),
|
|
504
|
+
persist = true
|
|
505
|
+
}) {
|
|
506
|
+
const version = parsePolicyVersion(policyObj?.version);
|
|
507
|
+
if (!version.ok) {
|
|
508
|
+
return {
|
|
509
|
+
ok: false,
|
|
510
|
+
reason: version.reason,
|
|
511
|
+
message: version.message,
|
|
512
|
+
updated: false
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const createdAt = parseCreatedAt(policyObj?.createdAt);
|
|
516
|
+
if (!createdAt.ok) {
|
|
517
|
+
return {
|
|
518
|
+
ok: false,
|
|
519
|
+
reason: createdAt.reason,
|
|
520
|
+
message: createdAt.message,
|
|
521
|
+
updated: false
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
const skew = Math.abs(nowSec - createdAt.epochSec);
|
|
525
|
+
const allowedSkew = Number.isFinite(maxCreatedAtSkewSec) && maxCreatedAtSkewSec > 0 ? Math.floor(maxCreatedAtSkewSec) : 31536e3;
|
|
526
|
+
if (skew > allowedSkew) {
|
|
527
|
+
return {
|
|
528
|
+
ok: false,
|
|
529
|
+
reason: "POLICY_CREATED_AT_SKEW_EXCEEDED",
|
|
530
|
+
message: `Policy createdAt skew ${skew}s exceeds allowed ${allowedSkew}s`,
|
|
531
|
+
updated: false
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
let state;
|
|
535
|
+
try {
|
|
536
|
+
state = loadPolicyState(statePath);
|
|
537
|
+
} catch (err) {
|
|
538
|
+
return {
|
|
539
|
+
ok: false,
|
|
540
|
+
reason: "POLICY_STATE_CORRUPT",
|
|
541
|
+
message: err.message,
|
|
542
|
+
updated: false
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
const previous = state.lastAccepted;
|
|
546
|
+
let previousVersion = null;
|
|
547
|
+
let previousCreatedAt = null;
|
|
548
|
+
let versionCompare = 0;
|
|
549
|
+
if (previous) {
|
|
550
|
+
previousVersion = parsePolicyVersion(previous.version);
|
|
551
|
+
previousCreatedAt = parseCreatedAt(previous.createdAt);
|
|
552
|
+
if (previousVersion.ok && previousCreatedAt.ok) {
|
|
553
|
+
versionCompare = compareVersions(version, previousVersion);
|
|
554
|
+
if (versionCompare < 0) {
|
|
555
|
+
return {
|
|
556
|
+
ok: false,
|
|
557
|
+
reason: "POLICY_VERSION_REGRESSION",
|
|
558
|
+
message: `Policy version regression: current '${version.raw}' < last '${previousVersion.raw}'`,
|
|
559
|
+
updated: false
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
if (versionCompare === 0 && createdAt.epochSec < previousCreatedAt.epochSec) {
|
|
563
|
+
return {
|
|
564
|
+
ok: false,
|
|
565
|
+
reason: "POLICY_CREATED_AT_REGRESSION",
|
|
566
|
+
message: `Policy createdAt regression: current '${policyObj.createdAt}' < last '${previous.createdAt}'`,
|
|
567
|
+
updated: false
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (versionCompare > 0 && createdAt.epochSec < previousCreatedAt.epochSec) {
|
|
571
|
+
return {
|
|
572
|
+
ok: false,
|
|
573
|
+
reason: "POLICY_CREATED_AT_REGRESSION",
|
|
574
|
+
message: `Policy createdAt must be monotonic when version increases`,
|
|
575
|
+
updated: false
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
const shouldUpdate = !previous || !previousVersion?.ok || !previousCreatedAt?.ok || versionCompare > 0 || versionCompare === 0 && createdAt.epochSec > previousCreatedAt.epochSec;
|
|
581
|
+
if (persist && shouldUpdate) {
|
|
582
|
+
const nextState = {
|
|
583
|
+
schemaVersion: "1",
|
|
584
|
+
lastAccepted: {
|
|
585
|
+
version: policyObj.version,
|
|
586
|
+
createdAt: policyObj.createdAt,
|
|
587
|
+
environment: policyObj.environment || null
|
|
588
|
+
},
|
|
589
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
590
|
+
};
|
|
591
|
+
savePolicyState(statePath, nextState);
|
|
592
|
+
}
|
|
593
|
+
return {
|
|
594
|
+
ok: true,
|
|
595
|
+
reason: null,
|
|
596
|
+
message: "Policy version guard passed",
|
|
597
|
+
updated: shouldUpdate
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
var init_policyVersionGuard = __esm({
|
|
601
|
+
"src/core/policyVersionGuard.js"() {
|
|
602
|
+
init_atomicWrite();
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
// runtime/engine.js
|
|
607
|
+
import fs9 from "fs";
|
|
608
|
+
import path9 from "path";
|
|
609
|
+
import { fileURLToPath } from "url";
|
|
610
|
+
function wasm() {
|
|
611
|
+
if (_instance) return _instance;
|
|
612
|
+
if (!fs9.existsSync(wasmPath)) throw new Error(`LBE engine missing: ${wasmPath}`);
|
|
613
|
+
const bytes = fs9.readFileSync(wasmPath);
|
|
614
|
+
_instance = new WebAssembly.Instance(new WebAssembly.Module(bytes), {});
|
|
615
|
+
return _instance;
|
|
616
|
+
}
|
|
617
|
+
function memory() {
|
|
618
|
+
return new Uint8Array(wasm().exports.memory.buffer);
|
|
619
|
+
}
|
|
620
|
+
function inPtr() {
|
|
621
|
+
return wasm().exports.lbe_in_ptr();
|
|
622
|
+
}
|
|
623
|
+
function outPtr() {
|
|
624
|
+
return wasm().exports.lbe_out_ptr();
|
|
625
|
+
}
|
|
626
|
+
function bufSize() {
|
|
627
|
+
return wasm().exports.lbe_buf_size();
|
|
628
|
+
}
|
|
629
|
+
function writeIn(str) {
|
|
630
|
+
const enc = new TextEncoder().encode(str);
|
|
631
|
+
const mem = memory();
|
|
632
|
+
const ptr = inPtr();
|
|
633
|
+
mem.set(enc, ptr);
|
|
634
|
+
mem[ptr + enc.length] = 0;
|
|
635
|
+
}
|
|
636
|
+
function readOut() {
|
|
637
|
+
const mem = memory();
|
|
638
|
+
const ptr = outPtr();
|
|
639
|
+
let end = ptr;
|
|
640
|
+
while (mem[end] !== 0 && end - ptr < bufSize()) end++;
|
|
641
|
+
return new TextDecoder().decode(mem.slice(ptr, end));
|
|
642
|
+
}
|
|
643
|
+
function writePipelineInput(fields) {
|
|
644
|
+
const mem = memory();
|
|
645
|
+
const ptr = inPtr();
|
|
646
|
+
const view = new DataView(mem.buffer, ptr);
|
|
647
|
+
fields.forEach((v, i) => view.setUint32(i * 4, v >>> 0, true));
|
|
648
|
+
}
|
|
649
|
+
function readPipelineOutput() {
|
|
650
|
+
const mem = memory();
|
|
651
|
+
const ptr = outPtr();
|
|
652
|
+
const view = new DataView(mem.buffer, ptr);
|
|
653
|
+
return { stage: view.getUint32(0, true), code: view.getUint32(4, true) };
|
|
654
|
+
}
|
|
655
|
+
function runValidationPipeline(flags) {
|
|
656
|
+
writePipelineInput([
|
|
657
|
+
// Schema flags [0..24]
|
|
658
|
+
flags.hasId ? 1 : 0,
|
|
659
|
+
flags.idValid ? 1 : 0,
|
|
660
|
+
flags.hasCommandId ? 1 : 0,
|
|
661
|
+
flags.commandIdValid ? 1 : 0,
|
|
662
|
+
flags.hasRequesterId ? 1 : 0,
|
|
663
|
+
flags.requesterIdValid ? 1 : 0,
|
|
664
|
+
flags.hasSessionId ? 1 : 0,
|
|
665
|
+
flags.sessionIdValid ? 1 : 0,
|
|
666
|
+
flags.hasTimestamp ? 1 : 0,
|
|
667
|
+
flags.timestampValid ? 1 : 0,
|
|
668
|
+
flags.hasNonce ? 1 : 0,
|
|
669
|
+
flags.nonceValid ? 1 : 0,
|
|
670
|
+
flags.hasRequires ? 1 : 0,
|
|
671
|
+
flags.requiresValid ? 1 : 0,
|
|
672
|
+
flags.hasPayload ? 1 : 0,
|
|
673
|
+
flags.hasPayloadAdapter ? 1 : 0,
|
|
674
|
+
flags.payloadAdapterValid ? 1 : 0,
|
|
675
|
+
flags.hasSignature ? 1 : 0,
|
|
676
|
+
flags.hasSignatureAlg ? 1 : 0,
|
|
677
|
+
flags.signatureAlgValid ? 1 : 0,
|
|
678
|
+
flags.hasSignatureKeyId ? 1 : 0,
|
|
679
|
+
flags.hasSignatureSig ? 1 : 0,
|
|
680
|
+
flags.signatureSigValid ? 1 : 0,
|
|
681
|
+
flags.hasRisk ? 1 : 0,
|
|
682
|
+
flags.riskValid ? 1 : 0,
|
|
683
|
+
// Timestamp [25..27]
|
|
684
|
+
flags.cmdTimestamp >>> 0,
|
|
685
|
+
flags.nowSec >>> 0,
|
|
686
|
+
flags.maxClockSkewSec >>> 0,
|
|
687
|
+
// Key lifecycle [28..34]
|
|
688
|
+
flags.keyIdFormatValid ? 1 : 0,
|
|
689
|
+
flags.keyFound ? 1 : 0,
|
|
690
|
+
flags.keyNotDeprecated ? 1 : 0,
|
|
691
|
+
flags.keyRequesterMatches ? 1 : 0,
|
|
692
|
+
flags.keyNotBeforeOk ? 1 : 0,
|
|
693
|
+
flags.keyNotExpired ? 1 : 0,
|
|
694
|
+
flags.keyLifecycleFieldsPresent ? 1 : 0,
|
|
695
|
+
// Signature [35]
|
|
696
|
+
flags.signatureValid ? 1 : 0,
|
|
697
|
+
// Rate limit [36..37]
|
|
698
|
+
flags.rateLimitOk ? 1 : 0,
|
|
699
|
+
flags.rateLimitRetryAfterSec >>> 0,
|
|
700
|
+
// Nonce [38]
|
|
701
|
+
flags.nonceOk ? 1 : 0,
|
|
702
|
+
// Policy [39..48]
|
|
703
|
+
flags.policyConfigured ? 1 : 0,
|
|
704
|
+
flags.requesterConfigured ? 1 : 0,
|
|
705
|
+
flags.commandAllowed ? 1 : 0,
|
|
706
|
+
flags.adapterAllowed ? 1 : 0,
|
|
707
|
+
flags.filesystemRequired ? 1 : 0,
|
|
708
|
+
flags.filesystemRootsDefined ? 1 : 0,
|
|
709
|
+
flags.filesystemOk ? 1 : 0,
|
|
710
|
+
flags.pathDenied ? 1 : 0,
|
|
711
|
+
flags.shellRequired ? 1 : 0,
|
|
712
|
+
flags.shellCommandOk ? 1 : 0
|
|
713
|
+
]);
|
|
714
|
+
wasm().exports.lbe_validate_pipeline();
|
|
715
|
+
const { stage, code } = readPipelineOutput();
|
|
716
|
+
const ok = stage === 255;
|
|
717
|
+
return {
|
|
718
|
+
ok,
|
|
719
|
+
stage,
|
|
720
|
+
stageLabel: PIPELINE_STAGES[stage] || "unknown",
|
|
721
|
+
code,
|
|
722
|
+
schemaError: stage === 0 ? SCHEMA_MESSAGES[code]?.error || "Schema invalid" : null,
|
|
723
|
+
keyReason: stage === 2 ? KEY_REASONS[code] || "KEY_ERROR" : null,
|
|
724
|
+
policyResult: stage === 6 ? { ...POLICY_MESSAGES[code] || POLICY_MESSAGES[1], code } : null,
|
|
725
|
+
retryAfterSec: stage === 4 ? code : 0,
|
|
726
|
+
skewSec: stage === 1 ? code : 0
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function checkNonce({ ttlSec, nowSec, newKey, existingEntries }) {
|
|
730
|
+
const lines = [`${ttlSec}:${nowSec}`, newKey, ...existingEntries].join("\n") + "\n";
|
|
731
|
+
writeIn(lines);
|
|
732
|
+
const isReplay = wasm().exports.lbe_nonce_check() !== 0;
|
|
733
|
+
if (isReplay) return { ok: false, updatedEntriesText: null };
|
|
734
|
+
const out = readOut();
|
|
735
|
+
return { ok: true, updatedEntriesText: out.startsWith("OK\n") ? out.slice(3) : out };
|
|
736
|
+
}
|
|
737
|
+
function checkRateLimit({ windowSec, maxRequests, nowSec, requesterId, existingEntries }) {
|
|
738
|
+
const lines = [
|
|
739
|
+
`${windowSec}:${maxRequests}:${nowSec}`,
|
|
740
|
+
requesterId,
|
|
741
|
+
...existingEntries
|
|
742
|
+
].join("\n") + "\n";
|
|
743
|
+
writeIn(lines);
|
|
744
|
+
const exceeded = wasm().exports.lbe_rate_check() !== 0;
|
|
745
|
+
const out = readOut();
|
|
746
|
+
if (exceeded) {
|
|
747
|
+
const retryAfterSec = parseInt(out.match(/^EXCEEDED:(\d+)/)?.[1] ?? "1", 10);
|
|
748
|
+
const entriesText = out.replace(/^EXCEEDED:\d+\n/, "");
|
|
749
|
+
return { ok: false, retryAfterSec, updatedEntriesText: entriesText };
|
|
750
|
+
}
|
|
751
|
+
return { ok: true, retryAfterSec: 0, updatedEntriesText: out.startsWith("OK\n") ? out.slice(3) : out };
|
|
752
|
+
}
|
|
753
|
+
function classifyRisk(commandId, shellCmdIsRm = false) {
|
|
754
|
+
const typeCode = COMMAND_TYPE[commandId] ?? 0;
|
|
755
|
+
const code = wasm().exports.lbe_classify_risk(typeCode, shellCmdIsRm ? 1 : 0);
|
|
756
|
+
return RISK_LABELS[code] ?? "LOW";
|
|
757
|
+
}
|
|
758
|
+
var runtimeDir, wasmPath, POLICY_MESSAGES, SCHEMA_MESSAGES, KEY_REASONS, PIPELINE_STAGES, RISK_LABELS, COMMAND_TYPE, _instance;
|
|
759
|
+
var init_engine = __esm({
|
|
760
|
+
"runtime/engine.js"() {
|
|
761
|
+
runtimeDir = path9.dirname(fileURLToPath(import.meta.url));
|
|
762
|
+
wasmPath = path9.join(runtimeDir, "lbe_engine.wasm");
|
|
763
|
+
POLICY_MESSAGES = {
|
|
764
|
+
0: { allowed: true, reason: null, message: "Policy check passed" },
|
|
765
|
+
1: { allowed: false, reason: "POLICY_NOT_CONFIGURED", message: "No policy configured" },
|
|
766
|
+
2: { allowed: false, reason: "REQUESTER_NOT_ALLOWED", message: "Requester not in policy" },
|
|
767
|
+
3: { allowed: false, reason: "COMMAND_NOT_ALLOWED", message: "Command not allowed for requester" },
|
|
768
|
+
4: { allowed: false, reason: "ADAPTER_NOT_ALLOWED", message: "Adapter not allowed" },
|
|
769
|
+
5: { allowed: false, reason: "NO_FILESYSTEM_ROOTS_DEFINED", message: "No filesystem roots defined for requester" },
|
|
770
|
+
6: { allowed: false, reason: "CWD_OUTSIDE_ALLOWED_ROOT", message: "Path not under allowed roots" },
|
|
771
|
+
7: { allowed: false, reason: "PATH_DENIED_BY_PATTERN", message: "Path matches deny pattern" },
|
|
772
|
+
8: { allowed: false, reason: "SHELL_CMD_DENIED", message: "Shell command not allowed" }
|
|
773
|
+
};
|
|
774
|
+
SCHEMA_MESSAGES = {
|
|
775
|
+
0: { valid: true, error: null },
|
|
776
|
+
1: { valid: false, error: "Missing required field: id" },
|
|
777
|
+
2: { valid: false, error: "Missing required field: commandId" },
|
|
778
|
+
3: { valid: false, error: "Missing required field: requesterId" },
|
|
779
|
+
4: { valid: false, error: "Missing required field: sessionId" },
|
|
780
|
+
5: { valid: false, error: "Missing required field: timestamp" },
|
|
781
|
+
6: { valid: false, error: "Missing required field: nonce" },
|
|
782
|
+
7: { valid: false, error: "Missing required field: requires" },
|
|
783
|
+
8: { valid: false, error: "Missing required field: payload" },
|
|
784
|
+
9: { valid: false, error: "Missing required field: signature" },
|
|
785
|
+
10: { valid: false, error: "Field 'id' is invalid" },
|
|
786
|
+
11: { valid: false, error: "Field 'commandId' is invalid" },
|
|
787
|
+
12: { valid: false, error: "Field 'requesterId' is invalid" },
|
|
788
|
+
13: { valid: false, error: "Field 'sessionId' is invalid" },
|
|
789
|
+
14: { valid: false, error: "Field 'timestamp' is invalid" },
|
|
790
|
+
15: { valid: false, error: "Field 'nonce' is invalid" },
|
|
791
|
+
16: { valid: false, error: "Field 'requires' is invalid" },
|
|
792
|
+
17: { valid: false, error: "payload: missing required field: adapter" },
|
|
793
|
+
18: { valid: false, error: "payload: field 'adapter' is invalid" },
|
|
794
|
+
19: { valid: false, error: "signature: missing required field: alg" },
|
|
795
|
+
20: { valid: false, error: "signature: missing required field: keyId" },
|
|
796
|
+
21: { valid: false, error: "signature: missing required field: sig" },
|
|
797
|
+
22: { valid: false, error: "signature: field 'alg' must be ed25519" },
|
|
798
|
+
23: { valid: false, error: "signature: field 'sig' is invalid" },
|
|
799
|
+
24: { valid: false, error: "Field 'risk' is invalid" }
|
|
800
|
+
};
|
|
801
|
+
KEY_REASONS = {
|
|
802
|
+
1: "KEY_ID_INVALID",
|
|
803
|
+
2: "KEY_NOT_TRUSTED",
|
|
804
|
+
3: "KEY_DEPRECATED",
|
|
805
|
+
4: "KEY_REQUESTER_MISMATCH",
|
|
806
|
+
5: "KEY_LIFECYCLE_INVALID",
|
|
807
|
+
6: "KEY_NOT_YET_VALID",
|
|
808
|
+
7: "KEY_EXPIRED"
|
|
809
|
+
};
|
|
810
|
+
PIPELINE_STAGES = {
|
|
811
|
+
0: "schema",
|
|
812
|
+
1: "timestamp",
|
|
813
|
+
2: "key",
|
|
814
|
+
3: "signature",
|
|
815
|
+
4: "rate_limit",
|
|
816
|
+
5: "nonce",
|
|
817
|
+
6: "policy",
|
|
818
|
+
255: "ok"
|
|
819
|
+
};
|
|
820
|
+
RISK_LABELS = ["LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
821
|
+
COMMAND_TYPE = { ECHO: 0, READ_FILE: 1, WRITE_FILE: 2, PATCH_FILE: 3, DELETE_FILE: 4, RUN_SHELL: 5 };
|
|
822
|
+
_instance = null;
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
// src/core/validator.js
|
|
827
|
+
import path10 from "path";
|
|
828
|
+
function extractSchemaFlags(cmd2) {
|
|
829
|
+
const has = (k) => cmd2 != null && Object.prototype.hasOwnProperty.call(cmd2, k);
|
|
830
|
+
const isStr = (v) => typeof v === "string";
|
|
831
|
+
const p = cmd2?.payload;
|
|
832
|
+
const sig = cmd2?.signature;
|
|
833
|
+
return {
|
|
834
|
+
hasId: has("id"),
|
|
835
|
+
idValid: isStr(cmd2?.id) && /^[A-Z_]+$/.test(cmd2.id) && cmd2.id.length >= 1 && cmd2.id.length <= 50,
|
|
836
|
+
hasCommandId: has("commandId"),
|
|
837
|
+
commandIdValid: isStr(cmd2?.commandId) && /^[a-f0-9-]+$/.test(cmd2.commandId) && cmd2.commandId.length === 36,
|
|
838
|
+
hasRequesterId: has("requesterId"),
|
|
839
|
+
requesterIdValid: isStr(cmd2?.requesterId) && cmd2.requesterId.length >= 3 && cmd2.requesterId.length <= 100,
|
|
840
|
+
hasSessionId: has("sessionId"),
|
|
841
|
+
sessionIdValid: isStr(cmd2?.sessionId) && cmd2.sessionId.length >= 3,
|
|
842
|
+
hasTimestamp: has("timestamp"),
|
|
843
|
+
timestampValid: typeof cmd2?.timestamp === "number" && cmd2.timestamp >= 1e9,
|
|
844
|
+
hasNonce: has("nonce"),
|
|
845
|
+
nonceValid: isStr(cmd2?.nonce) && cmd2.nonce.length >= 32 && cmd2.nonce.length <= 128,
|
|
846
|
+
hasRequires: has("requires"),
|
|
847
|
+
requiresValid: Array.isArray(cmd2?.requires) && cmd2.requires.length >= 1 && cmd2.requires.every(isStr),
|
|
848
|
+
hasPayload: has("payload") && typeof p === "object" && p !== null && !Array.isArray(p),
|
|
849
|
+
hasPayloadAdapter: p != null && Object.prototype.hasOwnProperty.call(p, "adapter"),
|
|
850
|
+
payloadAdapterValid: isStr(p?.adapter),
|
|
851
|
+
hasSignature: has("signature") && typeof sig === "object" && sig !== null && !Array.isArray(sig),
|
|
852
|
+
hasSignatureAlg: sig != null && Object.prototype.hasOwnProperty.call(sig, "alg"),
|
|
853
|
+
signatureAlgValid: sig?.alg === "ed25519",
|
|
854
|
+
hasSignatureKeyId: sig != null && Object.prototype.hasOwnProperty.call(sig, "keyId"),
|
|
855
|
+
hasSignatureSig: sig != null && Object.prototype.hasOwnProperty.call(sig, "sig"),
|
|
856
|
+
signatureSigValid: isStr(sig?.sig) && sig.sig.length >= 10,
|
|
857
|
+
hasRisk: has("risk"),
|
|
858
|
+
riskValid: ["LOW", "MEDIUM", "HIGH", "CRITICAL"].includes(cmd2?.risk)
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function extractPolicyFlags(policy, cmd2) {
|
|
862
|
+
const hasPolicy = !!(policy && policy.default === "DENY" && policy.requesters && typeof policy.requesters === "object");
|
|
863
|
+
const rp = policy?.requesters?.[cmd2.requesterId];
|
|
864
|
+
const cmdId = cmd2.id?.toLowerCase() ?? "";
|
|
865
|
+
const commandAllowed = !!rp?.allowCommands?.some((c) => c.toLowerCase() === cmdId);
|
|
866
|
+
const adapterAllowed = !!rp?.allowAdapters?.includes(cmd2.payload?.adapter);
|
|
867
|
+
const filesystemRequired = !!cmd2.payload?.cwd;
|
|
868
|
+
let filesystemRootsDefined = false;
|
|
869
|
+
let filesystemOk = false;
|
|
870
|
+
let pathDenied = false;
|
|
871
|
+
if (filesystemRequired) {
|
|
872
|
+
const roots = rp?.filesystem?.roots ?? [];
|
|
873
|
+
filesystemRootsDefined = roots.length > 0;
|
|
874
|
+
if (filesystemRootsDefined) {
|
|
875
|
+
const cwd = path10.resolve(cmd2.payload.cwd);
|
|
876
|
+
filesystemOk = roots.some((r) => {
|
|
877
|
+
const rr = path10.resolve(r);
|
|
878
|
+
return cwd === rr || cwd.startsWith(rr + path10.sep);
|
|
879
|
+
});
|
|
880
|
+
const denyPatterns = rp?.filesystem?.denyPatterns ?? [];
|
|
881
|
+
pathDenied = denyPatterns.some((pattern) => {
|
|
882
|
+
const re = new RegExp("^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*") + "$");
|
|
883
|
+
return re.test(cwd);
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
let shellRequired = false;
|
|
888
|
+
let shellCommandOk = true;
|
|
889
|
+
if (cmd2.id === "RUN_SHELL") {
|
|
890
|
+
shellRequired = true;
|
|
891
|
+
const allowCmds = rp?.exec?.allowCmds ?? [];
|
|
892
|
+
const denyCmds = rp?.exec?.denyCmds ?? [];
|
|
893
|
+
const shellCmd = cmd2.payload?.cmd;
|
|
894
|
+
if (denyCmds.includes(shellCmd)) {
|
|
895
|
+
shellCommandOk = false;
|
|
896
|
+
} else {
|
|
897
|
+
shellCommandOk = allowCmds.length === 0 || allowCmds.includes(shellCmd);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return {
|
|
901
|
+
policyConfigured: hasPolicy,
|
|
902
|
+
requesterConfigured: !!rp,
|
|
903
|
+
commandAllowed,
|
|
904
|
+
adapterAllowed,
|
|
905
|
+
filesystemRequired,
|
|
906
|
+
filesystemRootsDefined,
|
|
907
|
+
filesystemOk,
|
|
908
|
+
pathDenied,
|
|
909
|
+
shellRequired,
|
|
910
|
+
shellCommandOk
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
function extractKeyFlags(keyStore, keyId, requesterId, now = /* @__PURE__ */ new Date()) {
|
|
914
|
+
if (!keyStore || !keyId) {
|
|
915
|
+
return {
|
|
916
|
+
keyIdFormatValid: false,
|
|
917
|
+
keyFound: false,
|
|
918
|
+
keyNotDeprecated: false,
|
|
919
|
+
keyRequesterMatches: false,
|
|
920
|
+
keyNotBeforeOk: false,
|
|
921
|
+
keyNotExpired: false,
|
|
922
|
+
keyLifecycleFieldsPresent: false,
|
|
923
|
+
publicKey: null
|
|
924
|
+
};
|
|
925
|
+
}
|
|
926
|
+
const KEY_ID_RE = /^[A-Za-z0-9:_-]{3,128}$/;
|
|
927
|
+
const keyIdFormatValid = KEY_ID_RE.test(keyId) && keyId !== "default";
|
|
928
|
+
if (!keyIdFormatValid) {
|
|
929
|
+
return {
|
|
930
|
+
keyIdFormatValid,
|
|
931
|
+
keyFound: false,
|
|
932
|
+
keyNotDeprecated: false,
|
|
933
|
+
keyRequesterMatches: false,
|
|
934
|
+
keyNotBeforeOk: false,
|
|
935
|
+
keyNotExpired: false,
|
|
936
|
+
keyLifecycleFieldsPresent: false,
|
|
937
|
+
publicKey: null
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
const entry = keyStore.trustedKeys?.[keyId];
|
|
941
|
+
const keyFound = !!entry;
|
|
942
|
+
if (!keyFound) {
|
|
943
|
+
return {
|
|
944
|
+
keyIdFormatValid,
|
|
945
|
+
keyFound,
|
|
946
|
+
keyNotDeprecated: false,
|
|
947
|
+
keyRequesterMatches: false,
|
|
948
|
+
keyNotBeforeOk: false,
|
|
949
|
+
keyNotExpired: false,
|
|
950
|
+
keyLifecycleFieldsPresent: false,
|
|
951
|
+
publicKey: null
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
const keyNotDeprecated = !entry.deprecated;
|
|
955
|
+
const keyRequesterMatches = !entry.requesterId || entry.requesterId === requesterId;
|
|
956
|
+
const notBefore = entry.notBefore || entry.validFrom;
|
|
957
|
+
const expiresAt = entry.expiresAt || entry.validUntil;
|
|
958
|
+
const keyLifecycleFieldsPresent = typeof notBefore === "string" && typeof expiresAt === "string";
|
|
959
|
+
let keyNotBeforeOk = false;
|
|
960
|
+
let keyNotExpired = false;
|
|
961
|
+
if (keyLifecycleFieldsPresent) {
|
|
962
|
+
const nb = new Date(notBefore);
|
|
963
|
+
const exp = new Date(expiresAt);
|
|
964
|
+
if (!isNaN(nb.getTime()) && !isNaN(exp.getTime()) && nb < exp) {
|
|
965
|
+
keyNotBeforeOk = now >= nb;
|
|
966
|
+
keyNotExpired = now < exp;
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
keyIdFormatValid,
|
|
971
|
+
keyFound,
|
|
972
|
+
keyNotDeprecated,
|
|
973
|
+
keyRequesterMatches,
|
|
974
|
+
keyNotBeforeOk,
|
|
975
|
+
keyNotExpired,
|
|
976
|
+
keyLifecycleFieldsPresent,
|
|
977
|
+
publicKey: entry.publicKey ?? null
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function nonceEntriesToText(db) {
|
|
981
|
+
return (db?.entries ?? []).map((e) => `${e.key}:${e.timestamp}`);
|
|
982
|
+
}
|
|
983
|
+
function textToNonceEntries(text) {
|
|
984
|
+
return text.split("\n").filter(Boolean).map((line) => {
|
|
985
|
+
const lastColon = line.lastIndexOf(":");
|
|
986
|
+
return {
|
|
987
|
+
key: line.slice(0, lastColon),
|
|
988
|
+
timestamp: parseInt(line.slice(lastColon + 1), 10) || 0
|
|
989
|
+
};
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
function rateEntriesToText(db) {
|
|
993
|
+
return (db?.entries ?? []).map((e) => `${e.requesterId}:${e.timestamp}`);
|
|
994
|
+
}
|
|
995
|
+
function textToRateEntries(text) {
|
|
996
|
+
return text.split("\n").filter(Boolean).map((line) => {
|
|
997
|
+
const lastColon = line.lastIndexOf(":");
|
|
998
|
+
return {
|
|
999
|
+
requesterId: line.slice(0, lastColon),
|
|
1000
|
+
timestamp: parseInt(line.slice(lastColon + 1), 10) || 0
|
|
1001
|
+
};
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
function validateCommand({
|
|
1005
|
+
commandObj,
|
|
1006
|
+
pubKeyB64,
|
|
1007
|
+
keyStore,
|
|
1008
|
+
nonceDb,
|
|
1009
|
+
policy,
|
|
1010
|
+
rateLimiter,
|
|
1011
|
+
policyStatePath
|
|
1012
|
+
}) {
|
|
1013
|
+
const result = {
|
|
1014
|
+
valid: false,
|
|
1015
|
+
commandId: commandObj?.commandId,
|
|
1016
|
+
checks: {},
|
|
1017
|
+
errors: []
|
|
1018
|
+
};
|
|
1019
|
+
const nowSec = Math.floor(Date.now() / 1e3);
|
|
1020
|
+
const now = /* @__PURE__ */ new Date();
|
|
1021
|
+
const maxClockSkewSec = Number.isFinite(policy?.security?.maxClockSkewSec) ? policy.security.maxClockSkewSec : 600;
|
|
1022
|
+
if (policyStatePath && policy?.version !== void 0) {
|
|
1023
|
+
try {
|
|
1024
|
+
const vCheck = validateAndUpdatePolicyVersionState({ policyObj: policy, statePath: policyStatePath });
|
|
1025
|
+
result.checks.policyVersion = vCheck.ok;
|
|
1026
|
+
if (!vCheck.ok) {
|
|
1027
|
+
result.errors.push({ type: "POLICY_VERSION_INVALID", message: vCheck.message });
|
|
1028
|
+
return result;
|
|
1029
|
+
}
|
|
1030
|
+
} catch {
|
|
1031
|
+
result.checks.policyVersion = true;
|
|
1032
|
+
}
|
|
1033
|
+
} else {
|
|
1034
|
+
result.checks.policyVersion = true;
|
|
1035
|
+
}
|
|
1036
|
+
const schemaFlags = extractSchemaFlags(commandObj);
|
|
1037
|
+
const keyId = commandObj?.signature?.keyId;
|
|
1038
|
+
const keyFlags = extractKeyFlags(keyStore, keyId, commandObj?.requesterId, now);
|
|
1039
|
+
let signatureValid = false;
|
|
1040
|
+
let effectivePubKey = keyFlags.publicKey;
|
|
1041
|
+
if (!effectivePubKey && pubKeyB64) effectivePubKey = pubKeyB64;
|
|
1042
|
+
if (effectivePubKey) {
|
|
1043
|
+
const bodyWithoutSig = { ...commandObj };
|
|
1044
|
+
delete bodyWithoutSig.signature;
|
|
1045
|
+
const sigCheck = verifyEd25519({
|
|
1046
|
+
payloadObj: bodyWithoutSig,
|
|
1047
|
+
sigB64: commandObj?.signature?.sig,
|
|
1048
|
+
pubKeyB64: effectivePubKey
|
|
1049
|
+
});
|
|
1050
|
+
signatureValid = sigCheck.valid;
|
|
1051
|
+
}
|
|
1052
|
+
let rateLimitOk = true;
|
|
1053
|
+
let rateLimitRetryAfterSec = 0;
|
|
1054
|
+
if (signatureValid && rateLimiter && typeof rateLimiter.db !== "undefined") {
|
|
1055
|
+
const rateCfg = policy?.requesters?.[commandObj.requesterId]?.rateLimit || {};
|
|
1056
|
+
const dfltCfg = policy?.security?.defaultRateLimit || {};
|
|
1057
|
+
const windowSec = rateCfg.windowSec ?? dfltCfg.windowSec ?? 60;
|
|
1058
|
+
const maxRequests = rateCfg.maxRequests ?? dfltCfg.maxRequests ?? 30;
|
|
1059
|
+
const rateResult = checkRateLimit({
|
|
1060
|
+
windowSec,
|
|
1061
|
+
maxRequests,
|
|
1062
|
+
nowSec,
|
|
1063
|
+
requesterId: commandObj.requesterId,
|
|
1064
|
+
existingEntries: rateEntriesToText(rateLimiter.db)
|
|
1065
|
+
});
|
|
1066
|
+
rateLimitOk = rateResult.ok;
|
|
1067
|
+
rateLimitRetryAfterSec = rateResult.retryAfterSec;
|
|
1068
|
+
if (rateResult.ok) {
|
|
1069
|
+
rateLimiter.db.entries = textToRateEntries(rateResult.updatedEntriesText);
|
|
1070
|
+
}
|
|
1071
|
+
} else if (signatureValid && rateLimiter && typeof rateLimiter.checkAndRecord === "function") {
|
|
1072
|
+
const rateCfg = policy?.requesters?.[commandObj.requesterId]?.rateLimit || {};
|
|
1073
|
+
const dfltCfg = policy?.security?.defaultRateLimit || {};
|
|
1074
|
+
const rateCheck = rateLimiter.checkAndRecord({
|
|
1075
|
+
requesterId: commandObj.requesterId,
|
|
1076
|
+
nowSec,
|
|
1077
|
+
windowSec: rateCfg.windowSec ?? dfltCfg.windowSec ?? 60,
|
|
1078
|
+
maxRequests: rateCfg.maxRequests ?? dfltCfg.maxRequests ?? 30
|
|
1079
|
+
});
|
|
1080
|
+
rateLimitOk = rateCheck.ok;
|
|
1081
|
+
rateLimitRetryAfterSec = rateCheck.retryAfterSec ?? 0;
|
|
1082
|
+
}
|
|
1083
|
+
let nonceOk = true;
|
|
1084
|
+
const nonceKey = `${commandObj?.requesterId}|${commandObj?.sessionId}|${commandObj?.nonce}`;
|
|
1085
|
+
const ttlSec = 3600;
|
|
1086
|
+
if (signatureValid && rateLimitOk && nonceDb) {
|
|
1087
|
+
if (typeof nonceDb.checkAndRecord === "function") {
|
|
1088
|
+
if (nonceDb.db) {
|
|
1089
|
+
const nonceResult = checkNonce({
|
|
1090
|
+
ttlSec,
|
|
1091
|
+
nowSec,
|
|
1092
|
+
newKey: nonceKey,
|
|
1093
|
+
existingEntries: nonceEntriesToText(nonceDb.db)
|
|
1094
|
+
});
|
|
1095
|
+
nonceOk = nonceResult.ok;
|
|
1096
|
+
if (nonceResult.ok) {
|
|
1097
|
+
nonceDb.db.entries = textToNonceEntries(nonceResult.updatedEntriesText);
|
|
1098
|
+
}
|
|
1099
|
+
} else {
|
|
1100
|
+
const r = nonceDb.checkAndRecord({
|
|
1101
|
+
requesterId: commandObj.requesterId,
|
|
1102
|
+
sessionId: commandObj.sessionId,
|
|
1103
|
+
nonce: commandObj.nonce
|
|
1104
|
+
});
|
|
1105
|
+
nonceOk = r.ok;
|
|
1106
|
+
}
|
|
1107
|
+
} else {
|
|
1108
|
+
const nonceResult = checkNonce({
|
|
1109
|
+
ttlSec,
|
|
1110
|
+
nowSec,
|
|
1111
|
+
newKey: nonceKey,
|
|
1112
|
+
existingEntries: nonceEntriesToText(nonceDb)
|
|
1113
|
+
});
|
|
1114
|
+
nonceOk = nonceResult.ok;
|
|
1115
|
+
if (nonceResult.ok) {
|
|
1116
|
+
nonceDb.entries = textToNonceEntries(nonceResult.updatedEntriesText);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
const policyFlags = extractPolicyFlags(policy, commandObj ?? {});
|
|
1121
|
+
const pipeline = runValidationPipeline({
|
|
1122
|
+
...schemaFlags,
|
|
1123
|
+
cmdTimestamp: commandObj?.timestamp ?? 0,
|
|
1124
|
+
nowSec,
|
|
1125
|
+
maxClockSkewSec,
|
|
1126
|
+
...keyFlags,
|
|
1127
|
+
signatureValid,
|
|
1128
|
+
rateLimitOk,
|
|
1129
|
+
rateLimitRetryAfterSec,
|
|
1130
|
+
nonceOk,
|
|
1131
|
+
...policyFlags
|
|
1132
|
+
});
|
|
1133
|
+
const s = pipeline.stage;
|
|
1134
|
+
result.checks.schema = s !== 0;
|
|
1135
|
+
if (s >= 1) result.checks.timestamp = s !== 1;
|
|
1136
|
+
if (s >= 2) result.checks.keyId = s !== 2;
|
|
1137
|
+
if (s >= 2) result.checks.signature = s !== 2 && s !== 3;
|
|
1138
|
+
if (s >= 4) result.checks.rateLimit = s !== 4;
|
|
1139
|
+
if (s >= 5) result.checks.nonce = s !== 5;
|
|
1140
|
+
if (s >= 6 || pipeline.ok) result.checks.policy = s !== 6;
|
|
1141
|
+
if (!pipeline.ok) {
|
|
1142
|
+
const stage = pipeline.stageLabel;
|
|
1143
|
+
if (stage === "schema") {
|
|
1144
|
+
result.errors.push({ type: "SCHEMA_ERROR", message: pipeline.schemaError || "Schema invalid" });
|
|
1145
|
+
} else if (stage === "timestamp") {
|
|
1146
|
+
result.errors.push({ type: "TIMESTAMP_SKEW_EXCEEDED", message: `Command timestamp skew ${pipeline.skewSec}s exceeds allowed ${maxClockSkewSec}s` });
|
|
1147
|
+
} else if (stage === "key") {
|
|
1148
|
+
const reason = pipeline.keyReason || "KEY_ERROR";
|
|
1149
|
+
const msgs = {
|
|
1150
|
+
KEY_ID_INVALID: `Invalid keyId '${keyId}'`,
|
|
1151
|
+
KEY_NOT_TRUSTED: `Key '${keyId}' is not in trusted key store`,
|
|
1152
|
+
KEY_DEPRECATED: `Key '${keyId}' is deprecated`,
|
|
1153
|
+
KEY_REQUESTER_MISMATCH: `Key '${keyId}' is not authorized for requester '${commandObj?.requesterId}'`,
|
|
1154
|
+
KEY_LIFECYCLE_INVALID: `Key '${keyId}' must define notBefore and expiresAt`,
|
|
1155
|
+
KEY_NOT_YET_VALID: `Key '${keyId}' is not yet valid`,
|
|
1156
|
+
KEY_EXPIRED: `Key '${keyId}' has expired`
|
|
1157
|
+
};
|
|
1158
|
+
result.errors.push({ type: reason, message: msgs[reason] || reason });
|
|
1159
|
+
} else if (stage === "signature") {
|
|
1160
|
+
result.errors.push({ type: "SIGNATURE_INVALID", message: effectivePubKey ? "Signature verification failed" : "No public key available" });
|
|
1161
|
+
} else if (stage === "rate_limit") {
|
|
1162
|
+
result.errors.push({ type: "RATE_LIMIT_EXCEEDED", message: `Rate limit exceeded. Retry after ${pipeline.retryAfterSec}s` });
|
|
1163
|
+
} else if (stage === "nonce") {
|
|
1164
|
+
result.errors.push({ type: "REPLAY_NONCE", message: "Nonce has already been used" });
|
|
1165
|
+
} else if (stage === "policy" && pipeline.policyResult) {
|
|
1166
|
+
result.errors.push({ type: pipeline.policyResult.reason, message: pipeline.policyResult.message });
|
|
1167
|
+
} else {
|
|
1168
|
+
result.errors.push({ type: "VALIDATION_FAILED", message: `Failed at stage: ${stage}` });
|
|
1169
|
+
}
|
|
1170
|
+
return result;
|
|
1171
|
+
}
|
|
1172
|
+
result.valid = true;
|
|
1173
|
+
result.risk = classifyRisk(commandObj.id, commandObj.payload?.cmd === "rm");
|
|
1174
|
+
result.message = "Command validation successful";
|
|
1175
|
+
return result;
|
|
1176
|
+
}
|
|
1177
|
+
var init_validator = __esm({
|
|
1178
|
+
"src/core/validator.js"() {
|
|
1179
|
+
init_signature();
|
|
1180
|
+
init_policyVersionGuard();
|
|
1181
|
+
init_engine();
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
// src/adapters/noopAdapter.js
|
|
1186
|
+
async function noopAdapter(cmd2) {
|
|
1187
|
+
return {
|
|
1188
|
+
adapter: "noop",
|
|
1189
|
+
commandId: cmd2.commandId || "unknown",
|
|
1190
|
+
command: cmd2.id || "unknown",
|
|
1191
|
+
status: "completed",
|
|
1192
|
+
output: `[NOOP] Would execute: ${cmd2.id || "unknown"} on adapter: ${cmd2.payload?.adapter || "unknown"}`,
|
|
1193
|
+
exitCode: 0,
|
|
1194
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1195
|
+
};
|
|
1196
|
+
}
|
|
1197
|
+
var init_noopAdapter = __esm({
|
|
1198
|
+
"src/adapters/noopAdapter.js"() {
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
// src/adapters/shellAdapter.js
|
|
1203
|
+
import { spawnSync } from "child_process";
|
|
1204
|
+
import path11 from "path";
|
|
1205
|
+
import fs10 from "fs";
|
|
1206
|
+
function physicalPath(candidate) {
|
|
1207
|
+
try {
|
|
1208
|
+
return fs10.realpathSync(path11.resolve(candidate));
|
|
1209
|
+
} catch {
|
|
1210
|
+
return path11.resolve(candidate);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function normalizeArgs(args) {
|
|
1214
|
+
if (args === void 0) return { ok: true, args: [] };
|
|
1215
|
+
if (!Array.isArray(args)) {
|
|
1216
|
+
return { ok: false, error: "payload.args must be an array" };
|
|
1217
|
+
}
|
|
1218
|
+
const normalized = [];
|
|
1219
|
+
for (const arg of args) {
|
|
1220
|
+
if (typeof arg !== "string" && typeof arg !== "number" && typeof arg !== "boolean") {
|
|
1221
|
+
return { ok: false, error: "payload.args may only contain string, number, or boolean values" };
|
|
1222
|
+
}
|
|
1223
|
+
normalized.push(String(arg));
|
|
1224
|
+
}
|
|
1225
|
+
return { ok: true, args: normalized };
|
|
1226
|
+
}
|
|
1227
|
+
async function shellAdapter(cmd2, policy, requester) {
|
|
1228
|
+
const payload = cmd2.payload;
|
|
1229
|
+
const timeout = Math.min(Math.max(Number(payload.timeoutMs) || 3e4, 1), 3e4);
|
|
1230
|
+
const maxOutputSize = Math.min(Math.max(Number(payload.maxOutputBytes) || 1024 * 1024, 1024), 1024 * 1024);
|
|
1231
|
+
if (payload.adapter !== "shell") {
|
|
1232
|
+
return {
|
|
1233
|
+
adapter: "shell",
|
|
1234
|
+
commandId: cmd2.commandId,
|
|
1235
|
+
status: "error",
|
|
1236
|
+
error: "Adapter mismatch",
|
|
1237
|
+
exitCode: 1
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
const allowedCmds = requester?.exec?.allowCmds || [];
|
|
1241
|
+
const deniedCmds = requester?.exec?.denyCmds || [];
|
|
1242
|
+
if (deniedCmds.includes(payload.cmd)) {
|
|
1243
|
+
return {
|
|
1244
|
+
adapter: "shell",
|
|
1245
|
+
commandId: cmd2.commandId,
|
|
1246
|
+
status: "blocked",
|
|
1247
|
+
error: `Command '${payload.cmd}' is denied`,
|
|
1248
|
+
exitCode: 2
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
if (allowedCmds.length > 0 && !allowedCmds.includes(payload.cmd)) {
|
|
1252
|
+
return {
|
|
1253
|
+
adapter: "shell",
|
|
1254
|
+
commandId: cmd2.commandId,
|
|
1255
|
+
status: "blocked",
|
|
1256
|
+
error: `Command '${payload.cmd}' not in allowlist`,
|
|
1257
|
+
exitCode: 2
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
const roots = requester?.filesystem?.roots || [];
|
|
1261
|
+
const cwdAllow = roots.some((r) => {
|
|
1262
|
+
const resolvedRoot = physicalPath(r);
|
|
1263
|
+
const norm = physicalPath(payload.cwd);
|
|
1264
|
+
return norm === resolvedRoot || norm.startsWith(resolvedRoot + path11.sep);
|
|
1265
|
+
});
|
|
1266
|
+
if (!cwdAllow) {
|
|
1267
|
+
return {
|
|
1268
|
+
adapter: "shell",
|
|
1269
|
+
commandId: cmd2.commandId,
|
|
1270
|
+
status: "blocked",
|
|
1271
|
+
error: `CWD '${payload.cwd}' not authorized`,
|
|
1272
|
+
exitCode: 2
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
const argCheck = normalizeArgs(payload.args);
|
|
1276
|
+
if (!argCheck.ok) {
|
|
1277
|
+
return {
|
|
1278
|
+
adapter: "shell",
|
|
1279
|
+
commandId: cmd2.commandId,
|
|
1280
|
+
status: "blocked",
|
|
1281
|
+
error: argCheck.error,
|
|
1282
|
+
exitCode: 2
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1285
|
+
try {
|
|
1286
|
+
const result = spawnSync(payload.cmd, argCheck.args, {
|
|
1287
|
+
cwd: payload.cwd,
|
|
1288
|
+
timeout,
|
|
1289
|
+
encoding: "utf8",
|
|
1290
|
+
maxBuffer: maxOutputSize,
|
|
1291
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1292
|
+
shell: false
|
|
1293
|
+
});
|
|
1294
|
+
if (result.error) {
|
|
1295
|
+
throw result.error;
|
|
1296
|
+
}
|
|
1297
|
+
const output = `${result.stdout || ""}${result.stderr || ""}`;
|
|
1298
|
+
const exitCode = result.status ?? 1;
|
|
1299
|
+
if (exitCode !== 0) {
|
|
1300
|
+
return {
|
|
1301
|
+
adapter: "shell",
|
|
1302
|
+
commandId: cmd2.commandId,
|
|
1303
|
+
command: payload.cmd,
|
|
1304
|
+
status: "error",
|
|
1305
|
+
error: output.substring(0, maxOutputSize) || `Command exited with code ${exitCode}`,
|
|
1306
|
+
exitCode,
|
|
1307
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
return {
|
|
1311
|
+
adapter: "shell",
|
|
1312
|
+
commandId: cmd2.commandId,
|
|
1313
|
+
command: payload.cmd,
|
|
1314
|
+
status: "completed",
|
|
1315
|
+
output: output.substring(0, maxOutputSize),
|
|
1316
|
+
exitCode: 0,
|
|
1317
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1318
|
+
};
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
return {
|
|
1321
|
+
adapter: "shell",
|
|
1322
|
+
commandId: cmd2.commandId,
|
|
1323
|
+
command: payload.cmd,
|
|
1324
|
+
status: "error",
|
|
1325
|
+
error: err.message,
|
|
1326
|
+
exitCode: err.status || 1,
|
|
1327
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
var init_shellAdapter = __esm({
|
|
1332
|
+
"src/adapters/shellAdapter.js"() {
|
|
1333
|
+
}
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
// src/core/backup.js
|
|
1337
|
+
import fs11 from "fs";
|
|
1338
|
+
import path12 from "path";
|
|
1339
|
+
import crypto4 from "crypto";
|
|
1340
|
+
function createBackup(filePath, backupDir) {
|
|
1341
|
+
const dir = backupDir || path12.resolve(".lbe/data/backups");
|
|
1342
|
+
if (!fs11.existsSync(dir)) {
|
|
1343
|
+
fs11.mkdirSync(dir, { recursive: true });
|
|
1344
|
+
}
|
|
1345
|
+
const target = path12.resolve(filePath);
|
|
1346
|
+
const existed = fs11.existsSync(target);
|
|
1347
|
+
let content = null;
|
|
1348
|
+
let hash = null;
|
|
1349
|
+
if (existed) {
|
|
1350
|
+
content = fs11.readFileSync(target);
|
|
1351
|
+
hash = crypto4.createHash("sha256").update(content).digest("hex");
|
|
1352
|
+
}
|
|
1353
|
+
const basename = path12.basename(target).replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
1354
|
+
const backupName = `${Date.now()}-${hash ? hash.slice(0, 8) : "new"}-${basename}`;
|
|
1355
|
+
const backupPath = existed ? path12.join(dir, backupName) : null;
|
|
1356
|
+
if (existed && content !== null) {
|
|
1357
|
+
atomicWriteFileSync(backupPath, content);
|
|
1358
|
+
}
|
|
1359
|
+
return {
|
|
1360
|
+
originalPath: target,
|
|
1361
|
+
backupPath,
|
|
1362
|
+
existed,
|
|
1363
|
+
hash,
|
|
1364
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
function restoreBackup(backupMeta) {
|
|
1368
|
+
if (!backupMeta) return { restored: false, error: "No backup metadata" };
|
|
1369
|
+
const { originalPath, backupPath, existed } = backupMeta;
|
|
1370
|
+
if (!existed) {
|
|
1371
|
+
try {
|
|
1372
|
+
if (fs11.existsSync(originalPath)) fs11.unlinkSync(originalPath);
|
|
1373
|
+
return { restored: true, action: "deleted" };
|
|
1374
|
+
} catch (e) {
|
|
1375
|
+
return { restored: false, error: e.message };
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
if (!backupPath || !fs11.existsSync(backupPath)) {
|
|
1379
|
+
return { restored: false, error: "Backup file not found at: " + backupPath };
|
|
1380
|
+
}
|
|
1381
|
+
try {
|
|
1382
|
+
const content = fs11.readFileSync(backupPath);
|
|
1383
|
+
atomicWriteFileSync(originalPath, content);
|
|
1384
|
+
return { restored: true, action: "restored" };
|
|
1385
|
+
} catch (e) {
|
|
1386
|
+
return { restored: false, error: e.message };
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
var init_backup = __esm({
|
|
1390
|
+
"src/core/backup.js"() {
|
|
1391
|
+
init_atomicWrite();
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
// src/adapters/fileAdapter.js
|
|
1396
|
+
import fs12 from "fs";
|
|
1397
|
+
import path13 from "path";
|
|
1398
|
+
function resolvedTarget(target, cwd) {
|
|
1399
|
+
if (!target) return null;
|
|
1400
|
+
return path13.isAbsolute(target) ? path13.resolve(target) : path13.resolve(cwd || process.cwd(), target);
|
|
1401
|
+
}
|
|
1402
|
+
function isUnderRoot(targetPath, roots) {
|
|
1403
|
+
const norm = resolvePhysicalPath(targetPath);
|
|
1404
|
+
return roots.some((r) => {
|
|
1405
|
+
const root = resolvePhysicalPath(r);
|
|
1406
|
+
return norm === root || norm.startsWith(root + path13.sep);
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
function resolvePhysicalPath(candidate) {
|
|
1410
|
+
let current = path13.resolve(candidate);
|
|
1411
|
+
const suffix = [];
|
|
1412
|
+
while (!fs12.existsSync(current)) {
|
|
1413
|
+
const parent = path13.dirname(current);
|
|
1414
|
+
if (parent === current) break;
|
|
1415
|
+
suffix.unshift(path13.basename(current));
|
|
1416
|
+
current = parent;
|
|
1417
|
+
}
|
|
1418
|
+
try {
|
|
1419
|
+
current = fs12.realpathSync(current);
|
|
1420
|
+
} catch {
|
|
1421
|
+
}
|
|
1422
|
+
return path13.join(current, ...suffix);
|
|
1423
|
+
}
|
|
1424
|
+
function matchesDenyPattern(str, patterns) {
|
|
1425
|
+
for (const pattern of patterns || []) {
|
|
1426
|
+
const rx = new RegExp(
|
|
1427
|
+
"^" + pattern.replace(/\./g, "\\.").replace(/\*\*/g, ".*").replace(/\*/g, "[^/\\\\]*") + "$"
|
|
1428
|
+
);
|
|
1429
|
+
if (rx.test(str)) return pattern;
|
|
1430
|
+
}
|
|
1431
|
+
return null;
|
|
1432
|
+
}
|
|
1433
|
+
function blocked(cmd2, code, message, exitCode = 2) {
|
|
1434
|
+
return {
|
|
1435
|
+
adapter: "file",
|
|
1436
|
+
commandId: cmd2.commandId,
|
|
1437
|
+
status: "blocked",
|
|
1438
|
+
errorCode: code,
|
|
1439
|
+
error: message,
|
|
1440
|
+
exitCode
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
function fail(cmd2, code, message, backup = null, exitCode = 1) {
|
|
1444
|
+
return {
|
|
1445
|
+
adapter: "file",
|
|
1446
|
+
commandId: cmd2.commandId,
|
|
1447
|
+
status: "error",
|
|
1448
|
+
errorCode: code,
|
|
1449
|
+
error: message,
|
|
1450
|
+
backup: backup ? summariseBackup(backup) : null,
|
|
1451
|
+
exitCode
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
function summariseBackup(b) {
|
|
1455
|
+
return b ? { path: b.backupPath, existed: b.existed, hash: b.hash, createdAt: b.createdAt } : null;
|
|
1456
|
+
}
|
|
1457
|
+
async function fileAdapter(cmd2, policy, requester) {
|
|
1458
|
+
const payload = cmd2.payload;
|
|
1459
|
+
const action = payload.action;
|
|
1460
|
+
const cwd = payload.cwd || process.cwd();
|
|
1461
|
+
const target = resolvedTarget(payload.target, cwd);
|
|
1462
|
+
if (!action) return blocked(cmd2, "FILE_NO_ACTION", "payload.action is required");
|
|
1463
|
+
if (!target && action !== "noop") return blocked(cmd2, "FILE_NO_TARGET", "payload.target is required");
|
|
1464
|
+
const roots = requester?.filesystem?.roots || [];
|
|
1465
|
+
if (roots.length === 0) return blocked(cmd2, "FILE_NO_ROOTS", "No filesystem roots defined for requester");
|
|
1466
|
+
if (!isUnderRoot(target, roots)) return blocked(cmd2, "FILE_OUTSIDE_ROOT", `'${target}' is outside allowed roots`);
|
|
1467
|
+
const denied = matchesDenyPattern(target, requester?.filesystem?.denyPatterns);
|
|
1468
|
+
if (denied) return blocked(cmd2, "FILE_PATH_DENIED", `'${target}' matches deny pattern: ${denied}`);
|
|
1469
|
+
switch (action) {
|
|
1470
|
+
case "read":
|
|
1471
|
+
return doRead(cmd2, target);
|
|
1472
|
+
case "write":
|
|
1473
|
+
return doWrite(cmd2, target, payload);
|
|
1474
|
+
case "patch":
|
|
1475
|
+
return doPatch(cmd2, target, payload);
|
|
1476
|
+
case "delete":
|
|
1477
|
+
return doDelete(cmd2, target);
|
|
1478
|
+
default:
|
|
1479
|
+
return blocked(cmd2, "FILE_UNKNOWN_ACTION", `Unknown action: '${action}'`);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
function doRead(cmd2, target) {
|
|
1483
|
+
if (!fs12.existsSync(target)) return fail(cmd2, "FILE_NOT_FOUND", `Not found: ${target}`);
|
|
1484
|
+
try {
|
|
1485
|
+
const stat = fs12.statSync(target);
|
|
1486
|
+
if (stat.size > MAX_READ_BYTES) return fail(cmd2, "FILE_TOO_LARGE", "File exceeds 10 MB read limit");
|
|
1487
|
+
const content = fs12.readFileSync(target, "utf8");
|
|
1488
|
+
return {
|
|
1489
|
+
adapter: "file",
|
|
1490
|
+
action: "read",
|
|
1491
|
+
commandId: cmd2.commandId,
|
|
1492
|
+
status: "completed",
|
|
1493
|
+
target,
|
|
1494
|
+
output: content,
|
|
1495
|
+
bytesRead: stat.size,
|
|
1496
|
+
exitCode: 0
|
|
1497
|
+
};
|
|
1498
|
+
} catch (e) {
|
|
1499
|
+
return fail(cmd2, "FILE_READ_ERROR", e.message);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
function doWrite(cmd2, target, payload) {
|
|
1503
|
+
const content = payload.content;
|
|
1504
|
+
if (content === void 0 || content === null) {
|
|
1505
|
+
return fail(cmd2, "FILE_MISSING_CONTENT", "payload.content is required for write");
|
|
1506
|
+
}
|
|
1507
|
+
const backup = tryBackup(target);
|
|
1508
|
+
try {
|
|
1509
|
+
atomicWriteFileSync(target, content, { encoding: "utf8" });
|
|
1510
|
+
return {
|
|
1511
|
+
adapter: "file",
|
|
1512
|
+
action: "write",
|
|
1513
|
+
commandId: cmd2.commandId,
|
|
1514
|
+
status: "completed",
|
|
1515
|
+
target,
|
|
1516
|
+
backup: summariseBackup(backup),
|
|
1517
|
+
output: `Wrote ${Buffer.byteLength(content, "utf8")} bytes to ${target}`,
|
|
1518
|
+
exitCode: 0
|
|
1519
|
+
};
|
|
1520
|
+
} catch (e) {
|
|
1521
|
+
restoreBackup(backup);
|
|
1522
|
+
return fail(cmd2, "FILE_WRITE_ERROR", e.message, backup);
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
function doPatch(cmd2, target, payload) {
|
|
1526
|
+
const content = payload.content;
|
|
1527
|
+
if (content === void 0 || content === null) {
|
|
1528
|
+
return fail(cmd2, "FILE_MISSING_CONTENT", "payload.content is required for patch");
|
|
1529
|
+
}
|
|
1530
|
+
const backup = tryBackup(target);
|
|
1531
|
+
try {
|
|
1532
|
+
atomicWriteFileSync(target, content, { encoding: "utf8" });
|
|
1533
|
+
return {
|
|
1534
|
+
adapter: "file",
|
|
1535
|
+
action: "patch",
|
|
1536
|
+
commandId: cmd2.commandId,
|
|
1537
|
+
status: "completed",
|
|
1538
|
+
target,
|
|
1539
|
+
backup: summariseBackup(backup),
|
|
1540
|
+
output: `Patched ${target} (${Buffer.byteLength(content, "utf8")} bytes)`,
|
|
1541
|
+
exitCode: 0
|
|
1542
|
+
};
|
|
1543
|
+
} catch (e) {
|
|
1544
|
+
restoreBackup(backup);
|
|
1545
|
+
return fail(cmd2, "FILE_PATCH_ERROR", e.message, backup);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
function doDelete(cmd2, target) {
|
|
1549
|
+
if (!fs12.existsSync(target)) return fail(cmd2, "FILE_NOT_FOUND", `Not found: ${target}`);
|
|
1550
|
+
const backup = tryBackup(target);
|
|
1551
|
+
try {
|
|
1552
|
+
fs12.unlinkSync(target);
|
|
1553
|
+
return {
|
|
1554
|
+
adapter: "file",
|
|
1555
|
+
action: "delete",
|
|
1556
|
+
commandId: cmd2.commandId,
|
|
1557
|
+
status: "completed",
|
|
1558
|
+
target,
|
|
1559
|
+
backup: summariseBackup(backup),
|
|
1560
|
+
output: `Deleted ${target}`,
|
|
1561
|
+
exitCode: 0
|
|
1562
|
+
};
|
|
1563
|
+
} catch (e) {
|
|
1564
|
+
restoreBackup(backup);
|
|
1565
|
+
return fail(cmd2, "FILE_DELETE_ERROR", e.message, backup);
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
function tryBackup(target) {
|
|
1569
|
+
try {
|
|
1570
|
+
return createBackup(target);
|
|
1571
|
+
} catch {
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
var MAX_READ_BYTES;
|
|
1576
|
+
var init_fileAdapter = __esm({
|
|
1577
|
+
"src/adapters/fileAdapter.js"() {
|
|
1578
|
+
init_atomicWrite();
|
|
1579
|
+
init_backup();
|
|
1580
|
+
MAX_READ_BYTES = 10 * 1024 * 1024;
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
// src/adapters/index.js
|
|
1585
|
+
function getAdapter(name) {
|
|
1586
|
+
return ADAPTERS[name];
|
|
1587
|
+
}
|
|
1588
|
+
async function executeAdapter(adapterName, cmd2, policy, requester) {
|
|
1589
|
+
const adapter = getAdapter(adapterName);
|
|
1590
|
+
if (!adapter) {
|
|
1591
|
+
return {
|
|
1592
|
+
adapter: adapterName,
|
|
1593
|
+
commandId: cmd2.commandId,
|
|
1594
|
+
status: "error",
|
|
1595
|
+
error: `Adapter '${adapterName}' not found`,
|
|
1596
|
+
exitCode: 1
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
try {
|
|
1600
|
+
return await adapter(cmd2, policy, requester);
|
|
1601
|
+
} catch (err) {
|
|
1602
|
+
return {
|
|
1603
|
+
adapter: adapterName,
|
|
1604
|
+
commandId: cmd2.commandId,
|
|
1605
|
+
status: "error",
|
|
1606
|
+
error: `Adapter execution failed: ${err.message}`,
|
|
1607
|
+
exitCode: 9
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
var ADAPTERS, AVAILABLE_ADAPTERS;
|
|
1612
|
+
var init_adapters = __esm({
|
|
1613
|
+
"src/adapters/index.js"() {
|
|
1614
|
+
init_noopAdapter();
|
|
1615
|
+
init_shellAdapter();
|
|
1616
|
+
init_fileAdapter();
|
|
1617
|
+
ADAPTERS = {
|
|
1618
|
+
noop: noopAdapter,
|
|
1619
|
+
shell: shellAdapter,
|
|
1620
|
+
file: fileAdapter
|
|
1621
|
+
};
|
|
1622
|
+
AVAILABLE_ADAPTERS = Object.keys(ADAPTERS);
|
|
1623
|
+
}
|
|
1624
|
+
});
|
|
1625
|
+
|
|
1626
|
+
// src/exec/localExecutor.js
|
|
1627
|
+
var localExecutor_exports = {};
|
|
1628
|
+
__export(localExecutor_exports, {
|
|
1629
|
+
createLocalExecutor: () => createLocalExecutor
|
|
1630
|
+
});
|
|
1631
|
+
import crypto5 from "crypto";
|
|
1632
|
+
import fs13 from "fs";
|
|
1633
|
+
import path14 from "path";
|
|
1634
|
+
function error(code, message, recoverable = false) {
|
|
1635
|
+
return { ok: false, decision: "deny", executed: false, dryRun: false, error: { code, message, recoverable } };
|
|
1636
|
+
}
|
|
1637
|
+
function commandPolicy(rootDir, actor, shell = {}) {
|
|
1638
|
+
const now = /* @__PURE__ */ new Date();
|
|
1639
|
+
const expires = new Date(now.getTime() + 365 * 24 * 60 * 60 * 1e3);
|
|
1640
|
+
return {
|
|
1641
|
+
version: 1,
|
|
1642
|
+
default: "DENY",
|
|
1643
|
+
requesters: {
|
|
1644
|
+
[actor]: {
|
|
1645
|
+
allowCommands: Object.values(INTENTS).map((item) => item.id),
|
|
1646
|
+
allowAdapters: ["file", "shell"],
|
|
1647
|
+
filesystem: { roots: [rootDir], denyPatterns: [] },
|
|
1648
|
+
exec: { allowCmds: shell.allowCommands || [], denyCmds: shell.denyCommands || [] },
|
|
1649
|
+
rateLimit: { windowSec: 60, maxRequests: shell.maxRequests || 60 }
|
|
1650
|
+
}
|
|
1651
|
+
},
|
|
1652
|
+
security: { maxClockSkewSec: 600, defaultRateLimit: { windowSec: 60, maxRequests: 60 } },
|
|
1653
|
+
_keyWindow: { notBefore: now.toISOString(), expiresAt: expires.toISOString() }
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
function physicalPath2(candidate) {
|
|
1657
|
+
let current = path14.resolve(candidate);
|
|
1658
|
+
const suffix = [];
|
|
1659
|
+
while (!fs13.existsSync(current)) {
|
|
1660
|
+
const parent = path14.dirname(current);
|
|
1661
|
+
if (parent === current) break;
|
|
1662
|
+
suffix.unshift(path14.basename(current));
|
|
1663
|
+
current = parent;
|
|
1664
|
+
}
|
|
1665
|
+
try {
|
|
1666
|
+
current = fs13.realpathSync(current);
|
|
1667
|
+
} catch {
|
|
1668
|
+
}
|
|
1669
|
+
return path14.join(current, ...suffix);
|
|
1670
|
+
}
|
|
1671
|
+
function underRoot(candidate, root) {
|
|
1672
|
+
const target = physicalPath2(candidate);
|
|
1673
|
+
const resolvedRoot = physicalPath2(root);
|
|
1674
|
+
return target === resolvedRoot || target.startsWith(resolvedRoot + path14.sep);
|
|
1675
|
+
}
|
|
1676
|
+
function scanContent(value, fieldName) {
|
|
1677
|
+
if (typeof value !== "string") return null;
|
|
1678
|
+
for (const pattern of FORBIDDEN_CONTENT) {
|
|
1679
|
+
if (pattern.test(value)) {
|
|
1680
|
+
return error("PAYLOAD_CONTENT_REJECTED", `Forbidden pattern in ${fieldName}: ${pattern}`);
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
return null;
|
|
1684
|
+
}
|
|
1685
|
+
function normalize(rootDir, request, shell = {}) {
|
|
1686
|
+
if (!request || typeof request !== "object") return { error: error("REQUEST_INVALID", "request must be an object") };
|
|
1687
|
+
const detail = INTENTS[request.intent];
|
|
1688
|
+
if (!detail) return { error: error("INTENT_UNSUPPORTED", `Unsupported intent '${request.intent}'`) };
|
|
1689
|
+
const actor = typeof request.actor === "string" && request.actor ? request.actor : "agent:local";
|
|
1690
|
+
let target = null;
|
|
1691
|
+
if (detail.adapter === "file") {
|
|
1692
|
+
if (typeof request.target !== "string" || !request.target) return { error: error("TARGET_REQUIRED", "target is required for file intents") };
|
|
1693
|
+
target = path14.resolve(rootDir, request.target);
|
|
1694
|
+
if (!underRoot(target, rootDir)) return { error: error("PATH_OUTSIDE_ROOT", "target is outside project root") };
|
|
1695
|
+
if (["write_file", "patch_file"].includes(request.intent) && typeof request.content !== "string") {
|
|
1696
|
+
return { error: error("CONTENT_REQUIRED", "content is required for write and patch") };
|
|
1697
|
+
}
|
|
1698
|
+
const contentScan = scanContent(request.content, "content");
|
|
1699
|
+
if (contentScan) return { error: contentScan };
|
|
1700
|
+
}
|
|
1701
|
+
let command = null;
|
|
1702
|
+
if (detail.adapter === "shell") {
|
|
1703
|
+
command = request.command;
|
|
1704
|
+
if (!command || typeof command.cmd !== "string" || !Array.isArray(command.args) || command.args.some((arg) => typeof arg !== "string")) {
|
|
1705
|
+
return { error: error("COMMAND_INVALID", "command requires cmd and string args") };
|
|
1706
|
+
}
|
|
1707
|
+
const cwd = path14.resolve(rootDir, command.cwd || ".");
|
|
1708
|
+
if (!underRoot(cwd, rootDir)) return { error: error("CWD_OUTSIDE_ROOT", "command cwd is outside project root") };
|
|
1709
|
+
if (!Array.isArray(shell.allowCommands) || !shell.allowCommands.includes(command.cmd)) {
|
|
1710
|
+
return { error: error("SHELL_NOT_ALLOWLISTED", `command '${command.cmd}' is not explicitly allowlisted`) };
|
|
1711
|
+
}
|
|
1712
|
+
if (shell.denyCommands?.includes(command.cmd)) return { error: error("SHELL_DENIED", `command '${command.cmd}' is denied`) };
|
|
1713
|
+
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) };
|
|
1714
|
+
}
|
|
1715
|
+
return { actor, detail, target, command, request };
|
|
1716
|
+
}
|
|
1717
|
+
function envelope(normalized, keyId, secretKey) {
|
|
1718
|
+
const { actor, detail, target, command, request } = normalized;
|
|
1719
|
+
const body = {
|
|
1720
|
+
id: detail.id,
|
|
1721
|
+
risk: MUTATIONS.has(request.intent) ? "MEDIUM" : "LOW",
|
|
1722
|
+
commandId: crypto5.randomUUID(),
|
|
1723
|
+
requesterId: actor,
|
|
1724
|
+
sessionId: "local-host",
|
|
1725
|
+
timestamp: Math.floor(Date.now() / 1e3),
|
|
1726
|
+
nonce: crypto5.randomBytes(32).toString("hex"),
|
|
1727
|
+
requires: ["policy", "signature"],
|
|
1728
|
+
payload: {
|
|
1729
|
+
adapter: detail.adapter,
|
|
1730
|
+
action: detail.action,
|
|
1731
|
+
target,
|
|
1732
|
+
content: request.content,
|
|
1733
|
+
cmd: command?.cmd,
|
|
1734
|
+
args: command?.args,
|
|
1735
|
+
timeoutMs: command?.timeoutMs,
|
|
1736
|
+
maxOutputBytes: command?.maxOutputBytes,
|
|
1737
|
+
cwd: command?.cwd || (target ? path14.dirname(target) : process.cwd())
|
|
1738
|
+
}
|
|
1739
|
+
};
|
|
1740
|
+
const signed = signEd25519({ payloadObj: body, secretKeyB64: secretKey });
|
|
1741
|
+
if (signed.error) throw new Error(signed.error);
|
|
1742
|
+
return { ...body, signature: { alg: "ed25519", keyId, sig: signed.signature } };
|
|
1743
|
+
}
|
|
1744
|
+
function createLocalExecutor(options = {}) {
|
|
1745
|
+
const rootDir = path14.resolve(options.rootDir || process.cwd());
|
|
1746
|
+
const keyId = options.keyId || "host:local-exec";
|
|
1747
|
+
const keyPair = options.keyPair || generateKeyPair();
|
|
1748
|
+
const shell = options.shell || {};
|
|
1749
|
+
function prepare(request, { recordNonce = false } = {}) {
|
|
1750
|
+
const normalized = normalize(rootDir, request, shell);
|
|
1751
|
+
if (normalized.error) return normalized;
|
|
1752
|
+
const local = loadLocalPolicy(rootDir, options.mode || "enforce");
|
|
1753
|
+
const localDecision = evaluateLocalPolicy(local.policy, rootDir, { target: normalized.target, command: normalized.command?.cmd });
|
|
1754
|
+
const localBlocked = local.policy.mode === "enforce" && !localDecision.allowed;
|
|
1755
|
+
if (localBlocked) return { error: error("LOCAL_POLICY_DENY", `Blocked by rule(s): ${localDecision.winningRules.map((rule) => rule.id).join(", ")}`), local, localDecision, normalized };
|
|
1756
|
+
const policy = commandPolicy(rootDir, normalized.actor, shell);
|
|
1757
|
+
const keyStore = { defaultKeyId: keyId, trustedKeys: { [keyId]: { publicKey: keyPair.publicKey, notBefore: policy._keyWindow.notBefore, expiresAt: policy._keyWindow.expiresAt, deprecated: false } } };
|
|
1758
|
+
delete policy._keyWindow;
|
|
1759
|
+
const proposal = envelope(normalized, keyId, keyPair.secretKey);
|
|
1760
|
+
const nonceDb = { entries: [] };
|
|
1761
|
+
const validation = validateCommand({ commandObj: proposal, keyStore, nonceDb: recordNonce ? nonceDb : { entries: [] }, policy });
|
|
1762
|
+
if (!validation.valid) return { error: error(validation.errors[0]?.type || "VALIDATION_FAILED", validation.errors[0]?.message || "Validation failed"), local, localDecision, normalized, proposal, policy, validation };
|
|
1763
|
+
return { local, localDecision, normalized, proposal, policy, validation };
|
|
1764
|
+
}
|
|
1765
|
+
async function dryRun(request) {
|
|
1766
|
+
const prepared = prepare(request);
|
|
1767
|
+
if (prepared.error) return { ...prepared.error, dryRun: true };
|
|
1768
|
+
return {
|
|
1769
|
+
ok: true,
|
|
1770
|
+
decision: prepared.local.policy.mode === "observe" ? "observe" : "allow",
|
|
1771
|
+
executed: false,
|
|
1772
|
+
dryRun: true,
|
|
1773
|
+
matchedRules: prepared.localDecision.winningRules.map((rule) => rule.id),
|
|
1774
|
+
rollback: { available: MUTATIONS.has(prepared.normalized.request.intent), performed: false }
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
async function execute(request) {
|
|
1778
|
+
const prepared = prepare(request, { recordNonce: true });
|
|
1779
|
+
if (prepared.error) {
|
|
1780
|
+
auditLocalPolicy(rootDir, { action: request?.intent, actor: request?.actor || "agent:local", decision: "deny", error: prepared.error.error.code });
|
|
1781
|
+
return prepared.error;
|
|
1782
|
+
}
|
|
1783
|
+
if (prepared.local.policy.mode === "observe") {
|
|
1784
|
+
appendAudit(path14.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: "observe",
|
|
1790
|
+
status: "observed"
|
|
1791
|
+
});
|
|
1792
|
+
return {
|
|
1793
|
+
ok: true,
|
|
1794
|
+
decision: "observe",
|
|
1795
|
+
executed: false,
|
|
1796
|
+
dryRun: false,
|
|
1797
|
+
matchedRules: prepared.localDecision.winningRules.map((r) => r.id),
|
|
1798
|
+
rollback: { available: false, performed: false }
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
const requester = prepared.policy.requesters[prepared.normalized.actor];
|
|
1802
|
+
const adapterResult = await executeAdapter(prepared.normalized.detail.adapter, prepared.proposal, prepared.policy, requester);
|
|
1803
|
+
const ok = adapterResult.status === "completed";
|
|
1804
|
+
const audit = appendAudit(path14.join(rootDir, ".lbe/audit.jsonl"), {
|
|
1805
|
+
kind: "local_execution",
|
|
1806
|
+
commandId: prepared.proposal.commandId,
|
|
1807
|
+
requesterId: prepared.normalized.actor,
|
|
1808
|
+
intent: prepared.normalized.request.intent,
|
|
1809
|
+
decision: ok ? "allow" : "deny",
|
|
1810
|
+
status: adapterResult.status
|
|
1811
|
+
});
|
|
1812
|
+
return {
|
|
1813
|
+
ok,
|
|
1814
|
+
decision: ok ? "allow" : "deny",
|
|
1815
|
+
executed: ok,
|
|
1816
|
+
dryRun: false,
|
|
1817
|
+
matchedRules: prepared.localDecision.winningRules.map((rule) => rule.id),
|
|
1818
|
+
auditId: audit.hash,
|
|
1819
|
+
rollback: { available: MUTATIONS.has(prepared.normalized.request.intent), performed: false, backupId: adapterResult.backup?.hash },
|
|
1820
|
+
...ok ? {} : { error: { code: adapterResult.errorCode || "EXECUTION_FAILED", message: adapterResult.error || "Execution failed", recoverable: true } }
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
const writeFile = (target, content) => execute({ intent: "write_file", target, content });
|
|
1824
|
+
const readFile = (target) => execute({ intent: "read_file", target });
|
|
1825
|
+
const patchFile = (target, content) => execute({ intent: "patch_file", target, content });
|
|
1826
|
+
const deleteFile = (target) => execute({ intent: "delete_file", target });
|
|
1827
|
+
const runShell = (cmd2, args = [], opts2 = {}) => execute({ intent: "run_shell", command: { cmd: cmd2, args, ...opts2 } });
|
|
1828
|
+
return {
|
|
1829
|
+
rootDir,
|
|
1830
|
+
// High-level API — use these
|
|
1831
|
+
writeFile,
|
|
1832
|
+
readFile,
|
|
1833
|
+
patchFile,
|
|
1834
|
+
deleteFile,
|
|
1835
|
+
runShell,
|
|
1836
|
+
// Low-level API — for advanced use
|
|
1837
|
+
validate: async (request) => {
|
|
1838
|
+
const preview = await dryRun(request);
|
|
1839
|
+
return { ...preview, dryRun: false, executed: false };
|
|
1840
|
+
},
|
|
1841
|
+
dryRun,
|
|
1842
|
+
execute,
|
|
1843
|
+
policy: {
|
|
1844
|
+
read: () => loadLocalPolicy(rootDir, options.mode || "enforce").policy,
|
|
1845
|
+
proposeRule: proposePolicyRule,
|
|
1846
|
+
addRule: (rule) => addLocalPolicyRule(rootDir, rule, options.mode || "enforce")
|
|
1847
|
+
},
|
|
1848
|
+
audit: { verify: () => verifyAuditLogIntegrity(path14.join(rootDir, ".lbe/audit.jsonl")) }
|
|
1849
|
+
};
|
|
1850
|
+
}
|
|
1851
|
+
var INTENTS, MUTATIONS, FORBIDDEN_CONTENT;
|
|
1852
|
+
var init_localExecutor = __esm({
|
|
1853
|
+
"src/exec/localExecutor.js"() {
|
|
1854
|
+
init_signature();
|
|
1855
|
+
init_validator();
|
|
1856
|
+
init_adapters();
|
|
1857
|
+
init_auditLog();
|
|
1858
|
+
init_localPolicy();
|
|
1859
|
+
INTENTS = {
|
|
1860
|
+
read_file: { id: "READ_FILE", adapter: "file", action: "read" },
|
|
1861
|
+
write_file: { id: "WRITE_FILE", adapter: "file", action: "write" },
|
|
1862
|
+
patch_file: { id: "PATCH_FILE", adapter: "file", action: "patch" },
|
|
1863
|
+
delete_file: { id: "DELETE_FILE", adapter: "file", action: "delete" },
|
|
1864
|
+
run_shell: { id: "RUN_SHELL", adapter: "shell", action: "run" }
|
|
1865
|
+
};
|
|
1866
|
+
MUTATIONS = /* @__PURE__ */ new Set(["write_file", "patch_file", "delete_file"]);
|
|
1867
|
+
FORBIDDEN_CONTENT = [
|
|
1868
|
+
/\beval\s*\(/i,
|
|
1869
|
+
/\bFunction\s*\(/i,
|
|
1870
|
+
/\bexec\s*\(/i,
|
|
1871
|
+
/\brequire\s*\(/,
|
|
1872
|
+
/\bimport\s*\(/,
|
|
1873
|
+
/\bchild_process\b/,
|
|
1874
|
+
/\b__proto__\b/,
|
|
1875
|
+
/\bconstructor\s*\[/,
|
|
1876
|
+
/evalScript/i
|
|
1877
|
+
];
|
|
1878
|
+
}
|
|
1879
|
+
});
|
|
1880
|
+
|
|
1881
|
+
// exec/cli.js
|
|
1882
|
+
import fs14 from "fs";
|
|
1883
|
+
import path15 from "path";
|
|
1884
|
+
|
|
1885
|
+
// src/cli/commands/init.js
|
|
1886
|
+
init_signature();
|
|
1887
|
+
import fs4 from "fs";
|
|
1888
|
+
import path4 from "path";
|
|
1889
|
+
import readline from "readline";
|
|
1890
|
+
|
|
1891
|
+
// src/core/policySignature.js
|
|
1892
|
+
init_signature();
|
|
1893
|
+
import fs2 from "fs";
|
|
1894
|
+
import path2 from "path";
|
|
1895
|
+
|
|
1896
|
+
// src/core/trustedKeys.js
|
|
1897
|
+
import fs from "fs";
|
|
1898
|
+
import path from "path";
|
|
1899
|
+
|
|
1900
|
+
// src/core/policySignature.js
|
|
1901
|
+
function createPolicySignatureEnvelope({ policyObj, secretKeyB64, keyId }) {
|
|
1902
|
+
const signResult = signEd25519({
|
|
1903
|
+
payloadObj: policyObj,
|
|
1904
|
+
secretKeyB64
|
|
1905
|
+
});
|
|
1906
|
+
if (signResult.error) {
|
|
1907
|
+
return {
|
|
1908
|
+
ok: false,
|
|
1909
|
+
reason: "POLICY_SIGNATURE_CREATE_FAILED",
|
|
1910
|
+
message: signResult.error,
|
|
1911
|
+
envelope: null
|
|
1912
|
+
};
|
|
1913
|
+
}
|
|
1914
|
+
return {
|
|
1915
|
+
ok: true,
|
|
1916
|
+
reason: null,
|
|
1917
|
+
message: "Policy signature created",
|
|
1918
|
+
envelope: {
|
|
1919
|
+
alg: "ed25519",
|
|
1920
|
+
keyId,
|
|
1921
|
+
sig: signResult.signature,
|
|
1922
|
+
createdAt: Math.floor(Date.now() / 1e3)
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// src/core/workspaceScanner.js
|
|
1928
|
+
import fs3 from "fs";
|
|
1929
|
+
import path3 from "path";
|
|
1930
|
+
var PROJECT_SIGNALS = [
|
|
1931
|
+
// Code types — ordered by priority for primaryType resolution
|
|
1932
|
+
{ file: "package.json", type: "node" },
|
|
1933
|
+
{ file: "pyproject.toml", type: "python" },
|
|
1934
|
+
{ file: "requirements.txt", type: "python" },
|
|
1935
|
+
{ file: "go.mod", type: "go" },
|
|
1936
|
+
{ file: "Cargo.toml", type: "rust" },
|
|
1937
|
+
{ file: "pom.xml", type: "java" },
|
|
1938
|
+
{ file: "build.gradle", type: "java" },
|
|
1939
|
+
{ file: "build.gradle.kts", type: "java" },
|
|
1940
|
+
// Infrastructure types — supplementary, not primary
|
|
1941
|
+
{ file: "Dockerfile", type: "docker" },
|
|
1942
|
+
{ file: "docker-compose.yml", type: "docker" },
|
|
1943
|
+
{ dir: ".github/workflows", type: "ci" },
|
|
1944
|
+
{ file: ".gitlab-ci.yml", type: "ci" },
|
|
1945
|
+
{ dir: ".circleci", type: "ci" },
|
|
1946
|
+
{ file: "Jenkinsfile", type: "ci" },
|
|
1947
|
+
{ file: ".travis.yml", type: "ci" }
|
|
1948
|
+
];
|
|
1949
|
+
var CODE_TYPES = ["node", "python", "go", "rust", "java"];
|
|
1950
|
+
var SURFACE_MAP = {
|
|
1951
|
+
source: ["src", "lib", "app", "pages", "components", "core", "api", "server", "client", "pkg", "cmd"],
|
|
1952
|
+
generated: ["dist", "build", ".next", "out", "coverage", "target", ".cache", "__pycache__", ".turbo"],
|
|
1953
|
+
tests: ["test", "tests", "__tests__", "spec", "e2e"],
|
|
1954
|
+
docs: ["docs", "doc", "documentation"]
|
|
1955
|
+
};
|
|
1956
|
+
var SECRET_GLOBS = [".env", ".env.*", "keys/**", "secrets/**", "*.key", "*.pem", "*.p12", "*.pfx", "*.crt"];
|
|
1957
|
+
var ALWAYS_DENY = ["node_modules/**", ".git/**"];
|
|
1958
|
+
var LOCKFILES_BY_TYPE = {
|
|
1959
|
+
node: ["package-lock.json", "yarn.lock", "pnpm-lock.yaml"],
|
|
1960
|
+
python: ["Pipfile.lock", "poetry.lock"],
|
|
1961
|
+
go: ["go.sum"],
|
|
1962
|
+
rust: ["Cargo.lock"],
|
|
1963
|
+
java: ["gradle/wrapper/**"],
|
|
1964
|
+
docker: [],
|
|
1965
|
+
ci: [],
|
|
1966
|
+
generic: []
|
|
1967
|
+
};
|
|
1968
|
+
var CONFIG_FILES_BY_TYPE = {
|
|
1969
|
+
node: [
|
|
1970
|
+
"package.json",
|
|
1971
|
+
"tsconfig*.json",
|
|
1972
|
+
"jest.config.*",
|
|
1973
|
+
"vite.config.*",
|
|
1974
|
+
"next.config.*",
|
|
1975
|
+
"webpack.config.*",
|
|
1976
|
+
".eslintrc*",
|
|
1977
|
+
".eslint.config.*",
|
|
1978
|
+
".prettierrc*",
|
|
1979
|
+
"babel.config.*"
|
|
1980
|
+
],
|
|
1981
|
+
python: [
|
|
1982
|
+
"pyproject.toml",
|
|
1983
|
+
"setup.py",
|
|
1984
|
+
"setup.cfg",
|
|
1985
|
+
"tox.ini",
|
|
1986
|
+
"pytest.ini",
|
|
1987
|
+
"mypy.ini",
|
|
1988
|
+
".flake8",
|
|
1989
|
+
".pylintrc",
|
|
1990
|
+
"Pipfile"
|
|
1991
|
+
],
|
|
1992
|
+
go: ["go.mod", ".golangci.yml", ".golangci.yaml"],
|
|
1993
|
+
rust: ["Cargo.toml", "rust-toolchain.toml", "clippy.toml", ".rustfmt.toml"],
|
|
1994
|
+
java: [
|
|
1995
|
+
"pom.xml",
|
|
1996
|
+
"build.gradle",
|
|
1997
|
+
"build.gradle.kts",
|
|
1998
|
+
"gradle.properties",
|
|
1999
|
+
"settings.gradle",
|
|
2000
|
+
"settings.gradle.kts"
|
|
2001
|
+
],
|
|
2002
|
+
docker: ["Dockerfile", "docker-compose.yml", ".dockerignore"],
|
|
2003
|
+
ci: [".gitlab-ci.yml", "Jenkinsfile", ".travis.yml"],
|
|
2004
|
+
generic: ["Makefile", "CMakeLists.txt", "meson.build"]
|
|
2005
|
+
};
|
|
2006
|
+
var CONFIG_FILES_UNIVERSAL = [".editorconfig", ".nvmrc", ".node-version", ".python-version"];
|
|
2007
|
+
var CONFIG_DIRS_UNIVERSAL = ["config", ".github", ".gitlab", ".circleci", ".vscode"];
|
|
2008
|
+
var CONFIG_LABEL = {
|
|
2009
|
+
node: "dependency and build config",
|
|
2010
|
+
python: "package and environment config",
|
|
2011
|
+
go: "module definition",
|
|
2012
|
+
rust: "crate manifest",
|
|
2013
|
+
java: "build definition",
|
|
2014
|
+
docker: "container config",
|
|
2015
|
+
ci: "pipeline definition",
|
|
2016
|
+
generic: "project config"
|
|
2017
|
+
};
|
|
2018
|
+
var LOCKFILE_LABEL = {
|
|
2019
|
+
node: "package manager",
|
|
2020
|
+
python: "dependency resolver",
|
|
2021
|
+
go: "module checksums",
|
|
2022
|
+
rust: "dependency resolver",
|
|
2023
|
+
java: "Gradle wrapper"
|
|
2024
|
+
};
|
|
2025
|
+
var FALLBACK_MANIFESTS = [
|
|
2026
|
+
"composer.json",
|
|
2027
|
+
// PHP
|
|
2028
|
+
"Gemfile",
|
|
2029
|
+
// Ruby
|
|
2030
|
+
"mix.exs",
|
|
2031
|
+
// Elixir
|
|
2032
|
+
"pubspec.yaml",
|
|
2033
|
+
// Dart / Flutter
|
|
2034
|
+
"Package.swift",
|
|
2035
|
+
// Swift
|
|
2036
|
+
"project.clj",
|
|
2037
|
+
// Clojure
|
|
2038
|
+
"build.sbt",
|
|
2039
|
+
// Scala
|
|
2040
|
+
"stack.yaml",
|
|
2041
|
+
// Haskell
|
|
2042
|
+
"deno.json",
|
|
2043
|
+
"deno.jsonc",
|
|
2044
|
+
// Deno
|
|
2045
|
+
"Podfile"
|
|
2046
|
+
// CocoaPods (iOS/macOS)
|
|
2047
|
+
];
|
|
2048
|
+
var FALLBACK_LOCKFILES = [
|
|
2049
|
+
"composer.lock",
|
|
2050
|
+
// PHP
|
|
2051
|
+
"Gemfile.lock",
|
|
2052
|
+
// Ruby
|
|
2053
|
+
"mix.lock",
|
|
2054
|
+
// Elixir
|
|
2055
|
+
"pubspec.lock",
|
|
2056
|
+
// Dart / Flutter
|
|
2057
|
+
"Package.resolved"
|
|
2058
|
+
// Swift
|
|
2059
|
+
];
|
|
2060
|
+
var FALLBACK_EXTENSIONS = [".csproj", ".fsproj", ".sln", ".cabal"];
|
|
2061
|
+
function exists(p) {
|
|
2062
|
+
return fs3.existsSync(p);
|
|
2063
|
+
}
|
|
2064
|
+
function detectDirs(root, names) {
|
|
2065
|
+
return names.filter((n) => exists(path3.join(root, n))).map((n) => `${n}/**`);
|
|
2066
|
+
}
|
|
2067
|
+
function readGitignore(root) {
|
|
2068
|
+
const p = path3.join(root, ".gitignore");
|
|
2069
|
+
if (!exists(p)) return [];
|
|
2070
|
+
return fs3.readFileSync(p, "utf8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("!")).map((l) => l.endsWith("/") ? l + "**" : l);
|
|
2071
|
+
}
|
|
2072
|
+
function dedup(arr) {
|
|
2073
|
+
return arr.filter((v, i, a) => v && a.indexOf(v) === i);
|
|
2074
|
+
}
|
|
2075
|
+
function detectProjectTypes(root) {
|
|
2076
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2077
|
+
const types = [];
|
|
2078
|
+
for (const sig of PROJECT_SIGNALS) {
|
|
2079
|
+
if (seen.has(sig.type)) continue;
|
|
2080
|
+
const p = path3.join(root, sig.file || sig.dir);
|
|
2081
|
+
if (exists(p)) {
|
|
2082
|
+
seen.add(sig.type);
|
|
2083
|
+
types.push(sig.type);
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
return types.length > 0 ? types : ["generic"];
|
|
2087
|
+
}
|
|
2088
|
+
function primaryType(projectTypes) {
|
|
2089
|
+
return CODE_TYPES.find((t) => projectTypes.includes(t)) ?? "generic";
|
|
2090
|
+
}
|
|
2091
|
+
function scanFallbackManifests(root) {
|
|
2092
|
+
const manifests = FALLBACK_MANIFESTS.filter((f) => exists(path3.join(root, f)));
|
|
2093
|
+
const lockfiles = FALLBACK_LOCKFILES.filter((f) => exists(path3.join(root, f)));
|
|
2094
|
+
try {
|
|
2095
|
+
const entries = fs3.readdirSync(root);
|
|
2096
|
+
for (const e of entries) {
|
|
2097
|
+
if (FALLBACK_EXTENSIONS.some((ext) => e.endsWith(ext))) manifests.push(e);
|
|
2098
|
+
}
|
|
2099
|
+
} catch {
|
|
2100
|
+
}
|
|
2101
|
+
return { manifests, lockfiles };
|
|
2102
|
+
}
|
|
2103
|
+
function detectSurfaces(root, projectTypes) {
|
|
2104
|
+
const s = {};
|
|
2105
|
+
for (const [key, names] of Object.entries(SURFACE_MAP)) {
|
|
2106
|
+
s[key] = detectDirs(root, names);
|
|
2107
|
+
}
|
|
2108
|
+
s.secrets = SECRET_GLOBS.filter((g) => {
|
|
2109
|
+
const base = g.split("/")[0].replace(/\*.*/, "");
|
|
2110
|
+
return base.includes("*") || exists(path3.join(root, base));
|
|
2111
|
+
});
|
|
2112
|
+
const typeConfigFiles = dedup(
|
|
2113
|
+
projectTypes.flatMap((t) => CONFIG_FILES_BY_TYPE[t] || CONFIG_FILES_BY_TYPE.generic).concat(CONFIG_FILES_UNIVERSAL)
|
|
2114
|
+
);
|
|
2115
|
+
s.config = dedup([
|
|
2116
|
+
...typeConfigFiles.filter((f) => !f.includes("*") && !f.endsWith("/**") && exists(path3.join(root, f))),
|
|
2117
|
+
...typeConfigFiles.filter((f) => f.endsWith("/**") && exists(path3.join(root, f.replace("/**", "")))),
|
|
2118
|
+
...detectDirs(root, CONFIG_DIRS_UNIVERSAL)
|
|
2119
|
+
]);
|
|
2120
|
+
s.lockfiles = dedup(
|
|
2121
|
+
projectTypes.flatMap((t) => LOCKFILES_BY_TYPE[t] || []).filter((f) => {
|
|
2122
|
+
const base = f.replace(/\*.*/, "").split("/")[0];
|
|
2123
|
+
return base.includes("*") || exists(path3.join(root, base));
|
|
2124
|
+
})
|
|
2125
|
+
);
|
|
2126
|
+
if (!projectTypes.some((t) => CODE_TYPES.includes(t))) {
|
|
2127
|
+
const fb = scanFallbackManifests(root);
|
|
2128
|
+
s.config = dedup([...s.config, ...fb.manifests]);
|
|
2129
|
+
s.lockfiles = dedup([...s.lockfiles, ...fb.lockfiles]);
|
|
2130
|
+
}
|
|
2131
|
+
return s;
|
|
2132
|
+
}
|
|
2133
|
+
function buildSemantics(projectTypes, primary, surfaces) {
|
|
2134
|
+
const sem = {};
|
|
2135
|
+
sem.structure = "Preserve the existing folder structure. Add new files within established directories. Do not create top-level directories, reorganize, or rename existing folders.";
|
|
2136
|
+
if (surfaces.source.length > 0) {
|
|
2137
|
+
sem.source = `Source code lives in ${surfaces.source.join(", ")}. Make feature changes and bug fixes here only.`;
|
|
2138
|
+
}
|
|
2139
|
+
sem.secrets = `Never propose changes to credential or key files (${SECRET_GLOBS.slice(0, 4).join(", ")} \u2026). These are never task targets regardless of the instruction.`;
|
|
2140
|
+
if (surfaces.generated.length > 0) {
|
|
2141
|
+
sem.generated = `${surfaces.generated.join(", ")} contain generated output. Modify the source files that produce them; never write to generated directories directly.`;
|
|
2142
|
+
}
|
|
2143
|
+
if (surfaces.config.length > 0) {
|
|
2144
|
+
const codeTypes = projectTypes.filter((t) => CODE_TYPES.includes(t));
|
|
2145
|
+
const label = codeTypes.length === 1 ? CONFIG_LABEL[codeTypes[0]] : "project configuration";
|
|
2146
|
+
const listed = surfaces.config.slice(0, 5).join(", ");
|
|
2147
|
+
const trailer = surfaces.config.length > 5 ? " and related files" : "";
|
|
2148
|
+
sem.config = `Treat ${listed}${trailer} as ${label} files. Do not modify them unless the task explicitly requires a configuration or dependency change.`;
|
|
2149
|
+
}
|
|
2150
|
+
if (surfaces.tests.length > 0) {
|
|
2151
|
+
sem.tests = `Test files in ${surfaces.tests.join(", ")} validate behavior. Update them only when the behavior they cover changes.`;
|
|
2152
|
+
}
|
|
2153
|
+
if (surfaces.lockfiles?.length > 0) {
|
|
2154
|
+
const label = LOCKFILE_LABEL[primary] || "tooling";
|
|
2155
|
+
const listed = surfaces.lockfiles.slice(0, 3).join(", ");
|
|
2156
|
+
sem.lockfiles = `${listed} are generated by the ${label}. Never edit them directly.`;
|
|
2157
|
+
}
|
|
2158
|
+
if (primary === "generic") {
|
|
2159
|
+
const foundManifests = surfaces.config.filter((f) => !f.endsWith("/**"));
|
|
2160
|
+
if (foundManifests.length > 0) {
|
|
2161
|
+
sem.unknown = `This project uses an unrecognized toolchain. Treat ${foundManifests.slice(0, 3).join(", ")} as dependency/manifest files. Do not modify them unless the task explicitly requires a dependency change.`;
|
|
2162
|
+
} else {
|
|
2163
|
+
sem.unknown = "This project uses an unrecognized toolchain. Do not assume standard source layouts, dependency files, or build conventions apply. Confirm any structural assumption before acting.";
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
if (projectTypes.includes("docker")) {
|
|
2167
|
+
sem.docker = "Dockerfile and docker-compose.yml define the container environment. Treat them as infrastructure config \u2014 only modify when the task explicitly involves container or environment changes.";
|
|
2168
|
+
}
|
|
2169
|
+
if (projectTypes.includes("ci")) {
|
|
2170
|
+
sem.ci = "CI config files (.github/**, .gitlab-ci.yml, etc.) define the build and deployment pipeline. Do not modify them unless the task explicitly involves CI/CD changes.";
|
|
2171
|
+
}
|
|
2172
|
+
return sem;
|
|
2173
|
+
}
|
|
2174
|
+
function buildEnforcement(surfaces, gitignorePatterns) {
|
|
2175
|
+
const allow = dedup([...surfaces.source, ...surfaces.docs, ...surfaces.tests]);
|
|
2176
|
+
const approval = [...surfaces.config];
|
|
2177
|
+
const deny = dedup([
|
|
2178
|
+
...surfaces.secrets,
|
|
2179
|
+
...surfaces.generated,
|
|
2180
|
+
...surfaces.lockfiles || [],
|
|
2181
|
+
...ALWAYS_DENY,
|
|
2182
|
+
...gitignorePatterns.filter((p) => p.endsWith("/**")).slice(0, 8)
|
|
2183
|
+
]);
|
|
2184
|
+
return {
|
|
2185
|
+
allow: allow.length > 0 ? allow : ["src/**"],
|
|
2186
|
+
approval: approval.length > 0 ? approval : [],
|
|
2187
|
+
deny
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
function scanWorkspace(rootDir) {
|
|
2191
|
+
const root = path3.resolve(rootDir || process.cwd());
|
|
2192
|
+
const projectTypes = detectProjectTypes(root);
|
|
2193
|
+
const primary = primaryType(projectTypes);
|
|
2194
|
+
const surfaces = detectSurfaces(root, projectTypes);
|
|
2195
|
+
const gitignore = readGitignore(root);
|
|
2196
|
+
const semantics = buildSemantics(projectTypes, primary, surfaces);
|
|
2197
|
+
const enforcement = buildEnforcement(surfaces, gitignore);
|
|
2198
|
+
return { projectTypes, primaryType: primary, surfaces, semantics, enforcement };
|
|
2199
|
+
}
|
|
2200
|
+
function formatSummary(projectTypes, semantics, enforcement) {
|
|
2201
|
+
const lines = [];
|
|
2202
|
+
const label = Array.isArray(projectTypes) ? projectTypes.join(" + ") : projectTypes;
|
|
2203
|
+
lines.push(`Detected: ${label}`);
|
|
2204
|
+
lines.push("");
|
|
2205
|
+
lines.push("Agent semantics:");
|
|
2206
|
+
for (const [, v] of Object.entries(semantics)) {
|
|
2207
|
+
lines.push(` - ${v}`);
|
|
2208
|
+
}
|
|
2209
|
+
lines.push("");
|
|
2210
|
+
lines.push("Enforcement:");
|
|
2211
|
+
if (enforcement.allow.length) lines.push(` allow: ${enforcement.allow.join(", ")}`);
|
|
2212
|
+
if (enforcement.approval.length) lines.push(` approval: ${enforcement.approval.join(", ")}`);
|
|
2213
|
+
if (enforcement.deny.length) lines.push(` deny: ${enforcement.deny.slice(0, 6).join(", ")}${enforcement.deny.length > 6 ? " \u2026" : ""}`);
|
|
2214
|
+
return lines.join("\n");
|
|
2215
|
+
}
|
|
2216
|
+
|
|
2217
|
+
// src/cli/commands/init.js
|
|
2218
|
+
function ask(question) {
|
|
2219
|
+
if (!process.stdin.isTTY) return Promise.resolve("y");
|
|
2220
|
+
return new Promise((resolve) => {
|
|
2221
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
2222
|
+
rl.question(question, (ans) => {
|
|
2223
|
+
rl.close();
|
|
2224
|
+
resolve(ans.trim().toLowerCase());
|
|
2225
|
+
});
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
function applyStrict(enforcement) {
|
|
2229
|
+
return {
|
|
2230
|
+
...enforcement,
|
|
2231
|
+
deny: [.../* @__PURE__ */ new Set([...enforcement.deny, ...enforcement.approval, "*.json", "config/**"])],
|
|
2232
|
+
approval: []
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
function applyRelaxed(enforcement) {
|
|
2236
|
+
return { ...enforcement, approval: [] };
|
|
2237
|
+
}
|
|
2238
|
+
function setupCrypto(cwd) {
|
|
2239
|
+
const nowIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
2240
|
+
const expiresAt = new Date(Date.now() + 180 * 24 * 60 * 60 * 1e3).toISOString();
|
|
2241
|
+
const defaultKeyId = "agent:gpt-v1-2026Q1";
|
|
2242
|
+
const signerKeyId = "policy-signer-v1-2026Q1";
|
|
2243
|
+
const lbeDir = path4.join(cwd, ".lbe");
|
|
2244
|
+
for (const d of ["config", "keys", "data"]) {
|
|
2245
|
+
fs4.mkdirSync(path4.join(lbeDir, d), { recursive: true });
|
|
2246
|
+
}
|
|
2247
|
+
const dataFiles = {
|
|
2248
|
+
".lbe/data/nonce.db.json": JSON.stringify({ entries: [] }, null, 2),
|
|
2249
|
+
".lbe/data/rate-limit.db.json": JSON.stringify({ entries: [] }, null, 2),
|
|
2250
|
+
".lbe/data/policy.state.json": JSON.stringify({ schemaVersion: "1", lastAccepted: null, updatedAt: null }, null, 2),
|
|
2251
|
+
".lbe/data/audit.log.jsonl": ""
|
|
2252
|
+
};
|
|
2253
|
+
for (const [rel, content] of Object.entries(dataFiles)) {
|
|
2254
|
+
const p = path4.join(cwd, rel);
|
|
2255
|
+
if (!fs4.existsSync(p)) fs4.writeFileSync(p, content);
|
|
2256
|
+
}
|
|
2257
|
+
const keyDir = path4.join(lbeDir, "keys");
|
|
2258
|
+
const pubPath = path4.join(keyDir, "public.key");
|
|
2259
|
+
const secPath = path4.join(keyDir, "secret.key");
|
|
2260
|
+
let publicKeyB64, secretKeyB64;
|
|
2261
|
+
if (fs4.existsSync(pubPath) && fs4.existsSync(secPath)) {
|
|
2262
|
+
publicKeyB64 = fs4.readFileSync(pubPath, "utf8").trim();
|
|
2263
|
+
secretKeyB64 = fs4.readFileSync(secPath, "utf8").trim();
|
|
2264
|
+
} else {
|
|
2265
|
+
const kp = generateKeyPair();
|
|
2266
|
+
publicKeyB64 = kp.publicKey;
|
|
2267
|
+
secretKeyB64 = kp.secretKey;
|
|
2268
|
+
fs4.writeFileSync(pubPath, publicKeyB64);
|
|
2269
|
+
fs4.writeFileSync(secPath, secretKeyB64, { mode: 384 });
|
|
2270
|
+
}
|
|
2271
|
+
const keysPath = path4.join(lbeDir, "config/keys.json");
|
|
2272
|
+
const keysStore = fs4.existsSync(keysPath) ? JSON.parse(fs4.readFileSync(keysPath, "utf8")) : { schemaVersion: "1", defaultKeyId, trustedKeys: {} };
|
|
2273
|
+
for (const keyId of [defaultKeyId, signerKeyId]) {
|
|
2274
|
+
if (!keysStore.trustedKeys[keyId]) {
|
|
2275
|
+
keysStore.trustedKeys[keyId] = {
|
|
2276
|
+
publicKey: publicKeyB64,
|
|
2277
|
+
notBefore: nowIso,
|
|
2278
|
+
expiresAt,
|
|
2279
|
+
validFrom: nowIso,
|
|
2280
|
+
validUntil: expiresAt,
|
|
2281
|
+
deprecated: false
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
keysStore.defaultKeyId = defaultKeyId;
|
|
2286
|
+
fs4.writeFileSync(keysPath, JSON.stringify(keysStore, null, 2));
|
|
2287
|
+
const policyPath = path4.join(lbeDir, "config/policy.default.json");
|
|
2288
|
+
let policyObj;
|
|
2289
|
+
if (fs4.existsSync(policyPath)) {
|
|
2290
|
+
policyObj = JSON.parse(fs4.readFileSync(policyPath, "utf8"));
|
|
2291
|
+
} else {
|
|
2292
|
+
policyObj = {
|
|
2293
|
+
default: "DENY",
|
|
2294
|
+
version: "1.0.0",
|
|
2295
|
+
createdAt: nowIso,
|
|
2296
|
+
security: {
|
|
2297
|
+
maxClockSkewSec: 600,
|
|
2298
|
+
maxPolicyCreatedAtSkewSec: 31536e3,
|
|
2299
|
+
defaultRateLimit: { windowSec: 60, maxRequests: 30 }
|
|
2300
|
+
},
|
|
2301
|
+
requesters: {
|
|
2302
|
+
"agent:gpt": {
|
|
2303
|
+
allowAdapters: ["noop", "shell"],
|
|
2304
|
+
allowCommands: ["RUN_SHELL"],
|
|
2305
|
+
rateLimit: { windowSec: 60, maxRequests: 30 },
|
|
2306
|
+
filesystem: { roots: [cwd], denyPatterns: ["**/.git/**", "**/secrets/**", "**/*.key"] },
|
|
2307
|
+
exec: { allowCmds: ["ls", "node", "python", "echo"], denyCmds: ["rm", "chmod", "chown", "curl", "wget", "su", "sudo"] }
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
};
|
|
2311
|
+
fs4.writeFileSync(policyPath, JSON.stringify(policyObj, null, 2));
|
|
2312
|
+
}
|
|
2313
|
+
const sigResult = createPolicySignatureEnvelope({ policyObj, secretKeyB64, keyId: signerKeyId });
|
|
2314
|
+
if (sigResult.ok) {
|
|
2315
|
+
fs4.writeFileSync(path4.join(lbeDir, "config/policy.sig.json"), JSON.stringify(sigResult.envelope, null, 2));
|
|
2316
|
+
}
|
|
2317
|
+
return { defaultKeyId, secretKeyB64, publicKeyB64 };
|
|
2318
|
+
}
|
|
2319
|
+
function agentContractContent() {
|
|
2320
|
+
return `# LBE Governance Contract
|
|
2321
|
+
|
|
2322
|
+
This project has LetterBlack LBE (Local-first execution Governance) active.
|
|
2323
|
+
|
|
2324
|
+
## Your role as an agent
|
|
2325
|
+
|
|
2326
|
+
You do not have direct access to the file system or shell in this workspace.
|
|
2327
|
+
Every file write, file read, file delete, and shell command must go through the
|
|
2328
|
+
LBE executor. The executor validates every action against the project policy,
|
|
2329
|
+
signs it, audits it, and either allows or blocks it \u2014 transparently.
|
|
2330
|
+
|
|
2331
|
+
You do not need to know how this works. You only need to use the right methods.
|
|
2332
|
+
|
|
2333
|
+
## How to perform actions
|
|
2334
|
+
|
|
2335
|
+
\`\`\`js
|
|
2336
|
+
import { createLocalExecutor } from '@letterblack/lbe-exec';
|
|
2337
|
+
const lbe = createLocalExecutor({ rootDir: process.cwd() });
|
|
2338
|
+
|
|
2339
|
+
// File operations
|
|
2340
|
+
await lbe.writeFile('output/report.md', content);
|
|
2341
|
+
await lbe.readFile('src/config.json');
|
|
2342
|
+
await lbe.patchFile('src/index.js', patch);
|
|
2343
|
+
await lbe.deleteFile('tmp/scratch.txt');
|
|
2344
|
+
|
|
2345
|
+
// Shell (only explicitly allowlisted commands run)
|
|
2346
|
+
await lbe.runShell('node', ['scripts/build.js']);
|
|
2347
|
+
\`\`\`
|
|
2348
|
+
|
|
2349
|
+
## What you must never do
|
|
2350
|
+
|
|
2351
|
+
- Call file system APIs directly (fs.writeFile, fs.readFile, etc.)
|
|
2352
|
+
- Call child_process or exec directly
|
|
2353
|
+
- Modify \`lbe.policy.json\` directly \u2014 propose a rule via \`lbe.policy.proposeRule()\`
|
|
2354
|
+
- Attempt to bypass, patch, or disable the executor
|
|
2355
|
+
|
|
2356
|
+
## Proposing a new policy rule
|
|
2357
|
+
|
|
2358
|
+
If you believe a rule should be added or removed, propose it and let the
|
|
2359
|
+
controller decide:
|
|
2360
|
+
|
|
2361
|
+
\`\`\`js
|
|
2362
|
+
const proposal = lbe.policy.proposeRule({
|
|
2363
|
+
effect: 'deny',
|
|
2364
|
+
type: 'path',
|
|
2365
|
+
pattern: 'secrets/**',
|
|
2366
|
+
from: 'agent: these files should never be modified'
|
|
2367
|
+
});
|
|
2368
|
+
// Return proposal to the user. Never call lbe.policy.addRule() yourself.
|
|
2369
|
+
\`\`\`
|
|
2370
|
+
|
|
2371
|
+
## Result shape
|
|
2372
|
+
|
|
2373
|
+
Every method returns:
|
|
2374
|
+
\`\`\`
|
|
2375
|
+
{ ok: boolean, decision: 'allow' | 'deny' | 'observe', executed: boolean }
|
|
2376
|
+
\`\`\`
|
|
2377
|
+
|
|
2378
|
+
If \`ok\` is false, the action was blocked. Check \`result.error.message\` for reason.
|
|
2379
|
+
|
|
2380
|
+
## Current policy
|
|
2381
|
+
|
|
2382
|
+
See \`lbe.policy.json\` in the project root.
|
|
2383
|
+
See \`.lbe/audit.jsonl\` for the execution audit trail.
|
|
2384
|
+
|
|
2385
|
+
## CLI
|
|
2386
|
+
|
|
2387
|
+
\`\`\`bash
|
|
2388
|
+
npx lbe-exec status # mode, rule count, audit entry count
|
|
2389
|
+
npx lbe-exec policy # list active rules
|
|
2390
|
+
npx lbe-exec observe # switch to observer mode
|
|
2391
|
+
npx lbe-exec enforce # switch to enforcement mode
|
|
2392
|
+
\`\`\`
|
|
2393
|
+
`;
|
|
2394
|
+
}
|
|
2395
|
+
function claudeMdSection() {
|
|
2396
|
+
return `<!-- lbe-governance -->
|
|
2397
|
+
## LBE Execution Governance
|
|
2398
|
+
|
|
2399
|
+
This project has LetterBlack LBE active. All file and shell operations must
|
|
2400
|
+
route through the LBE executor \u2014 do not use file system tools directly.
|
|
2401
|
+
|
|
2402
|
+
### Use these methods
|
|
2403
|
+
|
|
2404
|
+
\`\`\`js
|
|
2405
|
+
import { createLocalExecutor } from '@letterblack/lbe-exec';
|
|
2406
|
+
const lbe = createLocalExecutor({ rootDir: process.cwd() });
|
|
2407
|
+
|
|
2408
|
+
await lbe.writeFile('path/to/file', content);
|
|
2409
|
+
await lbe.readFile('path/to/file');
|
|
2410
|
+
await lbe.patchFile('path/to/file', patch);
|
|
2411
|
+
await lbe.deleteFile('path/to/file');
|
|
2412
|
+
await lbe.runShell('node', ['script.js']);
|
|
2413
|
+
\`\`\`
|
|
2414
|
+
|
|
2415
|
+
### Rules
|
|
2416
|
+
|
|
2417
|
+
- Never call \`fs.*\` or \`child_process.*\` directly
|
|
2418
|
+
- Never write to \`lbe.policy.json\` directly \u2014 use \`lbe.policy.proposeRule()\`
|
|
2419
|
+
- Every blocked action has a reason in \`result.error.message\`
|
|
2420
|
+
- Audit trail is in \`.lbe/audit.jsonl\`
|
|
2421
|
+
|
|
2422
|
+
See \`.lbe/AGENT_CONTRACT.md\` for the full agent contract.
|
|
2423
|
+
<!-- /lbe-governance -->`;
|
|
2424
|
+
}
|
|
2425
|
+
function copilotInstructionsContent() {
|
|
2426
|
+
return `# LBE Governance \u2014 Copilot Instructions
|
|
2427
|
+
|
|
2428
|
+
This project has LetterBlack LBE active. All file and shell operations must
|
|
2429
|
+
route through the LBE executor.
|
|
2430
|
+
|
|
2431
|
+
## Required
|
|
2432
|
+
|
|
2433
|
+
Use \`@letterblack/lbe-exec\` for all file and shell actions:
|
|
2434
|
+
|
|
2435
|
+
\`\`\`js
|
|
2436
|
+
const lbe = createLocalExecutor({ rootDir: process.cwd() });
|
|
2437
|
+
await lbe.writeFile(path, content);
|
|
2438
|
+
await lbe.readFile(path);
|
|
2439
|
+
await lbe.runShell(cmd, args);
|
|
2440
|
+
\`\`\`
|
|
2441
|
+
|
|
2442
|
+
## Forbidden
|
|
2443
|
+
|
|
2444
|
+
- Direct \`fs.*\` calls
|
|
2445
|
+
- Direct \`child_process.*\` or \`exec\` calls
|
|
2446
|
+
- Writing to \`lbe.policy.json\` directly
|
|
2447
|
+
|
|
2448
|
+
See \`.lbe/AGENT_CONTRACT.md\` for the full contract.
|
|
2449
|
+
`;
|
|
2450
|
+
}
|
|
2451
|
+
function writeAgentGovernanceFiles(cwd) {
|
|
2452
|
+
const lbeDir = path4.join(cwd, ".lbe");
|
|
2453
|
+
fs4.mkdirSync(lbeDir, { recursive: true });
|
|
2454
|
+
fs4.writeFileSync(path4.join(lbeDir, "AGENT_CONTRACT.md"), agentContractContent());
|
|
2455
|
+
const claudePath = path4.join(cwd, "CLAUDE.md");
|
|
2456
|
+
const section = claudeMdSection();
|
|
2457
|
+
if (fs4.existsSync(claudePath)) {
|
|
2458
|
+
const existing = fs4.readFileSync(claudePath, "utf8");
|
|
2459
|
+
if (!existing.includes("<!-- lbe-governance -->")) {
|
|
2460
|
+
fs4.appendFileSync(claudePath, "\n\n" + section + "\n");
|
|
2461
|
+
}
|
|
2462
|
+
} else {
|
|
2463
|
+
fs4.writeFileSync(claudePath, section + "\n");
|
|
2464
|
+
}
|
|
2465
|
+
const githubDir = path4.join(cwd, ".github");
|
|
2466
|
+
fs4.mkdirSync(githubDir, { recursive: true });
|
|
2467
|
+
const copilotPath = path4.join(githubDir, "copilot-instructions.md");
|
|
2468
|
+
if (!fs4.existsSync(copilotPath)) {
|
|
2469
|
+
fs4.writeFileSync(copilotPath, copilotInstructionsContent());
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
async function initCommand(opts2 = {}) {
|
|
2473
|
+
const cwd = process.cwd();
|
|
2474
|
+
const yes = opts2.yes || opts2.y || !process.stdin.isTTY;
|
|
2475
|
+
const outPath = path4.join(cwd, "lbe.workspace.json");
|
|
2476
|
+
console.log("\nScanning workspace...\n");
|
|
2477
|
+
const { projectTypes, primaryType: primaryType2, semantics, enforcement } = scanWorkspace(cwd);
|
|
2478
|
+
console.log(formatSummary(projectTypes, semantics, enforcement));
|
|
2479
|
+
console.log("");
|
|
2480
|
+
let finalEnforcement = enforcement;
|
|
2481
|
+
if (!yes) {
|
|
2482
|
+
const answer = await ask("Accept? [Y = accept / s = strict / r = relaxed / n = cancel] ");
|
|
2483
|
+
if (answer === "n") {
|
|
2484
|
+
console.log("Cancelled.");
|
|
2485
|
+
return { success: false };
|
|
2486
|
+
}
|
|
2487
|
+
if (answer === "s") finalEnforcement = applyStrict(enforcement);
|
|
2488
|
+
if (answer === "r") finalEnforcement = applyRelaxed(enforcement);
|
|
2489
|
+
}
|
|
2490
|
+
const contract = {
|
|
2491
|
+
lbe: true,
|
|
2492
|
+
version: "0.4.0",
|
|
2493
|
+
state: "local",
|
|
2494
|
+
projectTypes,
|
|
2495
|
+
primaryType: primaryType2,
|
|
2496
|
+
semantics,
|
|
2497
|
+
enforcement: finalEnforcement
|
|
2498
|
+
};
|
|
2499
|
+
fs4.writeFileSync(outPath, JSON.stringify(contract, null, 2));
|
|
2500
|
+
console.log("\u2713 Wrote lbe.workspace.json");
|
|
2501
|
+
setupCrypto(cwd);
|
|
2502
|
+
const localPolicyPath = path4.join(cwd, "lbe.policy.json");
|
|
2503
|
+
if (!fs4.existsSync(localPolicyPath)) {
|
|
2504
|
+
fs4.writeFileSync(localPolicyPath, JSON.stringify({ version: 1, mode: "observe", workspace: cwd, rules: [] }, null, 2) + "\n");
|
|
2505
|
+
}
|
|
2506
|
+
const localAuditPath = path4.join(cwd, ".lbe", "audit.jsonl");
|
|
2507
|
+
if (!fs4.existsSync(localAuditPath)) fs4.writeFileSync(localAuditPath, "");
|
|
2508
|
+
console.log("\u2713 Keys and policy ready");
|
|
2509
|
+
writeAgentGovernanceFiles(cwd);
|
|
2510
|
+
console.log("\u2713 Agent contract written \u2192 .lbe/AGENT_CONTRACT.md");
|
|
2511
|
+
console.log("\u2713 CLAUDE.md updated with LBE governance section");
|
|
2512
|
+
console.log("\u2713 .github/copilot-instructions.md ready\n");
|
|
2513
|
+
console.log("Done. Any AI agent that reads project context will follow LBE governance automatically.");
|
|
2514
|
+
console.log("Run npx lbe status to see mode, rules, and audit entry count.\n");
|
|
2515
|
+
return { success: true, contract };
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
// src/cli/commands/policyMode.js
|
|
2519
|
+
init_localPolicy();
|
|
2520
|
+
async function policyModeCommand(mode, opts2 = {}) {
|
|
2521
|
+
const loaded = loadLocalPolicy(opts2.root || process.cwd(), mode);
|
|
2522
|
+
writeLocalPolicy(loaded.root, { ...loaded.policy, mode });
|
|
2523
|
+
console.log(JSON.stringify({ mode, policy: loaded.policyPath }, null, 2));
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
// exec/cli.js
|
|
2527
|
+
var [, , cmd, ...rest] = process.argv;
|
|
2528
|
+
var opts = Object.fromEntries(
|
|
2529
|
+
rest.flatMap((v, i, a) => v.startsWith("--") ? [[v.slice(2), a[i + 1] ?? true]] : [])
|
|
2530
|
+
);
|
|
2531
|
+
function loadPolicy() {
|
|
2532
|
+
const p = path15.join(process.cwd(), "lbe.policy.json");
|
|
2533
|
+
return fs14.existsSync(p) ? JSON.parse(fs14.readFileSync(p, "utf8")) : null;
|
|
2534
|
+
}
|
|
2535
|
+
function countAudit() {
|
|
2536
|
+
const p = path15.join(process.cwd(), ".lbe", "audit.jsonl");
|
|
2537
|
+
if (!fs14.existsSync(p)) return 0;
|
|
2538
|
+
return fs14.readFileSync(p, "utf8").split("\n").filter((l) => l.trim()).length;
|
|
2539
|
+
}
|
|
2540
|
+
switch (cmd) {
|
|
2541
|
+
case "init":
|
|
2542
|
+
initCommand(opts).catch((e) => {
|
|
2543
|
+
console.error(e.message);
|
|
2544
|
+
process.exit(1);
|
|
2545
|
+
});
|
|
2546
|
+
break;
|
|
2547
|
+
case "observe":
|
|
2548
|
+
case "enforce":
|
|
2549
|
+
policyModeCommand(cmd, opts).catch((e) => {
|
|
2550
|
+
console.error(e.message);
|
|
2551
|
+
process.exit(1);
|
|
2552
|
+
});
|
|
2553
|
+
break;
|
|
2554
|
+
case "status": {
|
|
2555
|
+
const policy = loadPolicy();
|
|
2556
|
+
if (!policy) {
|
|
2557
|
+
console.log("No lbe.policy.json found. Run: npx lbe init");
|
|
2558
|
+
break;
|
|
2559
|
+
}
|
|
2560
|
+
console.log(`mode: ${policy.mode}`);
|
|
2561
|
+
console.log(`rules: ${policy.rules?.length ?? 0}`);
|
|
2562
|
+
console.log(`audit: ${countAudit()} entries`);
|
|
2563
|
+
console.log(`workspace: ${policy.workspace || process.cwd()}`);
|
|
2564
|
+
break;
|
|
2565
|
+
}
|
|
2566
|
+
case "policy": {
|
|
2567
|
+
const policy = loadPolicy();
|
|
2568
|
+
if (!policy) {
|
|
2569
|
+
console.log("No lbe.policy.json found. Run: npx lbe init");
|
|
2570
|
+
break;
|
|
2571
|
+
}
|
|
2572
|
+
if (!policy.rules?.length) {
|
|
2573
|
+
console.log("No rules defined.");
|
|
2574
|
+
break;
|
|
2575
|
+
}
|
|
2576
|
+
for (const r of policy.rules) {
|
|
2577
|
+
console.log(`[${r.effect.toUpperCase()}] ${r.type}:${r.pattern} \u2014 ${r.from || ""} (${r.id || "?"})`);
|
|
2578
|
+
}
|
|
2579
|
+
break;
|
|
2580
|
+
}
|
|
2581
|
+
case "execute": {
|
|
2582
|
+
Promise.resolve().then(() => (init_localExecutor(), localExecutor_exports)).then(async ({ createLocalExecutor: createLocalExecutor2 }) => {
|
|
2583
|
+
const lbe = createLocalExecutor2({ rootDir: process.cwd() });
|
|
2584
|
+
let raw = "";
|
|
2585
|
+
if (opts.input) {
|
|
2586
|
+
raw = fs14.readFileSync(path15.resolve(opts.input), "utf8");
|
|
2587
|
+
} else {
|
|
2588
|
+
for await (const chunk of process.stdin) raw += chunk;
|
|
2589
|
+
}
|
|
2590
|
+
const request = JSON.parse(raw);
|
|
2591
|
+
const result = await lbe.execute(request);
|
|
2592
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2593
|
+
process.exit(result.ok ? 0 : result.decision === "deny" ? 1 : 2);
|
|
2594
|
+
}).catch((e) => {
|
|
2595
|
+
console.error(e.message);
|
|
2596
|
+
process.exit(2);
|
|
2597
|
+
});
|
|
2598
|
+
break;
|
|
2599
|
+
}
|
|
2600
|
+
default:
|
|
2601
|
+
console.log("Usage: lbe <command>\n");
|
|
2602
|
+
console.log(" init Set up LBE governance in this project");
|
|
2603
|
+
console.log(" status Show mode, rule count, and audit entry count");
|
|
2604
|
+
console.log(" policy List active rules");
|
|
2605
|
+
console.log(" observe Switch to observer mode (log only, nothing blocked)");
|
|
2606
|
+
console.log(" enforce Switch to enforcement mode (violations blocked)");
|
|
2607
|
+
console.log(" execute Send a JSON request from stdin or --input file");
|
|
2608
|
+
console.log("\nCLI: npx lbe-exec <command>");
|
|
2609
|
+
if (cmd && cmd !== "--help" && cmd !== "help") {
|
|
2610
|
+
console.error(`
|
|
2611
|
+
Unknown command: ${cmd}`);
|
|
2612
|
+
process.exit(1);
|
|
2613
|
+
}
|
|
2614
|
+
}
|