@bike4mind/cli 0.2.29-feat-rate-limit-tracking.18919 → 0.2.29-feat-cli-sandbox.18924

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.
Files changed (27) hide show
  1. package/dist/BubblewrapRuntime-HSAAHKZN.js +71 -0
  2. package/dist/ProxyManager-4ZZEYTD6.js +249 -0
  3. package/dist/SandboxOrchestrator-MYBNKKWL.js +162 -0
  4. package/dist/SandboxRuntimeAdapter-JERGOSEV.js +13 -0
  5. package/dist/SeatbeltRuntime-O7DSMFKS.js +98 -0
  6. package/dist/StderrViolationParser-7OYPM2DJ.js +59 -0
  7. package/dist/ViolationLogStore-RIIUVURH.js +104 -0
  8. package/dist/{artifactExtractor-5VADY6TA.js → artifactExtractor-5KWVJRK7.js} +1 -1
  9. package/dist/{chunk-LMSZGMUT.js → chunk-2OADXKGK.js} +2 -2
  10. package/dist/chunk-4BIBE3J7.js +48 -0
  11. package/dist/{chunk-XV4TIA2C.js → chunk-5JTIPGUF.js} +2 -2
  12. package/dist/{chunk-QHVYZO7D.js → chunk-CLA2MUX4.js} +2 -2
  13. package/dist/{chunk-ZSDL6IIF.js → chunk-FKJTU7KU.js} +0 -117
  14. package/dist/{chunk-NYF7RZLL.js → chunk-Q7CVAY4M.js} +1 -1
  15. package/dist/chunk-QJCBARQN.js +44 -0
  16. package/dist/{chunk-LBTTUQJM.js → chunk-TQROMSHY.js} +78 -3
  17. package/dist/commands/mcpCommand.js +2 -1
  18. package/dist/{create-TAFNEM55.js → create-BDV4MI3R.js} +3 -3
  19. package/dist/index.js +542 -126
  20. package/dist/{llmMarkdownGenerator-QEMHO5JW.js → llmMarkdownGenerator-UIJD4IKA.js} +1 -1
  21. package/dist/{markdownGenerator-CHWZATBH.js → markdownGenerator-UBZZJ3YM.js} +1 -1
  22. package/dist/{mementoService-NPS27IUP.js → mementoService-UP7777VB.js} +3 -3
  23. package/dist/{src-MVQECP5O.js → src-DOG6MRCB.js} +1 -7
  24. package/dist/{src-7IGGVABT.js → src-G5MW2D7G.js} +2 -9
  25. package/dist/{subtractCredits-QINURUBQ.js → subtractCredits-XGBHF6DI.js} +3 -3
  26. package/dist/types-KB5NP6T4.js +7 -0
  27. package/package.json +6 -6
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ expandPath,
4
+ isBinaryAvailable
5
+ } from "./chunk-QJCBARQN.js";
6
+
7
+ // src/sandbox/runtime/BubblewrapRuntime.ts
8
+ import os from "os";
9
+ var SYSTEM_RO_BINDS = ["/usr", "/bin", "/lib", "/lib64", "/sbin", "/etc"];
10
+ var SPECIAL_MOUNTS = {
11
+ dev: "/dev",
12
+ proc: "/proc",
13
+ tmp: "/tmp"
14
+ };
15
+ var BubblewrapRuntime = class {
16
+ constructor() {
17
+ this.platform = "linux";
18
+ this.name = "bubblewrap";
19
+ }
20
+ isAvailable() {
21
+ return isBinaryAvailable("bwrap");
22
+ }
23
+ wrapCommand(options) {
24
+ const { command, cwd, filesystemConfig, env } = options;
25
+ const expandedDenied = filesystemConfig.deniedPaths.map(expandPath);
26
+ const expandedAllowed = filesystemConfig.allowedReadPaths.map(expandPath);
27
+ const args = [];
28
+ for (const sysPath of SYSTEM_RO_BINDS) {
29
+ args.push("--ro-bind-try", sysPath, sysPath);
30
+ }
31
+ args.push("--dev", SPECIAL_MOUNTS.dev);
32
+ args.push("--proc", SPECIAL_MOUNTS.proc);
33
+ args.push("--tmpfs", SPECIAL_MOUNTS.tmp);
34
+ args.push("--bind", cwd, cwd);
35
+ const homeDir = os.homedir();
36
+ args.push("--ro-bind", homeDir, homeDir);
37
+ for (const allowedPath of expandedAllowed) {
38
+ if (!allowedPath.startsWith(homeDir)) {
39
+ args.push("--ro-bind-try", allowedPath, allowedPath);
40
+ }
41
+ }
42
+ for (const deniedPath of expandedDenied) {
43
+ args.push("--tmpfs", deniedPath);
44
+ }
45
+ args.push("--unshare-all");
46
+ args.push("--share-net");
47
+ args.push("--die-with-parent");
48
+ if (options.seccompProfile) {
49
+ args.push("--seccomp", options.seccompProfile);
50
+ }
51
+ for (const [key, value] of Object.entries(env ?? {})) {
52
+ args.push("--setenv", key, value);
53
+ }
54
+ args.push("--chdir", cwd);
55
+ args.push("bash", "-c", command);
56
+ const commandString = ["bwrap", ...args.map(shellEscape)].join(" ");
57
+ return {
58
+ executable: "bwrap",
59
+ args,
60
+ env: env ?? {},
61
+ commandString
62
+ };
63
+ }
64
+ };
65
+ function shellEscape(str) {
66
+ if (/^[a-zA-Z0-9._/=:-]+$/.test(str)) return str;
67
+ return `'${str.replace(/'/g, "'\\''")}'`;
68
+ }
69
+ export {
70
+ BubblewrapRuntime
71
+ };
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/sandbox/proxy/HttpConnectProxy.ts
4
+ import http from "http";
5
+ import net from "net";
6
+ import { EventEmitter } from "events";
7
+
8
+ // src/sandbox/proxy/domainMatcher.ts
9
+ function normalizeDomain(domain) {
10
+ let d = domain.toLowerCase().trim();
11
+ const colonIdx = d.lastIndexOf(":");
12
+ if (colonIdx > 0) {
13
+ const afterColon = d.slice(colonIdx + 1);
14
+ if (/^\d+$/.test(afterColon)) {
15
+ d = d.slice(0, colonIdx);
16
+ }
17
+ }
18
+ if (d.endsWith(".")) {
19
+ d = d.slice(0, -1);
20
+ }
21
+ return d;
22
+ }
23
+ function matchesDomain(domain, pattern) {
24
+ const d = normalizeDomain(domain);
25
+ const p = normalizeDomain(pattern);
26
+ if (d === p) return true;
27
+ if (p.startsWith("*.")) {
28
+ const suffix = p.slice(1);
29
+ return d.endsWith(suffix) && d.length > suffix.length;
30
+ }
31
+ return false;
32
+ }
33
+ function isDomainAllowed(domain, allowedDomains) {
34
+ if (!domain || allowedDomains.length === 0) return false;
35
+ return allowedDomains.some((pattern) => matchesDomain(domain, pattern));
36
+ }
37
+
38
+ // src/sandbox/proxy/HttpConnectProxy.ts
39
+ var HttpConnectProxy = class extends EventEmitter {
40
+ constructor(options) {
41
+ super();
42
+ this.server = null;
43
+ this.activeSockets = /* @__PURE__ */ new Set();
44
+ this.allowedDomains = [...options.allowedDomains];
45
+ this.requestedPort = options.port ?? 0;
46
+ }
47
+ async start() {
48
+ if (this.server) {
49
+ throw new Error("Proxy is already running");
50
+ }
51
+ return new Promise((resolve, reject) => {
52
+ const server = http.createServer((req, res) => {
53
+ this.handleHttpRequest(req, res);
54
+ });
55
+ server.on("connect", (req, clientSocket, head) => {
56
+ this.handleConnect(req, clientSocket, head);
57
+ });
58
+ server.on("connection", (socket) => {
59
+ this.activeSockets.add(socket);
60
+ socket.on("close", () => this.activeSockets.delete(socket));
61
+ });
62
+ server.on("error", reject);
63
+ server.listen(this.requestedPort, "127.0.0.1", () => {
64
+ const addr = server.address();
65
+ if (!addr || typeof addr === "string") {
66
+ reject(new Error("Failed to get server address"));
67
+ return;
68
+ }
69
+ this.server = server;
70
+ resolve(addr.port);
71
+ });
72
+ });
73
+ }
74
+ async stop() {
75
+ if (!this.server) return;
76
+ for (const socket of this.activeSockets) {
77
+ socket.destroy();
78
+ }
79
+ this.activeSockets.clear();
80
+ return new Promise((resolve, reject) => {
81
+ this.server.close((err) => {
82
+ this.server = null;
83
+ if (err) reject(err);
84
+ else resolve();
85
+ });
86
+ });
87
+ }
88
+ updateAllowedDomains(domains) {
89
+ this.allowedDomains = [...domains];
90
+ }
91
+ getPort() {
92
+ if (!this.server) return null;
93
+ const addr = this.server.address();
94
+ if (!addr || typeof addr === "string") return null;
95
+ return addr.port;
96
+ }
97
+ isRunning() {
98
+ return this.server !== null;
99
+ }
100
+ emitEvent(type, domain, method) {
101
+ const event = { type, domain, method, timestamp: /* @__PURE__ */ new Date() };
102
+ this.emit("proxy-event", event);
103
+ }
104
+ /**
105
+ * Handle CONNECT requests (HTTPS tunneling).
106
+ */
107
+ handleConnect(req, clientSocket, head) {
108
+ const target = req.url || "";
109
+ const [host] = target.split(":");
110
+ const port = parseInt(target.split(":")[1] || "443", 10);
111
+ if (!isDomainAllowed(host, this.allowedDomains)) {
112
+ this.emitEvent("blocked", host, "CONNECT");
113
+ clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
114
+ clientSocket.end();
115
+ return;
116
+ }
117
+ this.emitEvent("allowed", host, "CONNECT");
118
+ const serverSocket = net.connect(port, host, () => {
119
+ clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
120
+ if (head.length > 0) {
121
+ serverSocket.write(head);
122
+ }
123
+ serverSocket.pipe(clientSocket);
124
+ clientSocket.pipe(serverSocket);
125
+ });
126
+ serverSocket.setTimeout(5e3, () => {
127
+ serverSocket.destroy();
128
+ clientSocket.write("HTTP/1.1 504 Gateway Timeout\r\n\r\n");
129
+ clientSocket.end();
130
+ });
131
+ serverSocket.on("error", () => {
132
+ clientSocket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
133
+ clientSocket.end();
134
+ });
135
+ clientSocket.on("error", () => {
136
+ serverSocket.destroy();
137
+ });
138
+ }
139
+ /**
140
+ * Handle plain HTTP forward proxy requests.
141
+ */
142
+ handleHttpRequest(req, res) {
143
+ const url = req.url || "";
144
+ let host;
145
+ try {
146
+ const parsed2 = new URL(url);
147
+ host = parsed2.hostname;
148
+ } catch {
149
+ host = (req.headers.host || "").split(":")[0];
150
+ }
151
+ if (!host || !isDomainAllowed(host, this.allowedDomains)) {
152
+ this.emitEvent("blocked", host || "unknown", req.method || "GET");
153
+ res.writeHead(403, { "Content-Type": "text/plain" });
154
+ res.end("Blocked by sandbox network proxy");
155
+ return;
156
+ }
157
+ this.emitEvent("allowed", host, req.method || "GET");
158
+ const parsed = new URL(url);
159
+ const proxyReq = http.request(
160
+ {
161
+ hostname: parsed.hostname,
162
+ port: parsed.port || 80,
163
+ path: parsed.pathname + parsed.search,
164
+ method: req.method,
165
+ headers: req.headers
166
+ },
167
+ (proxyRes) => {
168
+ res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
169
+ proxyRes.pipe(res);
170
+ }
171
+ );
172
+ proxyReq.on("error", () => {
173
+ res.writeHead(502, { "Content-Type": "text/plain" });
174
+ res.end("Bad Gateway");
175
+ });
176
+ req.pipe(proxyReq);
177
+ }
178
+ };
179
+
180
+ // src/sandbox/proxy/ProxyManager.ts
181
+ var ProxyManager = class {
182
+ constructor(networkConfig) {
183
+ this.proxy = null;
184
+ this.eventHandlers = /* @__PURE__ */ new Set();
185
+ this.networkConfig = { ...networkConfig, allowedDomains: [...networkConfig.allowedDomains] };
186
+ }
187
+ async start() {
188
+ if (!this.networkConfig.enabled) return;
189
+ if (this.proxy?.isRunning()) return;
190
+ this.proxy = new HttpConnectProxy({
191
+ allowedDomains: this.networkConfig.allowedDomains
192
+ });
193
+ this.proxy.on("proxy-event", (event) => {
194
+ for (const handler of this.eventHandlers) {
195
+ handler(event);
196
+ }
197
+ });
198
+ await this.proxy.start();
199
+ }
200
+ async stop() {
201
+ if (!this.proxy) return;
202
+ await this.proxy.stop();
203
+ this.proxy = null;
204
+ }
205
+ /**
206
+ * Get proxy env vars for injecting into sandboxed processes.
207
+ * Returns both upper and lowercase variants for maximum compatibility.
208
+ */
209
+ getProxyEnv() {
210
+ if (!this.proxy?.isRunning()) return {};
211
+ const port = this.proxy.getPort();
212
+ if (!port) return {};
213
+ const proxyUrl = `http://127.0.0.1:${port}`;
214
+ const noProxy = "localhost,127.0.0.1,::1";
215
+ return {
216
+ HTTP_PROXY: proxyUrl,
217
+ http_proxy: proxyUrl,
218
+ HTTPS_PROXY: proxyUrl,
219
+ https_proxy: proxyUrl,
220
+ NO_PROXY: noProxy,
221
+ no_proxy: noProxy
222
+ };
223
+ }
224
+ addAllowedDomain(domain) {
225
+ if (!this.networkConfig.allowedDomains.includes(domain)) {
226
+ this.networkConfig.allowedDomains.push(domain);
227
+ this.proxy?.updateAllowedDomains(this.networkConfig.allowedDomains);
228
+ }
229
+ }
230
+ getAllowedDomains() {
231
+ return [...this.networkConfig.allowedDomains];
232
+ }
233
+ isRunning() {
234
+ return this.proxy?.isRunning() ?? false;
235
+ }
236
+ getPort() {
237
+ return this.proxy?.getPort() ?? null;
238
+ }
239
+ /**
240
+ * Subscribe to proxy events. Returns an unsubscribe function.
241
+ */
242
+ onEvent(handler) {
243
+ this.eventHandlers.add(handler);
244
+ return () => this.eventHandlers.delete(handler);
245
+ }
246
+ };
247
+ export {
248
+ ProxyManager
249
+ };
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_SANDBOX_CONFIG
4
+ } from "./chunk-4BIBE3J7.js";
5
+
6
+ // src/sandbox/SandboxOrchestrator.ts
7
+ var SandboxOrchestrator = class {
8
+ constructor(config, runtime, proxyManager) {
9
+ this.stats = { sandboxed: 0, unsandboxed: 0, blocked: 0, violations: 0 };
10
+ this.violationStore = null;
11
+ this.config = config ?? DEFAULT_SANDBOX_CONFIG;
12
+ this.runtime = runtime ?? null;
13
+ this.proxyManager = proxyManager ?? null;
14
+ }
15
+ /**
16
+ * Determine whether a command should be sandboxed.
17
+ *
18
+ * Decision logic:
19
+ * 1. If sandbox is disabled → unsandboxed (no permission change)
20
+ * 2. If command matches an excluded command → unsandboxed (requires permission)
21
+ * 3. If runtime is not available → unsandboxed with warning
22
+ * 4. Otherwise → sandbox the command
23
+ */
24
+ shouldSandbox(command, cwd) {
25
+ if (!this.config.enabled || this.config.mode === "disabled") {
26
+ return { type: "unsandboxed", requiresPermission: true };
27
+ }
28
+ const baseCommand = this.getBaseCommand(command);
29
+ if (this.isExcludedCommand(baseCommand)) {
30
+ if (!this.config.allowUnsandboxedCommands) {
31
+ return {
32
+ type: "blocked",
33
+ reason: `Command '${baseCommand}' is excluded from sandboxing and unsandboxed commands are not allowed`
34
+ };
35
+ }
36
+ return {
37
+ type: "unsandboxed",
38
+ requiresPermission: true,
39
+ reason: `Command '${baseCommand}' is excluded from sandboxing`
40
+ };
41
+ }
42
+ if (!this.runtime) {
43
+ return {
44
+ type: "unsandboxed",
45
+ requiresPermission: true,
46
+ reason: "Sandbox runtime not available on this platform"
47
+ };
48
+ }
49
+ const proxyEnv = this.proxyManager?.getProxyEnv() ?? {};
50
+ const wrappedCommand = this.runtime.wrapCommand({
51
+ command,
52
+ cwd,
53
+ filesystemConfig: this.config.filesystem,
54
+ env: proxyEnv,
55
+ ...this.runtime.platform === "linux" && this.config.platform.linux.seccompProfile && {
56
+ seccompProfile: this.config.platform.linux.seccompProfile
57
+ }
58
+ });
59
+ return { type: "sandbox", wrappedCommand };
60
+ }
61
+ /** Get the current sandbox mode */
62
+ getMode() {
63
+ return this.config.mode;
64
+ }
65
+ /** Set the sandbox mode (does not persist — caller must save config) */
66
+ setMode(mode) {
67
+ this.config.mode = mode;
68
+ this.config.enabled = mode !== "disabled";
69
+ }
70
+ /** Check if sandbox is enabled and runtime is available */
71
+ isAvailable() {
72
+ return this.runtime !== null && this.runtime.isAvailable();
73
+ }
74
+ /** Check if sandbox is currently active (enabled + available) */
75
+ isActive() {
76
+ return this.config.enabled && this.config.mode !== "disabled" && this.isAvailable();
77
+ }
78
+ /** Get the current sandbox configuration */
79
+ getConfig() {
80
+ return this.config;
81
+ }
82
+ /** Update config (does not persist — caller must save) */
83
+ updateConfig(config) {
84
+ this.config = config;
85
+ }
86
+ /** Get the ProxyManager instance (if any) */
87
+ getProxyManager() {
88
+ return this.proxyManager;
89
+ }
90
+ /** Start the network proxy (if configured) */
91
+ async startProxy() {
92
+ await this.proxyManager?.start();
93
+ }
94
+ /** Stop the network proxy */
95
+ async stopProxy() {
96
+ await this.proxyManager?.stop();
97
+ }
98
+ /** Get full status information for display */
99
+ getStatus() {
100
+ return {
101
+ mode: this.config.mode,
102
+ enabled: this.config.enabled,
103
+ platform: this.runtime?.platform ?? null,
104
+ runtimeAvailable: this.runtime?.isAvailable() ?? false,
105
+ runtimeName: this.runtime?.name ?? null,
106
+ proxyRunning: this.proxyManager?.isRunning() ?? false,
107
+ proxyPort: this.proxyManager?.getPort() ?? null,
108
+ config: this.config,
109
+ stats: { ...this.stats }
110
+ };
111
+ }
112
+ // --- Stats tracking ---
113
+ recordSandboxed() {
114
+ this.stats.sandboxed++;
115
+ }
116
+ recordUnsandboxed() {
117
+ this.stats.unsandboxed++;
118
+ }
119
+ recordBlocked() {
120
+ this.stats.blocked++;
121
+ }
122
+ recordViolations(count = 1) {
123
+ this.stats.violations += count;
124
+ }
125
+ getStats() {
126
+ return { ...this.stats };
127
+ }
128
+ resetStats() {
129
+ this.stats = { sandboxed: 0, unsandboxed: 0, blocked: 0, violations: 0 };
130
+ }
131
+ // --- Violation store ---
132
+ setViolationStore(store) {
133
+ this.violationStore = store;
134
+ }
135
+ getViolationStore() {
136
+ return this.violationStore;
137
+ }
138
+ /** Record a violation to store and increment stats */
139
+ async recordViolation(violation) {
140
+ this.stats.violations++;
141
+ await this.violationStore?.record(violation).catch(() => {
142
+ });
143
+ }
144
+ /**
145
+ * Extract the base command name from a full command string.
146
+ * e.g., "docker compose up -d" → "docker"
147
+ */
148
+ getBaseCommand(command) {
149
+ const trimmed = command.trim();
150
+ const withoutEnv = trimmed.replace(/^(\w+=\S+\s+)*/, "");
151
+ const parts = withoutEnv.split(/\s+/);
152
+ const first = parts[0] || "";
153
+ return first.split("/").pop() || first;
154
+ }
155
+ /** Check if a command is in the excluded list */
156
+ isExcludedCommand(baseCommand) {
157
+ return this.config.excludedCommands.some((excluded) => baseCommand === excluded);
158
+ }
159
+ };
160
+ export {
161
+ SandboxOrchestrator
162
+ };
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ createSandboxRuntime,
4
+ detectPlatform,
5
+ expandPath,
6
+ isBinaryAvailable
7
+ } from "./chunk-QJCBARQN.js";
8
+ export {
9
+ createSandboxRuntime,
10
+ detectPlatform,
11
+ expandPath,
12
+ isBinaryAvailable
13
+ };
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ expandPath,
4
+ isBinaryAvailable
5
+ } from "./chunk-QJCBARQN.js";
6
+
7
+ // src/sandbox/runtime/SeatbeltRuntime.ts
8
+ import { writeFileSync, mkdtempSync } from "fs";
9
+ import path from "path";
10
+ import os from "os";
11
+ function escapeSeatbeltPath(p) {
12
+ return p.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
13
+ }
14
+ var SeatbeltRuntime = class {
15
+ constructor() {
16
+ this.platform = "darwin";
17
+ this.name = "seatbelt";
18
+ }
19
+ isAvailable() {
20
+ return isBinaryAvailable("sandbox-exec");
21
+ }
22
+ /**
23
+ * Generate a Seatbelt profile string from the filesystem config.
24
+ *
25
+ * Strategy:
26
+ * - Start with (allow default) to permit most operations
27
+ * - Deny all file writes globally
28
+ * - Allow file writes only to the working directory
29
+ * - Deny all access to explicitly denied paths
30
+ * - Allow read access to explicitly allowed paths
31
+ */
32
+ generateProfile(options) {
33
+ const { cwd, filesystemConfig } = options;
34
+ const expandedDenied = filesystemConfig.deniedPaths.map(expandPath);
35
+ const expandedAllowed = filesystemConfig.allowedReadPaths.map(expandPath);
36
+ const lines = [
37
+ "(version 1)",
38
+ "",
39
+ "; Start with permissive defaults (process exec, network, sysctl, etc.)",
40
+ "(allow default)",
41
+ ""
42
+ ];
43
+ if (filesystemConfig.writeOnlyToWorkingDir) {
44
+ lines.push("; Deny all file writes globally");
45
+ lines.push("(deny file-write*)");
46
+ lines.push("");
47
+ lines.push("; Allow writes to working directory");
48
+ lines.push(`(allow file-write* (subpath "${escapeSeatbeltPath(cwd)}"))`);
49
+ lines.push("");
50
+ lines.push("; Allow writes to temp directories");
51
+ lines.push(`(allow file-write* (subpath "/tmp"))`);
52
+ lines.push(`(allow file-write* (subpath "/private/tmp"))`);
53
+ lines.push(`(allow file-write* (subpath "${escapeSeatbeltPath(os.tmpdir())}"))`);
54
+ lines.push("");
55
+ }
56
+ if (expandedDenied.length > 0) {
57
+ lines.push("; Deny access to sensitive paths");
58
+ for (const deniedPath of expandedDenied) {
59
+ lines.push(`(deny file-read* file-write* (subpath "${escapeSeatbeltPath(deniedPath)}"))`);
60
+ }
61
+ lines.push("");
62
+ }
63
+ if (expandedAllowed.length > 0) {
64
+ lines.push("; Explicitly allowed read paths");
65
+ for (const allowedPath of expandedAllowed) {
66
+ lines.push(`(allow file-read* (subpath "${escapeSeatbeltPath(allowedPath)}"))`);
67
+ }
68
+ lines.push("");
69
+ }
70
+ return lines.join("\n");
71
+ }
72
+ wrapCommand(options) {
73
+ try {
74
+ const profile = this.generateProfile(options);
75
+ const tmpDir = mkdtempSync(path.join(os.tmpdir(), "b4m-sandbox-"));
76
+ const profilePath = path.join(tmpDir, "sandbox.sb");
77
+ writeFileSync(profilePath, profile, "utf-8");
78
+ const args = ["-f", profilePath, "bash", "-c", options.command];
79
+ const envEntries = Object.entries(options.env ?? {});
80
+ const envPrefix = envEntries.length > 0 ? envEntries.map(([k, v]) => `${k}=${shellEscape(v)}`).join(" ") + " " : "";
81
+ return {
82
+ executable: "sandbox-exec",
83
+ args,
84
+ env: options.env ?? {},
85
+ commandString: `${envPrefix}sandbox-exec -f ${profilePath} bash -c ${shellEscape(options.command)}`,
86
+ cleanupPaths: [profilePath, tmpDir]
87
+ };
88
+ } catch (err) {
89
+ throw new Error(`Failed to create sandbox profile: ${err instanceof Error ? err.message : String(err)}`);
90
+ }
91
+ }
92
+ };
93
+ function shellEscape(str) {
94
+ return `'${str.replace(/'/g, "'\\''")}'`;
95
+ }
96
+ export {
97
+ SeatbeltRuntime
98
+ };
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/sandbox/logging/StderrViolationParser.ts
4
+ function parseSeatbeltStderr(stderr) {
5
+ const violations = [];
6
+ const regex = /sandbox-exec:\s*deny\(\d+\)\s+(\S+)\s+(.+)/g;
7
+ let match;
8
+ while ((match = regex.exec(stderr)) !== null) {
9
+ const operation = match[1];
10
+ const targetPath = match[2].trim();
11
+ const type = classifySeatbeltOperation(operation);
12
+ violations.push({
13
+ type,
14
+ operation,
15
+ path: type === "filesystem" ? targetPath : void 0,
16
+ detail: match[0]
17
+ });
18
+ }
19
+ return violations;
20
+ }
21
+ function parseBwrapStderr(stderr) {
22
+ const violations = [];
23
+ const regex = /bwrap:\s+(.+)/g;
24
+ let match;
25
+ while ((match = regex.exec(stderr)) !== null) {
26
+ const message = match[1].trim();
27
+ const pathMatch = message.match(/(?:Can't open file |Can't bind mount )(\S+)/);
28
+ const extractedPath = pathMatch?.[1]?.replace(/:$/, "");
29
+ violations.push({
30
+ type: "filesystem",
31
+ path: extractedPath,
32
+ detail: match[0]
33
+ });
34
+ }
35
+ return violations;
36
+ }
37
+ function parseSandboxStderr(stderr) {
38
+ return [...parseSeatbeltStderr(stderr), ...parseBwrapStderr(stderr)];
39
+ }
40
+ function toSandboxViolations(parsed, command) {
41
+ return parsed.map((p) => ({
42
+ type: p.type,
43
+ path: p.path,
44
+ command,
45
+ blockedBy: "sandbox",
46
+ timestamp: /* @__PURE__ */ new Date(),
47
+ detail: p.detail
48
+ }));
49
+ }
50
+ function classifySeatbeltOperation(operation) {
51
+ if (operation.startsWith("network")) return "network";
52
+ return "filesystem";
53
+ }
54
+ export {
55
+ parseBwrapStderr,
56
+ parseSandboxStderr,
57
+ parseSeatbeltStderr,
58
+ toSandboxViolations
59
+ };