@bike4mind/cli 0.2.64-worktree-refactor-extract-search-query-builders.21815 → 0.2.64
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/bin/bike4mind-cli.mjs +6 -6
- package/dist/BubblewrapRuntime-BHbtqvLx.mjs +72 -0
- package/dist/ConfigStore-CllM6jOf.mjs +8614 -0
- package/dist/ImageStore-DaKT_Ew8.mjs +202 -0
- package/dist/ProxyManager-Dl2nFk-A.mjs +259 -0
- package/dist/ProxyManager-kiOD1X8-.mjs +3 -0
- package/dist/SandboxOrchestrator-BEW3rqYi.mjs +159 -0
- package/dist/SandboxOrchestrator-CHZgSR3P.mjs +3 -0
- package/dist/SandboxRuntimeAdapter-C1B4t20N.mjs +57 -0
- package/dist/SandboxRuntimeAdapter-D7UAG13n.mjs +3 -0
- package/dist/SeatbeltRuntime-D4m0VOcD.mjs +116 -0
- package/dist/StderrViolationParser-D0afQ3-1.mjs +70 -0
- package/dist/ViolationLogStore-CZl35HcA.mjs +96 -0
- package/dist/bashExecute-BTkdqlSs-5foM20Lb.mjs +466 -0
- package/dist/commands/doctorCommand.mjs +101 -0
- package/dist/commands/headlessCommand.mjs +319 -0
- package/dist/commands/mcpCommand.mjs +218 -0
- package/dist/commands/updateCommand.mjs +40 -0
- package/dist/createFile-yQfh8uvk-I-yM5DxC.mjs +63 -0
- package/dist/deleteFile-DKHfnyny-G3b1Kj2T.mjs +66 -0
- package/dist/globFiles-D1en6joM-8jekiXdX.mjs +100 -0
- package/dist/grepSearch-aMamoBn_-DCJcY8JS.mjs +173 -0
- package/dist/index.mjs +6722 -0
- package/dist/pathValidation-Cgjh5WQO-DiCZTcq6.mjs +63 -0
- package/dist/store-Dw1nZX2Y.mjs +128 -0
- package/dist/store-nZExNOWX.mjs +3 -0
- package/dist/terminalSetup-rmr1P8KF.mjs +254 -0
- package/dist/tools-C6M5aW8W.mjs +20907 -0
- package/dist/treeSitterEngine-DCSXcm_3.mjs +309 -0
- package/dist/types-DBEjF9YS.mjs +59 -0
- package/dist/types-DK3P88Px.mjs +3 -0
- package/dist/updateChecker-Cu9dkHxV.mjs +120 -0
- package/package.json +10 -10
- package/dist/BubblewrapRuntime-PMIOLWKR.js +0 -71
- package/dist/HydrationEngine-YL2HWJ3V.js +0 -9
- package/dist/ImageStore-MMUOUPI2.js +0 -224
- package/dist/ProxyManager-HEB4TLVX.js +0 -7
- package/dist/SandboxOrchestrator-UIJ5GYBB.js +0 -8
- package/dist/SandboxRuntimeAdapter-FQ56MAB2.js +0 -13
- package/dist/SeatbeltRuntime-EE3TTLEP.js +0 -98
- package/dist/StderrViolationParser-7OYPM2DJ.js +0 -59
- package/dist/ViolationLogStore-RIIUVURH.js +0 -104
- package/dist/artifactExtractor-R7DIP2XO.js +0 -180
- package/dist/bashExecute-GLGLD3JD.js +0 -379
- package/dist/chunk-4BIBE3J7.js +0 -48
- package/dist/chunk-5LZS5CVJ.js +0 -161
- package/dist/chunk-BDQBOLYG.js +0 -120
- package/dist/chunk-BPFEGDC7.js +0 -192
- package/dist/chunk-EPIYC3LA.js +0 -13770
- package/dist/chunk-G4ZGEQFT.js +0 -250
- package/dist/chunk-GQGOWACU.js +0 -770
- package/dist/chunk-J6ZBI6TI.js +0 -1079
- package/dist/chunk-JW3JRHH7.js +0 -12433
- package/dist/chunk-KQAMBXAW.js +0 -163
- package/dist/chunk-KUVV2NAB.js +0 -19125
- package/dist/chunk-LTLJRF6I.js +0 -44
- package/dist/chunk-PFBYGCOW.js +0 -449
- package/dist/chunk-QWB6ZYY4.js +0 -48
- package/dist/chunk-SGPRXN4C.js +0 -245
- package/dist/chunk-UZUHPHZC.js +0 -95
- package/dist/chunk-WBE7SQUB.js +0 -241
- package/dist/chunk-Y4WOJJM3.js +0 -147
- package/dist/commands/doctorCommand.js +0 -87
- package/dist/commands/headlessCommand.js +0 -380
- package/dist/commands/mcpCommand.js +0 -203
- package/dist/commands/updateCommand.js +0 -42
- package/dist/create-C4VEEEYR.js +0 -12
- package/dist/createFile-6PSPLW6R.js +0 -71
- package/dist/deleteFile-AUSRLWIK.js +0 -73
- package/dist/formatConverter-5QEJDW24.js +0 -7
- package/dist/globFiles-TSRN64N2.js +0 -120
- package/dist/grepSearch-634XWZOJ.js +0 -216
- package/dist/index.js +0 -6779
- package/dist/llmMarkdownGenerator-Z6NB26TT.js +0 -371
- package/dist/markdownGenerator-SK2ZQQL4.js +0 -269
- package/dist/mementoService-N4IM6QAC.js +0 -12
- package/dist/notificationDeduplicator-HUC53NEW.js +0 -9
- package/dist/src-F4KZCAA2.js +0 -319
- package/dist/src-ISX322I7.js +0 -1101
- package/dist/store-CAB6BV3P.js +0 -11
- package/dist/subtractCredits-D4KEM6VU.js +0 -12
- package/dist/terminalSetup-C5FHMLC3.js +0 -214
- package/dist/treeSitterEngine-4SGFQDY3.js +0 -330
- package/dist/types-KB5NP6T4.js +0 -7
- package/dist/utils-JCHWDM4Z.js +0 -31
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { extname, join, resolve } from "path";
|
|
6
|
+
import Database from "better-sqlite3";
|
|
7
|
+
import sharp from "sharp";
|
|
8
|
+
//#region src/storage/ImageStore.ts
|
|
9
|
+
/**
|
|
10
|
+
* Local image cache for CLI
|
|
11
|
+
* Stores pasted/dropped images before upload to S3
|
|
12
|
+
*/
|
|
13
|
+
var ImageStore = class ImageStore {
|
|
14
|
+
static {
|
|
15
|
+
this.BASE_DIR = join(homedir(), ".bike4mind", "cli");
|
|
16
|
+
}
|
|
17
|
+
static {
|
|
18
|
+
this.IMAGES_DIR = join(ImageStore.BASE_DIR, "images");
|
|
19
|
+
}
|
|
20
|
+
static {
|
|
21
|
+
this.DB_PATH = join(ImageStore.BASE_DIR, "images.db");
|
|
22
|
+
}
|
|
23
|
+
static {
|
|
24
|
+
this.RETENTION_DAYS = 7;
|
|
25
|
+
}
|
|
26
|
+
static {
|
|
27
|
+
this.MAX_RAW_SIZE = 750 * 1024;
|
|
28
|
+
}
|
|
29
|
+
static {
|
|
30
|
+
this.MAX_PRECOMPRESS_SIZE = 50 * 1024 * 1024;
|
|
31
|
+
}
|
|
32
|
+
constructor() {
|
|
33
|
+
this.ensureDirectories();
|
|
34
|
+
this.db = new Database(ImageStore.DB_PATH);
|
|
35
|
+
this.initDatabase();
|
|
36
|
+
try {
|
|
37
|
+
this.cleanupOldImages();
|
|
38
|
+
} catch (err) {
|
|
39
|
+
console.warn("Failed to cleanup old images:", err);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
ensureDirectories() {
|
|
43
|
+
mkdirSync(ImageStore.BASE_DIR, { recursive: true });
|
|
44
|
+
mkdirSync(ImageStore.IMAGES_DIR, { recursive: true });
|
|
45
|
+
}
|
|
46
|
+
initDatabase() {
|
|
47
|
+
this.db.exec(`
|
|
48
|
+
CREATE TABLE IF NOT EXISTS images (
|
|
49
|
+
hash TEXT PRIMARY KEY,
|
|
50
|
+
format TEXT NOT NULL,
|
|
51
|
+
size INTEGER NOT NULL,
|
|
52
|
+
timestamp INTEGER NOT NULL,
|
|
53
|
+
uploaded INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
s3_url TEXT,
|
|
55
|
+
original_filename TEXT
|
|
56
|
+
)
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Store an image locally with content-based hashing for deduplication
|
|
61
|
+
*/
|
|
62
|
+
async store(imageData, originalFilename) {
|
|
63
|
+
const { buffer: processedData, format: processedFormat } = await this.processImage(imageData, originalFilename);
|
|
64
|
+
const hash = this.generateHash(processedData);
|
|
65
|
+
const format = processedFormat;
|
|
66
|
+
const ext = format.toLowerCase();
|
|
67
|
+
const localPath = join(ImageStore.IMAGES_DIR, `${hash}.${ext}`);
|
|
68
|
+
const existing = this.getMetadata(hash);
|
|
69
|
+
if (existing) return {
|
|
70
|
+
hash,
|
|
71
|
+
placeholder: "",
|
|
72
|
+
localPath,
|
|
73
|
+
metadata: existing
|
|
74
|
+
};
|
|
75
|
+
writeFileSync(localPath, processedData);
|
|
76
|
+
const metadata = {
|
|
77
|
+
hash,
|
|
78
|
+
format,
|
|
79
|
+
size: processedData.length,
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
uploaded: false,
|
|
82
|
+
originalFilename
|
|
83
|
+
};
|
|
84
|
+
this.db.prepare(`INSERT INTO images (hash, format, size, timestamp, uploaded, original_filename)
|
|
85
|
+
VALUES (?, ?, ?, ?, ?, ?)`).run(hash, format, metadata.size, metadata.timestamp, 0, originalFilename || null);
|
|
86
|
+
return {
|
|
87
|
+
hash,
|
|
88
|
+
placeholder: "",
|
|
89
|
+
localPath,
|
|
90
|
+
metadata
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Get image metadata by hash
|
|
95
|
+
*/
|
|
96
|
+
getMetadata(hash) {
|
|
97
|
+
const row = this.db.prepare("SELECT * FROM images WHERE hash = ?").get(hash);
|
|
98
|
+
if (!row) return null;
|
|
99
|
+
return {
|
|
100
|
+
hash: row.hash,
|
|
101
|
+
format: row.format,
|
|
102
|
+
size: row.size,
|
|
103
|
+
timestamp: row.timestamp,
|
|
104
|
+
uploaded: Boolean(row.uploaded),
|
|
105
|
+
s3Url: row.s3_url,
|
|
106
|
+
originalFilename: row.original_filename
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Read image data from local cache
|
|
111
|
+
*/
|
|
112
|
+
readImage(hash) {
|
|
113
|
+
const metadata = this.getMetadata(hash);
|
|
114
|
+
if (!metadata) return null;
|
|
115
|
+
const localPath = join(ImageStore.IMAGES_DIR, `${hash}.${metadata.format.toLowerCase()}`);
|
|
116
|
+
if (!resolve(localPath).startsWith(resolve(ImageStore.IMAGES_DIR))) throw new Error("Invalid image path");
|
|
117
|
+
if (!existsSync(localPath)) return null;
|
|
118
|
+
return readFileSync(localPath);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Generate content hash for deduplication
|
|
122
|
+
*/
|
|
123
|
+
generateHash(data) {
|
|
124
|
+
return createHash("sha256").update(data).digest("hex").substring(0, 16);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Process image: compress if needed to stay under size limit
|
|
128
|
+
*/
|
|
129
|
+
async processImage(data, originalFilename) {
|
|
130
|
+
if (data.length > ImageStore.MAX_PRECOMPRESS_SIZE) throw new Error(`Image too large to process (${Math.round(data.length / 1024 / 1024)}MB). Maximum size is ${ImageStore.MAX_PRECOMPRESS_SIZE / 1024 / 1024}MB.`);
|
|
131
|
+
const originalFormat = this.detectFormat(data, originalFilename);
|
|
132
|
+
if (data.length <= ImageStore.MAX_RAW_SIZE) return {
|
|
133
|
+
buffer: data,
|
|
134
|
+
format: originalFormat
|
|
135
|
+
};
|
|
136
|
+
let image = sharp(data);
|
|
137
|
+
const metadata = await image.metadata();
|
|
138
|
+
if (!metadata.width || !metadata.height) throw new Error("Unable to read image dimensions");
|
|
139
|
+
const maxDimension = 2048;
|
|
140
|
+
let width = metadata.width;
|
|
141
|
+
let height = metadata.height;
|
|
142
|
+
if (width > maxDimension || height > maxDimension) if (width > height) {
|
|
143
|
+
height = Math.round(height * maxDimension / width);
|
|
144
|
+
width = maxDimension;
|
|
145
|
+
} else {
|
|
146
|
+
width = Math.round(width * maxDimension / height);
|
|
147
|
+
height = maxDimension;
|
|
148
|
+
}
|
|
149
|
+
image = image.resize(width, height);
|
|
150
|
+
let quality = 85;
|
|
151
|
+
let buffer = await image.jpeg({ quality }).toBuffer();
|
|
152
|
+
while (buffer.length > ImageStore.MAX_RAW_SIZE && quality > 30) {
|
|
153
|
+
quality -= 10;
|
|
154
|
+
buffer = await image.jpeg({ quality }).toBuffer();
|
|
155
|
+
}
|
|
156
|
+
if (buffer.length > ImageStore.MAX_RAW_SIZE) throw new Error(`Unable to compress image below ${ImageStore.MAX_RAW_SIZE / 1024}KB limit`);
|
|
157
|
+
return {
|
|
158
|
+
buffer,
|
|
159
|
+
format: "jpg"
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Detect image format from buffer or filename
|
|
164
|
+
*/
|
|
165
|
+
detectFormat(data, filename) {
|
|
166
|
+
if (data[0] === 137 && data[1] === 80 && data[2] === 78 && data[3] === 71) return "png";
|
|
167
|
+
if (data[0] === 255 && data[1] === 216 && data[2] === 255) return "jpg";
|
|
168
|
+
if (data[0] === 71 && data[1] === 73 && data[2] === 70) return "gif";
|
|
169
|
+
if (data[0] === 82 && data[1] === 73 && data[2] === 70 && data[3] === 70) return "webp";
|
|
170
|
+
if (filename) {
|
|
171
|
+
const ext = extname(filename).substring(1).toLowerCase();
|
|
172
|
+
if ([
|
|
173
|
+
"png",
|
|
174
|
+
"jpg",
|
|
175
|
+
"jpeg",
|
|
176
|
+
"gif",
|
|
177
|
+
"webp"
|
|
178
|
+
].includes(ext)) return ext === "jpeg" ? "jpg" : ext;
|
|
179
|
+
}
|
|
180
|
+
return "png";
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Clean up images older than retention period
|
|
184
|
+
*/
|
|
185
|
+
cleanupOldImages() {
|
|
186
|
+
const cutoffTime = Date.now() - ImageStore.RETENTION_DAYS * 24 * 60 * 60 * 1e3;
|
|
187
|
+
const oldImages = this.db.prepare("SELECT hash, format FROM images WHERE timestamp < ?").all(cutoffTime);
|
|
188
|
+
for (const { hash, format } of oldImages) {
|
|
189
|
+
const localPath = join(ImageStore.IMAGES_DIR, `${hash}.${format.toLowerCase()}`);
|
|
190
|
+
if (existsSync(localPath)) unlinkSync(localPath);
|
|
191
|
+
}
|
|
192
|
+
this.db.prepare("DELETE FROM images WHERE timestamp < ?").run(cutoffTime);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Close database connection
|
|
196
|
+
*/
|
|
197
|
+
close() {
|
|
198
|
+
this.db.close();
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
//#endregion
|
|
202
|
+
export { ImageStore };
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { EventEmitter } from "events";
|
|
3
|
+
import http from "http";
|
|
4
|
+
import net from "net";
|
|
5
|
+
//#region src/sandbox/proxy/domainMatcher.ts
|
|
6
|
+
/**
|
|
7
|
+
* Domain matching utilities for the network proxy allowlist.
|
|
8
|
+
* Pure functions, zero dependencies.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a domain for comparison: lowercase, strip port, strip trailing dot.
|
|
12
|
+
*/
|
|
13
|
+
function normalizeDomain(domain) {
|
|
14
|
+
let d = domain.toLowerCase().trim();
|
|
15
|
+
const colonIdx = d.lastIndexOf(":");
|
|
16
|
+
if (colonIdx > 0) {
|
|
17
|
+
const afterColon = d.slice(colonIdx + 1);
|
|
18
|
+
if (/^\d+$/.test(afterColon)) d = d.slice(0, colonIdx);
|
|
19
|
+
}
|
|
20
|
+
if (d.endsWith(".")) d = d.slice(0, -1);
|
|
21
|
+
return d;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Check if a domain matches a pattern.
|
|
25
|
+
* - Exact match: "github.com" matches "github.com"
|
|
26
|
+
* - Wildcard: "*.github.com" matches "api.github.com" but NOT "github.com"
|
|
27
|
+
*/
|
|
28
|
+
function matchesDomain(domain, pattern) {
|
|
29
|
+
const d = normalizeDomain(domain);
|
|
30
|
+
const p = normalizeDomain(pattern);
|
|
31
|
+
if (d === p) return true;
|
|
32
|
+
if (p.startsWith("*.")) {
|
|
33
|
+
const suffix = p.slice(1);
|
|
34
|
+
return d.endsWith(suffix) && d.length > suffix.length;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Check if a domain is in the allowed list.
|
|
40
|
+
*/
|
|
41
|
+
function isDomainAllowed(domain, allowedDomains) {
|
|
42
|
+
if (!domain || allowedDomains.length === 0) return false;
|
|
43
|
+
return allowedDomains.some((pattern) => matchesDomain(domain, pattern));
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
//#region src/sandbox/proxy/HttpConnectProxy.ts
|
|
47
|
+
/**
|
|
48
|
+
* HTTP CONNECT proxy for network domain filtering.
|
|
49
|
+
*
|
|
50
|
+
* Intercepts outbound HTTP/HTTPS connections from sandboxed commands
|
|
51
|
+
* and filters by domain allowlist. Uses only Node.js built-ins.
|
|
52
|
+
*
|
|
53
|
+
* - CONNECT handler (HTTPS): Parse domain → check allowlist → tunnel or 403
|
|
54
|
+
* - HTTP handler (forward proxy): Parse host → check allowlist → forward or 403
|
|
55
|
+
*/
|
|
56
|
+
var HttpConnectProxy = class extends EventEmitter {
|
|
57
|
+
constructor(options) {
|
|
58
|
+
super();
|
|
59
|
+
this.server = null;
|
|
60
|
+
this.activeSockets = /* @__PURE__ */ new Set();
|
|
61
|
+
this.allowedDomains = [...options.allowedDomains];
|
|
62
|
+
this.requestedPort = options.port ?? 0;
|
|
63
|
+
}
|
|
64
|
+
async start() {
|
|
65
|
+
if (this.server) throw new Error("Proxy is already running");
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
const server = http.createServer((req, res) => {
|
|
68
|
+
this.handleHttpRequest(req, res);
|
|
69
|
+
});
|
|
70
|
+
server.on("connect", (req, clientSocket, head) => {
|
|
71
|
+
this.handleConnect(req, clientSocket, head);
|
|
72
|
+
});
|
|
73
|
+
server.on("connection", (socket) => {
|
|
74
|
+
this.activeSockets.add(socket);
|
|
75
|
+
socket.on("close", () => this.activeSockets.delete(socket));
|
|
76
|
+
});
|
|
77
|
+
server.on("error", reject);
|
|
78
|
+
server.listen(this.requestedPort, "127.0.0.1", () => {
|
|
79
|
+
const addr = server.address();
|
|
80
|
+
if (!addr || typeof addr === "string") {
|
|
81
|
+
reject(/* @__PURE__ */ new Error("Failed to get server address"));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
this.server = server;
|
|
85
|
+
resolve(addr.port);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async stop() {
|
|
90
|
+
if (!this.server) return;
|
|
91
|
+
for (const socket of this.activeSockets) socket.destroy();
|
|
92
|
+
this.activeSockets.clear();
|
|
93
|
+
return new Promise((resolve, reject) => {
|
|
94
|
+
this.server.close((err) => {
|
|
95
|
+
this.server = null;
|
|
96
|
+
if (err) reject(err);
|
|
97
|
+
else resolve();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
updateAllowedDomains(domains) {
|
|
102
|
+
this.allowedDomains = [...domains];
|
|
103
|
+
}
|
|
104
|
+
getPort() {
|
|
105
|
+
if (!this.server) return null;
|
|
106
|
+
const addr = this.server.address();
|
|
107
|
+
if (!addr || typeof addr === "string") return null;
|
|
108
|
+
return addr.port;
|
|
109
|
+
}
|
|
110
|
+
isRunning() {
|
|
111
|
+
return this.server !== null;
|
|
112
|
+
}
|
|
113
|
+
emitEvent(type, domain, method) {
|
|
114
|
+
const event = {
|
|
115
|
+
type,
|
|
116
|
+
domain,
|
|
117
|
+
method,
|
|
118
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
119
|
+
};
|
|
120
|
+
this.emit("proxy-event", event);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Handle CONNECT requests (HTTPS tunneling).
|
|
124
|
+
*/
|
|
125
|
+
handleConnect(req, clientSocket, head) {
|
|
126
|
+
const target = req.url || "";
|
|
127
|
+
const [host] = target.split(":");
|
|
128
|
+
const port = parseInt(target.split(":")[1] || "443", 10);
|
|
129
|
+
if (!isDomainAllowed(host, this.allowedDomains)) {
|
|
130
|
+
this.emitEvent("blocked", host, "CONNECT");
|
|
131
|
+
clientSocket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
132
|
+
clientSocket.end();
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
this.emitEvent("allowed", host, "CONNECT");
|
|
136
|
+
const serverSocket = net.connect(port, host, () => {
|
|
137
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
138
|
+
if (head.length > 0) serverSocket.write(head);
|
|
139
|
+
serverSocket.pipe(clientSocket);
|
|
140
|
+
clientSocket.pipe(serverSocket);
|
|
141
|
+
});
|
|
142
|
+
serverSocket.setTimeout(5e3, () => {
|
|
143
|
+
serverSocket.destroy();
|
|
144
|
+
clientSocket.write("HTTP/1.1 504 Gateway Timeout\r\n\r\n");
|
|
145
|
+
clientSocket.end();
|
|
146
|
+
});
|
|
147
|
+
serverSocket.on("error", () => {
|
|
148
|
+
clientSocket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
149
|
+
clientSocket.end();
|
|
150
|
+
});
|
|
151
|
+
clientSocket.on("error", () => {
|
|
152
|
+
serverSocket.destroy();
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Handle plain HTTP forward proxy requests.
|
|
157
|
+
*/
|
|
158
|
+
handleHttpRequest(req, res) {
|
|
159
|
+
const url = req.url || "";
|
|
160
|
+
let host;
|
|
161
|
+
try {
|
|
162
|
+
host = new URL(url).hostname;
|
|
163
|
+
} catch {
|
|
164
|
+
host = (req.headers.host || "").split(":")[0];
|
|
165
|
+
}
|
|
166
|
+
if (!host || !isDomainAllowed(host, this.allowedDomains)) {
|
|
167
|
+
this.emitEvent("blocked", host || "unknown", req.method || "GET");
|
|
168
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
169
|
+
res.end("Blocked by sandbox network proxy");
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
this.emitEvent("allowed", host, req.method || "GET");
|
|
173
|
+
const parsed = new URL(url);
|
|
174
|
+
const proxyReq = http.request({
|
|
175
|
+
hostname: parsed.hostname,
|
|
176
|
+
port: parsed.port || 80,
|
|
177
|
+
path: parsed.pathname + parsed.search,
|
|
178
|
+
method: req.method,
|
|
179
|
+
headers: req.headers
|
|
180
|
+
}, (proxyRes) => {
|
|
181
|
+
res.writeHead(proxyRes.statusCode || 502, proxyRes.headers);
|
|
182
|
+
proxyRes.pipe(res);
|
|
183
|
+
});
|
|
184
|
+
proxyReq.on("error", () => {
|
|
185
|
+
res.writeHead(502, { "Content-Type": "text/plain" });
|
|
186
|
+
res.end("Bad Gateway");
|
|
187
|
+
});
|
|
188
|
+
req.pipe(proxyReq);
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
//#endregion
|
|
192
|
+
//#region src/sandbox/proxy/ProxyManager.ts
|
|
193
|
+
var ProxyManager = class {
|
|
194
|
+
constructor(networkConfig) {
|
|
195
|
+
this.proxy = null;
|
|
196
|
+
this.eventHandlers = /* @__PURE__ */ new Set();
|
|
197
|
+
this.networkConfig = {
|
|
198
|
+
...networkConfig,
|
|
199
|
+
allowedDomains: [...networkConfig.allowedDomains]
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async start() {
|
|
203
|
+
if (!this.networkConfig.enabled) return;
|
|
204
|
+
if (this.proxy?.isRunning()) return;
|
|
205
|
+
this.proxy = new HttpConnectProxy({ allowedDomains: this.networkConfig.allowedDomains });
|
|
206
|
+
this.proxy.on("proxy-event", (event) => {
|
|
207
|
+
for (const handler of this.eventHandlers) handler(event);
|
|
208
|
+
});
|
|
209
|
+
await this.proxy.start();
|
|
210
|
+
}
|
|
211
|
+
async stop() {
|
|
212
|
+
if (!this.proxy) return;
|
|
213
|
+
await this.proxy.stop();
|
|
214
|
+
this.proxy = null;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get proxy env vars for injecting into sandboxed processes.
|
|
218
|
+
* Returns both upper and lowercase variants for maximum compatibility.
|
|
219
|
+
*/
|
|
220
|
+
getProxyEnv() {
|
|
221
|
+
if (!this.proxy?.isRunning()) return {};
|
|
222
|
+
const port = this.proxy.getPort();
|
|
223
|
+
if (!port) return {};
|
|
224
|
+
const proxyUrl = `http://127.0.0.1:${port}`;
|
|
225
|
+
const noProxy = "localhost,127.0.0.1,::1";
|
|
226
|
+
return {
|
|
227
|
+
HTTP_PROXY: proxyUrl,
|
|
228
|
+
http_proxy: proxyUrl,
|
|
229
|
+
HTTPS_PROXY: proxyUrl,
|
|
230
|
+
https_proxy: proxyUrl,
|
|
231
|
+
NO_PROXY: noProxy,
|
|
232
|
+
no_proxy: noProxy
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
addAllowedDomain(domain) {
|
|
236
|
+
if (!this.networkConfig.allowedDomains.includes(domain)) {
|
|
237
|
+
this.networkConfig.allowedDomains.push(domain);
|
|
238
|
+
this.proxy?.updateAllowedDomains(this.networkConfig.allowedDomains);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
getAllowedDomains() {
|
|
242
|
+
return [...this.networkConfig.allowedDomains];
|
|
243
|
+
}
|
|
244
|
+
isRunning() {
|
|
245
|
+
return this.proxy?.isRunning() ?? false;
|
|
246
|
+
}
|
|
247
|
+
getPort() {
|
|
248
|
+
return this.proxy?.getPort() ?? null;
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Subscribe to proxy events. Returns an unsubscribe function.
|
|
252
|
+
*/
|
|
253
|
+
onEvent(handler) {
|
|
254
|
+
this.eventHandlers.add(handler);
|
|
255
|
+
return () => this.eventHandlers.delete(handler);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
//#endregion
|
|
259
|
+
export { ProxyManager as t };
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { t as DEFAULT_SANDBOX_CONFIG } from "./types-DBEjF9YS.mjs";
|
|
3
|
+
//#region src/sandbox/SandboxOrchestrator.ts
|
|
4
|
+
var SandboxOrchestrator = class {
|
|
5
|
+
constructor(config, runtime, proxyManager) {
|
|
6
|
+
this.stats = {
|
|
7
|
+
sandboxed: 0,
|
|
8
|
+
unsandboxed: 0,
|
|
9
|
+
blocked: 0,
|
|
10
|
+
violations: 0
|
|
11
|
+
};
|
|
12
|
+
this.violationStore = null;
|
|
13
|
+
this.config = config ?? DEFAULT_SANDBOX_CONFIG;
|
|
14
|
+
this.runtime = runtime ?? null;
|
|
15
|
+
this.proxyManager = proxyManager ?? null;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Determine whether a command should be sandboxed.
|
|
19
|
+
*
|
|
20
|
+
* Decision logic:
|
|
21
|
+
* 1. If sandbox is disabled → unsandboxed (no permission change)
|
|
22
|
+
* 2. If command matches an excluded command → unsandboxed (requires permission)
|
|
23
|
+
* 3. If runtime is not available → unsandboxed with warning
|
|
24
|
+
* 4. Otherwise → sandbox the command
|
|
25
|
+
*/
|
|
26
|
+
shouldSandbox(command, cwd) {
|
|
27
|
+
if (!this.config.enabled || this.config.mode === "disabled") return {
|
|
28
|
+
type: "unsandboxed",
|
|
29
|
+
requiresPermission: true
|
|
30
|
+
};
|
|
31
|
+
const baseCommand = this.getBaseCommand(command);
|
|
32
|
+
if (this.isExcludedCommand(baseCommand)) {
|
|
33
|
+
if (!this.config.allowUnsandboxedCommands) return {
|
|
34
|
+
type: "blocked",
|
|
35
|
+
reason: `Command '${baseCommand}' is excluded from sandboxing and unsandboxed commands are not allowed`
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
type: "unsandboxed",
|
|
39
|
+
requiresPermission: true,
|
|
40
|
+
reason: `Command '${baseCommand}' is excluded from sandboxing`
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (!this.runtime) return {
|
|
44
|
+
type: "unsandboxed",
|
|
45
|
+
requiresPermission: true,
|
|
46
|
+
reason: "Sandbox runtime not available on this platform"
|
|
47
|
+
};
|
|
48
|
+
const proxyEnv = this.proxyManager?.getProxyEnv() ?? {};
|
|
49
|
+
return {
|
|
50
|
+
type: "sandbox",
|
|
51
|
+
wrappedCommand: this.runtime.wrapCommand({
|
|
52
|
+
command,
|
|
53
|
+
cwd,
|
|
54
|
+
filesystemConfig: this.config.filesystem,
|
|
55
|
+
env: proxyEnv,
|
|
56
|
+
...this.runtime.platform === "linux" && this.config.platform.linux.seccompProfile && { seccompProfile: this.config.platform.linux.seccompProfile }
|
|
57
|
+
})
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/** Get the current sandbox mode */
|
|
61
|
+
getMode() {
|
|
62
|
+
return this.config.mode;
|
|
63
|
+
}
|
|
64
|
+
/** Set the sandbox mode (does not persist — caller must save config) */
|
|
65
|
+
setMode(mode) {
|
|
66
|
+
this.config.mode = mode;
|
|
67
|
+
this.config.enabled = mode !== "disabled";
|
|
68
|
+
}
|
|
69
|
+
/** Check if sandbox is enabled and runtime is available */
|
|
70
|
+
isAvailable() {
|
|
71
|
+
return this.runtime !== null && this.runtime.isAvailable();
|
|
72
|
+
}
|
|
73
|
+
/** Check if sandbox is currently active (enabled + available) */
|
|
74
|
+
isActive() {
|
|
75
|
+
return this.config.enabled && this.config.mode !== "disabled" && this.isAvailable();
|
|
76
|
+
}
|
|
77
|
+
/** Get the current sandbox configuration */
|
|
78
|
+
getConfig() {
|
|
79
|
+
return this.config;
|
|
80
|
+
}
|
|
81
|
+
/** Update config (does not persist — caller must save) */
|
|
82
|
+
updateConfig(config) {
|
|
83
|
+
this.config = config;
|
|
84
|
+
}
|
|
85
|
+
/** Get the ProxyManager instance (if any) */
|
|
86
|
+
getProxyManager() {
|
|
87
|
+
return this.proxyManager;
|
|
88
|
+
}
|
|
89
|
+
/** Start the network proxy (if configured) */
|
|
90
|
+
async startProxy() {
|
|
91
|
+
await this.proxyManager?.start();
|
|
92
|
+
}
|
|
93
|
+
/** Stop the network proxy */
|
|
94
|
+
async stopProxy() {
|
|
95
|
+
await this.proxyManager?.stop();
|
|
96
|
+
}
|
|
97
|
+
/** Get full status information for display */
|
|
98
|
+
getStatus() {
|
|
99
|
+
return {
|
|
100
|
+
mode: this.config.mode,
|
|
101
|
+
enabled: this.config.enabled,
|
|
102
|
+
platform: this.runtime?.platform ?? null,
|
|
103
|
+
runtimeAvailable: this.runtime?.isAvailable() ?? false,
|
|
104
|
+
runtimeName: this.runtime?.name ?? null,
|
|
105
|
+
proxyRunning: this.proxyManager?.isRunning() ?? false,
|
|
106
|
+
proxyPort: this.proxyManager?.getPort() ?? null,
|
|
107
|
+
config: this.config,
|
|
108
|
+
stats: { ...this.stats }
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
recordSandboxed() {
|
|
112
|
+
this.stats.sandboxed++;
|
|
113
|
+
}
|
|
114
|
+
recordUnsandboxed() {
|
|
115
|
+
this.stats.unsandboxed++;
|
|
116
|
+
}
|
|
117
|
+
recordBlocked() {
|
|
118
|
+
this.stats.blocked++;
|
|
119
|
+
}
|
|
120
|
+
recordViolations(count = 1) {
|
|
121
|
+
this.stats.violations += count;
|
|
122
|
+
}
|
|
123
|
+
getStats() {
|
|
124
|
+
return { ...this.stats };
|
|
125
|
+
}
|
|
126
|
+
resetStats() {
|
|
127
|
+
this.stats = {
|
|
128
|
+
sandboxed: 0,
|
|
129
|
+
unsandboxed: 0,
|
|
130
|
+
blocked: 0,
|
|
131
|
+
violations: 0
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
setViolationStore(store) {
|
|
135
|
+
this.violationStore = store;
|
|
136
|
+
}
|
|
137
|
+
getViolationStore() {
|
|
138
|
+
return this.violationStore;
|
|
139
|
+
}
|
|
140
|
+
/** Record a violation to store and increment stats */
|
|
141
|
+
async recordViolation(violation) {
|
|
142
|
+
this.stats.violations++;
|
|
143
|
+
await this.violationStore?.record(violation).catch(() => {});
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Extract the base command name from a full command string.
|
|
147
|
+
* e.g., "docker compose up -d" → "docker"
|
|
148
|
+
*/
|
|
149
|
+
getBaseCommand(command) {
|
|
150
|
+
const first = command.trim().replace(/^(\w+=\S+\s+)*/, "").split(/\s+/)[0] || "";
|
|
151
|
+
return first.split("/").pop() || first;
|
|
152
|
+
}
|
|
153
|
+
/** Check if a command is in the excluded list */
|
|
154
|
+
isExcludedCommand(baseCommand) {
|
|
155
|
+
return this.config.excludedCommands.some((excluded) => baseCommand === excluded);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
//#endregion
|
|
159
|
+
export { SandboxOrchestrator as t };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { accessSync, constants } from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
//#region src/sandbox/runtime/SandboxRuntimeAdapter.ts
|
|
6
|
+
/**
|
|
7
|
+
* Abstract sandbox runtime adapter with platform detection and factory.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Detect the current platform.
|
|
11
|
+
* Returns null if the platform is not supported for sandboxing.
|
|
12
|
+
*/
|
|
13
|
+
function detectPlatform() {
|
|
14
|
+
const platform = os.platform();
|
|
15
|
+
if (platform === "darwin") return "darwin";
|
|
16
|
+
if (platform === "linux") return "linux";
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Check if a binary exists on the system PATH.
|
|
21
|
+
* Uses pure filesystem checks instead of shell execution to avoid command injection.
|
|
22
|
+
*/
|
|
23
|
+
function isBinaryAvailable(binary) {
|
|
24
|
+
const pathDirs = (process.env.PATH || "").split(path.delimiter);
|
|
25
|
+
for (const dir of pathDirs) try {
|
|
26
|
+
accessSync(path.join(dir, binary), constants.X_OK);
|
|
27
|
+
return true;
|
|
28
|
+
} catch {}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Expand environment variables ($HOME, $USER) in a path string.
|
|
33
|
+
*/
|
|
34
|
+
function expandPath(pathStr) {
|
|
35
|
+
return pathStr.replace(/\$HOME/g, os.homedir()).replace(/\$USER/g, os.userInfo().username).replace(/~\//g, `${os.homedir()}/`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Factory function to create the appropriate sandbox runtime for the current platform.
|
|
39
|
+
* Returns null if the platform is unsupported or the runtime binary is not available.
|
|
40
|
+
*/
|
|
41
|
+
async function createSandboxRuntime() {
|
|
42
|
+
const platform = detectPlatform();
|
|
43
|
+
if (!platform) return null;
|
|
44
|
+
if (platform === "darwin") {
|
|
45
|
+
const { SeatbeltRuntime } = await import("./SeatbeltRuntime-D4m0VOcD.mjs");
|
|
46
|
+
const runtime = new SeatbeltRuntime();
|
|
47
|
+
return runtime.isAvailable() ? runtime : null;
|
|
48
|
+
}
|
|
49
|
+
if (platform === "linux") {
|
|
50
|
+
const { BubblewrapRuntime } = await import("./BubblewrapRuntime-BHbtqvLx.mjs");
|
|
51
|
+
const runtime = new BubblewrapRuntime();
|
|
52
|
+
return runtime.isAvailable() ? runtime : null;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
//#endregion
|
|
57
|
+
export { isBinaryAvailable as i, detectPlatform as n, expandPath as r, createSandboxRuntime as t };
|