@workerdeck/server 2.0.0 → 2.2.0
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/build/index.d.mts +7 -1
- package/build/index.mjs +176 -12
- package/build/index.mjs.map +1 -1
- package/package.json +5 -5
package/build/index.d.mts
CHANGED
|
@@ -160,6 +160,11 @@ type WorkerServerOptions = {
|
|
|
160
160
|
maxEntries?: number;
|
|
161
161
|
ignore?: string[];
|
|
162
162
|
};
|
|
163
|
+
shell?: {
|
|
164
|
+
enabled?: boolean;
|
|
165
|
+
timeoutMs?: number;
|
|
166
|
+
maxOutputBytes?: number;
|
|
167
|
+
};
|
|
163
168
|
attachments?: {
|
|
164
169
|
maxFileBytes?: number;
|
|
165
170
|
maxSessionBytes?: number;
|
|
@@ -175,6 +180,7 @@ type WorkerServerOptions = {
|
|
|
175
180
|
};
|
|
176
181
|
maxBodyBytes?: number;
|
|
177
182
|
disableBypassPermissions?: boolean;
|
|
183
|
+
approvalTimeoutMs?: number | null;
|
|
178
184
|
requireApiKey?: boolean;
|
|
179
185
|
checkCredentials?: boolean | {
|
|
180
186
|
probe?: ClaudeAuthProbe;
|
|
@@ -270,7 +276,7 @@ type ProviderRunnerOptions = {
|
|
|
270
276
|
toolName: string;
|
|
271
277
|
input: unknown;
|
|
272
278
|
}) => boolean;
|
|
273
|
-
approvalTimeoutMs?: number;
|
|
279
|
+
approvalTimeoutMs?: number | null;
|
|
274
280
|
onClose?: () => void | Promise<void>;
|
|
275
281
|
};
|
|
276
282
|
declare function createProviderRunner(ctx: EngineRunnerContext, options: ProviderRunnerOptions): Promise<Runner>;
|
package/build/index.mjs
CHANGED
|
@@ -2,12 +2,13 @@ import { createServer } from "node:http";
|
|
|
2
2
|
import { WebSocketServer } from "ws";
|
|
3
3
|
import { BrowserBridgeExecutor, SessionRunner, attachmentKind, checkClaudeAuth, createEngineSession, getEngineAdapter, normalizeMediaType } from "@workerdeck/core";
|
|
4
4
|
import { JobQueue } from "@workerdeck/queue";
|
|
5
|
-
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, imagePartRef, sessionState, supportsPermissionMode } from "@workerdeck/protocol";
|
|
5
|
+
import { ENGINE_CAPABILITIES, PROTOCOL_VERSION, SHELL_COMMAND_MAX, imagePartRef, sessionState, supportsPermissionMode } from "@workerdeck/protocol";
|
|
6
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
7
7
|
import { closeSync, constants, createReadStream, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { homedir } from "node:os";
|
|
8
|
+
import { constants as constants$1, homedir } from "node:os";
|
|
9
9
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
10
|
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
11
12
|
//#region src/lib/http.ts
|
|
12
13
|
const CONTENT_TYPES = {
|
|
13
14
|
json: "application/json; charset=utf-8",
|
|
@@ -1626,9 +1627,152 @@ async function handleSessions(ctx, req, res, route, auth) {
|
|
|
1626
1627
|
}
|
|
1627
1628
|
json(res, 405, { error: "method not allowed" });
|
|
1628
1629
|
}
|
|
1630
|
+
const TIMEOUT_EXIT_CODE = 124;
|
|
1631
|
+
const SPAWN_FAILURE_EXIT_CODE = 127;
|
|
1632
|
+
const KILLED_EXIT_CODE = 128 + constants$1.signals.SIGKILL;
|
|
1633
|
+
function shellPermitted(shell, runner, operator) {
|
|
1634
|
+
if (shell === null || !operator) return false;
|
|
1635
|
+
const info = runner.info();
|
|
1636
|
+
return (info.capabilities ?? (info.engine ? ENGINE_CAPABILITIES[info.engine] : void 0))?.hostCwd === true;
|
|
1637
|
+
}
|
|
1638
|
+
function shellChildEnv(base) {
|
|
1639
|
+
const env = {};
|
|
1640
|
+
for (const [key, value] of Object.entries(base)) if (value !== void 0) env[key] = value;
|
|
1641
|
+
return env;
|
|
1642
|
+
}
|
|
1643
|
+
function createShellService(options = {}) {
|
|
1644
|
+
const timeoutMs = options.timeoutMs ?? 12e4;
|
|
1645
|
+
const maxOutputBytes = options.maxOutputBytes ?? 32768;
|
|
1646
|
+
const active = /* @__PURE__ */ new Map();
|
|
1647
|
+
const run = async (sessionId, cwd, command) => {
|
|
1648
|
+
if (active.has(sessionId)) throw new Error("a shell command is still running on this session");
|
|
1649
|
+
const child = spawnShell({
|
|
1650
|
+
command,
|
|
1651
|
+
cwd,
|
|
1652
|
+
env: shellChildEnv(process.env),
|
|
1653
|
+
timeoutMs,
|
|
1654
|
+
maxOutputBytes
|
|
1655
|
+
});
|
|
1656
|
+
active.set(sessionId, child);
|
|
1657
|
+
try {
|
|
1658
|
+
return await child.result;
|
|
1659
|
+
} finally {
|
|
1660
|
+
if (active.get(sessionId) === child) active.delete(sessionId);
|
|
1661
|
+
}
|
|
1662
|
+
};
|
|
1663
|
+
const kill = (sessionId, reason = "session closed") => {
|
|
1664
|
+
active.get(sessionId)?.kill(reason);
|
|
1665
|
+
};
|
|
1666
|
+
const watch = (runner) => runner.subscribe((event) => {
|
|
1667
|
+
if (event.type === "session_closed") kill(runner.id, "session closed");
|
|
1668
|
+
else if (event.type === "status_changed" && event.status === "parked") kill(runner.id, "session parked");
|
|
1669
|
+
}, runner.info().lastSeq);
|
|
1670
|
+
const killAll = () => {
|
|
1671
|
+
for (const sessionId of active.keys()) kill(sessionId, "server shutting down");
|
|
1672
|
+
};
|
|
1673
|
+
const running = (sessionId) => active.has(sessionId);
|
|
1674
|
+
return {
|
|
1675
|
+
run,
|
|
1676
|
+
kill,
|
|
1677
|
+
watch,
|
|
1678
|
+
killAll,
|
|
1679
|
+
running,
|
|
1680
|
+
timeoutMs,
|
|
1681
|
+
maxOutputBytes
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
var BoundedOutput = class {
|
|
1685
|
+
#chunks = [];
|
|
1686
|
+
#kept = 0;
|
|
1687
|
+
#dropped = 0;
|
|
1688
|
+
#budget;
|
|
1689
|
+
constructor(budget) {
|
|
1690
|
+
this.#budget = budget;
|
|
1691
|
+
}
|
|
1692
|
+
push(chunk) {
|
|
1693
|
+
const take = Math.min(chunk.length, this.#budget.remaining);
|
|
1694
|
+
if (take > 0) {
|
|
1695
|
+
this.#chunks.push(take === chunk.length ? chunk : chunk.subarray(0, take));
|
|
1696
|
+
this.#kept += take;
|
|
1697
|
+
this.#budget.remaining -= take;
|
|
1698
|
+
}
|
|
1699
|
+
this.#dropped += chunk.length - take;
|
|
1700
|
+
}
|
|
1701
|
+
text() {
|
|
1702
|
+
const head = Buffer.concat(this.#chunks, this.#kept).toString("utf8");
|
|
1703
|
+
return this.#dropped > 0 ? `${head}\n[output truncated: ${this.#dropped} more bytes dropped]` : head;
|
|
1704
|
+
}
|
|
1705
|
+
};
|
|
1706
|
+
function spawnShell(input) {
|
|
1707
|
+
const startedAt = Date.now();
|
|
1708
|
+
const budget = { remaining: input.maxOutputBytes };
|
|
1709
|
+
const stdout = new BoundedOutput(budget);
|
|
1710
|
+
const stderr = new BoundedOutput(budget);
|
|
1711
|
+
let settle;
|
|
1712
|
+
const result = new Promise((resolve) => {
|
|
1713
|
+
settle = resolve;
|
|
1714
|
+
});
|
|
1715
|
+
let done = false;
|
|
1716
|
+
const finish = (exitCode, note) => {
|
|
1717
|
+
if (done) return;
|
|
1718
|
+
done = true;
|
|
1719
|
+
clearTimeout(timer);
|
|
1720
|
+
const captured = stderr.text();
|
|
1721
|
+
const stderrText = note ? captured ? `${captured}\n[${note}]` : `[${note}]` : captured;
|
|
1722
|
+
settle?.({
|
|
1723
|
+
command: input.command,
|
|
1724
|
+
stdout: stdout.text(),
|
|
1725
|
+
stderr: stderrText,
|
|
1726
|
+
exitCode,
|
|
1727
|
+
durationMs: Date.now() - startedAt
|
|
1728
|
+
});
|
|
1729
|
+
};
|
|
1730
|
+
const child = spawn("/bin/sh", ["-c", input.command], {
|
|
1731
|
+
cwd: input.cwd,
|
|
1732
|
+
env: {
|
|
1733
|
+
...input.env,
|
|
1734
|
+
PWD: input.cwd
|
|
1735
|
+
},
|
|
1736
|
+
stdio: [
|
|
1737
|
+
"ignore",
|
|
1738
|
+
"pipe",
|
|
1739
|
+
"pipe"
|
|
1740
|
+
],
|
|
1741
|
+
detached: true
|
|
1742
|
+
});
|
|
1743
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
1744
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
1745
|
+
const killGroup = () => {
|
|
1746
|
+
if (child.pid === void 0) return;
|
|
1747
|
+
try {
|
|
1748
|
+
process.kill(-child.pid, "SIGKILL");
|
|
1749
|
+
} catch {
|
|
1750
|
+
try {
|
|
1751
|
+
child.kill("SIGKILL");
|
|
1752
|
+
} catch {}
|
|
1753
|
+
}
|
|
1754
|
+
child.stdout.destroy();
|
|
1755
|
+
child.stderr.destroy();
|
|
1756
|
+
};
|
|
1757
|
+
const kill = (reason, exitCode = KILLED_EXIT_CODE) => {
|
|
1758
|
+
if (done) return;
|
|
1759
|
+
killGroup();
|
|
1760
|
+
finish(exitCode, `killed: ${reason}`);
|
|
1761
|
+
};
|
|
1762
|
+
const timer = setTimeout(() => kill(`timed out after ${Math.round(input.timeoutMs / 1e3)}s`, TIMEOUT_EXIT_CODE), input.timeoutMs);
|
|
1763
|
+
child.on("error", (error) => finish(SPAWN_FAILURE_EXIT_CODE, error.message));
|
|
1764
|
+
child.on("close", (code, signal) => {
|
|
1765
|
+
const signalCode = signal ? constants$1.signals[signal] ?? 0 : 0;
|
|
1766
|
+
finish(code ?? (signalCode ? 128 + signalCode : 1), signal ? `killed by ${signal}` : void 0);
|
|
1767
|
+
});
|
|
1768
|
+
return {
|
|
1769
|
+
result,
|
|
1770
|
+
kill: (reason) => kill(reason)
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1629
1773
|
//#endregion
|
|
1630
1774
|
//#region src/routes/ws.ts
|
|
1631
|
-
function attachClient(ctx, ws, runner, req) {
|
|
1775
|
+
function attachClient(ctx, ws, runner, req, access) {
|
|
1632
1776
|
const { bridge, parking } = ctx;
|
|
1633
1777
|
const url = new URL(req.url ?? "/", "http://internal");
|
|
1634
1778
|
const afterSeq = Number(url.searchParams.get("afterSeq") ?? "0") || 0;
|
|
@@ -1641,7 +1785,8 @@ function attachClient(ctx, ws, runner, req) {
|
|
|
1641
1785
|
type: "attached",
|
|
1642
1786
|
protocolVersion: PROTOCOL_VERSION,
|
|
1643
1787
|
session: ctx.projects.withProject(runner.info()),
|
|
1644
|
-
replayingFrom: afterSeq
|
|
1788
|
+
replayingFrom: afterSeq,
|
|
1789
|
+
...shellPermitted(ctx.shell, runner, access.operator) ? { shell: true } : {}
|
|
1645
1790
|
});
|
|
1646
1791
|
const unsubscribe = runner.subscribe((event) => send({
|
|
1647
1792
|
type: "event",
|
|
@@ -1663,7 +1808,7 @@ function attachClient(ctx, ws, runner, req) {
|
|
|
1663
1808
|
});
|
|
1664
1809
|
return;
|
|
1665
1810
|
}
|
|
1666
|
-
handleCommand(ctx, frame, runner).catch((error) => {
|
|
1811
|
+
handleCommand(ctx, frame, runner, access).catch((error) => {
|
|
1667
1812
|
send({
|
|
1668
1813
|
type: "protocol_error",
|
|
1669
1814
|
message: error instanceof Error ? error.message : "command failed"
|
|
@@ -1676,7 +1821,7 @@ function attachClient(ctx, ws, runner, req) {
|
|
|
1676
1821
|
parking.onDetach(runner.id);
|
|
1677
1822
|
});
|
|
1678
1823
|
}
|
|
1679
|
-
async function handleCommand(ctx, frame, runner) {
|
|
1824
|
+
async function handleCommand(ctx, frame, runner, access) {
|
|
1680
1825
|
const { attachmentStore, bridge } = ctx;
|
|
1681
1826
|
switch (frame.type) {
|
|
1682
1827
|
case "user_message": {
|
|
@@ -1727,6 +1872,16 @@ async function handleCommand(ctx, frame, runner) {
|
|
|
1727
1872
|
logs: frame.logs
|
|
1728
1873
|
});
|
|
1729
1874
|
return;
|
|
1875
|
+
case "shell_command": {
|
|
1876
|
+
if (!shellPermitted(ctx.shell, runner, access.operator) || ctx.shell === null) throw new Error("shell commands are not available on this session");
|
|
1877
|
+
if (typeof frame.command !== "string" || frame.command.includes("\0")) throw new Error("shell command must be a string");
|
|
1878
|
+
if (frame.command.length > SHELL_COMMAND_MAX) throw new Error(`shell command exceeds ${SHELL_COMMAND_MAX} characters`);
|
|
1879
|
+
if (frame.command.trim() === "") throw new Error("shell command is empty");
|
|
1880
|
+
if (!runner.queueLocalCommand) throw new Error(`the ${runner.info().engine ?? "claude"} engine cannot take shell output`);
|
|
1881
|
+
const result = await ctx.shell.run(runner.id, runner.info().cwd, frame.command);
|
|
1882
|
+
runner.queueLocalCommand(result);
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1730
1885
|
case "close":
|
|
1731
1886
|
runner.close("client");
|
|
1732
1887
|
return;
|
|
@@ -2781,14 +2936,18 @@ function createSessionFactory(deps) {
|
|
|
2781
2936
|
...config,
|
|
2782
2937
|
scope
|
|
2783
2938
|
};
|
|
2939
|
+
const withApprovalDefault = (config) => deps.approvalTimeoutMs === void 0 ? config : {
|
|
2940
|
+
...config,
|
|
2941
|
+
defaultApprovalTimeoutMs: deps.approvalTimeoutMs
|
|
2942
|
+
};
|
|
2784
2943
|
const buildRunnerConfig = (req) => {
|
|
2785
2944
|
const profile = req.profile !== void 0 ? profiles.get(req.profile) : void 0;
|
|
2786
|
-
if (!profile) return withScope(deps.hostBuildRunnerConfig(req), req.scope);
|
|
2787
|
-
const config = withScope(deps.hostBuildRunnerConfig({
|
|
2945
|
+
if (!profile) return withApprovalDefault(withScope(deps.hostBuildRunnerConfig(req), req.scope));
|
|
2946
|
+
const config = withApprovalDefault(withScope(deps.hostBuildRunnerConfig({
|
|
2788
2947
|
...req,
|
|
2789
2948
|
model: req.model ?? profile.defaults?.model ?? profile.provider?.model,
|
|
2790
2949
|
permissionMode: req.permissionMode ?? profile.defaults?.permissionMode
|
|
2791
|
-
}), req.scope);
|
|
2950
|
+
}), req.scope));
|
|
2792
2951
|
if (engineOf(profile) !== "claude") return config;
|
|
2793
2952
|
const base = config.env ?? process.env;
|
|
2794
2953
|
const env = claudeSessionEnv(profile, base);
|
|
@@ -2953,6 +3112,7 @@ function createWorkerServer(options = {}) {
|
|
|
2953
3112
|
createEngineRunner: options.createEngineRunner,
|
|
2954
3113
|
allowedCwdRoots: options.allowedCwdRoots,
|
|
2955
3114
|
disableBypassPermissions: options.disableBypassPermissions,
|
|
3115
|
+
approvalTimeoutMs: options.approvalTimeoutMs,
|
|
2956
3116
|
requireApiKey: options.requireApiKey,
|
|
2957
3117
|
refs
|
|
2958
3118
|
});
|
|
@@ -2968,10 +3128,12 @@ function createWorkerServer(options = {}) {
|
|
|
2968
3128
|
decorateInfo: (info) => projects.withProject(info)
|
|
2969
3129
|
});
|
|
2970
3130
|
const producedFiles = new ProducedFileStore();
|
|
3131
|
+
const shell = options.shell?.enabled === true ? createShellService(options.shell) : null;
|
|
2971
3132
|
const registry = new SessionRegistry({ onRegister: (runner) => {
|
|
2972
3133
|
notifier.watch(runner);
|
|
2973
3134
|
producedFiles.watch(runner);
|
|
2974
3135
|
profileUsage.watch(runner);
|
|
3136
|
+
shell?.watch(runner);
|
|
2975
3137
|
const profile = runner.info().profile;
|
|
2976
3138
|
if (!profile) return;
|
|
2977
3139
|
runner.subscribe((event) => {
|
|
@@ -3073,7 +3235,8 @@ function createWorkerServer(options = {}) {
|
|
|
3073
3235
|
hostFiles,
|
|
3074
3236
|
hostFilesWritable: options.hostFiles?.write === true,
|
|
3075
3237
|
maxHostFileBytes: options.hostFiles?.maxFileBytes ?? 1048576,
|
|
3076
|
-
maxHostDirEntries: options.hostFiles?.maxEntries ?? 5e3
|
|
3238
|
+
maxHostDirEntries: options.hostFiles?.maxEntries ?? 5e3,
|
|
3239
|
+
shell
|
|
3077
3240
|
};
|
|
3078
3241
|
const handleRequest = async (req, res) => {
|
|
3079
3242
|
const pathname = new URL(req.url ?? "/", "http://internal").pathname;
|
|
@@ -3231,7 +3394,7 @@ function createWorkerServer(options = {}) {
|
|
|
3231
3394
|
return;
|
|
3232
3395
|
}
|
|
3233
3396
|
wss.handleUpgrade(req, socket, head, (ws) => {
|
|
3234
|
-
attachClient(ctx, ws, runner, req);
|
|
3397
|
+
attachClient(ctx, ws, runner, req, { operator: auth.isOperator(authCtx) });
|
|
3235
3398
|
});
|
|
3236
3399
|
})().catch(() => socket.destroy());
|
|
3237
3400
|
});
|
|
@@ -3278,6 +3441,7 @@ function createWorkerServer(options = {}) {
|
|
|
3278
3441
|
queue?.close();
|
|
3279
3442
|
parking.close();
|
|
3280
3443
|
registry.closeAll();
|
|
3444
|
+
shell?.killAll();
|
|
3281
3445
|
for (const ws of wss.clients) ws.close(1001, "server shutting down");
|
|
3282
3446
|
queueSockets.clear();
|
|
3283
3447
|
const force = setTimeout(() => {
|
|
@@ -3342,7 +3506,7 @@ async function createProviderRunner(ctx, options) {
|
|
|
3342
3506
|
instructions: options.instructions,
|
|
3343
3507
|
executionLimits: options.executionLimits,
|
|
3344
3508
|
shouldApprove: options.shouldApprove,
|
|
3345
|
-
approvalTimeoutMs: options.approvalTimeoutMs,
|
|
3509
|
+
approvalTimeoutMs: config.defaultApprovalTimeoutMs === void 0 ? options.approvalTimeoutMs : config.defaultApprovalTimeoutMs,
|
|
3346
3510
|
seedVfs: options.seedVfs
|
|
3347
3511
|
});
|
|
3348
3512
|
}
|