@agentuity/server 0.0.105 → 0.0.106
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/api/api.d.ts +11 -6
- package/dist/api/api.d.ts.map +1 -1
- package/dist/api/api.js +21 -13
- package/dist/api/api.js.map +1 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +1 -0
- package/dist/api/index.js.map +1 -1
- package/dist/api/project/deploy.d.ts +0 -6
- package/dist/api/project/deploy.d.ts.map +1 -1
- package/dist/api/project/deploy.js +0 -2
- package/dist/api/project/deploy.js.map +1 -1
- package/dist/api/project/get.d.ts +2 -1
- package/dist/api/project/get.d.ts.map +1 -1
- package/dist/api/project/get.js +10 -2
- package/dist/api/project/get.js.map +1 -1
- package/dist/api/region/create.d.ts +2 -0
- package/dist/api/region/create.d.ts.map +1 -1
- package/dist/api/region/create.js +1 -0
- package/dist/api/region/create.js.map +1 -1
- package/dist/api/region/delete.d.ts +12 -2
- package/dist/api/region/delete.d.ts.map +1 -1
- package/dist/api/region/delete.js +6 -1
- package/dist/api/region/delete.js.map +1 -1
- package/dist/api/region/resources.d.ts +4 -0
- package/dist/api/region/resources.d.ts.map +1 -1
- package/dist/api/region/resources.js +2 -0
- package/dist/api/region/resources.js.map +1 -1
- package/dist/api/sandbox/client.d.ts +125 -0
- package/dist/api/sandbox/client.d.ts.map +1 -0
- package/dist/api/sandbox/client.js +202 -0
- package/dist/api/sandbox/client.js.map +1 -0
- package/dist/api/sandbox/create.d.ts +24 -0
- package/dist/api/sandbox/create.d.ts.map +1 -0
- package/dist/api/sandbox/create.js +133 -0
- package/dist/api/sandbox/create.js.map +1 -0
- package/dist/api/sandbox/destroy.d.ts +14 -0
- package/dist/api/sandbox/destroy.d.ts.map +1 -0
- package/dist/api/sandbox/destroy.js +25 -0
- package/dist/api/sandbox/destroy.js.map +1 -0
- package/dist/api/sandbox/execute.d.ts +18 -0
- package/dist/api/sandbox/execute.d.ts.map +1 -0
- package/dist/api/sandbox/execute.js +77 -0
- package/dist/api/sandbox/execute.js.map +1 -0
- package/dist/api/sandbox/execution.d.ts +46 -0
- package/dist/api/sandbox/execution.d.ts.map +1 -0
- package/dist/api/sandbox/execution.js +101 -0
- package/dist/api/sandbox/execution.js.map +1 -0
- package/dist/api/sandbox/files.d.ts +41 -0
- package/dist/api/sandbox/files.d.ts.map +1 -0
- package/dist/api/sandbox/files.js +91 -0
- package/dist/api/sandbox/files.js.map +1 -0
- package/dist/api/sandbox/get.d.ts +16 -0
- package/dist/api/sandbox/get.d.ts.map +1 -0
- package/dist/api/sandbox/get.js +57 -0
- package/dist/api/sandbox/get.js.map +1 -0
- package/dist/api/sandbox/index.d.ts +22 -0
- package/dist/api/sandbox/index.d.ts.map +1 -0
- package/dist/api/sandbox/index.js +12 -0
- package/dist/api/sandbox/index.js.map +1 -0
- package/dist/api/sandbox/list.d.ts +15 -0
- package/dist/api/sandbox/list.d.ts.map +1 -0
- package/dist/api/sandbox/list.js +75 -0
- package/dist/api/sandbox/list.js.map +1 -0
- package/dist/api/sandbox/run.d.ts +28 -0
- package/dist/api/sandbox/run.d.ts.map +1 -0
- package/dist/api/sandbox/run.js +269 -0
- package/dist/api/sandbox/run.js.map +1 -0
- package/dist/api/sandbox/snapshot.d.ts +89 -0
- package/dist/api/sandbox/snapshot.d.ts.map +1 -0
- package/dist/api/sandbox/snapshot.js +140 -0
- package/dist/api/sandbox/snapshot.js.map +1 -0
- package/dist/api/sandbox/util.d.ts +37 -0
- package/dist/api/sandbox/util.d.ts.map +1 -0
- package/dist/api/sandbox/util.js +45 -0
- package/dist/api/sandbox/util.js.map +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/runtime-bootstrap.d.ts.map +1 -1
- package/dist/runtime-bootstrap.js +3 -0
- package/dist/runtime-bootstrap.js.map +1 -1
- package/package.json +4 -4
- package/src/api/api.ts +33 -13
- package/src/api/index.ts +1 -0
- package/src/api/project/deploy.ts +0 -2
- package/src/api/project/get.ts +10 -2
- package/src/api/region/create.ts +1 -0
- package/src/api/region/delete.ts +9 -2
- package/src/api/region/resources.ts +2 -0
- package/src/api/sandbox/client.ts +349 -0
- package/src/api/sandbox/create.ts +166 -0
- package/src/api/sandbox/destroy.ts +41 -0
- package/src/api/sandbox/execute.ts +102 -0
- package/src/api/sandbox/execution.ts +154 -0
- package/src/api/sandbox/files.ts +138 -0
- package/src/api/sandbox/get.ts +74 -0
- package/src/api/sandbox/index.ts +35 -0
- package/src/api/sandbox/list.ts +94 -0
- package/src/api/sandbox/run.ts +360 -0
- package/src/api/sandbox/snapshot.ts +247 -0
- package/src/api/sandbox/util.ts +55 -0
- package/src/config.ts +2 -0
- package/src/runtime-bootstrap.ts +3 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export { sandboxCreate } from './create';
|
|
2
|
+
export type { SandboxCreateResponse, SandboxCreateParams } from './create';
|
|
3
|
+
export { sandboxExecute } from './execute';
|
|
4
|
+
export type { SandboxExecuteParams } from './execute';
|
|
5
|
+
export { sandboxGet } from './get';
|
|
6
|
+
export type { SandboxGetParams } from './get';
|
|
7
|
+
export { sandboxList } from './list';
|
|
8
|
+
export type { SandboxListParams } from './list';
|
|
9
|
+
export { sandboxDestroy } from './destroy';
|
|
10
|
+
export type { SandboxDestroyParams } from './destroy';
|
|
11
|
+
export { sandboxRun } from './run';
|
|
12
|
+
export type { SandboxRunParams } from './run';
|
|
13
|
+
export { executionGet, executionList } from './execution';
|
|
14
|
+
export type {
|
|
15
|
+
ExecutionInfo,
|
|
16
|
+
ExecutionGetParams,
|
|
17
|
+
ExecutionListParams,
|
|
18
|
+
ExecutionListResponse,
|
|
19
|
+
} from './execution';
|
|
20
|
+
export { SandboxResponseError, writeAndDrain } from './util';
|
|
21
|
+
export { SandboxClient } from './client';
|
|
22
|
+
export type { SandboxClientOptions, SandboxInstance, ExecuteOptions } from './client';
|
|
23
|
+
export { sandboxWriteFiles, sandboxReadFile } from './files';
|
|
24
|
+
export type { WriteFilesParams, WriteFilesResult, ReadFileParams } from './files';
|
|
25
|
+
export { snapshotCreate, snapshotGet, snapshotList, snapshotDelete, snapshotTag } from './snapshot';
|
|
26
|
+
export type {
|
|
27
|
+
SnapshotInfo,
|
|
28
|
+
SnapshotFileInfo,
|
|
29
|
+
SnapshotCreateParams,
|
|
30
|
+
SnapshotGetParams,
|
|
31
|
+
SnapshotListParams,
|
|
32
|
+
SnapshotListResponse,
|
|
33
|
+
SnapshotDeleteParams,
|
|
34
|
+
SnapshotTagParams,
|
|
35
|
+
} from './snapshot';
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { APIClient, APIResponseSchema } from '../api';
|
|
3
|
+
import { SandboxResponseError, API_VERSION } from './util';
|
|
4
|
+
import type { ListSandboxesParams, ListSandboxesResponse, SandboxStatus } from '@agentuity/core';
|
|
5
|
+
|
|
6
|
+
const SandboxInfoSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
sandboxId: z.string().describe('Unique identifier for the sandbox'),
|
|
9
|
+
status: z
|
|
10
|
+
.enum(['creating', 'idle', 'running', 'terminated', 'failed'])
|
|
11
|
+
.describe('Current status of the sandbox'),
|
|
12
|
+
createdAt: z.string().describe('ISO timestamp when the sandbox was created'),
|
|
13
|
+
region: z.string().optional().describe('Region where the sandbox is running'),
|
|
14
|
+
snapshotId: z.string().optional().describe('Snapshot ID this sandbox was created from'),
|
|
15
|
+
snapshotTag: z.string().optional().describe('Snapshot tag this sandbox was created from'),
|
|
16
|
+
executions: z.number().describe('Total number of executions in this sandbox'),
|
|
17
|
+
stdoutStreamUrl: z.string().optional().describe('URL for streaming stdout output'),
|
|
18
|
+
stderrStreamUrl: z.string().optional().describe('URL for streaming stderr output'),
|
|
19
|
+
})
|
|
20
|
+
.describe('Summary information about a sandbox');
|
|
21
|
+
|
|
22
|
+
const ListSandboxesDataSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
sandboxes: z.array(SandboxInfoSchema).describe('List of sandbox entries'),
|
|
25
|
+
total: z.number().describe('Total number of sandboxes matching the query'),
|
|
26
|
+
})
|
|
27
|
+
.describe('Paginated list of sandboxes');
|
|
28
|
+
|
|
29
|
+
const ListSandboxesResponseSchema = APIResponseSchema(ListSandboxesDataSchema);
|
|
30
|
+
|
|
31
|
+
export interface SandboxListParams extends ListSandboxesParams {
|
|
32
|
+
orgId?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Lists sandboxes with optional filtering and pagination.
|
|
37
|
+
*
|
|
38
|
+
* @param client - The API client to use for the request
|
|
39
|
+
* @param params - Optional parameters for filtering by project, status, and pagination
|
|
40
|
+
* @returns Paginated list of sandboxes with total count
|
|
41
|
+
* @throws {SandboxResponseError} If the request fails
|
|
42
|
+
*/
|
|
43
|
+
export async function sandboxList(
|
|
44
|
+
client: APIClient,
|
|
45
|
+
params?: SandboxListParams
|
|
46
|
+
): Promise<ListSandboxesResponse> {
|
|
47
|
+
const queryParams = new URLSearchParams();
|
|
48
|
+
|
|
49
|
+
if (params?.orgId) {
|
|
50
|
+
queryParams.set('orgId', params.orgId);
|
|
51
|
+
}
|
|
52
|
+
if (params?.projectId) {
|
|
53
|
+
queryParams.set('projectId', params.projectId);
|
|
54
|
+
}
|
|
55
|
+
if (params?.snapshotId) {
|
|
56
|
+
queryParams.set('snapshotId', params.snapshotId);
|
|
57
|
+
}
|
|
58
|
+
if (params?.status) {
|
|
59
|
+
queryParams.set('status', params.status);
|
|
60
|
+
}
|
|
61
|
+
if (params?.limit !== undefined) {
|
|
62
|
+
queryParams.set('limit', params.limit.toString());
|
|
63
|
+
}
|
|
64
|
+
if (params?.offset !== undefined) {
|
|
65
|
+
queryParams.set('offset', params.offset.toString());
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const queryString = queryParams.toString();
|
|
69
|
+
const url = `/sandbox/${API_VERSION}${queryString ? `?${queryString}` : ''}`;
|
|
70
|
+
|
|
71
|
+
const resp = await client.get<z.infer<typeof ListSandboxesResponseSchema>>(
|
|
72
|
+
url,
|
|
73
|
+
ListSandboxesResponseSchema
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
if (resp.success) {
|
|
77
|
+
return {
|
|
78
|
+
sandboxes: resp.data.sandboxes.map((s) => ({
|
|
79
|
+
sandboxId: s.sandboxId,
|
|
80
|
+
status: s.status as SandboxStatus,
|
|
81
|
+
createdAt: s.createdAt,
|
|
82
|
+
region: s.region,
|
|
83
|
+
snapshotId: s.snapshotId,
|
|
84
|
+
snapshotTag: s.snapshotTag,
|
|
85
|
+
executions: s.executions,
|
|
86
|
+
stdoutStreamUrl: s.stdoutStreamUrl,
|
|
87
|
+
stderrStreamUrl: s.stderrStreamUrl,
|
|
88
|
+
})),
|
|
89
|
+
total: resp.data.total,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new SandboxResponseError({ message: resp.message });
|
|
94
|
+
}
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import type { Logger } from '@agentuity/core';
|
|
2
|
+
import type { Readable, Writable } from 'node:stream';
|
|
3
|
+
import { APIClient } from '../api';
|
|
4
|
+
import { sandboxCreate } from './create';
|
|
5
|
+
import { sandboxDestroy } from './destroy';
|
|
6
|
+
import { sandboxGet } from './get';
|
|
7
|
+
import { SandboxResponseError, writeAndDrain } from './util';
|
|
8
|
+
import type { SandboxRunOptions, SandboxRunResult } from '@agentuity/core';
|
|
9
|
+
import { getServiceUrls } from '../../config';
|
|
10
|
+
|
|
11
|
+
const POLL_INTERVAL_MS = 500;
|
|
12
|
+
const MAX_POLL_ATTEMPTS = 7200;
|
|
13
|
+
|
|
14
|
+
export interface SandboxRunParams {
|
|
15
|
+
options: SandboxRunOptions;
|
|
16
|
+
orgId?: string;
|
|
17
|
+
region?: string;
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
signal?: AbortSignal;
|
|
20
|
+
stdin?: Readable;
|
|
21
|
+
stdout?: Writable;
|
|
22
|
+
stderr?: Writable;
|
|
23
|
+
logger?: Logger;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Creates a sandbox, executes a command, and waits for completion.
|
|
28
|
+
*
|
|
29
|
+
* This is a high-level convenience function that handles the full lifecycle:
|
|
30
|
+
* creating a sandbox, streaming I/O, polling for completion, and cleanup.
|
|
31
|
+
*
|
|
32
|
+
* @param client - The API client to use for the request
|
|
33
|
+
* @param params - Parameters including command options, I/O streams, and timeout settings
|
|
34
|
+
* @returns The run result including exit code and duration
|
|
35
|
+
* @throws {SandboxResponseError} If sandbox creation fails, execution times out, or is cancelled
|
|
36
|
+
*/
|
|
37
|
+
export async function sandboxRun(
|
|
38
|
+
client: APIClient,
|
|
39
|
+
params: SandboxRunParams
|
|
40
|
+
): Promise<SandboxRunResult> {
|
|
41
|
+
const { options, orgId, region, apiKey, signal, stdin, stdout, stderr, logger } = params;
|
|
42
|
+
const started = Date.now();
|
|
43
|
+
|
|
44
|
+
let stdinStreamId: string | undefined;
|
|
45
|
+
let stdinStreamUrl: string | undefined;
|
|
46
|
+
|
|
47
|
+
// If stdin is provided and has data, create a stream for it
|
|
48
|
+
if (stdin && region && apiKey) {
|
|
49
|
+
const streamResult = await createStdinStream(region, apiKey, orgId, logger);
|
|
50
|
+
stdinStreamId = streamResult.id;
|
|
51
|
+
stdinStreamUrl = streamResult.url;
|
|
52
|
+
logger?.debug('created stdin stream: %s', stdinStreamId);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const createResponse = await sandboxCreate(client, {
|
|
56
|
+
options: {
|
|
57
|
+
...options,
|
|
58
|
+
command: {
|
|
59
|
+
exec: options.command.exec,
|
|
60
|
+
files: options.command.files,
|
|
61
|
+
mode: 'oneshot',
|
|
62
|
+
},
|
|
63
|
+
stream: {
|
|
64
|
+
...options.stream,
|
|
65
|
+
stdin: stdinStreamId,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
orgId,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const sandboxId = createResponse.sandboxId;
|
|
72
|
+
const stdoutStreamUrl = createResponse.stdoutStreamUrl;
|
|
73
|
+
const stderrStreamUrl = createResponse.stderrStreamUrl;
|
|
74
|
+
|
|
75
|
+
logger?.debug(
|
|
76
|
+
'sandbox created: %s, stdoutUrl: %s, stderrUrl: %s',
|
|
77
|
+
sandboxId,
|
|
78
|
+
stdoutStreamUrl ?? 'none',
|
|
79
|
+
stderrStreamUrl ?? 'none'
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const abortController = new AbortController();
|
|
83
|
+
const streamPromises: Promise<void>[] = [];
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
// Start stdin streaming if we have stdin and a stream URL
|
|
87
|
+
if (stdin && stdinStreamUrl && apiKey) {
|
|
88
|
+
const stdinPromise = streamStdinToUrl(
|
|
89
|
+
stdin,
|
|
90
|
+
stdinStreamUrl,
|
|
91
|
+
apiKey,
|
|
92
|
+
abortController.signal,
|
|
93
|
+
logger
|
|
94
|
+
);
|
|
95
|
+
streamPromises.push(stdinPromise);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Check if stdout and stderr are the same stream (combined output)
|
|
99
|
+
const isCombinedOutput =
|
|
100
|
+
stdoutStreamUrl && stderrStreamUrl && stdoutStreamUrl === stderrStreamUrl;
|
|
101
|
+
|
|
102
|
+
if (isCombinedOutput) {
|
|
103
|
+
// Stream combined output to stdout only to avoid duplicates
|
|
104
|
+
if (stdout) {
|
|
105
|
+
logger?.debug('using combined output stream (stdout === stderr)');
|
|
106
|
+
const combinedPromise = streamUrlToWritable(
|
|
107
|
+
stdoutStreamUrl,
|
|
108
|
+
stdout,
|
|
109
|
+
abortController.signal,
|
|
110
|
+
logger
|
|
111
|
+
);
|
|
112
|
+
streamPromises.push(combinedPromise);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
// Start stdout streaming
|
|
116
|
+
if (stdoutStreamUrl && stdout) {
|
|
117
|
+
const stdoutPromise = streamUrlToWritable(
|
|
118
|
+
stdoutStreamUrl,
|
|
119
|
+
stdout,
|
|
120
|
+
abortController.signal,
|
|
121
|
+
logger
|
|
122
|
+
);
|
|
123
|
+
streamPromises.push(stdoutPromise);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Start stderr streaming
|
|
127
|
+
if (stderrStreamUrl && stderr) {
|
|
128
|
+
const stderrPromise = streamUrlToWritable(
|
|
129
|
+
stderrStreamUrl,
|
|
130
|
+
stderr,
|
|
131
|
+
abortController.signal,
|
|
132
|
+
logger
|
|
133
|
+
);
|
|
134
|
+
streamPromises.push(stderrPromise);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Poll for sandbox completion in parallel with streaming
|
|
139
|
+
let attempts = 0;
|
|
140
|
+
let finalStatus: 'terminated' | 'failed' | null = null;
|
|
141
|
+
|
|
142
|
+
while (attempts < MAX_POLL_ATTEMPTS) {
|
|
143
|
+
if (signal?.aborted) {
|
|
144
|
+
abortController.abort();
|
|
145
|
+
throw new SandboxResponseError({
|
|
146
|
+
message: 'Sandbox execution cancelled',
|
|
147
|
+
sandboxId,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await sleep(POLL_INTERVAL_MS);
|
|
152
|
+
attempts++;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const sandboxInfo = await sandboxGet(client, { sandboxId, orgId });
|
|
156
|
+
|
|
157
|
+
if (sandboxInfo.status === 'terminated') {
|
|
158
|
+
finalStatus = 'terminated';
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (sandboxInfo.status === 'failed') {
|
|
163
|
+
finalStatus = 'failed';
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
} catch {
|
|
167
|
+
// Ignore polling errors, continue
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Sandbox completed - wait for streams to complete naturally (EOF)
|
|
173
|
+
// Pulse closes streams when the sandbox terminates, so streams should EOF
|
|
174
|
+
// We must wait for streams to fully drain before returning
|
|
175
|
+
logger?.debug('waiting for streams to complete...');
|
|
176
|
+
await Promise.allSettled(streamPromises);
|
|
177
|
+
logger?.debug('streams completed');
|
|
178
|
+
|
|
179
|
+
if (finalStatus === 'terminated') {
|
|
180
|
+
return {
|
|
181
|
+
sandboxId,
|
|
182
|
+
exitCode: 0,
|
|
183
|
+
durationMs: Date.now() - started,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (finalStatus === 'failed') {
|
|
188
|
+
return {
|
|
189
|
+
sandboxId,
|
|
190
|
+
exitCode: 1,
|
|
191
|
+
durationMs: Date.now() - started,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
throw new SandboxResponseError({
|
|
196
|
+
message: 'Sandbox execution polling timed out',
|
|
197
|
+
sandboxId,
|
|
198
|
+
});
|
|
199
|
+
} catch (error) {
|
|
200
|
+
abortController.abort();
|
|
201
|
+
try {
|
|
202
|
+
await sandboxDestroy(client, { sandboxId, orgId });
|
|
203
|
+
} catch {
|
|
204
|
+
// Ignore cleanup errors
|
|
205
|
+
}
|
|
206
|
+
throw error;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function createStdinStream(
|
|
211
|
+
region: string,
|
|
212
|
+
apiKey: string,
|
|
213
|
+
orgId?: string,
|
|
214
|
+
logger?: Logger
|
|
215
|
+
): Promise<{ id: string; url: string }> {
|
|
216
|
+
const urls = getServiceUrls(region);
|
|
217
|
+
const streamBaseUrl = urls.stream;
|
|
218
|
+
|
|
219
|
+
// Build URL with orgId query param for CLI token validation
|
|
220
|
+
const queryParams = new URLSearchParams();
|
|
221
|
+
if (orgId) {
|
|
222
|
+
queryParams.set('orgId', orgId);
|
|
223
|
+
}
|
|
224
|
+
const queryString = queryParams.toString();
|
|
225
|
+
const url = `${streamBaseUrl}${queryString ? `?${queryString}` : ''}`;
|
|
226
|
+
logger?.trace('creating stdin stream: %s', url);
|
|
227
|
+
|
|
228
|
+
const response = await fetch(url, {
|
|
229
|
+
method: 'POST',
|
|
230
|
+
headers: {
|
|
231
|
+
'Content-Type': 'application/json',
|
|
232
|
+
Authorization: `Bearer ${apiKey}`,
|
|
233
|
+
},
|
|
234
|
+
body: JSON.stringify({
|
|
235
|
+
name: `sandbox-stdin-${Date.now()}`,
|
|
236
|
+
}),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
if (!response.ok) {
|
|
240
|
+
throw new Error(`Failed to create stdin stream: ${response.status} ${response.statusText}`);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const data = (await response.json()) as { id: string };
|
|
244
|
+
logger?.debug('created stdin stream: %s', data.id);
|
|
245
|
+
|
|
246
|
+
// Include orgId in the URL for subsequent PUT requests (needed for CLI token auth)
|
|
247
|
+
const putQueryString = orgId ? `?orgId=${encodeURIComponent(orgId)}` : '';
|
|
248
|
+
return {
|
|
249
|
+
id: data.id,
|
|
250
|
+
url: `${streamBaseUrl}/${data.id}${putQueryString}`,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function streamStdinToUrl(
|
|
255
|
+
stdin: Readable,
|
|
256
|
+
url: string,
|
|
257
|
+
apiKey: string,
|
|
258
|
+
signal: AbortSignal,
|
|
259
|
+
logger?: Logger
|
|
260
|
+
): Promise<void> {
|
|
261
|
+
try {
|
|
262
|
+
logger?.debug('streaming stdin to: %s', url);
|
|
263
|
+
|
|
264
|
+
// Convert Node.js Readable to a web ReadableStream for fetch body
|
|
265
|
+
let controllerClosed = false;
|
|
266
|
+
const webStream = new ReadableStream({
|
|
267
|
+
start(controller) {
|
|
268
|
+
stdin.on('data', (chunk: Buffer) => {
|
|
269
|
+
if (!signal.aborted && !controllerClosed) {
|
|
270
|
+
controller.enqueue(chunk);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
stdin.on('end', () => {
|
|
274
|
+
if (!controllerClosed) {
|
|
275
|
+
controllerClosed = true;
|
|
276
|
+
controller.close();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
stdin.on('error', (err) => {
|
|
280
|
+
if (!controllerClosed) {
|
|
281
|
+
controllerClosed = true;
|
|
282
|
+
controller.error(err);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
signal.addEventListener('abort', () => {
|
|
286
|
+
if (!controllerClosed) {
|
|
287
|
+
controllerClosed = true;
|
|
288
|
+
controller.close();
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const response = await fetch(url, {
|
|
295
|
+
method: 'PUT',
|
|
296
|
+
headers: {
|
|
297
|
+
Authorization: `Bearer ${apiKey}`,
|
|
298
|
+
},
|
|
299
|
+
body: webStream,
|
|
300
|
+
signal,
|
|
301
|
+
duplex: 'half',
|
|
302
|
+
} as RequestInit);
|
|
303
|
+
|
|
304
|
+
if (!response.ok) {
|
|
305
|
+
logger?.debug('stdin stream PUT failed: %d', response.status);
|
|
306
|
+
} else {
|
|
307
|
+
logger?.debug('stdin stream completed');
|
|
308
|
+
}
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
311
|
+
logger?.debug('stdin stream aborted (expected on completion)');
|
|
312
|
+
} else {
|
|
313
|
+
logger?.debug('stdin stream error: %s', err);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
async function streamUrlToWritable(
|
|
319
|
+
url: string,
|
|
320
|
+
writable: Writable,
|
|
321
|
+
signal: AbortSignal,
|
|
322
|
+
logger?: Logger
|
|
323
|
+
): Promise<void> {
|
|
324
|
+
try {
|
|
325
|
+
logger?.debug('fetching stream: %s', url);
|
|
326
|
+
const response = await fetch(url, { signal });
|
|
327
|
+
logger?.debug('stream response status: %d', response.status);
|
|
328
|
+
|
|
329
|
+
if (!response.ok || !response.body) {
|
|
330
|
+
logger?.debug('stream response not ok or no body');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const reader = response.body.getReader();
|
|
335
|
+
|
|
336
|
+
// Read until EOF - Pulse will block until data is available
|
|
337
|
+
while (true) {
|
|
338
|
+
const { done, value } = await reader.read();
|
|
339
|
+
if (done) {
|
|
340
|
+
logger?.debug('stream EOF');
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (value) {
|
|
345
|
+
logger?.debug('stream chunk: %d bytes', value.length);
|
|
346
|
+
await writeAndDrain(writable, value);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} catch (err) {
|
|
350
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
351
|
+
logger?.debug('stream aborted');
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
logger?.debug('stream error: %s', err);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function sleep(ms: number): Promise<void> {
|
|
359
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
360
|
+
}
|