@rallycry/conveyor-agent 10.13.9 → 10.13.11

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.
@@ -0,0 +1,55 @@
1
+ // src/workbench/protocol.ts
2
+ import crypto from "crypto";
3
+ function isRecord(value) {
4
+ return typeof value === "object" && value !== null;
5
+ }
6
+ function parseLine(line) {
7
+ if (!line) return null;
8
+ try {
9
+ const parsed = JSON.parse(line);
10
+ return isRecord(parsed) ? parsed : null;
11
+ } catch {
12
+ return null;
13
+ }
14
+ }
15
+ var FrameReader = class {
16
+ constructor(onFrame) {
17
+ this.onFrame = onFrame;
18
+ }
19
+ onFrame;
20
+ buffer = "";
21
+ push(chunk) {
22
+ this.buffer += chunk.toString();
23
+ let index = this.buffer.indexOf("\n");
24
+ while (index >= 0) {
25
+ const line = this.buffer.slice(0, index);
26
+ this.buffer = this.buffer.slice(index + 1);
27
+ const frame = parseLine(line);
28
+ if (frame) this.onFrame(frame);
29
+ index = this.buffer.indexOf("\n");
30
+ }
31
+ }
32
+ };
33
+ function writeFrame(socket, frame) {
34
+ try {
35
+ socket.write(`${JSON.stringify(frame)}
36
+ `);
37
+ } catch {
38
+ }
39
+ }
40
+ var DEFAULT_WORKBENCH_PORT = 7411;
41
+ function timingSafeTokenEqual(a, b) {
42
+ const ab = Buffer.from(a);
43
+ const bb = Buffer.from(b);
44
+ if (ab.length !== bb.length) return false;
45
+ return crypto.timingSafeEqual(ab, bb);
46
+ }
47
+
48
+ export {
49
+ parseLine,
50
+ FrameReader,
51
+ writeFrame,
52
+ DEFAULT_WORKBENCH_PORT,
53
+ timingSafeTokenEqual
54
+ };
55
+ //# sourceMappingURL=chunk-IE7EOKMD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/workbench/protocol.ts"],"sourcesContent":["/**\n * Wire protocol for the workbench launcher — the loopback control channel\n * between the protected agent container and the restartable workbench\n * container (see docs/superpowers/specs/2026-07-16-workbench-container-split-design.md).\n *\n * Newline-delimited JSON frames over a plain TCP socket. Every connection\n * carries exactly ONE operation: the first client frame is a WorkbenchRequest\n * (with the shared token); everything after is a WorkbenchFrame in either\n * direction. Binary payloads (pty bytes, file contents, snapshot tarballs)\n * ride base64 `d` fields — intra-pod loopback makes the 33% inflation cheap,\n * and NDJSON keeps the framing trivially robust across partial reads.\n */\n\nimport crypto from \"node:crypto\";\nimport type { Socket } from \"node:net\";\n\n/** First frame of every connection (client → server). */\nexport type WorkbenchRequest =\n | { op: \"ping\"; token: string }\n | {\n op: \"exec\";\n token: string;\n /** Shell form — spawned as `sh -c command` (setup/start commands). */\n command?: string;\n /** Argv form — spawned directly, no shell (git and friends). */\n argv?: string[];\n cwd: string;\n env?: Record<string, string>;\n }\n | {\n op: \"pty\";\n token: string;\n file: string;\n args: string[];\n cwd: string;\n env: Record<string, string>;\n cols: number;\n rows: number;\n }\n | { op: \"snapshotBuild\"; token: string; cwd: string }\n | { op: \"snapshotExtract\"; token: string; cwd: string }\n | { op: \"readFile\"; token: string; path: string }\n | { op: \"stat\"; token: string; path: string }\n | { op: \"readdir\"; token: string; path: string };\n\n/** Mid-stream frames (either direction after the opening request). */\nexport type WorkbenchFrame =\n | { t: \"out\"; s: \"stdout\" | \"stderr\"; d: string }\n | { t: \"exit\"; code: number | null; signal: string | null }\n | { t: \"data\"; d: string }\n | { t: \"end\" }\n | { t: \"input\"; d: string }\n | { t: \"resize\"; cols: number; rows: number }\n | { t: \"kill\"; sig?: string }\n | { t: \"signal\"; mode: \"term-group\" }\n | {\n t: \"meta\";\n sizeBytes: number;\n fileCount: number;\n deletionCount: number;\n capturedAt: number;\n }\n | { t: \"result\"; value: unknown }\n | {\n t: \"stat\";\n exists: boolean;\n isFile: boolean;\n isDirectory: boolean;\n size: number;\n mtimeMs: number;\n }\n | { t: \"entries\"; names: string[] }\n | { t: \"pong\"; version: string }\n | { t: \"error\"; message: string; code?: string };\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\n/** Parse one NDJSON line into a frame-shaped record, or null when malformed. */\nexport function parseLine(line: string): Record<string, unknown> | null {\n if (!line) return null;\n try {\n const parsed: unknown = JSON.parse(line);\n return isRecord(parsed) ? parsed : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Incremental NDJSON reader. Feed raw socket chunks; complete lines are\n * parsed and handed to the callback (malformed lines are dropped — the same\n * contract as the hook socket's envelope parsing).\n */\nexport class FrameReader {\n private buffer = \"\";\n\n constructor(private readonly onFrame: (frame: Record<string, unknown>) => void) {}\n\n push(chunk: Buffer | string): void {\n this.buffer += chunk.toString();\n let index = this.buffer.indexOf(\"\\n\");\n while (index >= 0) {\n const line = this.buffer.slice(0, index);\n this.buffer = this.buffer.slice(index + 1);\n const frame = parseLine(line);\n if (frame) this.onFrame(frame);\n index = this.buffer.indexOf(\"\\n\");\n }\n }\n}\n\n/** Serialize + write one frame. Write errors are the socket's problem —\n * callers handle teardown via the socket's own error/close events. */\nexport function writeFrame(\n socket: Pick<Socket, \"write\">,\n frame: WorkbenchRequest | WorkbenchFrame,\n): void {\n try {\n socket.write(`${JSON.stringify(frame)}\\n`);\n } catch {\n /* socket already destroyed — close handling owns cleanup */\n }\n}\n\nexport const DEFAULT_WORKBENCH_PORT = 7411;\n\n/** Matches the timingSafeEqualStr helper used for every other shared-secret\n * comparison in the codebase (preview-resolve.ts, workspace-attach-token.ts,\n * snapshot-link.ts) — constant-time so the loopback auth check can't leak\n * the token via response-time. */\nexport function timingSafeTokenEqual(a: string, b: string): boolean {\n const ab = Buffer.from(a);\n const bb = Buffer.from(b);\n if (ab.length !== bb.length) return false;\n return crypto.timingSafeEqual(ab, bb);\n}\n"],"mappings":";AAaA,OAAO,YAAY;AA8DnB,SAAS,SAAS,OAAkD;AAClE,SAAO,OAAO,UAAU,YAAY,UAAU;AAChD;AAGO,SAAS,UAAU,MAA8C;AACtE,MAAI,CAAC,KAAM,QAAO;AAClB,MAAI;AACF,UAAM,SAAkB,KAAK,MAAM,IAAI;AACvC,WAAO,SAAS,MAAM,IAAI,SAAS;AAAA,EACrC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAOO,IAAM,cAAN,MAAkB;AAAA,EAGvB,YAA6B,SAAmD;AAAnD;AAAA,EAAoD;AAAA,EAApD;AAAA,EAFrB,SAAS;AAAA,EAIjB,KAAK,OAA8B;AACjC,SAAK,UAAU,MAAM,SAAS;AAC9B,QAAI,QAAQ,KAAK,OAAO,QAAQ,IAAI;AACpC,WAAO,SAAS,GAAG;AACjB,YAAM,OAAO,KAAK,OAAO,MAAM,GAAG,KAAK;AACvC,WAAK,SAAS,KAAK,OAAO,MAAM,QAAQ,CAAC;AACzC,YAAM,QAAQ,UAAU,IAAI;AAC5B,UAAI,MAAO,MAAK,QAAQ,KAAK;AAC7B,cAAQ,KAAK,OAAO,QAAQ,IAAI;AAAA,IAClC;AAAA,EACF;AACF;AAIO,SAAS,WACd,QACA,OACM;AACN,MAAI;AACF,WAAO,MAAM,GAAG,KAAK,UAAU,KAAK,CAAC;AAAA,CAAI;AAAA,EAC3C,QAAQ;AAAA,EAER;AACF;AAEO,IAAM,yBAAyB;AAM/B,SAAS,qBAAqB,GAAW,GAAoB;AAClE,QAAM,KAAK,OAAO,KAAK,CAAC;AACxB,QAAM,KAAK,OAAO,KAAK,CAAC;AACxB,MAAI,GAAG,WAAW,GAAG,OAAQ,QAAO;AACpC,SAAO,OAAO,gBAAgB,IAAI,EAAE;AACtC;","names":[]}
@@ -0,0 +1,413 @@
1
+ import {
2
+ workbenchPort,
3
+ workbenchToken
4
+ } from "./chunk-4VUQ2NPF.js";
5
+ import {
6
+ DEFAULT_WORKBENCH_PORT,
7
+ FrameReader,
8
+ writeFrame
9
+ } from "./chunk-IE7EOKMD.js";
10
+
11
+ // src/workbench/client.ts
12
+ import { createReadStream, createWriteStream } from "fs";
13
+ import { connect } from "net";
14
+
15
+ // src/workbench/errors.ts
16
+ var WorkbenchError = class extends Error {
17
+ code;
18
+ constructor(message, code) {
19
+ super(message);
20
+ this.name = "WorkbenchError";
21
+ if (code) this.code = code;
22
+ }
23
+ };
24
+
25
+ // src/workbench/remote-process.ts
26
+ import { EventEmitter } from "events";
27
+ var RemoteProcessHandle = class extends EventEmitter {
28
+ constructor(sendSignal) {
29
+ super();
30
+ this.sendSignal = sendSignal;
31
+ }
32
+ sendSignal;
33
+ pid = void 0;
34
+ exitCode = null;
35
+ kill(_signal) {
36
+ this.sendSignal();
37
+ return true;
38
+ }
39
+ };
40
+
41
+ // src/workbench/client.ts
42
+ function frameError(frame) {
43
+ return new WorkbenchError(frame.message, frame.code);
44
+ }
45
+ var WorkbenchClient = class {
46
+ port;
47
+ host;
48
+ token;
49
+ constructor(options = {}) {
50
+ this.port = options.port ?? workbenchPort() ?? DEFAULT_WORKBENCH_PORT;
51
+ this.host = options.host ?? "127.0.0.1";
52
+ this.token = options.token ?? workbenchToken();
53
+ }
54
+ /** Open a connection, send the request, route response frames. */
55
+ open(request, onFrame, onClose) {
56
+ const socket = connect(this.port, this.host);
57
+ let sawError;
58
+ const reader = new FrameReader((raw) => {
59
+ const frame = raw;
60
+ if (frame.t === "error") {
61
+ sawError = frameError(frame);
62
+ }
63
+ onFrame(frame, socket);
64
+ });
65
+ socket.on("connect", () => {
66
+ writeFrame(socket, { ...request, token: this.token });
67
+ });
68
+ socket.on("data", (chunk) => reader.push(chunk));
69
+ socket.on("error", (err) => {
70
+ sawError ??= err;
71
+ });
72
+ socket.on("close", () => onClose(sawError));
73
+ return socket;
74
+ }
75
+ ping() {
76
+ return new Promise((resolve, reject) => {
77
+ let version = null;
78
+ this.open(
79
+ { op: "ping" },
80
+ (frame) => {
81
+ if (frame.t === "pong") version = frame.version;
82
+ },
83
+ (error) => {
84
+ if (version === null) {
85
+ reject(error ?? new WorkbenchError("connection closed before pong"));
86
+ } else {
87
+ resolve(version);
88
+ }
89
+ }
90
+ );
91
+ });
92
+ }
93
+ /** Mirrors setup/commands.ts runSetupCommand (shell form, streamed output,
94
+ * abort → graceful term-group kill, non-zero exit rejects). */
95
+ runSetupCommand(cmd, cwd, onOutput, signal) {
96
+ return new Promise((resolve, reject) => {
97
+ if (signal?.aborted) {
98
+ const error = new Error("Operation aborted");
99
+ error.name = "AbortError";
100
+ reject(error);
101
+ return;
102
+ }
103
+ let exitCode = null;
104
+ let exited = false;
105
+ let aborting = false;
106
+ const socket = this.open(
107
+ { op: "exec", command: cmd, cwd },
108
+ (frame) => {
109
+ if (frame.t === "out" && !aborting) {
110
+ onOutput(frame.s, Buffer.from(frame.d, "base64").toString("utf8"));
111
+ } else if (frame.t === "exit") {
112
+ exited = true;
113
+ exitCode = frame.code;
114
+ }
115
+ },
116
+ (error) => {
117
+ signal?.removeEventListener("abort", onAbort);
118
+ if (aborting) {
119
+ const abortError = new Error("Operation aborted");
120
+ abortError.name = "AbortError";
121
+ reject(abortError);
122
+ } else if (!exited) {
123
+ reject(error ?? new WorkbenchError("connection closed before exit"));
124
+ } else if (exitCode === 0) {
125
+ resolve();
126
+ } else {
127
+ reject(new Error(`Setup command exited with code ${exitCode}`));
128
+ }
129
+ }
130
+ );
131
+ const onAbort = () => {
132
+ if (exited || aborting) return;
133
+ aborting = true;
134
+ writeFrame(socket, { t: "signal", mode: "term-group" });
135
+ };
136
+ signal?.addEventListener("abort", onAbort, { once: true });
137
+ });
138
+ }
139
+ /** Mirrors setup/commands.ts runStartCommand: returns a live handle that
140
+ * emits exit/error and whose kill() runs the graceful term-group stop. */
141
+ runStartCommand(cmd, cwd, onOutput) {
142
+ let socket;
143
+ const handle = new RemoteProcessHandle(() => {
144
+ if (socket) writeFrame(socket, { t: "signal", mode: "term-group" });
145
+ });
146
+ let exited = false;
147
+ socket = this.open(
148
+ { op: "exec", command: cmd, cwd },
149
+ (frame) => {
150
+ if (frame.t === "out") {
151
+ onOutput(frame.s, Buffer.from(frame.d, "base64").toString("utf8"));
152
+ } else if (frame.t === "exit") {
153
+ exited = true;
154
+ handle.exitCode = frame.code ?? (frame.signal ? null : 0);
155
+ handle.emit("exit", frame.code, frame.signal);
156
+ }
157
+ },
158
+ (error) => {
159
+ if (!exited) {
160
+ exited = true;
161
+ handle.exitCode = handle.exitCode ?? 1;
162
+ handle.emit(
163
+ "error",
164
+ error ?? new WorkbenchError("workbench connection lost", "workbench_gone")
165
+ );
166
+ handle.emit("exit", handle.exitCode, null);
167
+ }
168
+ }
169
+ );
170
+ return handle;
171
+ }
172
+ /** Mirrors promisified execFile (argv form, no shell): resolves stdout,
173
+ * rejects with stderr-bearing error on non-zero exit. */
174
+ execFile(file, args, opts = {}) {
175
+ return new Promise((resolve, reject) => {
176
+ const stdout = [];
177
+ const stderr = [];
178
+ let exitCode = null;
179
+ let exitSignal = null;
180
+ let exited = false;
181
+ let timedOut = false;
182
+ const socket = this.open(
183
+ { op: "exec", argv: [file, ...args], cwd: opts.cwd ?? process.cwd() },
184
+ (frame) => {
185
+ if (frame.t === "out") {
186
+ (frame.s === "stdout" ? stdout : stderr).push(Buffer.from(frame.d, "base64"));
187
+ } else if (frame.t === "exit") {
188
+ exited = true;
189
+ exitCode = frame.code;
190
+ exitSignal = frame.signal;
191
+ }
192
+ },
193
+ (error) => {
194
+ if (timer) clearTimeout(timer);
195
+ const out = Buffer.concat(stdout).toString("utf8");
196
+ const errText = Buffer.concat(stderr).toString("utf8");
197
+ if (!exited) {
198
+ reject(error ?? new WorkbenchError("connection closed before exit"));
199
+ } else if (exitCode === 0) {
200
+ resolve({ stdout: out, stderr: errText });
201
+ } else {
202
+ const failure = new Error(
203
+ timedOut ? `Command timed out: ${file}` : `Command failed: ${file} ${args.join(" ")}
204
+ ${errText}`
205
+ );
206
+ failure.code = exitCode;
207
+ failure.signal = exitSignal;
208
+ failure.stdout = out;
209
+ failure.stderr = errText;
210
+ reject(failure);
211
+ }
212
+ }
213
+ );
214
+ const timer = opts.timeout ? setTimeout(() => {
215
+ timedOut = true;
216
+ writeFrame(socket, { t: "signal", mode: "term-group" });
217
+ }, opts.timeout) : null;
218
+ timer?.unref();
219
+ });
220
+ }
221
+ /**
222
+ * Synchronous PtySpawn adapter: returns a PtyProcess immediately; writes
223
+ * and resizes are queued until the connection opens. Exit surfaces through
224
+ * onExit exactly as node-pty's would (a lost connection = exit code 1 —
225
+ * the session's finalizeOnExit path treats it as a crashed CLI).
226
+ */
227
+ spawnPty(file, args, options) {
228
+ const dataListeners = [];
229
+ const exitListeners = [];
230
+ let connected = false;
231
+ let exited = false;
232
+ const preConnectQueue = [];
233
+ const socket = this.open(
234
+ {
235
+ op: "pty",
236
+ file,
237
+ args,
238
+ cwd: options.cwd,
239
+ env: options.env,
240
+ cols: options.cols,
241
+ rows: options.rows
242
+ },
243
+ (frame) => {
244
+ if (frame.t === "data") {
245
+ const text = Buffer.from(frame.d, "base64").toString("utf8");
246
+ for (const listener of dataListeners) listener(text);
247
+ } else if (frame.t === "exit") {
248
+ exited = true;
249
+ for (const listener of exitListeners) listener({ exitCode: frame.code ?? 1 });
250
+ }
251
+ },
252
+ () => {
253
+ if (!exited) {
254
+ exited = true;
255
+ for (const listener of exitListeners) listener({ exitCode: 1 });
256
+ }
257
+ }
258
+ );
259
+ socket.on("connect", () => {
260
+ connected = true;
261
+ for (const frame of preConnectQueue.splice(0)) writeFrame(socket, frame);
262
+ });
263
+ const send = (frame) => {
264
+ if (exited) return;
265
+ if (connected) writeFrame(socket, frame);
266
+ else preConnectQueue.push(frame);
267
+ };
268
+ return {
269
+ onData: (listener) => dataListeners.push(listener),
270
+ onExit: (listener) => exitListeners.push(listener),
271
+ write: (data) => send({ t: "input", d: Buffer.from(data, "utf8").toString("base64") }),
272
+ resize: (cols, rows) => send({ t: "resize", cols, rows }),
273
+ kill: (sig) => send({ t: "kill", ...sig ? { sig } : {} })
274
+ };
275
+ }
276
+ /** Build a snapshot of `cwd` in the workbench and stream the tar.gz to
277
+ * `destPath` on the agent side (the upload path streams from disk). */
278
+ buildSnapshot(cwd, destPath) {
279
+ return new Promise((resolve, reject) => {
280
+ let meta = null;
281
+ let ended = false;
282
+ const out = createWriteStream(destPath);
283
+ let writeFailure = null;
284
+ out.on("error", (err) => {
285
+ writeFailure = err;
286
+ });
287
+ this.open(
288
+ { op: "snapshotBuild", cwd },
289
+ (frame) => {
290
+ if (frame.t === "meta") {
291
+ meta = {
292
+ sizeBytes: frame.sizeBytes,
293
+ fileCount: frame.fileCount,
294
+ deletionCount: frame.deletionCount,
295
+ capturedAt: frame.capturedAt
296
+ };
297
+ } else if (frame.t === "data") {
298
+ out.write(Buffer.from(frame.d, "base64"));
299
+ } else if (frame.t === "end") {
300
+ ended = true;
301
+ }
302
+ },
303
+ (error) => {
304
+ out.end(() => {
305
+ if (writeFailure) reject(writeFailure);
306
+ else if (error) reject(error);
307
+ else if (!ended || !meta) {
308
+ reject(new WorkbenchError("snapshot stream ended prematurely"));
309
+ } else resolve({ ...meta, tarPath: destPath });
310
+ });
311
+ }
312
+ );
313
+ });
314
+ }
315
+ /** Stream a local tar.gz up and extract it into `cwd` in the workbench. */
316
+ extractSnapshot(cwd, tarPath) {
317
+ return new Promise((resolve, reject) => {
318
+ let result = null;
319
+ const socket = this.open(
320
+ { op: "snapshotExtract", cwd },
321
+ (frame) => {
322
+ if (frame.t === "result") result = frame.value;
323
+ },
324
+ (error) => {
325
+ if (result) resolve(result);
326
+ else reject(error ?? new WorkbenchError("connection closed before result"));
327
+ }
328
+ );
329
+ socket.on("connect", () => {
330
+ const stream = createReadStream(tarPath, { highWaterMark: 64 * 1024 });
331
+ stream.on("data", (chunk) => {
332
+ writeFrame(socket, { t: "data", d: Buffer.from(chunk).toString("base64") });
333
+ });
334
+ stream.on("end", () => writeFrame(socket, { t: "end" }));
335
+ stream.on("error", (err) => {
336
+ socket.destroy(err);
337
+ });
338
+ });
339
+ });
340
+ }
341
+ readFile(path) {
342
+ return new Promise((resolve, reject) => {
343
+ const chunks = [];
344
+ let ended = false;
345
+ this.open(
346
+ { op: "readFile", path },
347
+ (frame) => {
348
+ if (frame.t === "data") chunks.push(Buffer.from(frame.d, "base64"));
349
+ else if (frame.t === "end") ended = true;
350
+ },
351
+ (error) => {
352
+ if (ended) resolve(Buffer.concat(chunks));
353
+ else reject(error ?? new WorkbenchError("connection closed before end"));
354
+ }
355
+ );
356
+ });
357
+ }
358
+ stat(path) {
359
+ return new Promise((resolve, reject) => {
360
+ let stat = null;
361
+ this.open(
362
+ { op: "stat", path },
363
+ (frame) => {
364
+ if (frame.t === "stat") {
365
+ stat = {
366
+ exists: frame.exists,
367
+ isFile: frame.isFile,
368
+ isDirectory: frame.isDirectory,
369
+ size: frame.size,
370
+ mtimeMs: frame.mtimeMs
371
+ };
372
+ }
373
+ },
374
+ (error) => {
375
+ if (stat) resolve(stat);
376
+ else reject(error ?? new WorkbenchError("connection closed before stat"));
377
+ }
378
+ );
379
+ });
380
+ }
381
+ readdir(path) {
382
+ return new Promise((resolve, reject) => {
383
+ let entries = null;
384
+ this.open(
385
+ { op: "readdir", path },
386
+ (frame) => {
387
+ if (frame.t === "entries") entries = frame.names;
388
+ },
389
+ (error) => {
390
+ if (entries) resolve(entries);
391
+ else reject(error ?? new WorkbenchError("connection closed before entries"));
392
+ }
393
+ );
394
+ });
395
+ }
396
+ };
397
+ var singleton = null;
398
+ function getWorkbenchClient() {
399
+ singleton ??= new WorkbenchClient();
400
+ return singleton;
401
+ }
402
+ function resetWorkbenchClient() {
403
+ singleton = null;
404
+ }
405
+
406
+ export {
407
+ WorkbenchError,
408
+ RemoteProcessHandle,
409
+ WorkbenchClient,
410
+ getWorkbenchClient,
411
+ resetWorkbenchClient
412
+ };
413
+ //# sourceMappingURL=chunk-YRC57EYG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/workbench/client.ts","../src/workbench/errors.ts","../src/workbench/remote-process.ts"],"sourcesContent":["/**\n * Client half of the workbench launcher protocol. Runs in the protected agent\n * container; every method opens one loopback connection, sends the request\n * frame, and adapts the stream back to the local signature it replaces\n * (runSetupCommand, runStartCommand, execFile, PtySpawn, snapshot build/\n * extract, workspace file reads). See server.ts / protocol.ts.\n */\n\nimport { createReadStream, createWriteStream } from \"node:fs\";\nimport { connect, type Socket } from \"node:net\";\nimport type { PtyProcess, PtySpawnOptions } from \"../harness/pty/pty-support.js\";\nimport type { SnapshotExtractResult } from \"../runner/work-preservation/snapshot-tar.js\";\nimport { WorkbenchError } from \"./errors.js\";\nimport { RemoteProcessHandle } from \"./remote-process.js\";\n\nexport { WorkbenchError } from \"./errors.js\";\nexport { RemoteProcessHandle } from \"./remote-process.js\";\nimport { workbenchPort, workbenchToken } from \"./mode.js\";\nimport {\n DEFAULT_WORKBENCH_PORT,\n FrameReader,\n writeFrame,\n type WorkbenchFrame,\n type WorkbenchRequest,\n} from \"./protocol.js\";\n\nexport interface WorkbenchClientOptions {\n port?: number;\n token?: string;\n host?: string;\n}\n\n/** Metadata for a snapshot built remotely and streamed to a local tar path. */\nexport interface RemoteSnapshotMeta {\n tarPath: string;\n sizeBytes: number;\n fileCount: number;\n deletionCount: number;\n capturedAt: number;\n}\n\n/** Omit that distributes over a discriminated union (plain Omit collapses\n * the union to its common properties). */\ntype DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;\n\nfunction frameError(frame: Extract<WorkbenchFrame, { t: \"error\" }>): WorkbenchError {\n return new WorkbenchError(frame.message, frame.code);\n}\n\nexport class WorkbenchClient {\n private readonly port: number;\n private readonly host: string;\n private readonly token: string;\n\n constructor(options: WorkbenchClientOptions = {}) {\n this.port = options.port ?? workbenchPort() ?? DEFAULT_WORKBENCH_PORT;\n this.host = options.host ?? \"127.0.0.1\";\n this.token = options.token ?? workbenchToken();\n }\n\n /** Open a connection, send the request, route response frames. */\n private open(\n request: DistributiveOmit<WorkbenchRequest, \"token\">,\n onFrame: (frame: WorkbenchFrame, socket: Socket) => void,\n onClose: (error?: Error) => void,\n ): Socket {\n const socket = connect(this.port, this.host);\n let sawError: Error | undefined;\n const reader = new FrameReader((raw) => {\n const frame = raw as unknown as WorkbenchFrame;\n if (frame.t === \"error\") {\n sawError = frameError(frame as Extract<WorkbenchFrame, { t: \"error\" }>);\n }\n onFrame(frame, socket);\n });\n socket.on(\"connect\", () => {\n writeFrame(socket, { ...request, token: this.token } as WorkbenchRequest);\n });\n socket.on(\"data\", (chunk) => reader.push(chunk));\n socket.on(\"error\", (err) => {\n sawError ??= err;\n });\n socket.on(\"close\", () => onClose(sawError));\n return socket;\n }\n\n ping(): Promise<string> {\n return new Promise((resolve, reject) => {\n let version: string | null = null;\n this.open(\n { op: \"ping\" },\n (frame) => {\n if (frame.t === \"pong\") version = frame.version;\n },\n (error) => {\n if (version === null) {\n reject(error ?? new WorkbenchError(\"connection closed before pong\"));\n } else {\n resolve(version);\n }\n },\n );\n });\n }\n\n /** Mirrors setup/commands.ts runSetupCommand (shell form, streamed output,\n * abort → graceful term-group kill, non-zero exit rejects). */\n runSetupCommand(\n cmd: string,\n cwd: string,\n onOutput: (stream: \"stdout\" | \"stderr\", data: string) => void,\n signal?: AbortSignal,\n ): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n const error = new Error(\"Operation aborted\");\n error.name = \"AbortError\";\n reject(error);\n return;\n }\n let exitCode: number | null = null;\n let exited = false;\n let aborting = false;\n const socket = this.open(\n { op: \"exec\", command: cmd, cwd },\n (frame) => {\n if (frame.t === \"out\" && !aborting) {\n onOutput(frame.s, Buffer.from(frame.d, \"base64\").toString(\"utf8\"));\n } else if (frame.t === \"exit\") {\n exited = true;\n exitCode = frame.code;\n }\n },\n (error) => {\n signal?.removeEventListener(\"abort\", onAbort);\n if (aborting) {\n const abortError = new Error(\"Operation aborted\");\n abortError.name = \"AbortError\";\n reject(abortError);\n } else if (!exited) {\n reject(error ?? new WorkbenchError(\"connection closed before exit\"));\n } else if (exitCode === 0) {\n resolve();\n } else {\n reject(new Error(`Setup command exited with code ${exitCode}`));\n }\n },\n );\n const onAbort = (): void => {\n if (exited || aborting) return;\n aborting = true;\n writeFrame(socket, { t: \"signal\", mode: \"term-group\" });\n };\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n }\n\n /** Mirrors setup/commands.ts runStartCommand: returns a live handle that\n * emits exit/error and whose kill() runs the graceful term-group stop. */\n runStartCommand(\n cmd: string,\n cwd: string,\n onOutput: (stream: \"stdout\" | \"stderr\", data: string) => void,\n ): RemoteProcessHandle {\n let socket: Socket | undefined;\n const handle = new RemoteProcessHandle(() => {\n if (socket) writeFrame(socket, { t: \"signal\", mode: \"term-group\" });\n });\n let exited = false;\n socket = this.open(\n { op: \"exec\", command: cmd, cwd },\n (frame) => {\n if (frame.t === \"out\") {\n onOutput(frame.s, Buffer.from(frame.d, \"base64\").toString(\"utf8\"));\n } else if (frame.t === \"exit\") {\n exited = true;\n handle.exitCode = frame.code ?? (frame.signal ? null : 0);\n handle.emit(\"exit\", frame.code, frame.signal);\n }\n },\n (error) => {\n if (!exited) {\n // Connection died without an exit frame (launcher restart, network\n // teardown): surface as an error + synthetic exit so the supervisor\n // observes the process ending rather than hanging on it forever.\n exited = true;\n handle.exitCode = handle.exitCode ?? 1;\n handle.emit(\n \"error\",\n error ?? new WorkbenchError(\"workbench connection lost\", \"workbench_gone\"),\n );\n handle.emit(\"exit\", handle.exitCode, null);\n }\n },\n );\n return handle;\n }\n\n /** Mirrors promisified execFile (argv form, no shell): resolves stdout,\n * rejects with stderr-bearing error on non-zero exit. */\n execFile(\n file: string,\n args: string[],\n opts: { cwd?: string; timeout?: number; maxBuffer?: number } = {},\n ): Promise<{ stdout: string; stderr: string }> {\n return new Promise((resolve, reject) => {\n const stdout: Buffer[] = [];\n const stderr: Buffer[] = [];\n let exitCode: number | null = null;\n let exitSignal: string | null = null;\n let exited = false;\n let timedOut = false;\n const socket = this.open(\n { op: \"exec\", argv: [file, ...args], cwd: opts.cwd ?? process.cwd() },\n (frame) => {\n if (frame.t === \"out\") {\n (frame.s === \"stdout\" ? stdout : stderr).push(Buffer.from(frame.d, \"base64\"));\n } else if (frame.t === \"exit\") {\n exited = true;\n exitCode = frame.code;\n exitSignal = frame.signal;\n }\n },\n (error) => {\n if (timer) clearTimeout(timer);\n const out = Buffer.concat(stdout).toString(\"utf8\");\n const errText = Buffer.concat(stderr).toString(\"utf8\");\n if (!exited) {\n reject(error ?? new WorkbenchError(\"connection closed before exit\"));\n } else if (exitCode === 0) {\n resolve({ stdout: out, stderr: errText });\n } else {\n const failure = new Error(\n timedOut\n ? `Command timed out: ${file}`\n : `Command failed: ${file} ${args.join(\" \")}\\n${errText}`,\n ) as Error & { code?: number | null; signal?: string | null; stdout: string; stderr: string };\n failure.code = exitCode;\n failure.signal = exitSignal;\n failure.stdout = out;\n failure.stderr = errText;\n reject(failure);\n }\n },\n );\n const timer = opts.timeout\n ? setTimeout(() => {\n timedOut = true;\n writeFrame(socket, { t: \"signal\", mode: \"term-group\" });\n }, opts.timeout)\n : null;\n timer?.unref();\n });\n }\n\n /**\n * Synchronous PtySpawn adapter: returns a PtyProcess immediately; writes\n * and resizes are queued until the connection opens. Exit surfaces through\n * onExit exactly as node-pty's would (a lost connection = exit code 1 —\n * the session's finalizeOnExit path treats it as a crashed CLI).\n */\n spawnPty(file: string, args: string[], options: PtySpawnOptions): PtyProcess {\n const dataListeners: Array<(data: string) => void> = [];\n const exitListeners: Array<(event: { exitCode: number }) => void> = [];\n let connected = false;\n let exited = false;\n const preConnectQueue: WorkbenchFrame[] = [];\n\n const socket = this.open(\n {\n op: \"pty\",\n file,\n args,\n cwd: options.cwd,\n env: options.env,\n cols: options.cols,\n rows: options.rows,\n },\n (frame) => {\n if (frame.t === \"data\") {\n const text = Buffer.from(frame.d, \"base64\").toString(\"utf8\");\n for (const listener of dataListeners) listener(text);\n } else if (frame.t === \"exit\") {\n exited = true;\n for (const listener of exitListeners) listener({ exitCode: frame.code ?? 1 });\n }\n },\n () => {\n if (!exited) {\n exited = true;\n for (const listener of exitListeners) listener({ exitCode: 1 });\n }\n },\n );\n socket.on(\"connect\", () => {\n connected = true;\n for (const frame of preConnectQueue.splice(0)) writeFrame(socket, frame);\n });\n\n const send = (frame: WorkbenchFrame): void => {\n if (exited) return;\n if (connected) writeFrame(socket, frame);\n else preConnectQueue.push(frame);\n };\n\n return {\n onData: (listener) => dataListeners.push(listener),\n onExit: (listener) => exitListeners.push(listener),\n write: (data) => send({ t: \"input\", d: Buffer.from(data, \"utf8\").toString(\"base64\") }),\n resize: (cols, rows) => send({ t: \"resize\", cols, rows }),\n kill: (sig) => send({ t: \"kill\", ...(sig ? { sig } : {}) }),\n };\n }\n\n /** Build a snapshot of `cwd` in the workbench and stream the tar.gz to\n * `destPath` on the agent side (the upload path streams from disk). */\n buildSnapshot(cwd: string, destPath: string): Promise<RemoteSnapshotMeta> {\n return new Promise((resolve, reject) => {\n let meta: Omit<RemoteSnapshotMeta, \"tarPath\"> | null = null;\n let ended = false;\n const out = createWriteStream(destPath);\n let writeFailure: Error | null = null;\n out.on(\"error\", (err) => {\n writeFailure = err;\n });\n this.open(\n { op: \"snapshotBuild\", cwd },\n (frame) => {\n if (frame.t === \"meta\") {\n meta = {\n sizeBytes: frame.sizeBytes,\n fileCount: frame.fileCount,\n deletionCount: frame.deletionCount,\n capturedAt: frame.capturedAt,\n };\n } else if (frame.t === \"data\") {\n out.write(Buffer.from(frame.d, \"base64\"));\n } else if (frame.t === \"end\") {\n ended = true;\n }\n },\n (error) => {\n out.end(() => {\n if (writeFailure) reject(writeFailure);\n else if (error) reject(error);\n else if (!ended || !meta) {\n reject(new WorkbenchError(\"snapshot stream ended prematurely\"));\n } else resolve({ ...meta, tarPath: destPath });\n });\n },\n );\n });\n }\n\n /** Stream a local tar.gz up and extract it into `cwd` in the workbench. */\n extractSnapshot(cwd: string, tarPath: string): Promise<SnapshotExtractResult> {\n return new Promise((resolve, reject) => {\n let result: SnapshotExtractResult | null = null;\n const socket = this.open(\n { op: \"snapshotExtract\", cwd },\n (frame) => {\n if (frame.t === \"result\") result = frame.value as SnapshotExtractResult;\n },\n (error) => {\n if (result) resolve(result);\n else reject(error ?? new WorkbenchError(\"connection closed before result\"));\n },\n );\n socket.on(\"connect\", () => {\n const stream = createReadStream(tarPath, { highWaterMark: 64 * 1024 });\n stream.on(\"data\", (chunk) => {\n writeFrame(socket, { t: \"data\", d: Buffer.from(chunk).toString(\"base64\") });\n });\n stream.on(\"end\", () => writeFrame(socket, { t: \"end\" }));\n stream.on(\"error\", (err) => {\n socket.destroy(err);\n });\n });\n });\n }\n\n readFile(path: string): Promise<Buffer> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = [];\n let ended = false;\n this.open(\n { op: \"readFile\", path },\n (frame) => {\n if (frame.t === \"data\") chunks.push(Buffer.from(frame.d, \"base64\"));\n else if (frame.t === \"end\") ended = true;\n },\n (error) => {\n if (ended) resolve(Buffer.concat(chunks));\n else reject(error ?? new WorkbenchError(\"connection closed before end\"));\n },\n );\n });\n }\n\n stat(path: string): Promise<{\n exists: boolean;\n isFile: boolean;\n isDirectory: boolean;\n size: number;\n mtimeMs: number;\n }> {\n return new Promise((resolve, reject) => {\n let stat: {\n exists: boolean;\n isFile: boolean;\n isDirectory: boolean;\n size: number;\n mtimeMs: number;\n } | null = null;\n this.open(\n { op: \"stat\", path },\n (frame) => {\n if (frame.t === \"stat\") {\n stat = {\n exists: frame.exists,\n isFile: frame.isFile,\n isDirectory: frame.isDirectory,\n size: frame.size,\n mtimeMs: frame.mtimeMs,\n };\n }\n },\n (error) => {\n if (stat) resolve(stat);\n else reject(error ?? new WorkbenchError(\"connection closed before stat\"));\n },\n );\n });\n }\n\n readdir(path: string): Promise<string[]> {\n return new Promise((resolve, reject) => {\n let entries: string[] | null = null;\n this.open(\n { op: \"readdir\", path },\n (frame) => {\n if (frame.t === \"entries\") entries = frame.names;\n },\n (error) => {\n if (entries) resolve(entries);\n else reject(error ?? new WorkbenchError(\"connection closed before entries\"));\n },\n );\n });\n }\n}\n\nlet singleton: WorkbenchClient | null = null;\n\n/** Lazy env-configured client (split-mode pods). */\nexport function getWorkbenchClient(): WorkbenchClient {\n singleton ??= new WorkbenchClient();\n return singleton;\n}\n\n/** Test seam. */\nexport function resetWorkbenchClient(): void {\n singleton = null;\n}\n","/** Error surfaced by the workbench client for launcher-side failures; `code`\n * carries the remote error code (e.g. \"ENOENT\", \"unauthorized\") so callers'\n * existing errno handling keeps working across the container boundary. */\nexport class WorkbenchError extends Error {\n code?: string;\n constructor(message: string, code?: string) {\n super(message);\n this.name = \"WorkbenchError\";\n if (code) this.code = code;\n }\n}\n","import { EventEmitter } from \"node:events\";\n\n/**\n * Structural stand-in for the ChildProcess surface the workspace-command\n * supervisor actually uses (pid/exitCode/kill + exit/error events) — see\n * ManagedChildProcess in setup/commands.ts. kill() requests the launcher's\n * graceful term-group sequence regardless of the signal argument.\n */\nexport class RemoteProcessHandle extends EventEmitter {\n pid: number | undefined = undefined;\n exitCode: number | null = null;\n\n constructor(private readonly sendSignal: () => void) {\n super();\n }\n\n kill(_signal?: NodeJS.Signals | number): boolean {\n this.sendSignal();\n return true;\n }\n}\n"],"mappings":"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","names":[]}
package/dist/cli.js CHANGED
@@ -12,7 +12,6 @@ import {
12
12
  TuiUnavailableError,
13
13
  applyBootstrapToEnv,
14
14
  awaitGitReady,
15
- buildPromptBytes,
16
15
  buildSessionPreviewPorts,
17
16
  buildSynthesizedCredentials,
18
17
  claudeJsonPath,
@@ -21,21 +20,31 @@ import {
21
20
  defineTool,
22
21
  fetchBootstrap,
23
22
  findOnPath,
24
- inheritedEnv,
25
23
  loadConveyorConfig,
26
24
  loadForwardPorts,
27
- loadPtySpawn,
28
25
  parseUsageGauges,
29
26
  resolvePlaywrightMcpServer,
30
27
  resolveSessionStart,
31
- runSetupCommand,
32
- runStartCommand,
33
28
  runUsageProbe,
34
29
  sampleKeyUsage,
35
- terminateProcessGroup,
36
30
  textResult
37
- } from "./chunk-PXQJ4NVO.js";
31
+ } from "./chunk-DJPSNHVJ.js";
38
32
  import "./chunk-7TQO4ZF4.js";
33
+ import {
34
+ getWorkbenchClient
35
+ } from "./chunk-YRC57EYG.js";
36
+ import {
37
+ workbenchEnabled
38
+ } from "./chunk-4VUQ2NPF.js";
39
+ import {
40
+ buildPromptBytes,
41
+ inheritedEnv,
42
+ loadPtySpawn,
43
+ runSetupCommand,
44
+ runStartCommand,
45
+ terminateProcessGroup
46
+ } from "./chunk-2K6RRTQC.js";
47
+ import "./chunk-IE7EOKMD.js";
39
48
 
40
49
  // src/cli.ts
41
50
  import { readFileSync } from "fs";
@@ -240,6 +249,19 @@ async function reportBootMilestone(opts) {
240
249
  }
241
250
 
242
251
  // src/setup/workspace-command-supervisor.ts
252
+ function defaultCommandExecutors() {
253
+ if (workbenchEnabled()) {
254
+ const client = getWorkbenchClient();
255
+ return {
256
+ runSetupCommand: (command, cwd, onOutput, signal) => client.runSetupCommand(command, cwd, onOutput, signal),
257
+ runStartCommand: (cmd, cwd, onOutput) => client.runStartCommand(cmd, cwd, onOutput)
258
+ };
259
+ }
260
+ return {
261
+ runSetupCommand: (command, cwd, onOutput, signal) => runSetupCommand(command, cwd, onOutput, signal),
262
+ runStartCommand
263
+ };
264
+ }
243
265
  var defaultWriteOutput = (stream, data) => {
244
266
  (stream === "stderr" ? process.stderr : process.stdout).write(data);
245
267
  };
@@ -291,8 +313,9 @@ var WorkspaceCommandSupervisor = class {
291
313
  }));
292
314
  this.startLazySidecarsFn = options.startLazySidecars ?? ((env, onLog, signal) => startLazySidecars(env, onLog, signal));
293
315
  this.waitForSidecarsFn = options.waitForSidecars ?? ((opts) => waitForSidecars({ onLog: opts.onLog, startLazy: opts.startLazy, signal: opts.signal }));
294
- this.runSetupCommandFn = options.runSetupCommand ?? ((command, cwd, onOutput, signal) => runSetupCommand(command, cwd, onOutput, signal));
295
- this.runStartCommandFn = options.runStartCommand ?? runStartCommand;
316
+ const executors = defaultCommandExecutors();
317
+ this.runSetupCommandFn = options.runSetupCommand ?? executors.runSetupCommand;
318
+ this.runStartCommandFn = options.runStartCommand ?? executors.runStartCommand;
296
319
  this.loadForwardPortsFn = options.loadForwardPorts ?? loadForwardPorts;
297
320
  this.writeOutput = options.writeOutput ?? defaultWriteOutput;
298
321
  this.terminateStartCommand = options.terminateStartCommand ?? terminateProcessGroup;
@@ -1989,6 +2012,27 @@ process.on("unhandledRejection", (reason) => {
1989
2012
  logAgentExit("unhandled_rejection", { exitCode: 1 });
1990
2013
  process.exit(1);
1991
2014
  });
2015
+ if (process.env.CONVEYOR_MODE === "workbench") {
2016
+ const { startWorkbenchServer } = await import("./server-UGE67KIZ.js");
2017
+ const { DEFAULT_WORKBENCH_PORT } = await import("./protocol-QJRHTTMQ.js");
2018
+ const port = Number(process.env.CONVEYOR_WORKBENCH_PORT) || DEFAULT_WORKBENCH_PORT;
2019
+ const token = process.env.CONVEYOR_WORKBENCH_TOKEN ?? process.env.POD_BOOTSTRAP_TOKEN ?? "";
2020
+ if (!token) {
2021
+ logger6.error("workbench mode requires POD_BOOTSTRAP_TOKEN (or CONVEYOR_WORKBENCH_TOKEN)");
2022
+ process.exit(1);
2023
+ }
2024
+ const pkgDir = dirname3(fileURLToPath(import.meta.url));
2025
+ const pkg = JSON.parse(readFileSync(join3(pkgDir, "..", "package.json"), "utf-8"));
2026
+ const handle = await startWorkbenchServer({
2027
+ port,
2028
+ token,
2029
+ version: pkg.version ?? "unknown"
2030
+ });
2031
+ logger6.info(`Workbench launcher listening on 127.0.0.1:${handle.port}`);
2032
+ process.on("SIGTERM", () => process.exit(0));
2033
+ await new Promise(() => {
2034
+ });
2035
+ }
1992
2036
  var DEFAULT_CONVEYOR_API_URL = "https://api.conveyor.rallycryapp.com";
1993
2037
  var conveyorApiUrl = process.env.CONVEYOR_API_URL || DEFAULT_CONVEYOR_API_URL;
1994
2038
  var INSTANCE_NAME = process.env.CODESPACE_NAME || process.env.CLAUDESPACE_NAME;