@workerdeck/server 2.13.0 → 2.14.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 +100 -4
- package/build/index.mjs +1250 -182
- package/build/index.mjs.map +1 -1
- package/package.json +8 -5
package/build/index.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
1
2
|
import { createServer } from "node:http";
|
|
2
3
|
import { WebSocketServer } from "ws";
|
|
3
|
-
import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SessionRunner, attachmentKind, checkClaudeAuth, createEngineSession, getEngineAdapter, installPeerDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines } from "@workerdeck/core";
|
|
4
|
+
import { BrowserBridgeExecutor, PEER_MESSAGE_MAX_CHARS, PEER_RECENT_DEFAULT, PEER_RECENT_MAX, SHELL_REFUSAL, SessionRunner, attachmentKind, checkClaudeAuth, clampShellTail, createEngineSession, getEngineAdapter, installPeerDirectory, installShellDirectory, isSlashCommand, normalizeMediaType, peerDirectoryHandle, peerSummary, recentLines, shellDirectoryHandle, shellSummary, shellTail, ttyText } from "@workerdeck/core";
|
|
4
5
|
import { JobQueue } from "@workerdeck/queue";
|
|
5
|
-
import { ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_COMMAND_MAX, costOfByModel, imagePartRef, isJobRun, mergeByModel, peerMentionKey, peerMentionSlug, pickCreateSessionRequest, scanPeerMentions, sessionState, setPricingOverrides, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
|
|
6
|
-
import {
|
|
6
|
+
import { ENGINE_CAPABILITIES, PEER_MENTION_MAX, PROTOCOL_VERSION, SHELL_ARTIFACT_MAX_BYTES, SHELL_ARTIFACT_TTL_MS, SHELL_ATTACH_REPLAY_BYTES, SHELL_COLS, SHELL_COMMAND_MAX, SHELL_INPUT_MAX, SHELL_LABEL_MAX, SHELL_LINGER_MS, SHELL_MAX_COLS, SHELL_MAX_ROWS, SHELL_MAX_RUNNING_PER_SESSION, SHELL_MAX_RUNNING_TOTAL, SHELL_MIN_COLS, SHELL_MIN_ROWS, SHELL_ROWS, SHELL_SPILL_BYTES, SHELL_TAIL_FLUSH_MS, SHELL_TAIL_RING_BYTES, costOfByModel, imagePartRef, isJobRun, mergeByModel, peerMentionKey, peerMentionSlug, pickCreateSessionRequest, scanPeerMentions, sessionState, setPricingOverrides, subscriptionComparison, supportsPermissionMode } from "@workerdeck/protocol";
|
|
7
|
+
import { arch, homedir, hostname, platform, tmpdir } from "node:os";
|
|
7
8
|
import { closeSync, constants, createReadStream, existsSync, fstatSync, ftruncateSync, lstatSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
-
import { constants as constants$1, homedir } from "node:os";
|
|
9
9
|
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
10
|
-
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
11
|
-
import {
|
|
10
|
+
import { mkdir, open, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
12
|
//#region src/lib/http.ts
|
|
13
13
|
const CONTENT_TYPES = {
|
|
14
14
|
json: "application/json; charset=utf-8",
|
|
@@ -61,6 +61,18 @@ function contentTypeFor(filename) {
|
|
|
61
61
|
return CONTENT_TYPES[ext] ?? "text/plain; charset=utf-8";
|
|
62
62
|
}
|
|
63
63
|
//#endregion
|
|
64
|
+
//#region src/lib/machine-id.ts
|
|
65
|
+
let cached;
|
|
66
|
+
function machineId() {
|
|
67
|
+
cached ??= createHash("sha256").update([
|
|
68
|
+
hostname(),
|
|
69
|
+
platform(),
|
|
70
|
+
arch(),
|
|
71
|
+
homedir()
|
|
72
|
+
].join("\0")).digest("hex").slice(0, 32);
|
|
73
|
+
return cached;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
64
76
|
//#region src/lib/profile-env.ts
|
|
65
77
|
function isProviderProfile(profile) {
|
|
66
78
|
return profile.engine === "provider";
|
|
@@ -183,6 +195,16 @@ function parseSessionRoute(basePath, url) {
|
|
|
183
195
|
produced: true,
|
|
184
196
|
producedFileId: parts[2] === void 0 ? void 0 : decodeURIComponent(parts[2])
|
|
185
197
|
};
|
|
198
|
+
if (parts[1] === "shells" && parts.length <= 4) {
|
|
199
|
+
const action = parts[3];
|
|
200
|
+
if (action !== void 0 && action !== "output" && action !== "kill") return null;
|
|
201
|
+
return {
|
|
202
|
+
id: decodeURIComponent(parts[0]),
|
|
203
|
+
shells: true,
|
|
204
|
+
shellId: parts[2] === void 0 ? void 0 : decodeURIComponent(parts[2]),
|
|
205
|
+
shellAction: action
|
|
206
|
+
};
|
|
207
|
+
}
|
|
186
208
|
if (parts.length === 3 && parts[1] === "project" && parts[2] === "icon") return {
|
|
187
209
|
id: decodeURIComponent(parts[0]),
|
|
188
210
|
projectIcon: true
|
|
@@ -763,6 +785,7 @@ const HOST_ONLY_KEYS = new Set(Object.keys({
|
|
|
763
785
|
historyFn: true,
|
|
764
786
|
sessionInfoFn: true,
|
|
765
787
|
peers: true,
|
|
788
|
+
shells: true,
|
|
766
789
|
connectFn: true,
|
|
767
790
|
codexHome: true,
|
|
768
791
|
codexPathOverride: true,
|
|
@@ -1110,7 +1133,7 @@ function createFileSessionStore(options = {}) {
|
|
|
1110
1133
|
try {
|
|
1111
1134
|
raw = await readFile(path, "utf8");
|
|
1112
1135
|
} catch (error) {
|
|
1113
|
-
if (isMissing(error)) return null;
|
|
1136
|
+
if (isMissing$1(error)) return null;
|
|
1114
1137
|
options.onError?.(error, {
|
|
1115
1138
|
path,
|
|
1116
1139
|
op: "read"
|
|
@@ -1165,7 +1188,7 @@ function createFileSessionStore(options = {}) {
|
|
|
1165
1188
|
try {
|
|
1166
1189
|
names = await readdir(dir);
|
|
1167
1190
|
} catch (error) {
|
|
1168
|
-
if (isMissing(error)) return [];
|
|
1191
|
+
if (isMissing$1(error)) return [];
|
|
1169
1192
|
options.onError?.(error, {
|
|
1170
1193
|
path: dir,
|
|
1171
1194
|
op: "read"
|
|
@@ -1189,7 +1212,7 @@ function createFileSessionStore(options = {}) {
|
|
|
1189
1212
|
await rm(path);
|
|
1190
1213
|
return true;
|
|
1191
1214
|
} catch (error) {
|
|
1192
|
-
if (isMissing(error)) return false;
|
|
1215
|
+
if (isMissing$1(error)) return false;
|
|
1193
1216
|
options.onError?.(error, {
|
|
1194
1217
|
path,
|
|
1195
1218
|
op: "delete"
|
|
@@ -1199,7 +1222,7 @@ function createFileSessionStore(options = {}) {
|
|
|
1199
1222
|
}
|
|
1200
1223
|
};
|
|
1201
1224
|
}
|
|
1202
|
-
function isMissing(error) {
|
|
1225
|
+
function isMissing$1(error) {
|
|
1203
1226
|
return error.code === "ENOENT";
|
|
1204
1227
|
}
|
|
1205
1228
|
function parseRecord(value) {
|
|
@@ -1359,17 +1382,19 @@ const DEFAULT_TTL_MS = 3e4;
|
|
|
1359
1382
|
const SWEEP_ABOVE = 256;
|
|
1360
1383
|
var ProjectInfoService = class {
|
|
1361
1384
|
#ttlMs;
|
|
1385
|
+
#decorate;
|
|
1362
1386
|
#byCwd = /* @__PURE__ */ new Map();
|
|
1363
1387
|
constructor(options = {}) {
|
|
1364
1388
|
this.#ttlMs = options.ttlMs ?? DEFAULT_TTL_MS;
|
|
1389
|
+
this.#decorate = options.decorate ?? ((info) => info);
|
|
1365
1390
|
}
|
|
1366
1391
|
withProject(info) {
|
|
1367
|
-
if (!info.cwd) return info;
|
|
1392
|
+
if (!info.cwd) return this.#decorate(info);
|
|
1368
1393
|
const project = this.#resolve(info.cwd).project;
|
|
1369
|
-
return project ? {
|
|
1394
|
+
return this.#decorate(project ? {
|
|
1370
1395
|
...info,
|
|
1371
1396
|
project
|
|
1372
|
-
} : info;
|
|
1397
|
+
} : info);
|
|
1373
1398
|
}
|
|
1374
1399
|
iconFor(cwd) {
|
|
1375
1400
|
if (!cwd) return;
|
|
@@ -1518,6 +1543,1062 @@ function handleProjectIcon(projects, req, res, cwd) {
|
|
|
1518
1543
|
res.end(read.data);
|
|
1519
1544
|
}
|
|
1520
1545
|
//#endregion
|
|
1546
|
+
//#region src/services/process-tree.ts
|
|
1547
|
+
const PS_ARGS = ["-eo", "pid=,ppid=,pgid="];
|
|
1548
|
+
const PS_TIMEOUT_MS = 2e3;
|
|
1549
|
+
const PS_MAX_BUFFER = 67108864;
|
|
1550
|
+
const MAX_ROUNDS = 5;
|
|
1551
|
+
function readProcessTable() {
|
|
1552
|
+
const result = spawnSync("ps", PS_ARGS, {
|
|
1553
|
+
encoding: "utf8",
|
|
1554
|
+
timeout: PS_TIMEOUT_MS,
|
|
1555
|
+
maxBuffer: PS_MAX_BUFFER,
|
|
1556
|
+
stdio: [
|
|
1557
|
+
"ignore",
|
|
1558
|
+
"pipe",
|
|
1559
|
+
"ignore"
|
|
1560
|
+
]
|
|
1561
|
+
});
|
|
1562
|
+
if (result.error || result.status !== 0 || typeof result.stdout !== "string") return null;
|
|
1563
|
+
return parseProcessTable(result.stdout);
|
|
1564
|
+
}
|
|
1565
|
+
function parseProcessTable(text) {
|
|
1566
|
+
const rows = [];
|
|
1567
|
+
for (const line of text.split("\n")) {
|
|
1568
|
+
const fields = line.trim().split(/\s+/);
|
|
1569
|
+
if (fields.length !== 3) continue;
|
|
1570
|
+
const [pid, ppid, pgid] = fields.map(Number);
|
|
1571
|
+
if (Number.isInteger(pid) && Number.isInteger(ppid) && Number.isInteger(pgid)) rows.push({
|
|
1572
|
+
pid,
|
|
1573
|
+
ppid,
|
|
1574
|
+
pgid
|
|
1575
|
+
});
|
|
1576
|
+
}
|
|
1577
|
+
return rows;
|
|
1578
|
+
}
|
|
1579
|
+
function killProcessTrees(roots, deps) {
|
|
1580
|
+
const { self } = deps;
|
|
1581
|
+
const signal = (pid, name) => {
|
|
1582
|
+
try {
|
|
1583
|
+
deps.signal(pid, name);
|
|
1584
|
+
return true;
|
|
1585
|
+
} catch {
|
|
1586
|
+
return false;
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
const owned = (pid) => Number.isInteger(pid) && pid > 1 && pid !== self;
|
|
1590
|
+
const live = new Set(roots.filter(owned));
|
|
1591
|
+
const closure = /* @__PURE__ */ new Map();
|
|
1592
|
+
const rootRows = /* @__PURE__ */ new Map();
|
|
1593
|
+
const foreign = [];
|
|
1594
|
+
let scanned = false;
|
|
1595
|
+
let selfPgid;
|
|
1596
|
+
for (let round = 0; round < MAX_ROUNDS; round++) {
|
|
1597
|
+
const table = readTable(deps.table);
|
|
1598
|
+
if (table === null) break;
|
|
1599
|
+
const byPid = /* @__PURE__ */ new Map();
|
|
1600
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
1601
|
+
for (const row of table) {
|
|
1602
|
+
byPid.set(row.pid, row);
|
|
1603
|
+
const siblings = byParent.get(row.ppid);
|
|
1604
|
+
if (siblings) siblings.push(row);
|
|
1605
|
+
else byParent.set(row.ppid, [row]);
|
|
1606
|
+
}
|
|
1607
|
+
let grew = false;
|
|
1608
|
+
if (!scanned) {
|
|
1609
|
+
scanned = true;
|
|
1610
|
+
selfPgid = byPid.get(self)?.pgid;
|
|
1611
|
+
for (const root of live) {
|
|
1612
|
+
const row = byPid.get(root);
|
|
1613
|
+
if (row === void 0) continue;
|
|
1614
|
+
if (row.ppid !== self) {
|
|
1615
|
+
foreign.push(root);
|
|
1616
|
+
live.delete(root);
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
rootRows.set(root, row);
|
|
1620
|
+
signal(-root, "SIGSTOP");
|
|
1621
|
+
grew = true;
|
|
1622
|
+
}
|
|
1623
|
+
}
|
|
1624
|
+
for (const root of rootRows.keys()) {
|
|
1625
|
+
const queue = [root];
|
|
1626
|
+
const seen = /* @__PURE__ */ new Set([root]);
|
|
1627
|
+
while (queue.length > 0) {
|
|
1628
|
+
const parent = queue.shift();
|
|
1629
|
+
for (const child of byParent.get(parent) ?? []) {
|
|
1630
|
+
if (!owned(child.pid) || seen.has(child.pid)) continue;
|
|
1631
|
+
seen.add(child.pid);
|
|
1632
|
+
queue.push(child.pid);
|
|
1633
|
+
if (!closure.has(child.pid)) {
|
|
1634
|
+
closure.set(child.pid, child);
|
|
1635
|
+
signal(child.pid, "SIGSTOP");
|
|
1636
|
+
grew = true;
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (!grew) break;
|
|
1642
|
+
}
|
|
1643
|
+
const unreached = [];
|
|
1644
|
+
const groups = /* @__PURE__ */ new Set();
|
|
1645
|
+
for (const root of live) {
|
|
1646
|
+
groups.add(root);
|
|
1647
|
+
if (!signal(-root, "SIGKILL")) unreached.push(root);
|
|
1648
|
+
}
|
|
1649
|
+
for (const pid of closure.keys()) signal(pid, "SIGKILL");
|
|
1650
|
+
if (selfPgid !== void 0) for (const row of [...rootRows.values(), ...closure.values()]) {
|
|
1651
|
+
const group = row.pgid;
|
|
1652
|
+
if (group <= 1 || group === self || group === selfPgid || groups.has(group)) continue;
|
|
1653
|
+
groups.add(group);
|
|
1654
|
+
signal(-group, "SIGKILL");
|
|
1655
|
+
}
|
|
1656
|
+
return {
|
|
1657
|
+
scanned,
|
|
1658
|
+
descendants: closure.size,
|
|
1659
|
+
foreign,
|
|
1660
|
+
unreached
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
function readTable(table) {
|
|
1664
|
+
try {
|
|
1665
|
+
return table();
|
|
1666
|
+
} catch {
|
|
1667
|
+
return null;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
//#endregion
|
|
1671
|
+
//#region src/services/shells.ts
|
|
1672
|
+
const NOTIFY_MS = 250;
|
|
1673
|
+
const SWEEP_INTERVAL_MS = 36e5;
|
|
1674
|
+
const ID_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
|
|
1675
|
+
const ID_CHARS = 12;
|
|
1676
|
+
const TERM = "xterm-256color";
|
|
1677
|
+
const INDEX_SUFFIX = ".json";
|
|
1678
|
+
const INDEX_VERSION = 1;
|
|
1679
|
+
const CTTY_WRAPPER = "true <>/dev/tty 2>/dev/null; exec \"$0\" -c \"$1\"";
|
|
1680
|
+
let ptyModule;
|
|
1681
|
+
async function loadPty() {
|
|
1682
|
+
if (ptyModule !== void 0) return ptyModule;
|
|
1683
|
+
try {
|
|
1684
|
+
ptyModule = await import("@lydell/node-pty");
|
|
1685
|
+
} catch {
|
|
1686
|
+
ptyModule = null;
|
|
1687
|
+
}
|
|
1688
|
+
return ptyModule;
|
|
1689
|
+
}
|
|
1690
|
+
function createShellDirectory(registry) {
|
|
1691
|
+
return {
|
|
1692
|
+
list: async (from) => registry.list(from).map(shellSummary),
|
|
1693
|
+
read: async (from, shellId, options) => {
|
|
1694
|
+
const info = registry.get(from, shellId);
|
|
1695
|
+
if (!info) return;
|
|
1696
|
+
const text = await registry.output(from, shellId, { view: "text" });
|
|
1697
|
+
if (text === void 0) return;
|
|
1698
|
+
return {
|
|
1699
|
+
shell: shellSummary(info),
|
|
1700
|
+
...shellTail(text, clampShellTail(options?.tail))
|
|
1701
|
+
};
|
|
1702
|
+
}
|
|
1703
|
+
};
|
|
1704
|
+
}
|
|
1705
|
+
function shellPermitted(shells, runner, operator) {
|
|
1706
|
+
if (shells === null || !operator) return false;
|
|
1707
|
+
const info = runner.info();
|
|
1708
|
+
return (info.capabilities ?? (info.engine ? ENGINE_CAPABILITIES[info.engine] : void 0))?.hostCwd === true;
|
|
1709
|
+
}
|
|
1710
|
+
function shellChildEnv(base) {
|
|
1711
|
+
const env = {};
|
|
1712
|
+
for (const [key, value] of Object.entries(base)) if (value !== void 0) env[key] = value;
|
|
1713
|
+
return env;
|
|
1714
|
+
}
|
|
1715
|
+
function clampSize(size) {
|
|
1716
|
+
return {
|
|
1717
|
+
cols: clamp(size.cols, SHELL_MIN_COLS, SHELL_MAX_COLS),
|
|
1718
|
+
rows: clamp(size.rows, SHELL_MIN_ROWS, SHELL_MAX_ROWS)
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
function loginShell(env) {
|
|
1722
|
+
const shell = env.SHELL;
|
|
1723
|
+
return shell && shell.startsWith("/") ? shell : "/bin/sh";
|
|
1724
|
+
}
|
|
1725
|
+
function createShellRegistry(options) {
|
|
1726
|
+
const { generation } = options;
|
|
1727
|
+
const dir = options.artifactDir;
|
|
1728
|
+
const base = dir ?? join(tmpdir(), `workerdeck-shells-${process.pid}`);
|
|
1729
|
+
const limits = {
|
|
1730
|
+
spill: options.spillBytes ?? SHELL_SPILL_BYTES,
|
|
1731
|
+
cap: options.artifactMaxBytes ?? SHELL_ARTIFACT_MAX_BYTES,
|
|
1732
|
+
ring: options.tailRingBytes ?? SHELL_TAIL_RING_BYTES
|
|
1733
|
+
};
|
|
1734
|
+
const tailFlushMs = options.tailFlushMs ?? SHELL_TAIL_FLUSH_MS;
|
|
1735
|
+
const ttlMs = options.artifactTtlMs ?? SHELL_ARTIFACT_TTL_MS;
|
|
1736
|
+
const perSession = options.maxRunningPerSession ?? SHELL_MAX_RUNNING_PER_SESSION;
|
|
1737
|
+
const total = options.maxRunningTotal ?? SHELL_MAX_RUNNING_TOTAL;
|
|
1738
|
+
const sweepIntervalMs = options.sweepIntervalMs ?? SWEEP_INTERVAL_MS;
|
|
1739
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
1740
|
+
const loading = /* @__PURE__ */ new Map();
|
|
1741
|
+
let sweeper;
|
|
1742
|
+
let stopped = false;
|
|
1743
|
+
const report = (error, context) => options.onError?.(error, context);
|
|
1744
|
+
const indexPath = (sessionId) => join(base, `${encodeURIComponent(sessionId)}${INDEX_SUFFIX}`);
|
|
1745
|
+
const pathsFor = (sessionId, shellId) => {
|
|
1746
|
+
const encoded = encodeURIComponent(sessionId);
|
|
1747
|
+
const sessionDir = join(base, encoded);
|
|
1748
|
+
return {
|
|
1749
|
+
dir: sessionDir,
|
|
1750
|
+
raw: join(sessionDir, `${shellId}.raw`),
|
|
1751
|
+
tail: join(sessionDir, `${shellId}.tail.raw`),
|
|
1752
|
+
rel: `${encoded}/${shellId}.raw`
|
|
1753
|
+
};
|
|
1754
|
+
};
|
|
1755
|
+
const find = (sessionId, id) => sessions.get(sessionId)?.entries.get(id);
|
|
1756
|
+
const runningAll = () => [...sessions.values()].flatMap(runningIn);
|
|
1757
|
+
const persist = (state) => {
|
|
1758
|
+
if (!dir || state.pending) return;
|
|
1759
|
+
state.pending = true;
|
|
1760
|
+
state.chain = state.chain.then(async () => {
|
|
1761
|
+
state.pending = false;
|
|
1762
|
+
const payload = JSON.stringify(serialize(state));
|
|
1763
|
+
await mkdir(dir, {
|
|
1764
|
+
recursive: true,
|
|
1765
|
+
mode: 448
|
|
1766
|
+
});
|
|
1767
|
+
const path = indexPath(state.sessionId);
|
|
1768
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
1769
|
+
await writeFile(temp, payload, { mode: 384 });
|
|
1770
|
+
await rename(temp, path);
|
|
1771
|
+
}).catch((error) => report(error, {
|
|
1772
|
+
op: "index",
|
|
1773
|
+
sessionId: state.sessionId
|
|
1774
|
+
}));
|
|
1775
|
+
};
|
|
1776
|
+
const readIndex = async (sessionId) => {
|
|
1777
|
+
const state = freshSession(sessionId);
|
|
1778
|
+
if (!dir) return state;
|
|
1779
|
+
let raw;
|
|
1780
|
+
try {
|
|
1781
|
+
raw = await readFile(indexPath(sessionId), "utf8");
|
|
1782
|
+
} catch (error) {
|
|
1783
|
+
if (!isMissing(error)) report(error, {
|
|
1784
|
+
op: "index",
|
|
1785
|
+
sessionId
|
|
1786
|
+
});
|
|
1787
|
+
return state;
|
|
1788
|
+
}
|
|
1789
|
+
let index;
|
|
1790
|
+
try {
|
|
1791
|
+
index = parseIndex(JSON.parse(raw));
|
|
1792
|
+
} catch (error) {
|
|
1793
|
+
report(error, {
|
|
1794
|
+
op: "index",
|
|
1795
|
+
sessionId
|
|
1796
|
+
});
|
|
1797
|
+
return state;
|
|
1798
|
+
}
|
|
1799
|
+
state.nextOrdinal = index.nextOrdinal;
|
|
1800
|
+
const now = Date.now();
|
|
1801
|
+
let dirty = false;
|
|
1802
|
+
for (const record of index.shells) {
|
|
1803
|
+
const { generation: recordGeneration, output, artifact, ...info } = record;
|
|
1804
|
+
if (info.status === "running" && recordGeneration !== generation) {
|
|
1805
|
+
info.status = "exited";
|
|
1806
|
+
info.endReason = "server_restarted";
|
|
1807
|
+
info.endedAt = now;
|
|
1808
|
+
delete info.exitCode;
|
|
1809
|
+
delete info.signal;
|
|
1810
|
+
dirty = true;
|
|
1811
|
+
}
|
|
1812
|
+
state.entries.set(info.id, {
|
|
1813
|
+
info,
|
|
1814
|
+
generation: recordGeneration,
|
|
1815
|
+
artifact: new StoredArtifact(pathsFor(sessionId, info.id), info, {
|
|
1816
|
+
output,
|
|
1817
|
+
artifact
|
|
1818
|
+
}),
|
|
1819
|
+
sinks: /* @__PURE__ */ new Set(),
|
|
1820
|
+
listeners: /* @__PURE__ */ new Set()
|
|
1821
|
+
});
|
|
1822
|
+
}
|
|
1823
|
+
if (dirty) persist(state);
|
|
1824
|
+
return state;
|
|
1825
|
+
};
|
|
1826
|
+
const load = (sessionId) => {
|
|
1827
|
+
const held = sessions.get(sessionId);
|
|
1828
|
+
if (held) return Promise.resolve(held);
|
|
1829
|
+
let inflight = loading.get(sessionId);
|
|
1830
|
+
if (!inflight) {
|
|
1831
|
+
inflight = readIndex(sessionId).then((state) => {
|
|
1832
|
+
sessions.set(sessionId, state);
|
|
1833
|
+
loading.delete(sessionId);
|
|
1834
|
+
return state;
|
|
1835
|
+
});
|
|
1836
|
+
loading.set(sessionId, inflight);
|
|
1837
|
+
}
|
|
1838
|
+
return inflight;
|
|
1839
|
+
};
|
|
1840
|
+
const fire = (entry) => {
|
|
1841
|
+
for (const listener of entry.listeners) try {
|
|
1842
|
+
listener();
|
|
1843
|
+
} catch (error) {
|
|
1844
|
+
report(error, {
|
|
1845
|
+
op: "listener",
|
|
1846
|
+
sessionId: entry.info.sessionId,
|
|
1847
|
+
shellId: entry.info.id
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
};
|
|
1851
|
+
const scheduleNotify = (entry) => {
|
|
1852
|
+
if (entry.notify) return;
|
|
1853
|
+
entry.notify = setTimeout(() => {
|
|
1854
|
+
entry.notify = void 0;
|
|
1855
|
+
if (entry.info.status === "running") fire(entry);
|
|
1856
|
+
}, NOTIFY_MS);
|
|
1857
|
+
entry.notify.unref();
|
|
1858
|
+
};
|
|
1859
|
+
const settle = (state, entry, reason) => {
|
|
1860
|
+
if (entry.info.status !== "running") return;
|
|
1861
|
+
entry.info.status = "exited";
|
|
1862
|
+
entry.info.endedAt = Date.now();
|
|
1863
|
+
entry.info.endReason = reason;
|
|
1864
|
+
clearTimeout(entry.clock);
|
|
1865
|
+
clearTimeout(entry.notify);
|
|
1866
|
+
clearInterval(entry.tailTimer);
|
|
1867
|
+
entry.clock = entry.notify = entry.tailTimer = void 0;
|
|
1868
|
+
entry.child = void 0;
|
|
1869
|
+
entry.pid = void 0;
|
|
1870
|
+
if (entry.artifact instanceof LiveArtifact) entry.artifact.close();
|
|
1871
|
+
for (const sink of entry.sinks) try {
|
|
1872
|
+
sink.end(reason);
|
|
1873
|
+
} catch {}
|
|
1874
|
+
entry.sinks.clear();
|
|
1875
|
+
persist(state);
|
|
1876
|
+
fire(entry);
|
|
1877
|
+
};
|
|
1878
|
+
const treeDeps = {
|
|
1879
|
+
table: options.processTable ?? readProcessTable,
|
|
1880
|
+
signal: (pid, name) => void process.kill(pid, name),
|
|
1881
|
+
self: process.pid
|
|
1882
|
+
};
|
|
1883
|
+
const killTrees = (entries) => {
|
|
1884
|
+
const live = entries.flatMap((entry) => entry.pid === void 0 ? [] : [{
|
|
1885
|
+
entry,
|
|
1886
|
+
pid: entry.pid
|
|
1887
|
+
}]);
|
|
1888
|
+
if (live.length === 0) return;
|
|
1889
|
+
const result = killProcessTrees(live.map(({ pid }) => pid), treeDeps);
|
|
1890
|
+
for (const { entry, pid } of live) {
|
|
1891
|
+
if (result.unreached.includes(pid)) try {
|
|
1892
|
+
entry.child?.kill("SIGKILL");
|
|
1893
|
+
} catch {}
|
|
1894
|
+
const context = {
|
|
1895
|
+
op: "kill",
|
|
1896
|
+
sessionId: entry.info.sessionId,
|
|
1897
|
+
shellId: entry.info.id
|
|
1898
|
+
};
|
|
1899
|
+
if (!result.scanned) report(/* @__PURE__ */ new Error("no process table (ps), only the process group was signalled"), context);
|
|
1900
|
+
else if (result.foreign.includes(pid)) report(/* @__PURE__ */ new Error(`pid ${pid} is no longer this shell's child and was left alone`), context);
|
|
1901
|
+
}
|
|
1902
|
+
};
|
|
1903
|
+
const killEntries = (targets, reason) => {
|
|
1904
|
+
const running = targets.filter(({ entry }) => entry.info.status === "running");
|
|
1905
|
+
killTrees(running.map(({ entry }) => entry));
|
|
1906
|
+
for (const { state, entry } of running) settle(state, entry, reason);
|
|
1907
|
+
return running.length;
|
|
1908
|
+
};
|
|
1909
|
+
const kill = (state, entry, reason) => {
|
|
1910
|
+
killEntries([{
|
|
1911
|
+
state,
|
|
1912
|
+
entry
|
|
1913
|
+
}], reason);
|
|
1914
|
+
};
|
|
1915
|
+
const killSession = (sessionId, reason) => {
|
|
1916
|
+
const state = sessions.get(sessionId);
|
|
1917
|
+
if (!state) return;
|
|
1918
|
+
killEntries(runningIn(state).map((entry) => ({
|
|
1919
|
+
state,
|
|
1920
|
+
entry
|
|
1921
|
+
})), reason);
|
|
1922
|
+
};
|
|
1923
|
+
const spawn = async ({ runner, command, owner }) => {
|
|
1924
|
+
if (stopped) throw new Error("the gateway is shutting down");
|
|
1925
|
+
const sessionId = runner.id;
|
|
1926
|
+
const state = await load(sessionId);
|
|
1927
|
+
const pty = await loadPty();
|
|
1928
|
+
if (!pty) throw new Error(SHELL_REFUSAL);
|
|
1929
|
+
const cwd = runner.info().cwd;
|
|
1930
|
+
if (!(await stat(cwd).catch(() => void 0))?.isDirectory()) throw new Error(`the session's cwd is not a directory: ${cwd}`);
|
|
1931
|
+
const inSession = runningIn(state).length;
|
|
1932
|
+
if (inSession >= perSession) throw new Error(`this session already has ${inSession} shells running (the limit is ${perSession})`);
|
|
1933
|
+
const overall = runningAll().length;
|
|
1934
|
+
if (overall >= total) throw new Error(`the gateway already has ${overall} shells running (the limit is ${total})`);
|
|
1935
|
+
const id = mintShellId();
|
|
1936
|
+
const info = {
|
|
1937
|
+
id,
|
|
1938
|
+
sessionId,
|
|
1939
|
+
ordinal: state.nextOrdinal++,
|
|
1940
|
+
command,
|
|
1941
|
+
label: shellLabel(command),
|
|
1942
|
+
cwd,
|
|
1943
|
+
owner,
|
|
1944
|
+
status: "running",
|
|
1945
|
+
startedAt: Date.now(),
|
|
1946
|
+
bytes: 0,
|
|
1947
|
+
cols: SHELL_COLS,
|
|
1948
|
+
rows: SHELL_ROWS
|
|
1949
|
+
};
|
|
1950
|
+
const artifact = new LiveArtifact(pathsFor(sessionId, id), limits, (error) => report(error, {
|
|
1951
|
+
op: "artifact",
|
|
1952
|
+
sessionId,
|
|
1953
|
+
shellId: id
|
|
1954
|
+
}));
|
|
1955
|
+
const entry = {
|
|
1956
|
+
info,
|
|
1957
|
+
generation,
|
|
1958
|
+
artifact,
|
|
1959
|
+
sinks: /* @__PURE__ */ new Set(),
|
|
1960
|
+
listeners: /* @__PURE__ */ new Set()
|
|
1961
|
+
};
|
|
1962
|
+
state.entries.set(id, entry);
|
|
1963
|
+
const env = shellChildEnv(process.env);
|
|
1964
|
+
env.TERM = TERM;
|
|
1965
|
+
env.COLORTERM = "truecolor";
|
|
1966
|
+
env.PWD = cwd;
|
|
1967
|
+
let child;
|
|
1968
|
+
try {
|
|
1969
|
+
child = pty.spawn("/bin/sh", [
|
|
1970
|
+
"-c",
|
|
1971
|
+
CTTY_WRAPPER,
|
|
1972
|
+
loginShell(process.env),
|
|
1973
|
+
command
|
|
1974
|
+
], {
|
|
1975
|
+
name: TERM,
|
|
1976
|
+
cols: SHELL_COLS,
|
|
1977
|
+
rows: SHELL_ROWS,
|
|
1978
|
+
cwd,
|
|
1979
|
+
env
|
|
1980
|
+
});
|
|
1981
|
+
} catch (error) {
|
|
1982
|
+
settle(state, entry, "spawn_failed");
|
|
1983
|
+
throw error instanceof Error ? error : /* @__PURE__ */ new Error("failed to start the shell");
|
|
1984
|
+
}
|
|
1985
|
+
entry.child = child;
|
|
1986
|
+
entry.pid = child.pid;
|
|
1987
|
+
child.onData((data) => {
|
|
1988
|
+
if (entry.info.status !== "running") return;
|
|
1989
|
+
const changed = artifact.append(data);
|
|
1990
|
+
entry.info.bytes = artifact.bytes;
|
|
1991
|
+
if (changed) {
|
|
1992
|
+
if (artifact.capped && !entry.info.capped) {
|
|
1993
|
+
entry.info.capped = true;
|
|
1994
|
+
entry.tailTimer = setInterval(() => artifact.writeTail(), tailFlushMs);
|
|
1995
|
+
entry.tailTimer.unref();
|
|
1996
|
+
}
|
|
1997
|
+
persist(state);
|
|
1998
|
+
}
|
|
1999
|
+
for (const sink of entry.sinks) try {
|
|
2000
|
+
sink.write(data);
|
|
2001
|
+
} catch {
|
|
2002
|
+
entry.sinks.delete(sink);
|
|
2003
|
+
}
|
|
2004
|
+
scheduleNotify(entry);
|
|
2005
|
+
});
|
|
2006
|
+
child.onExit(({ exitCode, signal }) => {
|
|
2007
|
+
if (entry.info.status !== "running") return;
|
|
2008
|
+
if (signal) entry.info.signal = signal;
|
|
2009
|
+
else entry.info.exitCode = exitCode;
|
|
2010
|
+
settle(state, entry, "exit");
|
|
2011
|
+
});
|
|
2012
|
+
if (options.timeoutMs !== void 0 && options.timeoutMs > 0) {
|
|
2013
|
+
entry.clock = setTimeout(() => kill(state, entry, "timeout"), options.timeoutMs);
|
|
2014
|
+
entry.clock.unref();
|
|
2015
|
+
}
|
|
2016
|
+
persist(state);
|
|
2017
|
+
return {
|
|
2018
|
+
shell: { ...info },
|
|
2019
|
+
source: sourceFor(entry, artifact)
|
|
2020
|
+
};
|
|
2021
|
+
};
|
|
2022
|
+
const sweep = async () => {
|
|
2023
|
+
const now = Date.now();
|
|
2024
|
+
for (const state of sessions.values()) {
|
|
2025
|
+
let dirty = false;
|
|
2026
|
+
for (const entry of state.entries.values()) {
|
|
2027
|
+
const { status, endedAt } = entry.info;
|
|
2028
|
+
if (status !== "exited" || endedAt === void 0 || now - endedAt < ttlMs) continue;
|
|
2029
|
+
state.entries.delete(entry.info.id);
|
|
2030
|
+
dirty = true;
|
|
2031
|
+
try {
|
|
2032
|
+
await entry.artifact.remove();
|
|
2033
|
+
} catch (error) {
|
|
2034
|
+
report(error, {
|
|
2035
|
+
op: "sweep",
|
|
2036
|
+
sessionId: state.sessionId,
|
|
2037
|
+
shellId: entry.info.id
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
if (dirty) persist(state);
|
|
2042
|
+
}
|
|
2043
|
+
};
|
|
2044
|
+
return {
|
|
2045
|
+
spawn,
|
|
2046
|
+
get: (sessionId, shellId) => {
|
|
2047
|
+
const entry = find(sessionId, shellId);
|
|
2048
|
+
return entry ? { ...entry.info } : void 0;
|
|
2049
|
+
},
|
|
2050
|
+
list: (sessionId) => {
|
|
2051
|
+
const state = sessions.get(sessionId);
|
|
2052
|
+
if (!state) return [];
|
|
2053
|
+
return [...state.entries.values()].map((entry) => ({ ...entry.info })).sort((a, b) => b.ordinal - a.ordinal);
|
|
2054
|
+
},
|
|
2055
|
+
running: () => runningAll().map((entry) => ({ ...entry.info })),
|
|
2056
|
+
kill: (sessionId, shellId, reason = "killed") => {
|
|
2057
|
+
const state = sessions.get(sessionId);
|
|
2058
|
+
const entry = state?.entries.get(shellId);
|
|
2059
|
+
if (!state || !entry) return;
|
|
2060
|
+
kill(state, entry, reason);
|
|
2061
|
+
return { ...entry.info };
|
|
2062
|
+
},
|
|
2063
|
+
killAll: (reason) => {
|
|
2064
|
+
stopped = true;
|
|
2065
|
+
if (sweeper) {
|
|
2066
|
+
clearInterval(sweeper);
|
|
2067
|
+
sweeper = void 0;
|
|
2068
|
+
}
|
|
2069
|
+
return killEntries([...sessions.values()].flatMap((state) => runningIn(state).map((entry) => ({
|
|
2070
|
+
state,
|
|
2071
|
+
entry
|
|
2072
|
+
}))), reason);
|
|
2073
|
+
},
|
|
2074
|
+
killAllSync: () => {
|
|
2075
|
+
killTrees(runningAll());
|
|
2076
|
+
},
|
|
2077
|
+
attach: async (sessionId, shellId, sink) => {
|
|
2078
|
+
const entry = find(sessionId, shellId);
|
|
2079
|
+
if (!entry) throw new Error("unknown shell");
|
|
2080
|
+
const pending = [];
|
|
2081
|
+
let live = false;
|
|
2082
|
+
let ended;
|
|
2083
|
+
const proxy = {
|
|
2084
|
+
write: (data) => {
|
|
2085
|
+
if (live) sink.write(data);
|
|
2086
|
+
else pending.push(data);
|
|
2087
|
+
},
|
|
2088
|
+
end: (reason) => {
|
|
2089
|
+
if (live) sink.end(reason);
|
|
2090
|
+
else ended = reason;
|
|
2091
|
+
}
|
|
2092
|
+
};
|
|
2093
|
+
if (entry.info.status === "running") entry.sinks.add(proxy);
|
|
2094
|
+
let replay;
|
|
2095
|
+
try {
|
|
2096
|
+
replay = await entry.artifact.replay(SHELL_ATTACH_REPLAY_BYTES);
|
|
2097
|
+
} catch (error) {
|
|
2098
|
+
entry.sinks.delete(proxy);
|
|
2099
|
+
throw error;
|
|
2100
|
+
}
|
|
2101
|
+
live = true;
|
|
2102
|
+
if (ended !== void 0) {
|
|
2103
|
+
const reason = ended;
|
|
2104
|
+
queueMicrotask(() => sink.end(reason));
|
|
2105
|
+
}
|
|
2106
|
+
return {
|
|
2107
|
+
shell: { ...entry.info },
|
|
2108
|
+
scrollback: replay.toString("utf8") + pending.join(""),
|
|
2109
|
+
detach: () => {
|
|
2110
|
+
entry.sinks.delete(proxy);
|
|
2111
|
+
}
|
|
2112
|
+
};
|
|
2113
|
+
},
|
|
2114
|
+
write: (sessionId, shellId, data) => {
|
|
2115
|
+
const entry = find(sessionId, shellId);
|
|
2116
|
+
if (!entry) throw new Error("unknown shell");
|
|
2117
|
+
if (typeof data !== "string" || data.length > SHELL_INPUT_MAX) throw new Error(`shell input must be a string of at most ${SHELL_INPUT_MAX} characters`);
|
|
2118
|
+
if (!entry.child) throw new Error("the shell has exited");
|
|
2119
|
+
entry.child.write(data);
|
|
2120
|
+
},
|
|
2121
|
+
resize: (sessionId, shellId, size) => {
|
|
2122
|
+
const entry = find(sessionId, shellId);
|
|
2123
|
+
if (!entry) throw new Error("unknown shell");
|
|
2124
|
+
const next = clampSize(size);
|
|
2125
|
+
if (entry.child) {
|
|
2126
|
+
try {
|
|
2127
|
+
entry.child.resize(next.cols, next.rows);
|
|
2128
|
+
} catch {}
|
|
2129
|
+
entry.info.cols = next.cols;
|
|
2130
|
+
entry.info.rows = next.rows;
|
|
2131
|
+
}
|
|
2132
|
+
return next;
|
|
2133
|
+
},
|
|
2134
|
+
output: async (sessionId, shellId, query) => {
|
|
2135
|
+
const entry = find(sessionId, shellId);
|
|
2136
|
+
if (!entry) return;
|
|
2137
|
+
if (query.tail === void 0 && query.view === "text") return entry.artifact.textView();
|
|
2138
|
+
const text = (await entry.artifact.read(query.tail)).toString("utf8");
|
|
2139
|
+
return query.view === "raw" ? text : ttyText(text);
|
|
2140
|
+
},
|
|
2141
|
+
hydrate: async () => {
|
|
2142
|
+
if (dir) {
|
|
2143
|
+
let names = [];
|
|
2144
|
+
try {
|
|
2145
|
+
names = await readdir(dir);
|
|
2146
|
+
} catch (error) {
|
|
2147
|
+
if (!isMissing(error)) report(error, { op: "index" });
|
|
2148
|
+
}
|
|
2149
|
+
for (const name of names) {
|
|
2150
|
+
if (!name.endsWith(INDEX_SUFFIX)) continue;
|
|
2151
|
+
let sessionId;
|
|
2152
|
+
try {
|
|
2153
|
+
sessionId = decodeURIComponent(name.slice(0, -5));
|
|
2154
|
+
} catch {
|
|
2155
|
+
continue;
|
|
2156
|
+
}
|
|
2157
|
+
await load(sessionId);
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
await sweep();
|
|
2161
|
+
if (!sweeper && !stopped) {
|
|
2162
|
+
sweeper = setInterval(() => void sweep(), sweepIntervalMs);
|
|
2163
|
+
sweeper.unref();
|
|
2164
|
+
}
|
|
2165
|
+
},
|
|
2166
|
+
sweep,
|
|
2167
|
+
flush: async () => {
|
|
2168
|
+
const states = [...sessions.values()];
|
|
2169
|
+
await Promise.all(states.flatMap((state) => [...state.entries.values()].map((entry) => entry.artifact.idle())));
|
|
2170
|
+
await Promise.all(states.map((state) => state.chain));
|
|
2171
|
+
if (!dir && stopped) await rm(base, {
|
|
2172
|
+
recursive: true,
|
|
2173
|
+
force: true
|
|
2174
|
+
}).catch(() => {});
|
|
2175
|
+
},
|
|
2176
|
+
decorate: (info) => {
|
|
2177
|
+
const state = sessions.get(info.id);
|
|
2178
|
+
if (!state) return info;
|
|
2179
|
+
const now = Date.now();
|
|
2180
|
+
const shells = [...state.entries.values()].filter((entry) => {
|
|
2181
|
+
const { status, exitCode, endedAt } = entry.info;
|
|
2182
|
+
return status === "running" || typeof exitCode === "number" && exitCode !== 0 && endedAt !== void 0 && now - endedAt < SHELL_LINGER_MS;
|
|
2183
|
+
}).map((entry) => ({ ...entry.info }));
|
|
2184
|
+
return shells.length > 0 ? {
|
|
2185
|
+
...info,
|
|
2186
|
+
shells
|
|
2187
|
+
} : info;
|
|
2188
|
+
},
|
|
2189
|
+
watch: (runner) => runner.subscribe((event) => {
|
|
2190
|
+
if (event.type === "session_closed" || event.type === "status_changed" && event.status === "parked") killSession(runner.id, "killed");
|
|
2191
|
+
}, runner.info().lastSeq)
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
var LiveArtifact = class {
|
|
2195
|
+
#paths;
|
|
2196
|
+
#limits;
|
|
2197
|
+
#onError;
|
|
2198
|
+
#ring;
|
|
2199
|
+
#ringAt = 0;
|
|
2200
|
+
#ringLen = 0;
|
|
2201
|
+
#chunks = [];
|
|
2202
|
+
#chunkBytes = 0;
|
|
2203
|
+
#bytes = 0;
|
|
2204
|
+
#spilled = false;
|
|
2205
|
+
#capped = false;
|
|
2206
|
+
#closed = false;
|
|
2207
|
+
#broken = false;
|
|
2208
|
+
#io = Promise.resolve();
|
|
2209
|
+
#handle;
|
|
2210
|
+
#done = "";
|
|
2211
|
+
#pending = "";
|
|
2212
|
+
#textCache;
|
|
2213
|
+
constructor(paths, limits, onError) {
|
|
2214
|
+
this.#paths = paths;
|
|
2215
|
+
this.#limits = limits;
|
|
2216
|
+
this.#onError = onError;
|
|
2217
|
+
this.#ring = Buffer.alloc(Math.max(0, limits.ring));
|
|
2218
|
+
}
|
|
2219
|
+
get bytes() {
|
|
2220
|
+
return this.#bytes;
|
|
2221
|
+
}
|
|
2222
|
+
get capped() {
|
|
2223
|
+
return this.#capped;
|
|
2224
|
+
}
|
|
2225
|
+
append(data) {
|
|
2226
|
+
const chunk = Buffer.from(data, "utf8");
|
|
2227
|
+
const before = this.#bytes;
|
|
2228
|
+
this.#bytes += chunk.length;
|
|
2229
|
+
this.#ringPush(chunk);
|
|
2230
|
+
this.#textCache = void 0;
|
|
2231
|
+
if (this.#capped) return false;
|
|
2232
|
+
const room = Math.max(0, this.#limits.cap - before);
|
|
2233
|
+
const kept = chunk.length <= room ? chunk : chunk.subarray(0, room);
|
|
2234
|
+
this.#textAppend(kept === chunk ? data : kept.toString("utf8"));
|
|
2235
|
+
let changed = false;
|
|
2236
|
+
if (this.#spilled) this.#write(kept);
|
|
2237
|
+
else {
|
|
2238
|
+
this.#chunks.push(kept);
|
|
2239
|
+
this.#chunkBytes += kept.length;
|
|
2240
|
+
if (this.#chunkBytes > this.#limits.spill) {
|
|
2241
|
+
this.#spill();
|
|
2242
|
+
changed = true;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (this.#bytes > this.#limits.cap) {
|
|
2246
|
+
this.#capped = true;
|
|
2247
|
+
changed = true;
|
|
2248
|
+
}
|
|
2249
|
+
return changed;
|
|
2250
|
+
}
|
|
2251
|
+
text() {
|
|
2252
|
+
if (this.#textCache === void 0) {
|
|
2253
|
+
let text = this.#done + ttyText(this.#pending);
|
|
2254
|
+
if (this.#capped) {
|
|
2255
|
+
const after = this.#bytes - this.#limits.cap;
|
|
2256
|
+
const kept = Math.min(after, this.#ringLen);
|
|
2257
|
+
if (kept > 0) text += (after > kept ? "\n" : "") + ttyText(this.tail(kept).toString("utf8"));
|
|
2258
|
+
}
|
|
2259
|
+
this.#textCache = text;
|
|
2260
|
+
}
|
|
2261
|
+
return this.#textCache;
|
|
2262
|
+
}
|
|
2263
|
+
textView() {
|
|
2264
|
+
return Promise.resolve(this.text());
|
|
2265
|
+
}
|
|
2266
|
+
tail(n = this.#ringLen) {
|
|
2267
|
+
const size = this.#ring.length;
|
|
2268
|
+
const take = Math.max(0, Math.min(n, this.#ringLen));
|
|
2269
|
+
const out = Buffer.allocUnsafe(take);
|
|
2270
|
+
if (take === 0) return out;
|
|
2271
|
+
const start = (this.#ringAt - take + size) % size;
|
|
2272
|
+
const first = Math.min(take, size - start);
|
|
2273
|
+
this.#ring.copy(out, 0, start, start + first);
|
|
2274
|
+
if (first < take) this.#ring.copy(out, first, 0, take - first);
|
|
2275
|
+
return out;
|
|
2276
|
+
}
|
|
2277
|
+
replay(max) {
|
|
2278
|
+
const want = Math.max(0, Math.floor(max));
|
|
2279
|
+
if (!this.#spilled) {
|
|
2280
|
+
const all = Buffer.concat(this.#chunks, this.#chunkBytes);
|
|
2281
|
+
return Promise.resolve(all.subarray(Math.max(0, all.length - want)));
|
|
2282
|
+
}
|
|
2283
|
+
if (this.#capped || want <= this.#ringLen) return Promise.resolve(this.tail(want));
|
|
2284
|
+
const end = this.#bytes;
|
|
2285
|
+
return this.#io.then(() => readRange(this.#paths.raw, Math.max(0, end - want), end));
|
|
2286
|
+
}
|
|
2287
|
+
async read(tail) {
|
|
2288
|
+
if (tail !== void 0) return this.replay(tail);
|
|
2289
|
+
if (!this.#spilled) return Buffer.concat(this.#chunks, this.#chunkBytes);
|
|
2290
|
+
await this.#io;
|
|
2291
|
+
return readWhole(this.#paths.raw);
|
|
2292
|
+
}
|
|
2293
|
+
stored() {
|
|
2294
|
+
return this.#spilled ? { artifact: this.#paths.rel } : { output: Buffer.concat(this.#chunks, this.#chunkBytes).toString("utf8") };
|
|
2295
|
+
}
|
|
2296
|
+
writeTail() {
|
|
2297
|
+
const tail = this.tail();
|
|
2298
|
+
this.#queue(async () => {
|
|
2299
|
+
await mkdir(this.#paths.dir, {
|
|
2300
|
+
recursive: true,
|
|
2301
|
+
mode: 448
|
|
2302
|
+
});
|
|
2303
|
+
const temp = `${this.#paths.tail}.${process.pid}.tmp`;
|
|
2304
|
+
await writeFile(temp, tail, { mode: 384 });
|
|
2305
|
+
await rename(temp, this.#paths.tail);
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
close() {
|
|
2309
|
+
if (this.#closed) return;
|
|
2310
|
+
this.#closed = true;
|
|
2311
|
+
if (this.#capped) this.writeTail();
|
|
2312
|
+
this.#io = this.#io.then(async () => {
|
|
2313
|
+
const handle = this.#handle;
|
|
2314
|
+
this.#handle = void 0;
|
|
2315
|
+
await handle?.close().catch(() => {});
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2318
|
+
idle() {
|
|
2319
|
+
return this.#io;
|
|
2320
|
+
}
|
|
2321
|
+
async remove() {
|
|
2322
|
+
this.close();
|
|
2323
|
+
await this.#io;
|
|
2324
|
+
await rm(this.#paths.raw, { force: true });
|
|
2325
|
+
await rm(this.#paths.tail, { force: true });
|
|
2326
|
+
}
|
|
2327
|
+
#textAppend(data) {
|
|
2328
|
+
const newline = data.lastIndexOf("\n");
|
|
2329
|
+
if (newline === -1) {
|
|
2330
|
+
this.#pending += data;
|
|
2331
|
+
return;
|
|
2332
|
+
}
|
|
2333
|
+
this.#done += ttyText(this.#pending + data.slice(0, newline + 1));
|
|
2334
|
+
this.#pending = data.slice(newline + 1);
|
|
2335
|
+
}
|
|
2336
|
+
#ringPush(chunk) {
|
|
2337
|
+
const size = this.#ring.length;
|
|
2338
|
+
if (size === 0 || chunk.length === 0) return;
|
|
2339
|
+
if (chunk.length >= size) {
|
|
2340
|
+
chunk.copy(this.#ring, 0, chunk.length - size);
|
|
2341
|
+
this.#ringAt = 0;
|
|
2342
|
+
this.#ringLen = size;
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
const first = Math.min(chunk.length, size - this.#ringAt);
|
|
2346
|
+
chunk.copy(this.#ring, this.#ringAt, 0, first);
|
|
2347
|
+
if (first < chunk.length) chunk.copy(this.#ring, 0, first);
|
|
2348
|
+
this.#ringAt = (this.#ringAt + chunk.length) % size;
|
|
2349
|
+
this.#ringLen = Math.min(size, this.#ringLen + chunk.length);
|
|
2350
|
+
}
|
|
2351
|
+
#spill() {
|
|
2352
|
+
this.#spilled = true;
|
|
2353
|
+
const buffered = Buffer.concat(this.#chunks, this.#chunkBytes);
|
|
2354
|
+
this.#chunks = [];
|
|
2355
|
+
this.#chunkBytes = 0;
|
|
2356
|
+
this.#queue(async () => {
|
|
2357
|
+
await mkdir(this.#paths.dir, {
|
|
2358
|
+
recursive: true,
|
|
2359
|
+
mode: 448
|
|
2360
|
+
});
|
|
2361
|
+
this.#handle = await open(this.#paths.raw, "w", 384);
|
|
2362
|
+
await this.#handle.write(buffered);
|
|
2363
|
+
});
|
|
2364
|
+
}
|
|
2365
|
+
#write(chunk) {
|
|
2366
|
+
if (chunk.length === 0) return;
|
|
2367
|
+
this.#queue(async () => {
|
|
2368
|
+
await this.#handle?.write(chunk);
|
|
2369
|
+
});
|
|
2370
|
+
}
|
|
2371
|
+
#queue(task) {
|
|
2372
|
+
this.#io = this.#io.then(async () => {
|
|
2373
|
+
if (this.#broken) return;
|
|
2374
|
+
try {
|
|
2375
|
+
await task();
|
|
2376
|
+
} catch (error) {
|
|
2377
|
+
this.#broken = true;
|
|
2378
|
+
this.#onError(error);
|
|
2379
|
+
}
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
var StoredArtifact = class {
|
|
2384
|
+
#paths;
|
|
2385
|
+
#bytes;
|
|
2386
|
+
#capped;
|
|
2387
|
+
#stored;
|
|
2388
|
+
constructor(paths, info, stored) {
|
|
2389
|
+
this.#paths = paths;
|
|
2390
|
+
this.#bytes = info.bytes;
|
|
2391
|
+
this.#capped = info.capped === true;
|
|
2392
|
+
this.#stored = stored.artifact !== void 0 ? { artifact: stored.artifact } : { output: stored.output ?? "" };
|
|
2393
|
+
}
|
|
2394
|
+
async read(tail) {
|
|
2395
|
+
if (tail !== void 0) return this.replay(tail);
|
|
2396
|
+
return this.#head();
|
|
2397
|
+
}
|
|
2398
|
+
async replay(max) {
|
|
2399
|
+
const want = Math.max(0, Math.floor(max));
|
|
2400
|
+
if (this.#capped) {
|
|
2401
|
+
const ring = await readWhole(this.#paths.tail);
|
|
2402
|
+
return ring.subarray(Math.max(0, ring.length - want));
|
|
2403
|
+
}
|
|
2404
|
+
if (this.#stored.output !== void 0) {
|
|
2405
|
+
const all = Buffer.from(this.#stored.output, "utf8");
|
|
2406
|
+
return all.subarray(Math.max(0, all.length - want));
|
|
2407
|
+
}
|
|
2408
|
+
const size = await stat(this.#paths.raw).then((s) => s.size, () => 0);
|
|
2409
|
+
return readRange(this.#paths.raw, Math.max(0, size - want), size);
|
|
2410
|
+
}
|
|
2411
|
+
async textView() {
|
|
2412
|
+
const head = await this.#head();
|
|
2413
|
+
let text = ttyText(head.toString("utf8"));
|
|
2414
|
+
if (this.#capped) {
|
|
2415
|
+
const ring = await readWhole(this.#paths.tail);
|
|
2416
|
+
const after = this.#bytes - head.length;
|
|
2417
|
+
const kept = Math.min(after, ring.length);
|
|
2418
|
+
if (kept > 0) text += (after > kept ? "\n" : "") + ttyText(ring.subarray(ring.length - kept).toString("utf8"));
|
|
2419
|
+
}
|
|
2420
|
+
return text;
|
|
2421
|
+
}
|
|
2422
|
+
stored() {
|
|
2423
|
+
return this.#stored;
|
|
2424
|
+
}
|
|
2425
|
+
idle() {
|
|
2426
|
+
return Promise.resolve();
|
|
2427
|
+
}
|
|
2428
|
+
async remove() {
|
|
2429
|
+
await rm(this.#paths.raw, { force: true });
|
|
2430
|
+
await rm(this.#paths.tail, { force: true });
|
|
2431
|
+
}
|
|
2432
|
+
#head() {
|
|
2433
|
+
return this.#stored.output !== void 0 ? Promise.resolve(Buffer.from(this.#stored.output, "utf8")) : readWhole(this.#paths.raw);
|
|
2434
|
+
}
|
|
2435
|
+
};
|
|
2436
|
+
function serialize(state) {
|
|
2437
|
+
return {
|
|
2438
|
+
version: INDEX_VERSION,
|
|
2439
|
+
sessionId: state.sessionId,
|
|
2440
|
+
nextOrdinal: state.nextOrdinal,
|
|
2441
|
+
shells: [...state.entries.values()].map((entry) => ({
|
|
2442
|
+
...entry.info,
|
|
2443
|
+
generation: entry.generation,
|
|
2444
|
+
...entry.artifact.stored()
|
|
2445
|
+
}))
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
function parseIndex(value) {
|
|
2449
|
+
const index = value;
|
|
2450
|
+
if (!index || index.version !== INDEX_VERSION || typeof index.sessionId !== "string" || !Array.isArray(index.shells)) throw new Error("unrecognised shell index");
|
|
2451
|
+
const shells = index.shells.filter((record) => typeof record === "object" && record !== null && typeof record.id === "string");
|
|
2452
|
+
const nextOrdinal = typeof index.nextOrdinal === "number" && index.nextOrdinal > 0 ? index.nextOrdinal : 1;
|
|
2453
|
+
return {
|
|
2454
|
+
version: INDEX_VERSION,
|
|
2455
|
+
sessionId: index.sessionId,
|
|
2456
|
+
nextOrdinal,
|
|
2457
|
+
shells
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
function freshSession(sessionId) {
|
|
2461
|
+
return {
|
|
2462
|
+
sessionId,
|
|
2463
|
+
nextOrdinal: 1,
|
|
2464
|
+
entries: /* @__PURE__ */ new Map(),
|
|
2465
|
+
chain: Promise.resolve(),
|
|
2466
|
+
pending: false
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
function runningIn(state) {
|
|
2470
|
+
return [...state.entries.values()].filter((entry) => entry.info.status === "running");
|
|
2471
|
+
}
|
|
2472
|
+
function sourceFor(entry, artifact) {
|
|
2473
|
+
return {
|
|
2474
|
+
info: () => entry.info,
|
|
2475
|
+
text: () => artifact.text(),
|
|
2476
|
+
subscribe: (listener) => {
|
|
2477
|
+
entry.listeners.add(listener);
|
|
2478
|
+
return () => {
|
|
2479
|
+
entry.listeners.delete(listener);
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
};
|
|
2483
|
+
}
|
|
2484
|
+
function mintShellId() {
|
|
2485
|
+
const bytes = randomBytes(ID_CHARS);
|
|
2486
|
+
let id = "sh_";
|
|
2487
|
+
for (let i = 0; i < ID_CHARS; i++) id += ID_ALPHABET[bytes[i] & 31];
|
|
2488
|
+
return id;
|
|
2489
|
+
}
|
|
2490
|
+
function shellLabel(command) {
|
|
2491
|
+
return (command.split("\n")[0] ?? "").trim().slice(0, SHELL_LABEL_MAX);
|
|
2492
|
+
}
|
|
2493
|
+
function clamp(value, min, max) {
|
|
2494
|
+
return Number.isFinite(value) ? Math.min(max, Math.max(min, Math.trunc(value))) : min;
|
|
2495
|
+
}
|
|
2496
|
+
function isMissing(error) {
|
|
2497
|
+
return error?.code === "ENOENT";
|
|
2498
|
+
}
|
|
2499
|
+
async function readWhole(path) {
|
|
2500
|
+
try {
|
|
2501
|
+
return await readFile(path);
|
|
2502
|
+
} catch (error) {
|
|
2503
|
+
if (isMissing(error)) return Buffer.alloc(0);
|
|
2504
|
+
throw error;
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
async function readRange(path, start, end) {
|
|
2508
|
+
const length = Math.max(0, end - start);
|
|
2509
|
+
if (length === 0) return Buffer.alloc(0);
|
|
2510
|
+
let handle;
|
|
2511
|
+
try {
|
|
2512
|
+
handle = await open(path, "r");
|
|
2513
|
+
} catch (error) {
|
|
2514
|
+
if (isMissing(error)) return Buffer.alloc(0);
|
|
2515
|
+
throw error;
|
|
2516
|
+
}
|
|
2517
|
+
try {
|
|
2518
|
+
const out = Buffer.allocUnsafe(length);
|
|
2519
|
+
let got = 0;
|
|
2520
|
+
while (got < length) {
|
|
2521
|
+
const { bytesRead } = await handle.read(out, got, length - got, start + got);
|
|
2522
|
+
if (bytesRead === 0) break;
|
|
2523
|
+
got += bytesRead;
|
|
2524
|
+
}
|
|
2525
|
+
return got === length ? out : out.subarray(0, got);
|
|
2526
|
+
} finally {
|
|
2527
|
+
await handle.close();
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
//#endregion
|
|
2531
|
+
//#region src/routes/shells.ts
|
|
2532
|
+
async function handleShells(ctx, req, res, route, runner, operator) {
|
|
2533
|
+
const { shells } = ctx;
|
|
2534
|
+
const sessionId = route.id;
|
|
2535
|
+
if (!shells) {
|
|
2536
|
+
json(res, 404, { error: SHELL_REFUSAL });
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2539
|
+
if (!operator) {
|
|
2540
|
+
json(res, 403, { error: SHELL_REFUSAL });
|
|
2541
|
+
return;
|
|
2542
|
+
}
|
|
2543
|
+
if (route.shellId === void 0) {
|
|
2544
|
+
if (req.method !== "GET") {
|
|
2545
|
+
json(res, 405, { error: "method not allowed" });
|
|
2546
|
+
return;
|
|
2547
|
+
}
|
|
2548
|
+
json(res, 200, { shells: shells.list(sessionId) });
|
|
2549
|
+
return;
|
|
2550
|
+
}
|
|
2551
|
+
if (route.shellAction === "kill") {
|
|
2552
|
+
if (req.method !== "POST") {
|
|
2553
|
+
json(res, 405, { error: "method not allowed" });
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
if (!runner || !shellPermitted(shells, runner, operator)) {
|
|
2557
|
+
json(res, 403, { error: SHELL_REFUSAL });
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2560
|
+
const shell = shells.kill(sessionId, route.shellId);
|
|
2561
|
+
if (!shell) {
|
|
2562
|
+
json(res, 404, { error: "shell not found" });
|
|
2563
|
+
return;
|
|
2564
|
+
}
|
|
2565
|
+
json(res, 200, { shell });
|
|
2566
|
+
return;
|
|
2567
|
+
}
|
|
2568
|
+
if (req.method !== "GET") {
|
|
2569
|
+
json(res, 405, { error: "method not allowed" });
|
|
2570
|
+
return;
|
|
2571
|
+
}
|
|
2572
|
+
if (route.shellAction === "output") {
|
|
2573
|
+
const url = new URL(req.url ?? "/", "http://internal");
|
|
2574
|
+
const view = url.searchParams.get("view") === "raw" ? "raw" : "text";
|
|
2575
|
+
const tailParam = url.searchParams.get("tail");
|
|
2576
|
+
const tail = tailParam === null ? void 0 : Number(tailParam);
|
|
2577
|
+
if (tail !== void 0 && (!Number.isInteger(tail) || tail <= 0)) {
|
|
2578
|
+
json(res, 400, { error: "tail must be a positive integer" });
|
|
2579
|
+
return;
|
|
2580
|
+
}
|
|
2581
|
+
const output = await shells.output(sessionId, route.shellId, {
|
|
2582
|
+
view,
|
|
2583
|
+
tail
|
|
2584
|
+
});
|
|
2585
|
+
if (output === void 0) {
|
|
2586
|
+
json(res, 404, { error: "shell not found" });
|
|
2587
|
+
return;
|
|
2588
|
+
}
|
|
2589
|
+
const body = Buffer.from(output, "utf8");
|
|
2590
|
+
res.writeHead(200, untrustedDownloadHeaders(`${route.shellId}.txt`, "text/plain; charset=utf-8", body.length));
|
|
2591
|
+
res.end(body);
|
|
2592
|
+
return;
|
|
2593
|
+
}
|
|
2594
|
+
const shell = shells.get(sessionId, route.shellId);
|
|
2595
|
+
if (!shell) {
|
|
2596
|
+
json(res, 404, { error: "shell not found" });
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
json(res, 200, { shell });
|
|
2600
|
+
}
|
|
2601
|
+
//#endregion
|
|
1521
2602
|
//#region src/routes/tool-results.ts
|
|
1522
2603
|
function handleToolResult(req, res, lookup, seq) {
|
|
1523
2604
|
if (req.method !== "GET") {
|
|
@@ -1650,6 +2731,10 @@ async function handleSessions(ctx, req, res, route, auth) {
|
|
|
1650
2731
|
await handleProducedFiles(ctx, req, res, route.id, route.producedFileId);
|
|
1651
2732
|
return;
|
|
1652
2733
|
}
|
|
2734
|
+
if (route.shells) {
|
|
2735
|
+
await handleShells(ctx, req, res, route, runner ?? null, authSvc.isOperator(auth));
|
|
2736
|
+
return;
|
|
2737
|
+
}
|
|
1653
2738
|
if (route.projectIcon) {
|
|
1654
2739
|
handleProjectIcon(projects, req, res, (runner?.info() ?? parked.info).cwd);
|
|
1655
2740
|
return;
|
|
@@ -1724,151 +2809,7 @@ async function handleSessions(ctx, req, res, route, auth) {
|
|
|
1724
2809
|
}
|
|
1725
2810
|
json(res, 405, { error: "method not allowed" });
|
|
1726
2811
|
}
|
|
1727
|
-
const
|
|
1728
|
-
const SPAWN_FAILURE_EXIT_CODE = 127;
|
|
1729
|
-
const KILLED_EXIT_CODE = 128 + constants$1.signals.SIGKILL;
|
|
1730
|
-
function shellPermitted(shell, runner, operator) {
|
|
1731
|
-
if (shell === null || !operator) return false;
|
|
1732
|
-
const info = runner.info();
|
|
1733
|
-
return (info.capabilities ?? (info.engine ? ENGINE_CAPABILITIES[info.engine] : void 0))?.hostCwd === true;
|
|
1734
|
-
}
|
|
1735
|
-
function shellChildEnv(base) {
|
|
1736
|
-
const env = {};
|
|
1737
|
-
for (const [key, value] of Object.entries(base)) if (value !== void 0) env[key] = value;
|
|
1738
|
-
return env;
|
|
1739
|
-
}
|
|
1740
|
-
function createShellService(options = {}) {
|
|
1741
|
-
const timeoutMs = options.timeoutMs ?? 12e4;
|
|
1742
|
-
const maxOutputBytes = options.maxOutputBytes ?? 32768;
|
|
1743
|
-
const active = /* @__PURE__ */ new Map();
|
|
1744
|
-
const run = async (sessionId, cwd, command) => {
|
|
1745
|
-
if (active.has(sessionId)) throw new Error("a shell command is still running on this session");
|
|
1746
|
-
const child = spawnShell({
|
|
1747
|
-
command,
|
|
1748
|
-
cwd,
|
|
1749
|
-
env: shellChildEnv(process.env),
|
|
1750
|
-
timeoutMs,
|
|
1751
|
-
maxOutputBytes
|
|
1752
|
-
});
|
|
1753
|
-
active.set(sessionId, child);
|
|
1754
|
-
try {
|
|
1755
|
-
return await child.result;
|
|
1756
|
-
} finally {
|
|
1757
|
-
if (active.get(sessionId) === child) active.delete(sessionId);
|
|
1758
|
-
}
|
|
1759
|
-
};
|
|
1760
|
-
const kill = (sessionId, reason = "session closed") => {
|
|
1761
|
-
active.get(sessionId)?.kill(reason);
|
|
1762
|
-
};
|
|
1763
|
-
const watch = (runner) => runner.subscribe((event) => {
|
|
1764
|
-
if (event.type === "session_closed") kill(runner.id, "session closed");
|
|
1765
|
-
else if (event.type === "status_changed" && event.status === "parked") kill(runner.id, "session parked");
|
|
1766
|
-
}, runner.info().lastSeq);
|
|
1767
|
-
const killAll = () => {
|
|
1768
|
-
for (const sessionId of active.keys()) kill(sessionId, "server shutting down");
|
|
1769
|
-
};
|
|
1770
|
-
const running = (sessionId) => active.has(sessionId);
|
|
1771
|
-
return {
|
|
1772
|
-
run,
|
|
1773
|
-
kill,
|
|
1774
|
-
watch,
|
|
1775
|
-
killAll,
|
|
1776
|
-
running,
|
|
1777
|
-
timeoutMs,
|
|
1778
|
-
maxOutputBytes
|
|
1779
|
-
};
|
|
1780
|
-
}
|
|
1781
|
-
var BoundedOutput = class {
|
|
1782
|
-
#chunks = [];
|
|
1783
|
-
#kept = 0;
|
|
1784
|
-
#dropped = 0;
|
|
1785
|
-
#budget;
|
|
1786
|
-
constructor(budget) {
|
|
1787
|
-
this.#budget = budget;
|
|
1788
|
-
}
|
|
1789
|
-
push(chunk) {
|
|
1790
|
-
const take = Math.min(chunk.length, this.#budget.remaining);
|
|
1791
|
-
if (take > 0) {
|
|
1792
|
-
this.#chunks.push(take === chunk.length ? chunk : chunk.subarray(0, take));
|
|
1793
|
-
this.#kept += take;
|
|
1794
|
-
this.#budget.remaining -= take;
|
|
1795
|
-
}
|
|
1796
|
-
this.#dropped += chunk.length - take;
|
|
1797
|
-
}
|
|
1798
|
-
text() {
|
|
1799
|
-
const head = Buffer.concat(this.#chunks, this.#kept).toString("utf8");
|
|
1800
|
-
return this.#dropped > 0 ? `${head}\n[output truncated: ${this.#dropped} more bytes dropped]` : head;
|
|
1801
|
-
}
|
|
1802
|
-
};
|
|
1803
|
-
function spawnShell(input) {
|
|
1804
|
-
const startedAt = Date.now();
|
|
1805
|
-
const budget = { remaining: input.maxOutputBytes };
|
|
1806
|
-
const stdout = new BoundedOutput(budget);
|
|
1807
|
-
const stderr = new BoundedOutput(budget);
|
|
1808
|
-
let settle;
|
|
1809
|
-
const result = new Promise((resolve) => {
|
|
1810
|
-
settle = resolve;
|
|
1811
|
-
});
|
|
1812
|
-
let done = false;
|
|
1813
|
-
const finish = (exitCode, note) => {
|
|
1814
|
-
if (done) return;
|
|
1815
|
-
done = true;
|
|
1816
|
-
clearTimeout(timer);
|
|
1817
|
-
const captured = stderr.text();
|
|
1818
|
-
const stderrText = note ? captured ? `${captured}\n[${note}]` : `[${note}]` : captured;
|
|
1819
|
-
settle?.({
|
|
1820
|
-
command: input.command,
|
|
1821
|
-
stdout: stdout.text(),
|
|
1822
|
-
stderr: stderrText,
|
|
1823
|
-
exitCode,
|
|
1824
|
-
durationMs: Date.now() - startedAt
|
|
1825
|
-
});
|
|
1826
|
-
};
|
|
1827
|
-
const child = spawn("/bin/sh", ["-c", input.command], {
|
|
1828
|
-
cwd: input.cwd,
|
|
1829
|
-
env: {
|
|
1830
|
-
...input.env,
|
|
1831
|
-
PWD: input.cwd
|
|
1832
|
-
},
|
|
1833
|
-
stdio: [
|
|
1834
|
-
"ignore",
|
|
1835
|
-
"pipe",
|
|
1836
|
-
"pipe"
|
|
1837
|
-
],
|
|
1838
|
-
detached: true
|
|
1839
|
-
});
|
|
1840
|
-
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
1841
|
-
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
1842
|
-
const killGroup = () => {
|
|
1843
|
-
if (child.pid === void 0) return;
|
|
1844
|
-
try {
|
|
1845
|
-
process.kill(-child.pid, "SIGKILL");
|
|
1846
|
-
} catch {
|
|
1847
|
-
try {
|
|
1848
|
-
child.kill("SIGKILL");
|
|
1849
|
-
} catch {}
|
|
1850
|
-
}
|
|
1851
|
-
child.stdout.destroy();
|
|
1852
|
-
child.stderr.destroy();
|
|
1853
|
-
};
|
|
1854
|
-
const kill = (reason, exitCode = KILLED_EXIT_CODE) => {
|
|
1855
|
-
if (done) return;
|
|
1856
|
-
killGroup();
|
|
1857
|
-
finish(exitCode, `killed: ${reason}`);
|
|
1858
|
-
};
|
|
1859
|
-
const timer = setTimeout(() => kill(`timed out after ${Math.round(input.timeoutMs / 1e3)}s`, TIMEOUT_EXIT_CODE), input.timeoutMs);
|
|
1860
|
-
child.on("error", (error) => finish(SPAWN_FAILURE_EXIT_CODE, error.message));
|
|
1861
|
-
child.on("close", (code, signal) => {
|
|
1862
|
-
const signalCode = signal ? constants$1.signals[signal] ?? 0 : 0;
|
|
1863
|
-
finish(code ?? (signalCode ? 128 + signalCode : 1), signal ? `killed by ${signal}` : void 0);
|
|
1864
|
-
});
|
|
1865
|
-
return {
|
|
1866
|
-
result,
|
|
1867
|
-
kill: (reason) => kill(reason)
|
|
1868
|
-
};
|
|
1869
|
-
}
|
|
1870
|
-
//#endregion
|
|
1871
|
-
//#region src/routes/ws.ts
|
|
2812
|
+
const SHELL_DETACHED_BACKPRESSURE = "backpressure";
|
|
1872
2813
|
function attachClient(ctx, ws, runner, req, access) {
|
|
1873
2814
|
const { bridge, parking } = ctx;
|
|
1874
2815
|
const url = new URL(req.url ?? "/", "http://internal");
|
|
@@ -1878,12 +2819,18 @@ function attachClient(ctx, ws, runner, req, access) {
|
|
|
1878
2819
|
const send = (frame) => {
|
|
1879
2820
|
if (ws.readyState === ws.OPEN) ws.send(JSON.stringify(frame));
|
|
1880
2821
|
};
|
|
2822
|
+
const client = {
|
|
2823
|
+
send,
|
|
2824
|
+
open: () => ws.readyState === ws.OPEN,
|
|
2825
|
+
buffered: () => ws.bufferedAmount,
|
|
2826
|
+
shells: /* @__PURE__ */ new Map()
|
|
2827
|
+
};
|
|
1881
2828
|
send({
|
|
1882
2829
|
type: "attached",
|
|
1883
2830
|
protocolVersion: PROTOCOL_VERSION,
|
|
1884
2831
|
session: ctx.projects.withProject(runner.info()),
|
|
1885
2832
|
replayingFrom: afterSeq,
|
|
1886
|
-
...shellPermitted(ctx.
|
|
2833
|
+
...shellPermitted(ctx.shells, runner, access.operator) ? { shell: true } : {},
|
|
1887
2834
|
...ctx.pricingOverrides ? { pricingOverrides: ctx.pricingOverrides } : {}
|
|
1888
2835
|
});
|
|
1889
2836
|
const unsubscribe = runner.subscribe((event) => send({
|
|
@@ -1907,7 +2854,7 @@ function attachClient(ctx, ws, runner, req, access) {
|
|
|
1907
2854
|
});
|
|
1908
2855
|
return;
|
|
1909
2856
|
}
|
|
1910
|
-
handleCommand(ctx, frame, runner, access).catch((error) => {
|
|
2857
|
+
handleCommand(ctx, frame, runner, access, client).catch((error) => {
|
|
1911
2858
|
send({
|
|
1912
2859
|
type: "protocol_error",
|
|
1913
2860
|
message: error instanceof Error ? error.message : "command failed"
|
|
@@ -1915,12 +2862,13 @@ function attachClient(ctx, ws, runner, req, access) {
|
|
|
1915
2862
|
});
|
|
1916
2863
|
});
|
|
1917
2864
|
ws.on("close", () => {
|
|
2865
|
+
detachShells(client);
|
|
1918
2866
|
unsubscribe();
|
|
1919
2867
|
detachBridge();
|
|
1920
2868
|
parking.onDetach(runner.id);
|
|
1921
2869
|
});
|
|
1922
2870
|
}
|
|
1923
|
-
async function handleCommand(ctx, frame, runner, access) {
|
|
2871
|
+
async function handleCommand(ctx, frame, runner, access, client) {
|
|
1924
2872
|
const { attachmentStore, bridge } = ctx;
|
|
1925
2873
|
switch (frame.type) {
|
|
1926
2874
|
case "user_message": {
|
|
@@ -1974,21 +2922,107 @@ async function handleCommand(ctx, frame, runner, access) {
|
|
|
1974
2922
|
});
|
|
1975
2923
|
return;
|
|
1976
2924
|
case "shell_command": {
|
|
1977
|
-
|
|
2925
|
+
const shells = permittedShells(ctx, runner, access);
|
|
1978
2926
|
if (typeof frame.command !== "string" || frame.command.includes("\0")) throw new Error("shell command must be a string");
|
|
1979
2927
|
if (frame.command.length > SHELL_COMMAND_MAX) throw new Error(`shell command exceeds ${SHELL_COMMAND_MAX} characters`);
|
|
1980
2928
|
if (frame.command.trim() === "") throw new Error("shell command is empty");
|
|
1981
2929
|
if (!runner.queueLocalCommand) throw new Error(`the ${runner.info().engine ?? "claude"} engine cannot take shell output`);
|
|
1982
|
-
const
|
|
1983
|
-
|
|
2930
|
+
const { shell, source } = await shells.spawn({
|
|
2931
|
+
runner,
|
|
2932
|
+
command: frame.command,
|
|
2933
|
+
owner: "user"
|
|
2934
|
+
});
|
|
2935
|
+
try {
|
|
2936
|
+
runner.queueLocalCommand(source);
|
|
2937
|
+
} catch (error) {
|
|
2938
|
+
shells.kill(runner.id, shell.id);
|
|
2939
|
+
throw error;
|
|
2940
|
+
}
|
|
1984
2941
|
return;
|
|
1985
2942
|
}
|
|
2943
|
+
case "shell_attach":
|
|
2944
|
+
await attachShell(permittedShells(ctx, runner, access), client, runner.id, frame.shellId, {
|
|
2945
|
+
cols: frame.cols,
|
|
2946
|
+
rows: frame.rows
|
|
2947
|
+
});
|
|
2948
|
+
return;
|
|
2949
|
+
case "shell_input":
|
|
2950
|
+
permittedShells(ctx, runner, access).write(runner.id, frame.shellId, frame.data);
|
|
2951
|
+
return;
|
|
2952
|
+
case "shell_resize":
|
|
2953
|
+
permittedShells(ctx, runner, access).resize(runner.id, frame.shellId, {
|
|
2954
|
+
cols: frame.cols,
|
|
2955
|
+
rows: frame.rows
|
|
2956
|
+
});
|
|
2957
|
+
return;
|
|
2958
|
+
case "shell_detach":
|
|
2959
|
+
detachShell(client, frame.shellId);
|
|
2960
|
+
return;
|
|
1986
2961
|
case "close":
|
|
1987
2962
|
runner.close("client");
|
|
1988
2963
|
return;
|
|
1989
2964
|
default: throw new Error(`unknown command: ${frame.type}`);
|
|
1990
2965
|
}
|
|
1991
2966
|
}
|
|
2967
|
+
function permittedShells(ctx, runner, access) {
|
|
2968
|
+
if (ctx.shells === null || !shellPermitted(ctx.shells, runner, access.operator)) throw new Error(SHELL_REFUSAL);
|
|
2969
|
+
return ctx.shells;
|
|
2970
|
+
}
|
|
2971
|
+
async function attachShell(shells, client, sessionId, shellId, size) {
|
|
2972
|
+
shells.resize(sessionId, shellId, size);
|
|
2973
|
+
let detach;
|
|
2974
|
+
const drop = (reason) => {
|
|
2975
|
+
if (detach === void 0 || client.shells.get(shellId) !== detach) return;
|
|
2976
|
+
client.shells.delete(shellId);
|
|
2977
|
+
detach();
|
|
2978
|
+
client.send({
|
|
2979
|
+
type: "shell_detached",
|
|
2980
|
+
shellId,
|
|
2981
|
+
reason
|
|
2982
|
+
});
|
|
2983
|
+
};
|
|
2984
|
+
const sink = {
|
|
2985
|
+
write: (data) => {
|
|
2986
|
+
if (client.buffered() > 4194304) {
|
|
2987
|
+
drop(SHELL_DETACHED_BACKPRESSURE);
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
client.send({
|
|
2991
|
+
type: "shell_output",
|
|
2992
|
+
shellId,
|
|
2993
|
+
data
|
|
2994
|
+
});
|
|
2995
|
+
},
|
|
2996
|
+
end: drop
|
|
2997
|
+
};
|
|
2998
|
+
const attached = await shells.attach(sessionId, shellId, sink);
|
|
2999
|
+
if (!client.open()) {
|
|
3000
|
+
attached.detach();
|
|
3001
|
+
return;
|
|
3002
|
+
}
|
|
3003
|
+
detachShell(client, shellId);
|
|
3004
|
+
if (attached.shell.status === "running") {
|
|
3005
|
+
detach = attached.detach;
|
|
3006
|
+
client.shells.set(shellId, detach);
|
|
3007
|
+
}
|
|
3008
|
+
const { shell, scrollback } = attached;
|
|
3009
|
+
client.send({
|
|
3010
|
+
type: "shell_attached",
|
|
3011
|
+
shellId,
|
|
3012
|
+
shell,
|
|
3013
|
+
cols: shell.cols,
|
|
3014
|
+
rows: shell.rows,
|
|
3015
|
+
scrollback
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
function detachShell(client, shellId) {
|
|
3019
|
+
client.shells.get(shellId)?.();
|
|
3020
|
+
client.shells.delete(shellId);
|
|
3021
|
+
}
|
|
3022
|
+
function detachShells(client) {
|
|
3023
|
+
for (const detach of client.shells.values()) detach();
|
|
3024
|
+
client.shells.clear();
|
|
3025
|
+
}
|
|
1992
3026
|
//#endregion
|
|
1993
3027
|
//#region src/services/attachments.ts
|
|
1994
3028
|
const DEFAULT_MAX_FILE_BYTES = 10485760;
|
|
@@ -3465,14 +4499,15 @@ function createSessionFactory(deps) {
|
|
|
3465
4499
|
}
|
|
3466
4500
|
};
|
|
3467
4501
|
const buildRunner = async (built, restore, id) => {
|
|
3468
|
-
const
|
|
3469
|
-
...built,
|
|
3470
|
-
peers: deps.peers
|
|
3471
|
-
} : built;
|
|
3472
|
-
const name = config.profile;
|
|
4502
|
+
const name = built.profile;
|
|
3473
4503
|
const profile = name !== void 0 ? profiles.get(name) : void 0;
|
|
3474
4504
|
if (name !== void 0 && !profile) throw new Error(`unknown profile: ${name}`);
|
|
3475
4505
|
const capabilities = profile?.capabilities ?? adapterFor(profile?.engine).capabilities;
|
|
4506
|
+
const config = {
|
|
4507
|
+
...built,
|
|
4508
|
+
...deps.peers ? { peers: deps.peers } : {},
|
|
4509
|
+
...deps.shells && capabilities.hostCwd === true ? { shells: deps.shells } : {}
|
|
4510
|
+
};
|
|
3476
4511
|
if (config.instructions !== void 0 && capabilities.systemInstructions === false) throw new Error(`the ${engineOf(profile)} engine cannot deliver system instructions`);
|
|
3477
4512
|
const runner = profile && isProviderProfile(profile) ? await deps.createEngineRunner({
|
|
3478
4513
|
config,
|
|
@@ -3567,7 +4602,7 @@ function createSessionFactory(deps) {
|
|
|
3567
4602
|
const SOCKET_CLOSE_GRACE_MS = 250;
|
|
3568
4603
|
const CLOSE_DEADLINE_MS = 1e3;
|
|
3569
4604
|
const USAGE_STALE_MS = 3e5;
|
|
3570
|
-
function surveyDrain(registry) {
|
|
4605
|
+
function surveyDrain(registry, shells) {
|
|
3571
4606
|
const working = [];
|
|
3572
4607
|
const awaitingHuman = [];
|
|
3573
4608
|
for (const info of registry.list()) {
|
|
@@ -3578,7 +4613,12 @@ function surveyDrain(registry) {
|
|
|
3578
4613
|
return {
|
|
3579
4614
|
working,
|
|
3580
4615
|
awaitingHuman,
|
|
3581
|
-
timedOut: false
|
|
4616
|
+
timedOut: false,
|
|
4617
|
+
shells: (shells?.running() ?? []).map(({ sessionId, id, label }) => ({
|
|
4618
|
+
sessionId,
|
|
4619
|
+
id,
|
|
4620
|
+
label
|
|
4621
|
+
}))
|
|
3582
4622
|
};
|
|
3583
4623
|
}
|
|
3584
4624
|
function sameDrain(a, b) {
|
|
@@ -3638,13 +4678,24 @@ function createWorkerServer(options = {}) {
|
|
|
3638
4678
|
if (invalid) throw new Error(`createWorkerServer: ${invalid}`);
|
|
3639
4679
|
}
|
|
3640
4680
|
const refs = {};
|
|
3641
|
-
const
|
|
4681
|
+
const generation = randomUUID();
|
|
4682
|
+
const shells = options.shell?.enabled === true ? createShellRegistry({
|
|
4683
|
+
generation,
|
|
4684
|
+
artifactDir: options.shell.artifactDir ?? null,
|
|
4685
|
+
timeoutMs: options.shell.timeoutMs,
|
|
4686
|
+
artifactMaxBytes: options.shell.artifactMaxBytes,
|
|
4687
|
+
artifactTtlMs: options.shell.artifactTtlMs,
|
|
4688
|
+
maxRunningPerSession: options.shell.maxRunningPerSession,
|
|
4689
|
+
onError: (error, context) => console.warn(`[workerdeck] shell ${context.op} error (${context.shellId ?? context.sessionId ?? "-"}): ${String(error)}`)
|
|
4690
|
+
}) : null;
|
|
4691
|
+
const projects = new ProjectInfoService({ decorate: (info) => shells ? shells.decorate(info) : info });
|
|
3642
4692
|
const peers = options.peers?.enabled === false ? void 0 : createPeerService({
|
|
3643
4693
|
refs,
|
|
3644
4694
|
projects,
|
|
3645
4695
|
options: options.peers
|
|
3646
4696
|
});
|
|
3647
4697
|
if (peers) installPeerDirectory(peers);
|
|
4698
|
+
installShellDirectory(shells ? createShellDirectory(shells) : void 0);
|
|
3648
4699
|
const factory = createSessionFactory({
|
|
3649
4700
|
adapterFor,
|
|
3650
4701
|
profiles,
|
|
@@ -3655,6 +4706,7 @@ function createWorkerServer(options = {}) {
|
|
|
3655
4706
|
approvalTimeoutMs: options.approvalTimeoutMs,
|
|
3656
4707
|
requireApiKey: options.requireApiKey,
|
|
3657
4708
|
peers: peers ? peerDirectoryHandle() : void 0,
|
|
4709
|
+
shells: shells ? shellDirectoryHandle() : void 0,
|
|
3658
4710
|
refs
|
|
3659
4711
|
});
|
|
3660
4712
|
const availability = new AvailabilityTracker({
|
|
@@ -3668,14 +4720,13 @@ function createWorkerServer(options = {}) {
|
|
|
3668
4720
|
decorateInfo: (info) => projects.withProject(info)
|
|
3669
4721
|
});
|
|
3670
4722
|
const producedFiles = new ProducedFileStore();
|
|
3671
|
-
const shell = options.shell?.enabled === true ? createShellService(options.shell) : null;
|
|
3672
4723
|
const registry = new SessionRegistry({ onRegister: (runner) => {
|
|
3673
4724
|
const detachers = [
|
|
3674
4725
|
notifier.watch(runner),
|
|
3675
4726
|
producedFiles.watch(runner),
|
|
3676
4727
|
profileUsage.watch(runner),
|
|
3677
4728
|
spendLedger.watch(runner),
|
|
3678
|
-
|
|
4729
|
+
shells?.watch(runner),
|
|
3679
4730
|
peers?.watch(runner)
|
|
3680
4731
|
];
|
|
3681
4732
|
const profile = runner.info().profile;
|
|
@@ -3786,7 +4837,8 @@ function createWorkerServer(options = {}) {
|
|
|
3786
4837
|
hostFilesWritable: options.hostFiles?.write === true,
|
|
3787
4838
|
maxHostFileBytes: options.hostFiles?.maxFileBytes ?? 1048576,
|
|
3788
4839
|
maxHostDirEntries: options.hostFiles?.maxEntries ?? 5e3,
|
|
3789
|
-
|
|
4840
|
+
shells,
|
|
4841
|
+
generation,
|
|
3790
4842
|
pricingOverrides: Object.keys(pricing.overrides).length > 0 ? pricing.overrides : void 0
|
|
3791
4843
|
};
|
|
3792
4844
|
const handleRequest = async (req, res) => {
|
|
@@ -3855,6 +4907,18 @@ function createWorkerServer(options = {}) {
|
|
|
3855
4907
|
await handleSdkSessions(ctx, req, res, authCtx);
|
|
3856
4908
|
return;
|
|
3857
4909
|
}
|
|
4910
|
+
if (pathname === basePath + "/meta") {
|
|
4911
|
+
const authCtx = await auth.authenticate(req);
|
|
4912
|
+
if (!authCtx.ok) {
|
|
4913
|
+
json(res, 401, { error: "unauthorized" });
|
|
4914
|
+
return;
|
|
4915
|
+
}
|
|
4916
|
+
json(res, 200, {
|
|
4917
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
4918
|
+
...auth.isOperator(authCtx) ? { machineId: machineId() } : {}
|
|
4919
|
+
});
|
|
4920
|
+
return;
|
|
4921
|
+
}
|
|
3858
4922
|
if (pathname.startsWith(basePath + "/fs/")) {
|
|
3859
4923
|
const authCtx = await auth.authenticate(req);
|
|
3860
4924
|
if (!authCtx.ok) {
|
|
@@ -3955,10 +5019,12 @@ function createWorkerServer(options = {}) {
|
|
|
3955
5019
|
queue,
|
|
3956
5020
|
bridge,
|
|
3957
5021
|
parking,
|
|
5022
|
+
shells,
|
|
3958
5023
|
listen: async (port, host) => {
|
|
3959
5024
|
await profiles.refreshStored();
|
|
3960
5025
|
await profiles.seedStore();
|
|
3961
5026
|
await parking.hydrate();
|
|
5027
|
+
await shells?.hydrate();
|
|
3962
5028
|
return new Promise((resolve, reject) => {
|
|
3963
5029
|
server.once("error", reject);
|
|
3964
5030
|
server.listen(port, host, () => {
|
|
@@ -3990,16 +5056,16 @@ function createWorkerServer(options = {}) {
|
|
|
3990
5056
|
const { timeoutMs = 3e4, pollMs = 250, onProgress } = drainOptions;
|
|
3991
5057
|
draining = true;
|
|
3992
5058
|
const deadline = Date.now() + timeoutMs;
|
|
3993
|
-
let report = surveyDrain(registry);
|
|
5059
|
+
let report = surveyDrain(registry, shells);
|
|
3994
5060
|
onProgress?.(report);
|
|
3995
5061
|
while (report.working.length > 0 && Date.now() < deadline) {
|
|
3996
5062
|
await new Promise((resolve) => setTimeout(resolve, pollMs));
|
|
3997
|
-
const next = surveyDrain(registry);
|
|
5063
|
+
const next = surveyDrain(registry, shells);
|
|
3998
5064
|
if (!sameDrain(next, report)) onProgress?.(next);
|
|
3999
5065
|
report = next;
|
|
4000
5066
|
}
|
|
4001
5067
|
report = {
|
|
4002
|
-
...surveyDrain(registry),
|
|
5068
|
+
...surveyDrain(registry, shells),
|
|
4003
5069
|
timedOut: false
|
|
4004
5070
|
};
|
|
4005
5071
|
report.timedOut = report.working.length > 0;
|
|
@@ -4009,23 +5075,25 @@ function createWorkerServer(options = {}) {
|
|
|
4009
5075
|
close: () => {
|
|
4010
5076
|
closing ??= new Promise((resolve) => {
|
|
4011
5077
|
queue?.close();
|
|
5078
|
+
shells?.killAll("server_stopped");
|
|
5079
|
+
const flushed = shells?.flush().catch(() => {}) ?? Promise.resolve();
|
|
4012
5080
|
parking.close();
|
|
4013
5081
|
registry.closeAll();
|
|
4014
|
-
shell?.killAll();
|
|
4015
5082
|
for (const ws of wss.clients) ws.close(1001, "server shutting down");
|
|
4016
5083
|
queueSockets.clear();
|
|
4017
5084
|
const force = setTimeout(() => {
|
|
4018
5085
|
for (const ws of wss.clients) ws.terminate();
|
|
4019
5086
|
}, SOCKET_CLOSE_GRACE_MS);
|
|
4020
5087
|
force.unref();
|
|
4021
|
-
const
|
|
5088
|
+
const finish = () => void flushed.then(() => resolve());
|
|
5089
|
+
const deadline = setTimeout(finish, CLOSE_DEADLINE_MS);
|
|
4022
5090
|
deadline.unref();
|
|
4023
5091
|
const drained = Promise.all([new Promise((done) => wss.close(() => done())), new Promise((done) => server.close(() => done()))]);
|
|
4024
5092
|
server.closeAllConnections();
|
|
4025
5093
|
drained.then(() => {
|
|
4026
5094
|
clearTimeout(force);
|
|
4027
5095
|
clearTimeout(deadline);
|
|
4028
|
-
|
|
5096
|
+
finish();
|
|
4029
5097
|
});
|
|
4030
5098
|
});
|
|
4031
5099
|
return closing;
|
|
@@ -4123,6 +5191,6 @@ function createFileProfileStore(path = join(process.cwd(), ".workerdeck", "profi
|
|
|
4123
5191
|
};
|
|
4124
5192
|
}
|
|
4125
5193
|
//#endregion
|
|
4126
|
-
export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SessionNotifier, SessionParkManager, SessionRegistry, SpendLedger, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createWorkerServer, dayKey, isDormant, reloadPlan, sandboxedProviderProfile, toDurableRecord };
|
|
5194
|
+
export { AttachmentStore, BridgeHub, MemorySessionStore, ProducedFileStore, ProfileUsageTracker, SHELL_REFUSAL, SessionNotifier, SessionParkManager, SessionRegistry, SpendLedger, createFileProfileStore, createFileSessionStore, createFileSpendStore, createMemoryProfileStore, createProviderRunner, createShellRegistry, createWorkerServer, dayKey, isDormant, reloadPlan, sandboxedProviderProfile, shellPermitted, toDurableRecord };
|
|
4127
5195
|
|
|
4128
5196
|
//# sourceMappingURL=index.mjs.map
|