@hachej/boring-agent 0.1.77 → 0.1.79
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/chunk-LCEBDU6G.js +531 -0
- package/dist/{chunk-ORURYKNY.js → chunk-P23XFC4W.js} +1 -1
- package/dist/chunk-Q42DWR3T.js +264 -0
- package/dist/chunk-RFHSFIHF.js +14037 -0
- package/dist/{chunk-ZUEITFIJ.js → chunk-WSQ5QNIY.js} +1 -12
- package/dist/{chunk-TS3QGFKK.js → chunk-X4WT3TPU.js} +85 -6
- package/dist/{chunk-6AEK34XU.js → chunk-YVON2BHN.js} +11 -1
- package/dist/core/index.d.ts +68 -6
- package/dist/core/index.js +3 -9
- package/dist/{createHarness-RZUU6MJQ.js → createHarness-XHMR3QN3.js} +2 -3
- package/dist/front/index.d.ts +1 -2
- package/dist/front/index.js +2 -2
- package/dist/{harness-OsJBlx4u.d.ts → harness-DD0zj704.d.ts} +3 -11
- package/dist/piChatEvent-D0yuLiJh.d.ts +358 -0
- package/dist/protocol-B_OQKfbI.d.ts +104 -0
- package/dist/{workspaceAgentDispatcher-Cl3EBveh.d.ts → sandbox-DthfJaOB.d.ts} +1 -77
- package/dist/server/index.d.ts +28 -108
- package/dist/server/index.js +64 -10939
- package/dist/server/worker/index.d.ts +65 -0
- package/dist/server/worker/index.js +367 -0
- package/dist/shared/index.d.ts +26 -25
- package/dist/shared/index.js +22 -8
- package/dist/workspaceAgentDispatcher-D3ZEBsH1.d.ts +146 -0
- package/docs/ERROR_CODES.md +1 -1
- package/docs/runtime.md +6 -24
- package/package.json +8 -2
- package/dist/chunk-3WWLQAJB.js +0 -2794
- package/dist/chunk-AJZHR626.js +0 -85
- package/dist/chunk-ZD7MM2LQ.js +0 -14
- package/dist/piChatCommand-BuWXytap.d.ts +0 -60
- package/dist/session-FUiMWsyX.d.ts +0 -297
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { FastifyInstance, FastifyRequest, FastifyReply, FastifyServerOptions } from 'fastify';
|
|
2
|
+
import { B as BwrapResourceLimits, R as RemoteWorkerWorkspaceOp, a as RemoteWorkerWorkspaceResult } from '../../protocol-B_OQKfbI.js';
|
|
3
|
+
import { a as Workspace, S as Sandbox } from '../../sandbox-DthfJaOB.js';
|
|
4
|
+
|
|
5
|
+
interface WorkerConfig {
|
|
6
|
+
workspaceRoot: string;
|
|
7
|
+
internalToken: string;
|
|
8
|
+
port: number;
|
|
9
|
+
host: string;
|
|
10
|
+
execConcurrency: number;
|
|
11
|
+
bwrapNetwork: 'isolated' | 'shared';
|
|
12
|
+
resourceLimits: BwrapResourceLimits;
|
|
13
|
+
}
|
|
14
|
+
declare function loadWorkerConfig(): WorkerConfig;
|
|
15
|
+
|
|
16
|
+
declare function registerWorkerRoutes(app: FastifyInstance, config: WorkerConfig): Promise<void>;
|
|
17
|
+
|
|
18
|
+
declare const WORKER_ERROR_CODES: {
|
|
19
|
+
readonly AUTH_INVALID: "auth_invalid";
|
|
20
|
+
readonly VALIDATION_ERROR: "validation_error";
|
|
21
|
+
readonly INVALID_WORKSPACE_ID: "invalid_workspace_id";
|
|
22
|
+
readonly NOT_IMPLEMENTED: "not_implemented";
|
|
23
|
+
readonly EXEC_CONCURRENCY_LIMIT: "exec_concurrency_limit";
|
|
24
|
+
readonly UNSUPPORTED_WORKSPACE_OP: "unsupported_workspace_op";
|
|
25
|
+
};
|
|
26
|
+
type WorkerErrorCode = (typeof WORKER_ERROR_CODES)[keyof typeof WORKER_ERROR_CODES];
|
|
27
|
+
|
|
28
|
+
interface WorkerRuntime {
|
|
29
|
+
workspace: Workspace;
|
|
30
|
+
sandbox: Sandbox;
|
|
31
|
+
}
|
|
32
|
+
declare function assertSafeWorkspaceId(workspaceId: string): string;
|
|
33
|
+
declare function createWorkerRuntime(root: string, workspaceId: string, options: {
|
|
34
|
+
bwrapNetwork: 'isolated' | 'shared';
|
|
35
|
+
resourceLimits?: BwrapResourceLimits;
|
|
36
|
+
}): Promise<WorkerRuntime>;
|
|
37
|
+
declare function runWorkspaceOp(workspace: Workspace, op: RemoteWorkerWorkspaceOp): Promise<RemoteWorkerWorkspaceResult>;
|
|
38
|
+
|
|
39
|
+
declare class ExecSemaphore {
|
|
40
|
+
private readonly limit;
|
|
41
|
+
private active;
|
|
42
|
+
constructor(limit: number);
|
|
43
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
44
|
+
}
|
|
45
|
+
declare function buildExecEnv(input: Record<string, string> | undefined): Record<string, string>;
|
|
46
|
+
|
|
47
|
+
declare function verifyInternalToken(request: FastifyRequest, reply: FastifyReply, expectedToken: string): boolean;
|
|
48
|
+
|
|
49
|
+
interface CreateWorkerServerOptions {
|
|
50
|
+
/** Worker configuration. Defaults to {@link loadWorkerConfig} (reads env). */
|
|
51
|
+
config?: WorkerConfig;
|
|
52
|
+
/** Extra Fastify server options merged over the worker defaults. */
|
|
53
|
+
fastify?: FastifyServerOptions;
|
|
54
|
+
}
|
|
55
|
+
interface WorkerServer {
|
|
56
|
+
app: FastifyInstance;
|
|
57
|
+
config: WorkerConfig;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Build the internal worker Fastify server with worker routes registered.
|
|
61
|
+
* Callers own listening/lifecycle.
|
|
62
|
+
*/
|
|
63
|
+
declare function createWorkerServer(options?: CreateWorkerServerOptions): Promise<WorkerServer>;
|
|
64
|
+
|
|
65
|
+
export { type CreateWorkerServerOptions, ExecSemaphore, WORKER_ERROR_CODES, type WorkerConfig, type WorkerErrorCode, type WorkerRuntime, type WorkerServer, assertSafeWorkspaceId, buildExecEnv, createWorkerRuntime, createWorkerServer, loadWorkerConfig, registerWorkerRoutes, runWorkspaceOp, verifyInternalToken };
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import {
|
|
2
|
+
REMOTE_WORKER_RUNTIME_CWD,
|
|
3
|
+
WORKER_INTERNAL_TOKEN_HEADER,
|
|
4
|
+
constantTimeTokenEqual,
|
|
5
|
+
createBwrapSandbox,
|
|
6
|
+
createNodeWorkspace
|
|
7
|
+
} from "../../chunk-RFHSFIHF.js";
|
|
8
|
+
import "../../chunk-Q42DWR3T.js";
|
|
9
|
+
import "../../chunk-LCEBDU6G.js";
|
|
10
|
+
import "../../chunk-WSQ5QNIY.js";
|
|
11
|
+
import "../../chunk-P23XFC4W.js";
|
|
12
|
+
import "../../chunk-X4WT3TPU.js";
|
|
13
|
+
import "../../chunk-AQBXNPMD.js";
|
|
14
|
+
import "../../chunk-YVON2BHN.js";
|
|
15
|
+
|
|
16
|
+
// src/server/worker/index.ts
|
|
17
|
+
import Fastify from "fastify";
|
|
18
|
+
|
|
19
|
+
// src/server/config/workerConfig.ts
|
|
20
|
+
var FILE_SIZE_BLOCKS_PER_MIB = 2048;
|
|
21
|
+
var KIB_PER_MIB = 1024;
|
|
22
|
+
function requireEnv(name) {
|
|
23
|
+
const value = process.env[name]?.trim();
|
|
24
|
+
if (!value) throw new Error(`${name} is required`);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
function numberEnv(name, fallback) {
|
|
28
|
+
const raw = process.env[name]?.trim();
|
|
29
|
+
if (!raw) return fallback;
|
|
30
|
+
const parsed = Number.parseInt(raw, 10);
|
|
31
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
32
|
+
}
|
|
33
|
+
function bwrapNetworkEnv() {
|
|
34
|
+
const raw = process.env.BORING_WORKER_BWRAP_NETWORK?.trim().toLowerCase();
|
|
35
|
+
return raw === "shared" ? "shared" : "isolated";
|
|
36
|
+
}
|
|
37
|
+
function resourceLimitsEnv() {
|
|
38
|
+
return {
|
|
39
|
+
cpuSeconds: numberEnv("BORING_WORKER_EXEC_CPU_SECONDS", 30),
|
|
40
|
+
fileSizeBlocks: numberEnv("BORING_WORKER_EXEC_FILE_SIZE_MIB", 64) * FILE_SIZE_BLOCKS_PER_MIB,
|
|
41
|
+
maxProcesses: numberEnv("BORING_WORKER_EXEC_MAX_PROCESSES", 512),
|
|
42
|
+
openFiles: numberEnv("BORING_WORKER_EXEC_OPEN_FILES", 256),
|
|
43
|
+
virtualMemoryKb: numberEnv("BORING_WORKER_EXEC_VIRTUAL_MEMORY_MIB", 1024) * KIB_PER_MIB
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function loadWorkerConfig() {
|
|
47
|
+
return {
|
|
48
|
+
workspaceRoot: requireEnv("BORING_WORKER_WORKSPACE_ROOT"),
|
|
49
|
+
internalToken: requireEnv("BORING_WORKER_INTERNAL_TOKEN"),
|
|
50
|
+
port: numberEnv("PORT", 3e3),
|
|
51
|
+
host: process.env.HOST?.trim() || "0.0.0.0",
|
|
52
|
+
execConcurrency: numberEnv("BORING_WORKER_EXEC_CONCURRENCY", 2),
|
|
53
|
+
bwrapNetwork: bwrapNetworkEnv(),
|
|
54
|
+
resourceLimits: resourceLimitsEnv()
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/server/worker/error-codes.ts
|
|
59
|
+
var WORKER_ERROR_CODES = {
|
|
60
|
+
AUTH_INVALID: "auth_invalid",
|
|
61
|
+
VALIDATION_ERROR: "validation_error",
|
|
62
|
+
INVALID_WORKSPACE_ID: "invalid_workspace_id",
|
|
63
|
+
NOT_IMPLEMENTED: "not_implemented",
|
|
64
|
+
EXEC_CONCURRENCY_LIMIT: "exec_concurrency_limit",
|
|
65
|
+
UNSUPPORTED_WORKSPACE_OP: "unsupported_workspace_op"
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// src/server/worker/auth.ts
|
|
69
|
+
function verifyInternalToken(request, reply, expectedToken) {
|
|
70
|
+
const provided = request.headers[WORKER_INTERNAL_TOKEN_HEADER];
|
|
71
|
+
const token = Array.isArray(provided) ? provided[0] : provided;
|
|
72
|
+
if (typeof token !== "string" || !constantTimeTokenEqual(token, expectedToken)) {
|
|
73
|
+
reply.code(401).send({
|
|
74
|
+
error: {
|
|
75
|
+
code: WORKER_ERROR_CODES.AUTH_INVALID,
|
|
76
|
+
message: "invalid internal token",
|
|
77
|
+
statusCode: 401
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// src/server/worker/exec.ts
|
|
86
|
+
var ExecSemaphore = class {
|
|
87
|
+
constructor(limit) {
|
|
88
|
+
this.limit = limit;
|
|
89
|
+
}
|
|
90
|
+
limit;
|
|
91
|
+
active = 0;
|
|
92
|
+
async run(fn) {
|
|
93
|
+
if (this.active >= this.limit) {
|
|
94
|
+
throw Object.assign(new Error("worker exec concurrency limit reached"), {
|
|
95
|
+
statusCode: 429,
|
|
96
|
+
code: WORKER_ERROR_CODES.EXEC_CONCURRENCY_LIMIT
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
this.active += 1;
|
|
100
|
+
try {
|
|
101
|
+
return await fn();
|
|
102
|
+
} finally {
|
|
103
|
+
this.active -= 1;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
function buildExecEnv(input) {
|
|
108
|
+
const safe = {
|
|
109
|
+
PATH: "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
|
|
110
|
+
HOME: "/workspace",
|
|
111
|
+
LANG: "C.UTF-8"
|
|
112
|
+
};
|
|
113
|
+
if (!input) return safe;
|
|
114
|
+
for (const [key, value] of Object.entries(input)) {
|
|
115
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) continue;
|
|
116
|
+
if (key.startsWith("BORING_WORKER_")) continue;
|
|
117
|
+
if (key.endsWith("_API_KEY") || key.endsWith("_TOKEN") || key.endsWith("_SECRET")) continue;
|
|
118
|
+
if (key === "DATABASE_URL") continue;
|
|
119
|
+
safe[key] = String(value);
|
|
120
|
+
}
|
|
121
|
+
return safe;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// src/server/worker/workspace.ts
|
|
125
|
+
import { mkdir } from "fs/promises";
|
|
126
|
+
import { join } from "path";
|
|
127
|
+
var WORKSPACE_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
128
|
+
function assertSafeWorkspaceId(workspaceId) {
|
|
129
|
+
const normalized = workspaceId.trim().toLowerCase();
|
|
130
|
+
if (!WORKSPACE_UUID.test(normalized)) {
|
|
131
|
+
throw Object.assign(new Error("workspace id must be a uuid"), { statusCode: 400, code: WORKER_ERROR_CODES.INVALID_WORKSPACE_ID });
|
|
132
|
+
}
|
|
133
|
+
return normalized;
|
|
134
|
+
}
|
|
135
|
+
async function createWorkerRuntime(root, workspaceId, options) {
|
|
136
|
+
const safeId = assertSafeWorkspaceId(workspaceId);
|
|
137
|
+
const hostRoot = join(root, safeId);
|
|
138
|
+
await mkdir(hostRoot, { recursive: true });
|
|
139
|
+
const workspace = createNodeWorkspace(hostRoot, { runtimeContext: { runtimeCwd: REMOTE_WORKER_RUNTIME_CWD } });
|
|
140
|
+
const sandbox = createBwrapSandbox({
|
|
141
|
+
hostWorkspaceRoot: hostRoot,
|
|
142
|
+
runtimeContext: { runtimeCwd: REMOTE_WORKER_RUNTIME_CWD },
|
|
143
|
+
network: options.bwrapNetwork,
|
|
144
|
+
dropAllCapabilities: true,
|
|
145
|
+
resourceLimits: options.resourceLimits
|
|
146
|
+
});
|
|
147
|
+
await sandbox.init?.({ workspace, sessionId: safeId });
|
|
148
|
+
return { workspace, sandbox };
|
|
149
|
+
}
|
|
150
|
+
async function runWorkspaceOp(workspace, op) {
|
|
151
|
+
switch (op.op) {
|
|
152
|
+
case "readFile":
|
|
153
|
+
return { content: await workspace.readFile(op.path) };
|
|
154
|
+
case "readBinaryFile":
|
|
155
|
+
if (!workspace.readBinaryFile) throw Object.assign(new Error("binary read unsupported"), { statusCode: 501, code: WORKER_ERROR_CODES.NOT_IMPLEMENTED });
|
|
156
|
+
return { dataBase64: Buffer.from(await workspace.readBinaryFile(op.path)).toString("base64") };
|
|
157
|
+
case "writeFile":
|
|
158
|
+
await workspace.writeFile(op.path, op.data);
|
|
159
|
+
return { ok: true };
|
|
160
|
+
case "writeBinaryFile":
|
|
161
|
+
if (!workspace.writeBinaryFile) throw Object.assign(new Error("binary write unsupported"), { statusCode: 501, code: WORKER_ERROR_CODES.NOT_IMPLEMENTED });
|
|
162
|
+
await workspace.writeBinaryFile(op.path, new Uint8Array(Buffer.from(op.dataBase64, "base64")));
|
|
163
|
+
return { ok: true };
|
|
164
|
+
case "readFileWithStat":
|
|
165
|
+
if (!workspace.readFileWithStat) {
|
|
166
|
+
return { content: await workspace.readFile(op.path), stat: await workspace.stat(op.path) };
|
|
167
|
+
}
|
|
168
|
+
return await workspace.readFileWithStat(op.path);
|
|
169
|
+
case "writeFileWithStat":
|
|
170
|
+
if (!workspace.writeFileWithStat) {
|
|
171
|
+
await workspace.writeFile(op.path, op.data);
|
|
172
|
+
return { stat: await workspace.stat(op.path) };
|
|
173
|
+
}
|
|
174
|
+
return { stat: await workspace.writeFileWithStat(op.path, op.data) };
|
|
175
|
+
case "writeBinaryFileWithStat":
|
|
176
|
+
if (!workspace.writeBinaryFile) throw Object.assign(new Error("binary write unsupported"), { statusCode: 501, code: WORKER_ERROR_CODES.NOT_IMPLEMENTED });
|
|
177
|
+
if (!workspace.writeBinaryFileWithStat) {
|
|
178
|
+
await workspace.writeBinaryFile(op.path, new Uint8Array(Buffer.from(op.dataBase64, "base64")));
|
|
179
|
+
return { stat: await workspace.stat(op.path) };
|
|
180
|
+
}
|
|
181
|
+
return { stat: await workspace.writeBinaryFileWithStat(op.path, new Uint8Array(Buffer.from(op.dataBase64, "base64"))) };
|
|
182
|
+
case "unlink":
|
|
183
|
+
await workspace.unlink(op.path);
|
|
184
|
+
return { ok: true };
|
|
185
|
+
case "readdir":
|
|
186
|
+
return { entries: await workspace.readdir(op.path) };
|
|
187
|
+
case "stat":
|
|
188
|
+
return { stat: await workspace.stat(op.path) };
|
|
189
|
+
case "mkdir":
|
|
190
|
+
await workspace.mkdir(op.path, { recursive: op.recursive });
|
|
191
|
+
return { ok: true };
|
|
192
|
+
case "rename":
|
|
193
|
+
await workspace.rename(op.from, op.to);
|
|
194
|
+
return { ok: true };
|
|
195
|
+
default: {
|
|
196
|
+
const _never = op;
|
|
197
|
+
throw Object.assign(new Error(`unsupported workspace op ${op.op ?? "unknown"}`), {
|
|
198
|
+
statusCode: 400,
|
|
199
|
+
code: WORKER_ERROR_CODES.UNSUPPORTED_WORKSPACE_OP,
|
|
200
|
+
details: _never
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/server/worker/routes.ts
|
|
207
|
+
function errorPayload(error) {
|
|
208
|
+
const record = error;
|
|
209
|
+
const statusCode = typeof record.statusCode === "number" ? record.statusCode : 500;
|
|
210
|
+
return {
|
|
211
|
+
error: {
|
|
212
|
+
code: typeof record.code === "string" ? record.code : statusCode >= 500 ? "internal" : "bad_request",
|
|
213
|
+
message: typeof record.message === "string" ? record.message : "worker request failed",
|
|
214
|
+
statusCode,
|
|
215
|
+
...record.details === void 0 ? {} : { details: record.details }
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function sendError(reply, error) {
|
|
220
|
+
const payload = errorPayload(error);
|
|
221
|
+
reply.code(payload.error.statusCode).send(payload);
|
|
222
|
+
}
|
|
223
|
+
function resultToResponse(result) {
|
|
224
|
+
return {
|
|
225
|
+
stdoutBase64: Buffer.from(result.stdout).toString("base64"),
|
|
226
|
+
stderrBase64: Buffer.from(result.stderr).toString("base64"),
|
|
227
|
+
exitCode: result.exitCode,
|
|
228
|
+
durationMs: result.durationMs,
|
|
229
|
+
truncated: result.truncated,
|
|
230
|
+
stdoutEncoding: result.stdoutEncoding,
|
|
231
|
+
stderrEncoding: result.stderrEncoding
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
async function registerWorkerRoutes(app, config) {
|
|
235
|
+
const semaphore = new ExecSemaphore(config.execConcurrency);
|
|
236
|
+
const runtimes = /* @__PURE__ */ new Map();
|
|
237
|
+
const getRuntime = (workspaceId) => {
|
|
238
|
+
const safeId = assertSafeWorkspaceId(workspaceId);
|
|
239
|
+
let runtime = runtimes.get(safeId);
|
|
240
|
+
if (!runtime) {
|
|
241
|
+
runtime = createWorkerRuntime(config.workspaceRoot, safeId, {
|
|
242
|
+
bwrapNetwork: config.bwrapNetwork,
|
|
243
|
+
resourceLimits: config.resourceLimits
|
|
244
|
+
});
|
|
245
|
+
runtimes.set(safeId, runtime);
|
|
246
|
+
}
|
|
247
|
+
return runtime;
|
|
248
|
+
};
|
|
249
|
+
app.addHook("onClose", async () => {
|
|
250
|
+
for (const runtimePromise of runtimes.values()) {
|
|
251
|
+
try {
|
|
252
|
+
const runtime = await runtimePromise;
|
|
253
|
+
runtime.workspace.watch?.().close();
|
|
254
|
+
} catch {
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
app.get("/health", async () => ({ ok: true }));
|
|
259
|
+
app.get("/internal/health", async () => ({ ok: true }));
|
|
260
|
+
app.addHook("preHandler", async (request, reply) => {
|
|
261
|
+
if (request.method === "GET" && request.url === "/health") return;
|
|
262
|
+
if (!verifyInternalToken(request, reply, config.internalToken)) return reply;
|
|
263
|
+
});
|
|
264
|
+
app.post(
|
|
265
|
+
"/internal/workspaces/:workspaceId/fs",
|
|
266
|
+
async (request, reply) => {
|
|
267
|
+
try {
|
|
268
|
+
const runtime = await getRuntime(request.params.workspaceId);
|
|
269
|
+
if (!request.body || typeof request.body.op !== "string") {
|
|
270
|
+
throw Object.assign(new Error("workspace op is required"), { statusCode: 400, code: WORKER_ERROR_CODES.VALIDATION_ERROR });
|
|
271
|
+
}
|
|
272
|
+
return await runWorkspaceOp(runtime.workspace, request.body);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
sendError(reply, error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
);
|
|
278
|
+
app.post(
|
|
279
|
+
"/internal/workspaces/:workspaceId/exec",
|
|
280
|
+
async (request, reply) => {
|
|
281
|
+
try {
|
|
282
|
+
const runtime = await getRuntime(request.params.workspaceId);
|
|
283
|
+
const body = request.body;
|
|
284
|
+
if (!body || typeof body.cmd !== "string" || body.cmd.length === 0) {
|
|
285
|
+
throw Object.assign(new Error("cmd is required"), { statusCode: 400, code: WORKER_ERROR_CODES.VALIDATION_ERROR });
|
|
286
|
+
}
|
|
287
|
+
const abortController = new AbortController();
|
|
288
|
+
const abort = () => abortController.abort();
|
|
289
|
+
request.raw.on("aborted", abort);
|
|
290
|
+
reply.raw.on("close", abort);
|
|
291
|
+
const result = await semaphore.run(() => runtime.sandbox.exec(body.cmd, {
|
|
292
|
+
cwd: body.cwd,
|
|
293
|
+
env: buildExecEnv(body.env),
|
|
294
|
+
timeoutMs: body.timeoutMs,
|
|
295
|
+
maxOutputBytes: body.maxOutputBytes,
|
|
296
|
+
signal: abortController.signal
|
|
297
|
+
}));
|
|
298
|
+
return resultToResponse(result);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
sendError(reply, error);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
app.get(
|
|
305
|
+
"/internal/workspaces/:workspaceId/fs/events",
|
|
306
|
+
async (request, reply) => {
|
|
307
|
+
let unsubscribe = null;
|
|
308
|
+
let heartbeat = null;
|
|
309
|
+
try {
|
|
310
|
+
const runtime = await getRuntime(request.params.workspaceId);
|
|
311
|
+
reply.hijack();
|
|
312
|
+
reply.raw.writeHead(200, {
|
|
313
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
314
|
+
"cache-control": "no-cache, no-transform",
|
|
315
|
+
connection: "keep-alive",
|
|
316
|
+
"x-accel-buffering": "no"
|
|
317
|
+
});
|
|
318
|
+
reply.raw.flushHeaders?.();
|
|
319
|
+
reply.raw.write(": connected\n\n");
|
|
320
|
+
const send = (payload) => {
|
|
321
|
+
reply.raw.write(`event: change
|
|
322
|
+
data: ${JSON.stringify(payload)}
|
|
323
|
+
|
|
324
|
+
`);
|
|
325
|
+
};
|
|
326
|
+
if (!runtime.workspace.watch) {
|
|
327
|
+
throw Object.assign(new Error("workspace events unsupported"), { statusCode: 501, code: WORKER_ERROR_CODES.NOT_IMPLEMENTED });
|
|
328
|
+
}
|
|
329
|
+
unsubscribe = runtime.workspace.watch().subscribe((event) => send({ event }));
|
|
330
|
+
heartbeat = setInterval(() => reply.raw.write(": heartbeat\n\n"), 15e3);
|
|
331
|
+
const cleanup = () => {
|
|
332
|
+
unsubscribe?.();
|
|
333
|
+
unsubscribe = null;
|
|
334
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
335
|
+
heartbeat = null;
|
|
336
|
+
};
|
|
337
|
+
request.raw.on("close", cleanup);
|
|
338
|
+
reply.raw.on("close", cleanup);
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
341
|
+
unsubscribe?.();
|
|
342
|
+
if (!reply.sent) sendError(reply, error);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/server/worker/index.ts
|
|
349
|
+
var DEFAULT_BODY_LIMIT = 20 * 1024 * 1024;
|
|
350
|
+
async function createWorkerServer(options = {}) {
|
|
351
|
+
const config = options.config ?? loadWorkerConfig();
|
|
352
|
+
const app = Fastify({ logger: true, bodyLimit: DEFAULT_BODY_LIMIT, ...options.fastify });
|
|
353
|
+
await registerWorkerRoutes(app, config);
|
|
354
|
+
return { app, config };
|
|
355
|
+
}
|
|
356
|
+
export {
|
|
357
|
+
ExecSemaphore,
|
|
358
|
+
WORKER_ERROR_CODES,
|
|
359
|
+
assertSafeWorkspaceId,
|
|
360
|
+
buildExecEnv,
|
|
361
|
+
createWorkerRuntime,
|
|
362
|
+
createWorkerServer,
|
|
363
|
+
loadWorkerConfig,
|
|
364
|
+
registerWorkerRoutes,
|
|
365
|
+
runWorkspaceOp,
|
|
366
|
+
verifyInternalToken
|
|
367
|
+
};
|