@letterblack/lbe-exec 1.2.11 → 1.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +251 -25
- package/dist/index.js +40 -1
- package/hooks/register.cjs +473 -0
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
#!/usr/bin/env node
|
|
3
2
|
var __defProp = Object.defineProperty;
|
|
4
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
4
|
var __esm = (fn, res) => function __init() {
|
|
@@ -1762,6 +1761,43 @@ function createLocalExecutor(options = {}) {
|
|
|
1762
1761
|
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
1762
|
return { local, localDecision, normalized, proposal, policy, validation };
|
|
1764
1763
|
}
|
|
1764
|
+
function evaluateSync(action) {
|
|
1765
|
+
const local = loadLocalPolicy(rootDir, options.mode || "observe");
|
|
1766
|
+
const mode = local.policy.mode;
|
|
1767
|
+
let target = null;
|
|
1768
|
+
let command = null;
|
|
1769
|
+
if (action.path) {
|
|
1770
|
+
try {
|
|
1771
|
+
target = path14.resolve(rootDir, action.path);
|
|
1772
|
+
if (!underRoot(target, rootDir)) {
|
|
1773
|
+
return { decision: "deny", deny: true, matchedRules: ["path:outside_root"], mode, enforced: mode === "enforce", reason: "PATH_OUTSIDE_ROOT" };
|
|
1774
|
+
}
|
|
1775
|
+
} catch (e) {
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
if (action.cmd) command = action.cmd;
|
|
1779
|
+
const localDecision = evaluateLocalPolicy(local.policy, rootDir, { target, command });
|
|
1780
|
+
const isDeny = !localDecision.allowed;
|
|
1781
|
+
return {
|
|
1782
|
+
decision: isDeny ? "deny" : "allow",
|
|
1783
|
+
deny: isDeny,
|
|
1784
|
+
matchedRules: localDecision.winningRules.map((r) => r.id),
|
|
1785
|
+
mode,
|
|
1786
|
+
enforced: mode === "enforce"
|
|
1787
|
+
};
|
|
1788
|
+
}
|
|
1789
|
+
function auditSync(entry) {
|
|
1790
|
+
const eventsPath = path14.join(rootDir, ".lbe", "events.jsonl");
|
|
1791
|
+
const dir = path14.dirname(eventsPath);
|
|
1792
|
+
if (!fs13.existsSync(dir)) fs13.mkdirSync(dir, { recursive: true });
|
|
1793
|
+
const line = JSON.stringify({ ts: Math.floor(Date.now() / 1e3), ...entry }) + "\n";
|
|
1794
|
+
const fd = fs13.openSync(eventsPath, "a");
|
|
1795
|
+
try {
|
|
1796
|
+
fs13.writeSync(fd, line);
|
|
1797
|
+
} finally {
|
|
1798
|
+
fs13.closeSync(fd);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1765
1801
|
async function dryRun(request) {
|
|
1766
1802
|
const prepared = prepare(request);
|
|
1767
1803
|
if (prepared.error) return { ...prepared.error, dryRun: true };
|
|
@@ -1845,7 +1881,9 @@ function createLocalExecutor(options = {}) {
|
|
|
1845
1881
|
proposeRule: proposePolicyRule,
|
|
1846
1882
|
addRule: (rule) => addLocalPolicyRule(rootDir, rule, options.mode || "enforce")
|
|
1847
1883
|
},
|
|
1848
|
-
audit: { verify: () => verifyAuditLogIntegrity(path14.join(rootDir, ".lbe/audit.jsonl")) }
|
|
1884
|
+
audit: { verify: () => verifyAuditLogIntegrity(path14.join(rootDir, ".lbe/audit.jsonl")) },
|
|
1885
|
+
evaluateSync,
|
|
1886
|
+
auditSync
|
|
1849
1887
|
};
|
|
1850
1888
|
}
|
|
1851
1889
|
var INTENTS, MUTATIONS, FORBIDDEN_CONTENT;
|
|
@@ -1881,6 +1919,8 @@ var init_localExecutor = __esm({
|
|
|
1881
1919
|
// exec/cli.js
|
|
1882
1920
|
import fs14 from "fs";
|
|
1883
1921
|
import path15 from "path";
|
|
1922
|
+
import { spawnSync as spawnSync2, spawn } from "child_process";
|
|
1923
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1884
1924
|
|
|
1885
1925
|
// src/cli/commands/init.js
|
|
1886
1926
|
init_signature();
|
|
@@ -2528,6 +2568,8 @@ var [, , cmd, ...rest] = process.argv;
|
|
|
2528
2568
|
var opts = Object.fromEntries(
|
|
2529
2569
|
rest.flatMap((v, i, a) => v.startsWith("--") ? [[v.slice(2), a[i + 1] ?? true]] : [])
|
|
2530
2570
|
);
|
|
2571
|
+
var positional = rest.filter((v) => !v.startsWith("--") && rest[rest.indexOf(v) - 1]?.startsWith("--") === false);
|
|
2572
|
+
var __dir = path15.dirname(fileURLToPath2(import.meta.url));
|
|
2531
2573
|
function loadPolicy() {
|
|
2532
2574
|
const p = path15.join(process.cwd(), "lbe.policy.json");
|
|
2533
2575
|
return fs14.existsSync(p) ? JSON.parse(fs14.readFileSync(p, "utf8")) : null;
|
|
@@ -2537,36 +2579,216 @@ function countAudit() {
|
|
|
2537
2579
|
if (!fs14.existsSync(p)) return 0;
|
|
2538
2580
|
return fs14.readFileSync(p, "utf8").split("\n").filter((l) => l.trim()).length;
|
|
2539
2581
|
}
|
|
2582
|
+
function findHookPath() {
|
|
2583
|
+
const candidates = [
|
|
2584
|
+
path15.resolve(__dir, "../hooks/register.cjs"),
|
|
2585
|
+
// npm: dist/ → ../hooks/
|
|
2586
|
+
path15.resolve(__dir, "../src/hooks/register.cjs")
|
|
2587
|
+
// dev: exec/ → ../src/hooks/
|
|
2588
|
+
];
|
|
2589
|
+
return candidates.find((p) => fs14.existsSync(p)) || candidates[0];
|
|
2590
|
+
}
|
|
2591
|
+
function detectNodeScripts(scripts) {
|
|
2592
|
+
const pattern = /(?:^|\s)node\s+(\S+)/;
|
|
2593
|
+
return Object.entries(scripts || {}).filter(([name, cmd2]) => {
|
|
2594
|
+
if (name.includes(":lbe") || name.startsWith("lbe")) return false;
|
|
2595
|
+
return pattern.test(cmd2);
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
function extractNodeArgs(cmd2) {
|
|
2599
|
+
const match = cmd2.match(/(?:^|\s)node\s+(.+)/);
|
|
2600
|
+
return match ? match[1].trim() : null;
|
|
2601
|
+
}
|
|
2602
|
+
function injectScripts(wrapScript) {
|
|
2603
|
+
const pkgPath = path15.join(process.cwd(), "package.json");
|
|
2604
|
+
if (!fs14.existsSync(pkgPath)) return [];
|
|
2605
|
+
const pkg = JSON.parse(fs14.readFileSync(pkgPath, "utf8"));
|
|
2606
|
+
const scripts = pkg.scripts || {};
|
|
2607
|
+
const added = [];
|
|
2608
|
+
if (wrapScript) {
|
|
2609
|
+
const original = scripts[wrapScript];
|
|
2610
|
+
if (!original) {
|
|
2611
|
+
console.error(`No script named "${wrapScript}" found.`);
|
|
2612
|
+
return [];
|
|
2613
|
+
}
|
|
2614
|
+
const args = extractNodeArgs(original);
|
|
2615
|
+
if (!args) {
|
|
2616
|
+
console.error(`Script "${wrapScript}" does not look like a node script.`);
|
|
2617
|
+
return [];
|
|
2618
|
+
}
|
|
2619
|
+
scripts[wrapScript] = `lbe-exec run-node --mode observe ${args}`;
|
|
2620
|
+
added.push(wrapScript);
|
|
2621
|
+
} else {
|
|
2622
|
+
const candidates = detectNodeScripts(scripts);
|
|
2623
|
+
for (const [name, scriptCmd] of candidates) {
|
|
2624
|
+
const args = extractNodeArgs(scriptCmd);
|
|
2625
|
+
if (!args) continue;
|
|
2626
|
+
const lbeName = name + ":lbe";
|
|
2627
|
+
const lbeEnforceName = name + ":lbe:enforce";
|
|
2628
|
+
if (!scripts[lbeName]) {
|
|
2629
|
+
scripts[lbeName] = `lbe-exec run-node --mode observe ${args}`;
|
|
2630
|
+
added.push(lbeName);
|
|
2631
|
+
}
|
|
2632
|
+
if (!scripts[lbeEnforceName]) {
|
|
2633
|
+
scripts[lbeEnforceName] = `lbe-exec run-node --mode enforce ${args}`;
|
|
2634
|
+
added.push(lbeEnforceName);
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
}
|
|
2638
|
+
if (!scripts["lbe:status"]) {
|
|
2639
|
+
scripts["lbe:status"] = "lbe-exec status";
|
|
2640
|
+
added.push("lbe:status");
|
|
2641
|
+
}
|
|
2642
|
+
if (!scripts["lbe:audit"]) {
|
|
2643
|
+
scripts["lbe:audit"] = "lbe-exec audit";
|
|
2644
|
+
added.push("lbe:audit");
|
|
2645
|
+
}
|
|
2646
|
+
if (added.length) {
|
|
2647
|
+
pkg.scripts = scripts;
|
|
2648
|
+
fs14.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
2649
|
+
for (const s of added) console.log(` added: ${s}`);
|
|
2650
|
+
}
|
|
2651
|
+
return added;
|
|
2652
|
+
}
|
|
2540
2653
|
switch (cmd) {
|
|
2541
|
-
case "
|
|
2542
|
-
|
|
2543
|
-
|
|
2654
|
+
case "run-node": {
|
|
2655
|
+
const mode = opts.mode || "observe";
|
|
2656
|
+
if (!["observe", "enforce"].includes(mode)) {
|
|
2657
|
+
console.error("--mode must be observe or enforce");
|
|
2658
|
+
process.exit(1);
|
|
2659
|
+
}
|
|
2660
|
+
const scriptIdx = rest.findIndex((v, i) => !v.startsWith("--") && (i === 0 || !rest[i - 1].startsWith("--")));
|
|
2661
|
+
if (scriptIdx === -1) {
|
|
2662
|
+
console.error("Usage: lbe-exec run-node [--mode observe|enforce] <script> [...args]");
|
|
2544
2663
|
process.exit(1);
|
|
2664
|
+
}
|
|
2665
|
+
const scriptAndArgs = rest.slice(scriptIdx);
|
|
2666
|
+
const hookPath = findHookPath();
|
|
2667
|
+
if (!fs14.existsSync(hookPath)) {
|
|
2668
|
+
console.error("Hook not found: " + hookPath + "\nRun: npm install @letterblack/lbe-exec");
|
|
2669
|
+
process.exit(1);
|
|
2670
|
+
}
|
|
2671
|
+
const child = spawn(process.execPath, ["--require", hookPath, ...scriptAndArgs], {
|
|
2672
|
+
stdio: "inherit",
|
|
2673
|
+
env: { ...process.env, LBE_MODE: mode, LBE_ROOT: process.cwd() }
|
|
2545
2674
|
});
|
|
2675
|
+
child.on("close", (code) => process.exit(code ?? 0));
|
|
2546
2676
|
break;
|
|
2547
|
-
|
|
2548
|
-
case "
|
|
2549
|
-
|
|
2550
|
-
|
|
2677
|
+
}
|
|
2678
|
+
case "npm": {
|
|
2679
|
+
console.error('[lbe] Note: Use "lbe-exec run-node" for reliable hook preload.');
|
|
2680
|
+
console.error("[lbe] NODE_OPTIONS --require may not fire for all npm lifecycle hooks.\n");
|
|
2681
|
+
const hookPath = findHookPath();
|
|
2682
|
+
if (!fs14.existsSync(hookPath)) {
|
|
2683
|
+
console.error("Hook not found: " + hookPath);
|
|
2551
2684
|
process.exit(1);
|
|
2685
|
+
}
|
|
2686
|
+
const existing = process.env.NODE_OPTIONS || "";
|
|
2687
|
+
const hookFlag = "--require " + hookPath;
|
|
2688
|
+
const nodeOptions = existing.includes(hookFlag) ? existing : (existing + " " + hookFlag).trim();
|
|
2689
|
+
const npmArgs = rest;
|
|
2690
|
+
const isWindows = process.platform === "win32";
|
|
2691
|
+
const child = spawn(isWindows ? "npm.cmd" : "npm", npmArgs, {
|
|
2692
|
+
stdio: "inherit",
|
|
2693
|
+
shell: false,
|
|
2694
|
+
env: { ...process.env, NODE_OPTIONS: nodeOptions, LBE_MODE: opts.mode || "observe", LBE_ROOT: process.cwd() }
|
|
2552
2695
|
});
|
|
2696
|
+
child.on("close", (code) => process.exit(code ?? 0));
|
|
2553
2697
|
break;
|
|
2698
|
+
}
|
|
2554
2699
|
case "status": {
|
|
2555
2700
|
const policy = loadPolicy();
|
|
2556
|
-
|
|
2557
|
-
|
|
2701
|
+
console.log("\u2500\u2500 LBE Status \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2702
|
+
console.log("workspace: " + process.cwd());
|
|
2703
|
+
if (policy) {
|
|
2704
|
+
console.log("mode: " + policy.mode);
|
|
2705
|
+
console.log("rules: " + (policy.rules?.length ?? 0));
|
|
2706
|
+
console.log("audit: " + countAudit() + " entries (.lbe/audit.jsonl)");
|
|
2707
|
+
} else {
|
|
2708
|
+
console.log("policy: not found \u2014 run: npx lbe-exec init");
|
|
2709
|
+
}
|
|
2710
|
+
const statusFile = path15.join(process.cwd(), ".lbe", "runtime", "hook-status.json");
|
|
2711
|
+
if (!fs14.existsSync(statusFile)) {
|
|
2712
|
+
console.log("\nhook: inactive \u2014 use: npx lbe-exec run-node ./agent.js");
|
|
2713
|
+
break;
|
|
2714
|
+
}
|
|
2715
|
+
let hookStatus;
|
|
2716
|
+
try {
|
|
2717
|
+
hookStatus = JSON.parse(fs14.readFileSync(statusFile, "utf8"));
|
|
2718
|
+
} catch (e) {
|
|
2719
|
+
console.log("\nhook: status file unreadable \u2014 " + e.message);
|
|
2720
|
+
break;
|
|
2721
|
+
}
|
|
2722
|
+
let pidAlive = false;
|
|
2723
|
+
try {
|
|
2724
|
+
process.kill(hookStatus.pid, 0);
|
|
2725
|
+
pidAlive = true;
|
|
2726
|
+
} catch (_) {
|
|
2727
|
+
}
|
|
2728
|
+
console.log("\nhook: " + (pidAlive ? "ACTIVE" : "stale (process exited)"));
|
|
2729
|
+
console.log("hook pid: " + hookStatus.pid + (pidAlive ? " (alive)" : " (gone)"));
|
|
2730
|
+
console.log("hook mode: " + hookStatus.mode);
|
|
2731
|
+
console.log("hook root: " + hookStatus.root);
|
|
2732
|
+
console.log("hook start: " + hookStatus.started_at);
|
|
2733
|
+
if (hookStatus.patched) {
|
|
2734
|
+
console.log("\nPatched functions:");
|
|
2735
|
+
for (const [fn, active] of Object.entries(hookStatus.patched)) {
|
|
2736
|
+
console.log(" " + (active ? "\u2713" : "\u2013") + " " + fn);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
console.log("\nevents log: .lbe/events.jsonl");
|
|
2740
|
+
break;
|
|
2741
|
+
}
|
|
2742
|
+
case "audit": {
|
|
2743
|
+
const eventsPath = path15.join(process.cwd(), ".lbe", "events.jsonl");
|
|
2744
|
+
if (!fs14.existsSync(eventsPath)) {
|
|
2745
|
+
console.log("No events log found. Run an agent with: npx lbe-exec run-node ./agent.js");
|
|
2746
|
+
break;
|
|
2747
|
+
}
|
|
2748
|
+
const lines = fs14.readFileSync(eventsPath, "utf8").split("\n").filter((l) => l.trim());
|
|
2749
|
+
if (!lines.length) {
|
|
2750
|
+
console.log("No events recorded yet.");
|
|
2558
2751
|
break;
|
|
2559
2752
|
}
|
|
2560
|
-
console.log(
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2753
|
+
console.log("\u2500\u2500 LBE Event Log (" + lines.length + " entries) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2754
|
+
for (const line of lines) {
|
|
2755
|
+
try {
|
|
2756
|
+
const e = JSON.parse(line);
|
|
2757
|
+
const ts = new Date(e.ts * 1e3).toISOString().replace("T", " ").slice(0, 19);
|
|
2758
|
+
const target = e.path || e.cmd || "?";
|
|
2759
|
+
const status = e.enforced && e.decision === "deny" ? "BLOCKED" : e.decision === "deny" ? "WOULD-BLOCK" : "allowed";
|
|
2760
|
+
console.log(`${ts} [${e.mode}] ${e.action} ${target} \u2192 ${status}`);
|
|
2761
|
+
} catch (_) {
|
|
2762
|
+
}
|
|
2763
|
+
}
|
|
2564
2764
|
break;
|
|
2565
2765
|
}
|
|
2766
|
+
case "init":
|
|
2767
|
+
initCommand(opts).then(() => {
|
|
2768
|
+
const added = injectScripts(opts.wrap || null);
|
|
2769
|
+
if (added.length) {
|
|
2770
|
+
console.log("\n\u2713 Added LBE script variants to package.json");
|
|
2771
|
+
console.log(" Run your agent through LBE: npm run <name>:lbe");
|
|
2772
|
+
} else {
|
|
2773
|
+
console.log("\nNo node agent scripts detected in package.json.");
|
|
2774
|
+
console.log("Use: npx lbe-exec run-node [--mode observe|enforce] ./your-agent.js");
|
|
2775
|
+
}
|
|
2776
|
+
}).catch((e) => {
|
|
2777
|
+
console.error(e.message);
|
|
2778
|
+
process.exit(1);
|
|
2779
|
+
});
|
|
2780
|
+
break;
|
|
2781
|
+
case "observe":
|
|
2782
|
+
case "enforce":
|
|
2783
|
+
policyModeCommand(cmd, opts).catch((e) => {
|
|
2784
|
+
console.error(e.message);
|
|
2785
|
+
process.exit(1);
|
|
2786
|
+
});
|
|
2787
|
+
break;
|
|
2566
2788
|
case "policy": {
|
|
2567
2789
|
const policy = loadPolicy();
|
|
2568
2790
|
if (!policy) {
|
|
2569
|
-
console.log("No lbe.policy.json found. Run: npx lbe init");
|
|
2791
|
+
console.log("No lbe.policy.json found. Run: npx lbe-exec init");
|
|
2570
2792
|
break;
|
|
2571
2793
|
}
|
|
2572
2794
|
if (!policy.rules?.length) {
|
|
@@ -2598,17 +2820,21 @@ switch (cmd) {
|
|
|
2598
2820
|
break;
|
|
2599
2821
|
}
|
|
2600
2822
|
default:
|
|
2601
|
-
console.log("Usage: lbe <command>\n");
|
|
2602
|
-
console.log(" init
|
|
2603
|
-
console.log("
|
|
2604
|
-
console.log("
|
|
2605
|
-
console.log("
|
|
2606
|
-
console.log("
|
|
2607
|
-
console.log("
|
|
2823
|
+
console.log("Usage: lbe-exec <command>\n");
|
|
2824
|
+
console.log(" init Bootstrap governance \u2014 policy, keys, agent files");
|
|
2825
|
+
console.log(" run-node Run a Node.js agent under LBE governance");
|
|
2826
|
+
console.log(" [--mode observe|enforce] <script> [...args]");
|
|
2827
|
+
console.log(" npm Wrap npm command with LBE hook (via NODE_OPTIONS)");
|
|
2828
|
+
console.log(" [...npm-args]");
|
|
2829
|
+
console.log(" status Show workspace, mode, hook state, patched functions");
|
|
2830
|
+
console.log(" audit Show unified event log (.lbe/events.jsonl)");
|
|
2831
|
+
console.log(" policy List active policy rules");
|
|
2832
|
+
console.log(" observe Switch to observer mode (log only, nothing blocked)");
|
|
2833
|
+
console.log(" enforce Switch to enforcement mode (violations blocked)");
|
|
2834
|
+
console.log(" execute Send a JSON request from stdin or --input file");
|
|
2608
2835
|
console.log("\nCLI: npx lbe-exec <command>");
|
|
2609
2836
|
if (cmd && cmd !== "--help" && cmd !== "help") {
|
|
2610
|
-
console.error(
|
|
2611
|
-
Unknown command: ${cmd}`);
|
|
2837
|
+
console.error("\nUnknown command: " + cmd);
|
|
2612
2838
|
process.exit(1);
|
|
2613
2839
|
}
|
|
2614
2840
|
}
|
package/dist/index.js
CHANGED
|
@@ -1705,6 +1705,43 @@ function createLocalExecutor(options = {}) {
|
|
|
1705
1705
|
if (!validation.valid) return { error: error(validation.errors[0]?.type || "VALIDATION_FAILED", validation.errors[0]?.message || "Validation failed"), local, localDecision, normalized, proposal, policy, validation };
|
|
1706
1706
|
return { local, localDecision, normalized, proposal, policy, validation };
|
|
1707
1707
|
}
|
|
1708
|
+
function evaluateSync(action) {
|
|
1709
|
+
const local = loadLocalPolicy(rootDir, options.mode || "observe");
|
|
1710
|
+
const mode = local.policy.mode;
|
|
1711
|
+
let target = null;
|
|
1712
|
+
let command = null;
|
|
1713
|
+
if (action.path) {
|
|
1714
|
+
try {
|
|
1715
|
+
target = path10.resolve(rootDir, action.path);
|
|
1716
|
+
if (!underRoot(target, rootDir)) {
|
|
1717
|
+
return { decision: "deny", deny: true, matchedRules: ["path:outside_root"], mode, enforced: mode === "enforce", reason: "PATH_OUTSIDE_ROOT" };
|
|
1718
|
+
}
|
|
1719
|
+
} catch (e) {
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
if (action.cmd) command = action.cmd;
|
|
1723
|
+
const localDecision = evaluateLocalPolicy(local.policy, rootDir, { target, command });
|
|
1724
|
+
const isDeny = !localDecision.allowed;
|
|
1725
|
+
return {
|
|
1726
|
+
decision: isDeny ? "deny" : "allow",
|
|
1727
|
+
deny: isDeny,
|
|
1728
|
+
matchedRules: localDecision.winningRules.map((r) => r.id),
|
|
1729
|
+
mode,
|
|
1730
|
+
enforced: mode === "enforce"
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
function auditSync(entry) {
|
|
1734
|
+
const eventsPath = path10.join(rootDir, ".lbe", "events.jsonl");
|
|
1735
|
+
const dir = path10.dirname(eventsPath);
|
|
1736
|
+
if (!fs9.existsSync(dir)) fs9.mkdirSync(dir, { recursive: true });
|
|
1737
|
+
const line = JSON.stringify({ ts: Math.floor(Date.now() / 1e3), ...entry }) + "\n";
|
|
1738
|
+
const fd = fs9.openSync(eventsPath, "a");
|
|
1739
|
+
try {
|
|
1740
|
+
fs9.writeSync(fd, line);
|
|
1741
|
+
} finally {
|
|
1742
|
+
fs9.closeSync(fd);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1708
1745
|
async function dryRun(request) {
|
|
1709
1746
|
const prepared = prepare(request);
|
|
1710
1747
|
if (prepared.error) return { ...prepared.error, dryRun: true };
|
|
@@ -1788,7 +1825,9 @@ function createLocalExecutor(options = {}) {
|
|
|
1788
1825
|
proposeRule: proposePolicyRule,
|
|
1789
1826
|
addRule: (rule) => addLocalPolicyRule(rootDir, rule, options.mode || "enforce")
|
|
1790
1827
|
},
|
|
1791
|
-
audit: { verify: () => verifyAuditLogIntegrity(path10.join(rootDir, ".lbe/audit.jsonl")) }
|
|
1828
|
+
audit: { verify: () => verifyAuditLogIntegrity(path10.join(rootDir, ".lbe/audit.jsonl")) },
|
|
1829
|
+
evaluateSync,
|
|
1830
|
+
auditSync
|
|
1792
1831
|
};
|
|
1793
1832
|
}
|
|
1794
1833
|
export {
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// LBE Agent Bridge — CJS preload hook
|
|
3
|
+
// Load with: node --require @letterblack/lbe-exec/hooks/register.cjs agent.js
|
|
4
|
+
// Or via: npx lbe-exec run-node [--mode observe|enforce] agent.js
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { EventEmitter } = require('events');
|
|
9
|
+
const { Readable } = require('stream');
|
|
10
|
+
|
|
11
|
+
const ROOT_DIR = process.env.LBE_ROOT || process.cwd();
|
|
12
|
+
const MODE = process.env.LBE_MODE || 'observe';
|
|
13
|
+
|
|
14
|
+
// ── Policy loader (inline CJS — ESM cannot be require()'d synchronously) ────
|
|
15
|
+
|
|
16
|
+
function loadPolicy() {
|
|
17
|
+
const policyPath = path.join(ROOT_DIR, 'lbe.policy.json');
|
|
18
|
+
try {
|
|
19
|
+
if (fs.existsSync(policyPath)) {
|
|
20
|
+
return JSON.parse(fs.readFileSync(policyPath, 'utf8'));
|
|
21
|
+
}
|
|
22
|
+
} catch (e) { /* fall through to default */ }
|
|
23
|
+
return { version: 1, mode: MODE, workspace: ROOT_DIR, rules: [] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function globToRegex(pattern) {
|
|
27
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
28
|
+
return new RegExp('^' + escaped
|
|
29
|
+
.replace(/\*\*\//g, '(?:.*/)?')
|
|
30
|
+
.replace(/\*\*/g, '.*')
|
|
31
|
+
.replace(/\*/g, '[^/]*') + '$');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function evaluatePolicy(action) {
|
|
35
|
+
const policy = loadPolicy();
|
|
36
|
+
const mode = policy.mode || MODE;
|
|
37
|
+
const rules = Array.isArray(policy.rules) ? policy.rules : [];
|
|
38
|
+
|
|
39
|
+
if (action.path) {
|
|
40
|
+
try {
|
|
41
|
+
const abs = path.resolve(ROOT_DIR, action.path);
|
|
42
|
+
const sep = path.sep;
|
|
43
|
+
if (!abs.startsWith(ROOT_DIR + sep) && abs !== ROOT_DIR) {
|
|
44
|
+
return { decision: 'deny', deny: true, reason: 'PATH_OUTSIDE_ROOT', matchedRules: ['path:outside_root'], mode, enforced: mode === 'enforce' };
|
|
45
|
+
}
|
|
46
|
+
const rel = path.relative(ROOT_DIR, abs).split(sep).join('/');
|
|
47
|
+
const matched = rules.filter(r => r.type === 'path' && globToRegex(r.pattern).test(rel));
|
|
48
|
+
const denied = matched.filter(r => r.effect === 'deny');
|
|
49
|
+
const isDeny = denied.length > 0;
|
|
50
|
+
return {
|
|
51
|
+
decision: isDeny ? 'deny' : 'allow', deny: isDeny,
|
|
52
|
+
matchedRules: (isDeny ? denied : matched.filter(r => r.effect === 'allow')).map(r => r.id),
|
|
53
|
+
mode, enforced: mode === 'enforce',
|
|
54
|
+
};
|
|
55
|
+
} catch (e) {
|
|
56
|
+
if (mode === 'enforce') {
|
|
57
|
+
return { decision: 'deny', deny: true, reason: 'PATH_RESOLUTION_ERROR', matchedRules: [], mode, enforced: true };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (action.cmd) {
|
|
63
|
+
const matched = rules.filter(r => r.type === 'command' && globToRegex(r.pattern).test(String(action.cmd)));
|
|
64
|
+
const denied = matched.filter(r => r.effect === 'deny');
|
|
65
|
+
const isDeny = denied.length > 0;
|
|
66
|
+
return {
|
|
67
|
+
decision: isDeny ? 'deny' : 'allow', deny: isDeny,
|
|
68
|
+
matchedRules: (isDeny ? denied : matched.filter(r => r.effect === 'allow')).map(r => r.id),
|
|
69
|
+
mode, enforced: mode === 'enforce',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return { decision: 'allow', deny: false, matchedRules: [], mode, enforced: mode === 'enforce' };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ── Audit writer ──────────────────────────────────────────────────────────────
|
|
77
|
+
// fs.appendFileSync → fs.writeFileSync (Node.js internal) → would recurse.
|
|
78
|
+
// fs.openSync/writeSync/closeSync are low-level bindings — never call back into JS.
|
|
79
|
+
// Re-entrant guard prevents recursion from any code path we missed.
|
|
80
|
+
|
|
81
|
+
var _auditInFlight = false;
|
|
82
|
+
|
|
83
|
+
function auditEvent(entry) {
|
|
84
|
+
if (_auditInFlight) return;
|
|
85
|
+
_auditInFlight = true;
|
|
86
|
+
try {
|
|
87
|
+
var eventsPath = path.join(ROOT_DIR, '.lbe', 'events.jsonl');
|
|
88
|
+
var dir = path.dirname(eventsPath);
|
|
89
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
90
|
+
var line = JSON.stringify({ ts: Math.floor(Date.now() / 1000), ...entry }) + '\n';
|
|
91
|
+
// Use open/write/close directly — bypasses all JS wrappers including writeFileSync
|
|
92
|
+
var fd = fs.openSync(eventsPath, 'a');
|
|
93
|
+
try { fs.writeSync(fd, line); } finally { fs.closeSync(fd); }
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.warn('[lbe] audit write failed:', e.message);
|
|
96
|
+
} finally {
|
|
97
|
+
_auditInFlight = false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
class LBEPermissionError extends Error {
|
|
102
|
+
constructor(decision, action) {
|
|
103
|
+
const target = action.path || action.cmd || 'unknown';
|
|
104
|
+
super('[LBE:' + decision.mode + '] DENIED ' + action.action + ' on ' + target);
|
|
105
|
+
this.name = 'LBEPermissionError';
|
|
106
|
+
this.code = 'LBE_PERMISSION_DENIED';
|
|
107
|
+
this.lbeDecision = decision;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ── Originals — captured BEFORE any patch is applied ────────────────────────
|
|
112
|
+
|
|
113
|
+
const origFs = {
|
|
114
|
+
writeFile: fs.writeFile.bind(fs),
|
|
115
|
+
writeFileSync: fs.writeFileSync.bind(fs),
|
|
116
|
+
rm: fs.rm ? fs.rm.bind(fs) : null,
|
|
117
|
+
rmSync: fs.rmSync ? fs.rmSync.bind(fs) : null,
|
|
118
|
+
unlink: fs.unlink.bind(fs),
|
|
119
|
+
unlinkSync: fs.unlinkSync.bind(fs),
|
|
120
|
+
rename: fs.rename.bind(fs),
|
|
121
|
+
renameSync: fs.renameSync.bind(fs),
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const origPromises = {
|
|
125
|
+
writeFile: fs.promises.writeFile.bind(fs.promises),
|
|
126
|
+
rm: fs.promises.rm ? fs.promises.rm.bind(fs.promises) : null,
|
|
127
|
+
unlink: fs.promises.unlink.bind(fs.promises),
|
|
128
|
+
rename: fs.promises.rename.bind(fs.promises),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const cp = require('child_process');
|
|
132
|
+
const origCp = {
|
|
133
|
+
spawn: cp.spawn.bind(cp),
|
|
134
|
+
spawnSync: cp.spawnSync.bind(cp),
|
|
135
|
+
exec: cp.exec.bind(cp),
|
|
136
|
+
execSync: cp.execSync.bind(cp),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
// ── Denied spawn stub — EventEmitter-compatible ───────────────────────────────
|
|
140
|
+
|
|
141
|
+
function makeFailedChildProcess(err) {
|
|
142
|
+
const emitter = new EventEmitter();
|
|
143
|
+
emitter.pid = null;
|
|
144
|
+
emitter.killed = false;
|
|
145
|
+
emitter.exitCode = 1;
|
|
146
|
+
emitter.signalCode = null;
|
|
147
|
+
emitter.stdout = new Readable({ read() {} });
|
|
148
|
+
emitter.stderr = new Readable({ read() {} });
|
|
149
|
+
emitter.stdin = { write() { return false; }, end() {}, destroy() {} };
|
|
150
|
+
emitter.kill = () => false;
|
|
151
|
+
emitter.ref = () => emitter;
|
|
152
|
+
emitter.unref = () => emitter;
|
|
153
|
+
process.nextTick(function () {
|
|
154
|
+
emitter.stdout.push(null);
|
|
155
|
+
emitter.stderr.push(null);
|
|
156
|
+
emitter.emit('error', err);
|
|
157
|
+
emitter.emit('close', 1, null);
|
|
158
|
+
emitter.emit('exit', 1, null);
|
|
159
|
+
});
|
|
160
|
+
return emitter;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Decision + pre-block audit ────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
function decide(action) {
|
|
166
|
+
var decision;
|
|
167
|
+
try {
|
|
168
|
+
decision = evaluatePolicy(action);
|
|
169
|
+
} catch (e) {
|
|
170
|
+
if (MODE === 'enforce') {
|
|
171
|
+
var failErr = new LBEPermissionError({ mode: 'enforce', enforced: true }, action);
|
|
172
|
+
try { auditEvent({ action: action.action, path: action.path, cmd: action.cmd, actor: 'agent:lbe-hooks', decision: 'deny', mode: 'enforce', enforced: true, executed: false, matched_rules: [], error: e.message }); } catch (_) {}
|
|
173
|
+
return { blocked: true, error: failErr };
|
|
174
|
+
}
|
|
175
|
+
console.warn('[lbe] policy evaluation failed (observe mode, allowing):', e.message);
|
|
176
|
+
return { blocked: false, decision: { decision: 'allow', deny: false, matchedRules: [], mode: 'observe', enforced: false } };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (decision.deny && decision.enforced) {
|
|
180
|
+
var err = new LBEPermissionError(decision, action);
|
|
181
|
+
try { auditEvent({ action: action.action, path: action.path, cmd: action.cmd, actor: 'agent:lbe-hooks', decision: 'deny', mode: decision.mode, enforced: true, executed: false, matched_rules: decision.matchedRules }); } catch (_) {}
|
|
182
|
+
return { blocked: true, error: err };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return { blocked: false, decision: decision };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function postAudit(action, decision, executed, extra) {
|
|
189
|
+
try {
|
|
190
|
+
auditEvent(Object.assign({ action: action.action, path: action.path, cmd: action.cmd, actor: 'agent:lbe-hooks', decision: decision.decision, mode: decision.mode, enforced: decision.enforced, executed: executed, matched_rules: decision.matchedRules }, extra || {}));
|
|
191
|
+
} catch (e) {
|
|
192
|
+
console.warn('[lbe] post-action audit failed (result unaffected):', e.message);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ── Patch fs callbacks ────────────────────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
fs.writeFile = function lbeWriteFile(filePath, data, options, callback) {
|
|
199
|
+
if (typeof options === 'function') { callback = options; options = undefined; }
|
|
200
|
+
var action = { action: 'file_write', path: String(filePath) };
|
|
201
|
+
var check = decide(action);
|
|
202
|
+
if (check.blocked) { process.nextTick(function () { callback(check.error); }); return; }
|
|
203
|
+
function done(err) {
|
|
204
|
+
postAudit(action, check.decision, !err, err ? { error: err.message } : null);
|
|
205
|
+
callback(err);
|
|
206
|
+
}
|
|
207
|
+
if (options !== undefined) { origFs.writeFile(filePath, data, options, done); }
|
|
208
|
+
else { origFs.writeFile(filePath, data, done); }
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
fs.writeFileSync = function lbeWriteFileSync(filePath, data, options) {
|
|
212
|
+
var action = { action: 'file_write', path: String(filePath) };
|
|
213
|
+
var check = decide(action);
|
|
214
|
+
if (check.blocked) { throw check.error; }
|
|
215
|
+
try {
|
|
216
|
+
var result = options !== undefined ? origFs.writeFileSync(filePath, data, options) : origFs.writeFileSync(filePath, data);
|
|
217
|
+
postAudit(action, check.decision, true);
|
|
218
|
+
return result;
|
|
219
|
+
} catch (e) {
|
|
220
|
+
postAudit(action, check.decision, false, { error: e.message });
|
|
221
|
+
throw e;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (origFs.rm) {
|
|
226
|
+
fs.rm = function lbeRm(filePath, options, callback) {
|
|
227
|
+
if (typeof options === 'function') { callback = options; options = undefined; }
|
|
228
|
+
var action = { action: 'file_delete', path: String(filePath) };
|
|
229
|
+
var check = decide(action);
|
|
230
|
+
if (check.blocked) { process.nextTick(function () { callback(check.error); }); return; }
|
|
231
|
+
function done(err) {
|
|
232
|
+
postAudit(action, check.decision, !err, err ? { error: err.message } : null);
|
|
233
|
+
callback(err);
|
|
234
|
+
}
|
|
235
|
+
if (options !== undefined) { origFs.rm(filePath, options, done); }
|
|
236
|
+
else { origFs.rm(filePath, done); }
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (origFs.rmSync) {
|
|
241
|
+
fs.rmSync = function lbeRmSync(filePath, options) {
|
|
242
|
+
var action = { action: 'file_delete', path: String(filePath) };
|
|
243
|
+
var check = decide(action);
|
|
244
|
+
if (check.blocked) { throw check.error; }
|
|
245
|
+
try {
|
|
246
|
+
var result = options !== undefined ? origFs.rmSync(filePath, options) : origFs.rmSync(filePath);
|
|
247
|
+
postAudit(action, check.decision, true);
|
|
248
|
+
return result;
|
|
249
|
+
} catch (e) {
|
|
250
|
+
postAudit(action, check.decision, false, { error: e.message });
|
|
251
|
+
throw e;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fs.unlink = function lbeUnlink(filePath, options, callback) {
|
|
257
|
+
if (typeof options === 'function') { callback = options; options = undefined; }
|
|
258
|
+
var action = { action: 'file_delete', path: String(filePath) };
|
|
259
|
+
var check = decide(action);
|
|
260
|
+
if (check.blocked) { process.nextTick(function () { callback(check.error); }); return; }
|
|
261
|
+
origFs.unlink(filePath, function done(err) {
|
|
262
|
+
postAudit(action, check.decision, !err, err ? { error: err.message } : null);
|
|
263
|
+
callback(err);
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
fs.unlinkSync = function lbeUnlinkSync(filePath) {
|
|
268
|
+
var action = { action: 'file_delete', path: String(filePath) };
|
|
269
|
+
var check = decide(action);
|
|
270
|
+
if (check.blocked) { throw check.error; }
|
|
271
|
+
try {
|
|
272
|
+
var result = origFs.unlinkSync(filePath);
|
|
273
|
+
postAudit(action, check.decision, true);
|
|
274
|
+
return result;
|
|
275
|
+
} catch (e) {
|
|
276
|
+
postAudit(action, check.decision, false, { error: e.message });
|
|
277
|
+
throw e;
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
fs.rename = function lbeRename(oldPath, newPath, options, callback) {
|
|
282
|
+
if (typeof options === 'function') { callback = options; options = undefined; }
|
|
283
|
+
var action = { action: 'file_rename', path: String(oldPath), dest: String(newPath) };
|
|
284
|
+
var check = decide(action);
|
|
285
|
+
if (check.blocked) { process.nextTick(function () { callback(check.error); }); return; }
|
|
286
|
+
origFs.rename(oldPath, newPath, function done(err) {
|
|
287
|
+
postAudit(action, check.decision, !err, err ? { error: err.message } : null);
|
|
288
|
+
callback(err);
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
fs.renameSync = function lbeRenameSync(oldPath, newPath) {
|
|
293
|
+
var action = { action: 'file_rename', path: String(oldPath), dest: String(newPath) };
|
|
294
|
+
var check = decide(action);
|
|
295
|
+
if (check.blocked) { throw check.error; }
|
|
296
|
+
try {
|
|
297
|
+
var result = origFs.renameSync(oldPath, newPath);
|
|
298
|
+
postAudit(action, check.decision, true);
|
|
299
|
+
return result;
|
|
300
|
+
} catch (e) {
|
|
301
|
+
postAudit(action, check.decision, false, { error: e.message });
|
|
302
|
+
throw e;
|
|
303
|
+
}
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// ── Patch fs.promises ─────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
fs.promises.writeFile = async function lbePromisesWriteFile(filePath, data, options) {
|
|
309
|
+
var action = { action: 'file_write', path: String(filePath) };
|
|
310
|
+
var check = decide(action);
|
|
311
|
+
if (check.blocked) { throw check.error; }
|
|
312
|
+
try {
|
|
313
|
+
var result = options !== undefined
|
|
314
|
+
? await origPromises.writeFile(filePath, data, options)
|
|
315
|
+
: await origPromises.writeFile(filePath, data);
|
|
316
|
+
postAudit(action, check.decision, true);
|
|
317
|
+
return result;
|
|
318
|
+
} catch (e) {
|
|
319
|
+
postAudit(action, check.decision, false, { error: e.message });
|
|
320
|
+
throw e;
|
|
321
|
+
}
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
if (origPromises.rm) {
|
|
325
|
+
fs.promises.rm = async function lbePromisesRm(filePath, options) {
|
|
326
|
+
var action = { action: 'file_delete', path: String(filePath) };
|
|
327
|
+
var check = decide(action);
|
|
328
|
+
if (check.blocked) { throw check.error; }
|
|
329
|
+
try {
|
|
330
|
+
var result = options !== undefined
|
|
331
|
+
? await origPromises.rm(filePath, options)
|
|
332
|
+
: await origPromises.rm(filePath);
|
|
333
|
+
postAudit(action, check.decision, true);
|
|
334
|
+
return result;
|
|
335
|
+
} catch (e) {
|
|
336
|
+
postAudit(action, check.decision, false, { error: e.message });
|
|
337
|
+
throw e;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
fs.promises.unlink = async function lbePromisesUnlink(filePath) {
|
|
343
|
+
var action = { action: 'file_delete', path: String(filePath) };
|
|
344
|
+
var check = decide(action);
|
|
345
|
+
if (check.blocked) { throw check.error; }
|
|
346
|
+
try {
|
|
347
|
+
var result = await origPromises.unlink(filePath);
|
|
348
|
+
postAudit(action, check.decision, true);
|
|
349
|
+
return result;
|
|
350
|
+
} catch (e) {
|
|
351
|
+
postAudit(action, check.decision, false, { error: e.message });
|
|
352
|
+
throw e;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
fs.promises.rename = async function lbePromisesRename(oldPath, newPath) {
|
|
357
|
+
var action = { action: 'file_rename', path: String(oldPath), dest: String(newPath) };
|
|
358
|
+
var check = decide(action);
|
|
359
|
+
if (check.blocked) { throw check.error; }
|
|
360
|
+
try {
|
|
361
|
+
var result = await origPromises.rename(oldPath, newPath);
|
|
362
|
+
postAudit(action, check.decision, true);
|
|
363
|
+
return result;
|
|
364
|
+
} catch (e) {
|
|
365
|
+
postAudit(action, check.decision, false, { error: e.message });
|
|
366
|
+
throw e;
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
// ── Patch child_process ───────────────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
cp.spawn = function lbeSpawn(cmd, args, options) {
|
|
373
|
+
if (args && !Array.isArray(args)) { options = args; args = []; }
|
|
374
|
+
var cwd = options && options.cwd ? String(options.cwd) : ROOT_DIR;
|
|
375
|
+
var shell = !!(options && options.shell);
|
|
376
|
+
var action = { action: 'run_shell', cmd: String(cmd), args: args || [], cwd: cwd, shell: shell };
|
|
377
|
+
var check = decide(action);
|
|
378
|
+
if (check.blocked) { return makeFailedChildProcess(check.error); }
|
|
379
|
+
var child = origCp.spawn(cmd, args || [], options || {});
|
|
380
|
+
child.on('close', function (code) { postAudit(action, check.decision, code === 0, { exit_code: code }); });
|
|
381
|
+
return child;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
cp.spawnSync = function lbeSpawnSync(cmd, args, options) {
|
|
385
|
+
if (args && !Array.isArray(args)) { options = args; args = []; }
|
|
386
|
+
var cwd = options && options.cwd ? String(options.cwd) : ROOT_DIR;
|
|
387
|
+
var shell = !!(options && options.shell);
|
|
388
|
+
var action = { action: 'run_shell', cmd: String(cmd), args: args || [], cwd: cwd, shell: shell };
|
|
389
|
+
var check = decide(action);
|
|
390
|
+
if (check.blocked) {
|
|
391
|
+
return { pid: 0, output: [null, Buffer.alloc(0), Buffer.alloc(0)], stdout: Buffer.alloc(0), stderr: Buffer.alloc(0), status: 1, signal: null, error: check.error };
|
|
392
|
+
}
|
|
393
|
+
var result = origCp.spawnSync(cmd, args || [], options || {});
|
|
394
|
+
postAudit(action, check.decision, result.status === 0, { exit_code: result.status });
|
|
395
|
+
return result;
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
cp.exec = function lbeExec(command, options, callback) {
|
|
399
|
+
if (typeof options === 'function') { callback = options; options = undefined; }
|
|
400
|
+
var cwd = options && options.cwd ? String(options.cwd) : ROOT_DIR;
|
|
401
|
+
var action = { action: 'run_shell', cmd: String(command), args: [], cwd: cwd, shell: true };
|
|
402
|
+
var check = decide(action);
|
|
403
|
+
if (check.blocked) {
|
|
404
|
+
process.nextTick(function () { if (callback) callback(check.error, '', ''); });
|
|
405
|
+
return makeFailedChildProcess(check.error);
|
|
406
|
+
}
|
|
407
|
+
var cb = function done(err, stdout, stderr) {
|
|
408
|
+
postAudit(action, check.decision, !err, err ? { error: err.message } : null);
|
|
409
|
+
if (callback) callback(err, stdout, stderr);
|
|
410
|
+
};
|
|
411
|
+
return options !== undefined ? origCp.exec(command, options, cb) : origCp.exec(command, cb);
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
cp.execSync = function lbeExecSync(command, options) {
|
|
415
|
+
var cwd = options && options.cwd ? String(options.cwd) : ROOT_DIR;
|
|
416
|
+
var action = { action: 'run_shell', cmd: String(command), args: [], cwd: cwd, shell: true };
|
|
417
|
+
var check = decide(action);
|
|
418
|
+
if (check.blocked) { throw check.error; }
|
|
419
|
+
try {
|
|
420
|
+
var result = options !== undefined ? origCp.execSync(command, options) : origCp.execSync(command);
|
|
421
|
+
postAudit(action, check.decision, true);
|
|
422
|
+
return result;
|
|
423
|
+
} catch (e) {
|
|
424
|
+
postAudit(action, check.decision, false, { error: e.message });
|
|
425
|
+
throw e;
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
// ── Write hook-status.json (uses origFs — captured before patching) ───────────
|
|
430
|
+
|
|
431
|
+
process.env.LBE_HOOK_ACTIVE = '1';
|
|
432
|
+
|
|
433
|
+
try {
|
|
434
|
+
var statusDir = path.join(ROOT_DIR, '.lbe', 'runtime');
|
|
435
|
+
if (!fs.existsSync(statusDir)) fs.mkdirSync(statusDir, { recursive: true });
|
|
436
|
+
var status = {
|
|
437
|
+
active: true,
|
|
438
|
+
pid: process.pid,
|
|
439
|
+
started_at: new Date().toISOString(),
|
|
440
|
+
mode: MODE,
|
|
441
|
+
root: ROOT_DIR,
|
|
442
|
+
patched: {
|
|
443
|
+
'fs.writeFile': true,
|
|
444
|
+
'fs.writeFileSync': true,
|
|
445
|
+
'fs.rm': !!origFs.rm,
|
|
446
|
+
'fs.rmSync': !!origFs.rmSync,
|
|
447
|
+
'fs.unlink': true,
|
|
448
|
+
'fs.unlinkSync': true,
|
|
449
|
+
'fs.rename': true,
|
|
450
|
+
'fs.renameSync': true,
|
|
451
|
+
'fs.promises.writeFile': true,
|
|
452
|
+
'fs.promises.rm': !!origPromises.rm,
|
|
453
|
+
'fs.promises.unlink': true,
|
|
454
|
+
'fs.promises.rename': true,
|
|
455
|
+
'child_process.spawn': true,
|
|
456
|
+
'child_process.spawnSync': true,
|
|
457
|
+
'child_process.exec': true,
|
|
458
|
+
'child_process.execSync': true,
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
// Use original writeFileSync (pre-patch) to avoid triggering our own hook
|
|
462
|
+
origFs.writeFileSync(path.join(statusDir, 'hook-status.json'), JSON.stringify(status, null, 2) + '\n', 'utf8');
|
|
463
|
+
} catch (e) {
|
|
464
|
+
console.warn('[lbe] could not write hook-status.json:', e.message);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ── Banner ────────────────────────────────────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
if (MODE === 'observe') {
|
|
470
|
+
process.stderr.write('[lbe] OBSERVE mode — no actions blocked, all writes logged to .lbe/events.jsonl\n');
|
|
471
|
+
} else if (MODE === 'enforce') {
|
|
472
|
+
process.stderr.write('[lbe] ENFORCE mode — policy denials will block execution\n');
|
|
473
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@letterblack/lbe-exec",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.13",
|
|
4
4
|
"description": "Local host-signed execution layer for LetterBlack LBE.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"types": "./types.d.ts",
|
|
11
11
|
"default": "./dist/index.js"
|
|
12
|
-
}
|
|
12
|
+
},
|
|
13
|
+
"./hooks/register.cjs": "./hooks/register.cjs"
|
|
13
14
|
},
|
|
14
15
|
"bin": {
|
|
15
16
|
"lbe-exec": "dist/cli.js"
|
|
16
17
|
},
|
|
17
18
|
"files": [
|
|
18
19
|
"dist/",
|
|
20
|
+
"hooks/",
|
|
19
21
|
"assets/",
|
|
20
22
|
"README.md",
|
|
21
23
|
"types.d.ts",
|