@watasu/sdk 0.1.70 → 0.1.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands.d.ts +15 -0
- package/dist/commands.js +86 -13
- package/dist/processSocket.js +10 -1
- package/dist/sandbox.js +1 -1
- package/package.json +1 -1
package/dist/commands.d.ts
CHANGED
|
@@ -8,6 +8,11 @@ export interface CommandResult {
|
|
|
8
8
|
error?: string;
|
|
9
9
|
stdout: string;
|
|
10
10
|
stderr: string;
|
|
11
|
+
/**
|
|
12
|
+
* True when the runtime dropped stream output that could not be replayed
|
|
13
|
+
* (retention evicted the events before the SDK could backfill them).
|
|
14
|
+
*/
|
|
15
|
+
truncated?: boolean;
|
|
11
16
|
}
|
|
12
17
|
/** Error thrown by `CommandHandle.wait()` when a process exits non-zero. */
|
|
13
18
|
export declare class CommandExitError extends SandboxError implements CommandResult {
|
|
@@ -17,6 +22,7 @@ export declare class CommandExitError extends SandboxError implements CommandRes
|
|
|
17
22
|
get error(): string | undefined;
|
|
18
23
|
get stdout(): string;
|
|
19
24
|
get stderr(): string;
|
|
25
|
+
get truncated(): boolean | undefined;
|
|
20
26
|
}
|
|
21
27
|
export interface ProcessInfo {
|
|
22
28
|
pid: number | string;
|
|
@@ -39,6 +45,11 @@ export interface ProcessStatus {
|
|
|
39
45
|
startedAt?: string;
|
|
40
46
|
finishedAt?: string;
|
|
41
47
|
exitCode?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Set when a kill has been requested but the process has not finished yet
|
|
50
|
+
* (the runtime reports a non-terminal `killing` status in that window).
|
|
51
|
+
*/
|
|
52
|
+
killRequestedAt?: string;
|
|
42
53
|
}
|
|
43
54
|
export interface ProcessOutputEvent {
|
|
44
55
|
cursor: number;
|
|
@@ -98,6 +109,8 @@ type ProcessReconnect = (cursor: number) => Promise<{
|
|
|
98
109
|
socket: ProcessSocket;
|
|
99
110
|
events: AsyncIterable<ProcessFrame>;
|
|
100
111
|
}>;
|
|
112
|
+
/** Client-side wait deadline for a command: its timeout plus grace. */
|
|
113
|
+
export declare function commandWaitDeadlineMs(timeoutMs?: number): number;
|
|
101
114
|
/** Live handle for one sandbox process stream. */
|
|
102
115
|
export declare class CommandHandle implements Partial<CommandResult> {
|
|
103
116
|
readonly pid: number | string;
|
|
@@ -115,6 +128,8 @@ export declare class CommandHandle implements Partial<CommandResult> {
|
|
|
115
128
|
private readonly pending;
|
|
116
129
|
private nextCursor;
|
|
117
130
|
private disconnected;
|
|
131
|
+
private pendingLagged;
|
|
132
|
+
private outputDropped;
|
|
118
133
|
constructor(pid: number | string, socket: ProcessSocket, handleKill: () => Promise<boolean>, events: AsyncIterable<ProcessFrame>, onStdout?: ((data: string) => void | Promise<void>) | undefined, onStderr?: ((data: string) => void | Promise<void>) | undefined, onPty?: ((data: Uint8Array) => void | Promise<void>) | undefined, onExit?: ((exitCode: number) => void | Promise<void>) | undefined, reconnect?: ProcessReconnect | undefined);
|
|
119
134
|
get stdout(): string;
|
|
120
135
|
get stderr(): string;
|
package/dist/commands.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { withQuery } from './transport.js';
|
|
2
2
|
import { ProcessSocket, base64DecodeBytes, base64DecodeText } from './processSocket.js';
|
|
3
|
-
import { SandboxError, TimeoutError } from './errors.js';
|
|
3
|
+
import { NotFoundError, SandboxError, TimeoutError } from './errors.js';
|
|
4
4
|
/** Error thrown by `CommandHandle.wait()` when a process exits non-zero. */
|
|
5
5
|
export class CommandExitError extends SandboxError {
|
|
6
6
|
result;
|
|
7
7
|
constructor(result) {
|
|
8
|
-
super(result.error ?? `Command exited with code ${result.exitCode}`)
|
|
8
|
+
super((result.error ?? `Command exited with code ${result.exitCode}`) +
|
|
9
|
+
(result.truncated ? '; some process output was dropped before delivery' : ''));
|
|
9
10
|
this.result = result;
|
|
10
11
|
this.name = 'CommandExitError';
|
|
11
12
|
}
|
|
@@ -13,10 +14,26 @@ export class CommandExitError extends SandboxError {
|
|
|
13
14
|
get error() { return this.result.error; }
|
|
14
15
|
get stdout() { return this.result.stdout; }
|
|
15
16
|
get stderr() { return this.result.stderr; }
|
|
17
|
+
get truncated() { return this.result.truncated; }
|
|
16
18
|
}
|
|
17
19
|
const STREAM_RECONNECT_ATTEMPTS = 12;
|
|
18
20
|
const STREAM_RECONNECT_BASE_DELAY_MS = 250;
|
|
19
21
|
const STREAM_RECONNECT_MAX_DELAY_MS = 2_000;
|
|
22
|
+
/**
|
|
23
|
+
* Sentinel exit code reported when the runtime finishes a command without an
|
|
24
|
+
* explicit exit code (for example when it was killed or failed). Success is
|
|
25
|
+
* only ever an explicit exit code of 0.
|
|
26
|
+
*/
|
|
27
|
+
const MISSING_EXIT_CODE = -1;
|
|
28
|
+
/** Server-side command ceiling used when the caller supplied no timeout. */
|
|
29
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 3_600_000;
|
|
30
|
+
/** Extra client-side grace on top of the command timeout before giving up. */
|
|
31
|
+
const WAIT_TIMEOUT_GRACE_MS = 60_000;
|
|
32
|
+
/** Client-side wait deadline for a command: its timeout plus grace. */
|
|
33
|
+
export function commandWaitDeadlineMs(timeoutMs) {
|
|
34
|
+
const base = timeoutMs !== undefined && timeoutMs > 0 ? timeoutMs : DEFAULT_WAIT_TIMEOUT_MS;
|
|
35
|
+
return base + WAIT_TIMEOUT_GRACE_MS;
|
|
36
|
+
}
|
|
20
37
|
/** Live handle for one sandbox process stream. */
|
|
21
38
|
export class CommandHandle {
|
|
22
39
|
pid;
|
|
@@ -34,6 +51,8 @@ export class CommandHandle {
|
|
|
34
51
|
pending;
|
|
35
52
|
nextCursor = 0;
|
|
36
53
|
disconnected = false;
|
|
54
|
+
pendingLagged = false;
|
|
55
|
+
outputDropped = false;
|
|
37
56
|
constructor(pid, socket, handleKill, events, onStdout, onStderr, onPty, onExit, reconnect) {
|
|
38
57
|
this.pid = pid;
|
|
39
58
|
this.socket = socket;
|
|
@@ -52,7 +71,7 @@ export class CommandHandle {
|
|
|
52
71
|
get error() { return this.result?.error; }
|
|
53
72
|
/** Wait until the process exits and return captured output. */
|
|
54
73
|
async wait(timeoutMs) {
|
|
55
|
-
await waitFor(this.pending, timeoutMs);
|
|
74
|
+
await waitFor(this.pending, commandWaitDeadlineMs(timeoutMs), `timed out waiting for command ${this.pid} to exit; the command may still be running, check its status with commands.process(pid)`);
|
|
56
75
|
if (!this.result)
|
|
57
76
|
throw new SandboxError('Command ended without an exit event');
|
|
58
77
|
if (this.result.exitCode !== 0)
|
|
@@ -84,8 +103,30 @@ export class CommandHandle {
|
|
|
84
103
|
while (!this.disconnected && !this.result) {
|
|
85
104
|
let streamError;
|
|
86
105
|
for await (const frame of this.events) {
|
|
87
|
-
this.advanceCursor(frame);
|
|
88
106
|
const type = frame.type;
|
|
107
|
+
if (type === 'lagged') {
|
|
108
|
+
// The runtime dropped frames from this stream. Replay them from the
|
|
109
|
+
// next undelivered cursor when the stream supports reconnecting;
|
|
110
|
+
// otherwise flag the dropped output. Do not advance the cursor from
|
|
111
|
+
// a lagged frame, or the replay would skip the dropped events.
|
|
112
|
+
if (!this.reconnect) {
|
|
113
|
+
this.outputDropped = true;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
this.pendingLagged = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if (this.pendingLagged) {
|
|
120
|
+
const cursor = numberValue(frame.cursor);
|
|
121
|
+
if (cursor !== undefined) {
|
|
122
|
+
// Backfill was impossible when the first replayed event sits past
|
|
123
|
+
// the requested cursor: the runtime evicted the events between.
|
|
124
|
+
if (cursor > this.nextCursor)
|
|
125
|
+
this.outputDropped = true;
|
|
126
|
+
this.pendingLagged = false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
this.advanceCursor(frame);
|
|
89
130
|
if (type === 'started' || type === 'ready' || type === 'pong')
|
|
90
131
|
continue;
|
|
91
132
|
if (type === 'stdout') {
|
|
@@ -105,12 +146,25 @@ export class CommandHandle {
|
|
|
105
146
|
await this.onPty?.(bytes);
|
|
106
147
|
}
|
|
107
148
|
else if (type === 'exit') {
|
|
108
|
-
const
|
|
149
|
+
const rawExitCode = numericExitCode(frame.exit_code ?? frame.exitCode);
|
|
150
|
+
let error = typeof frame.error === 'string' ? frame.error : undefined;
|
|
151
|
+
let exitCode;
|
|
152
|
+
if (rawExitCode === undefined) {
|
|
153
|
+
// Killed/failed commands may finish without an exit code. Keep
|
|
154
|
+
// exitCode a plain number and take the failure path.
|
|
155
|
+
const status = typeof frame.status === 'string' && frame.status ? frame.status : 'failed';
|
|
156
|
+
exitCode = MISSING_EXIT_CODE;
|
|
157
|
+
error ??= `command ${status} without an exit code${this._stderr ? `; stderr:\n${this._stderr}` : ''}`;
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
exitCode = rawExitCode;
|
|
161
|
+
}
|
|
109
162
|
this.result = {
|
|
110
163
|
exitCode,
|
|
111
|
-
error
|
|
164
|
+
error,
|
|
112
165
|
stdout: this._stdout,
|
|
113
166
|
stderr: this._stderr,
|
|
167
|
+
truncated: this.outputDropped,
|
|
114
168
|
};
|
|
115
169
|
await this.onExit?.(exitCode);
|
|
116
170
|
this.socket.close();
|
|
@@ -228,15 +282,19 @@ export class Commands {
|
|
|
228
282
|
}
|
|
229
283
|
/** Look up process status without attaching a WebSocket. */
|
|
230
284
|
async process(pid, opts = {}) {
|
|
231
|
-
const payload = await this.dataPlane
|
|
285
|
+
const payload = await this.dataPlane
|
|
286
|
+
.getJson(`/runtime/v1/process/${encodeURIComponent(String(pid))}`, opts)
|
|
287
|
+
.catch((error) => { throw processPurgedNotFound(error, pid); });
|
|
232
288
|
return processStatus(payload);
|
|
233
289
|
}
|
|
234
290
|
/** Read available process output since a cursor without blocking. */
|
|
235
291
|
async readProcessOutput(pid, opts = {}) {
|
|
236
|
-
const payload = await this.dataPlane
|
|
292
|
+
const payload = await this.dataPlane
|
|
293
|
+
.getJson(withQuery(`/runtime/v1/process/${encodeURIComponent(String(pid))}/output`, {
|
|
237
294
|
since: opts.since,
|
|
238
295
|
limit_bytes: opts.limitBytes,
|
|
239
|
-
}), opts)
|
|
296
|
+
}), opts)
|
|
297
|
+
.catch((error) => { throw processPurgedNotFound(error, pid); });
|
|
240
298
|
return processOutputSnapshot(payload);
|
|
241
299
|
}
|
|
242
300
|
/** Stop a process, optionally signalling the full process group. */
|
|
@@ -292,14 +350,22 @@ function processStartConfig(cmd, opts) {
|
|
|
292
350
|
}
|
|
293
351
|
return { cmd: '/bin/bash', args: ['-l', '-c', cmd] };
|
|
294
352
|
}
|
|
295
|
-
function waitFor(promise, timeoutMs) {
|
|
296
|
-
if (timeoutMs === undefined || timeoutMs <= 0)
|
|
297
|
-
return promise;
|
|
353
|
+
function waitFor(promise, timeoutMs, message) {
|
|
298
354
|
return new Promise((resolve, reject) => {
|
|
299
|
-
const timer = setTimeout(() => reject(new TimeoutError()), timeoutMs);
|
|
355
|
+
const timer = setTimeout(() => reject(new TimeoutError(message)), timeoutMs);
|
|
300
356
|
promise.then(resolve, reject).finally(() => clearTimeout(timer));
|
|
301
357
|
});
|
|
302
358
|
}
|
|
359
|
+
function numericExitCode(value) {
|
|
360
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
361
|
+
return value;
|
|
362
|
+
if (typeof value === 'string' && value !== '') {
|
|
363
|
+
const parsed = Number(value);
|
|
364
|
+
if (Number.isFinite(parsed))
|
|
365
|
+
return parsed;
|
|
366
|
+
}
|
|
367
|
+
return undefined;
|
|
368
|
+
}
|
|
303
369
|
function sleep(ms) {
|
|
304
370
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
305
371
|
}
|
|
@@ -355,8 +421,15 @@ function processStatus(value) {
|
|
|
355
421
|
startedAt: stringValue(process.started_at ?? process.startedAt),
|
|
356
422
|
finishedAt: stringValue(process.finished_at ?? process.finishedAt),
|
|
357
423
|
exitCode: numberValue(process.exit_code ?? process.exitCode),
|
|
424
|
+
killRequestedAt: stringValue(process.kill_requested_at ?? process.killRequestedAt),
|
|
358
425
|
};
|
|
359
426
|
}
|
|
427
|
+
function processPurgedNotFound(error, pid) {
|
|
428
|
+
if (error instanceof NotFoundError) {
|
|
429
|
+
return new NotFoundError(`process ${pid} not found: process records are purged when the sandbox is destroyed; terminal results were delivered via wait()/callbacks`);
|
|
430
|
+
}
|
|
431
|
+
return error;
|
|
432
|
+
}
|
|
360
433
|
function processOutputSnapshot(value) {
|
|
361
434
|
const payload = value && typeof value === 'object' ? value : {};
|
|
362
435
|
return {
|
package/dist/processSocket.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import WebSocket from 'ws';
|
|
3
3
|
import { KEEPALIVE_PING_INTERVAL_SEC } from './connectionConfig.js';
|
|
4
|
-
import { SandboxError, TimeoutError } from './errors.js';
|
|
4
|
+
import { NotFoundError, SandboxError, TimeoutError } from './errors.js';
|
|
5
5
|
/** Streaming WebSocket connection to the sandbox process runtime. */
|
|
6
6
|
export class ProcessSocket {
|
|
7
7
|
baseUrl;
|
|
@@ -36,6 +36,15 @@ export class ProcessSocket {
|
|
|
36
36
|
clearTimeout(timeout);
|
|
37
37
|
resolve();
|
|
38
38
|
});
|
|
39
|
+
ws.once('unexpected-response', (_request, response) => {
|
|
40
|
+
clearTimeout(timeout);
|
|
41
|
+
if (response.statusCode === 404) {
|
|
42
|
+
reject(new NotFoundError('process not found: process records are purged when the sandbox is destroyed; terminal results were delivered via wait()/callbacks'));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
reject(new SandboxError(`process websocket failed to connect (status ${response.statusCode})`));
|
|
46
|
+
}
|
|
47
|
+
});
|
|
39
48
|
ws.once('error', () => {
|
|
40
49
|
clearTimeout(timeout);
|
|
41
50
|
reject(new SandboxError('process websocket failed to connect'));
|
package/dist/sandbox.js
CHANGED
|
@@ -163,8 +163,8 @@ export class Sandbox {
|
|
|
163
163
|
metadata: opts.metadata ?? {},
|
|
164
164
|
envs: opts.envs ?? {},
|
|
165
165
|
secure: opts.secure ?? true,
|
|
166
|
-
allow_internet_access: opts.allowInternetAccess ?? true,
|
|
167
166
|
};
|
|
167
|
+
putIfPresent(sandboxPayload, 'allow_internet_access', opts.allowInternetAccess);
|
|
168
168
|
putIfPresent(sandboxPayload, 'template', template);
|
|
169
169
|
putIfPresent(sandboxPayload, 'mcp', opts.mcp);
|
|
170
170
|
putIfPresent(sandboxPayload, 'lifecycle', lifecyclePayload(opts.lifecycle));
|