deepline 0.3.247 → 0.3.249
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/bundling-sources/sdk/src/client.ts +177 -13
- package/dist/bundling-sources/sdk/src/index.ts +8 -0
- package/dist/bundling-sources/sdk/src/plays/input-example.ts +40 -0
- package/dist/bundling-sources/sdk/src/release.ts +1 -1
- package/dist/bundling-sources/sdk/src/runs/overview.ts +457 -0
- package/dist/bundling-sources/sdk/src/runs/receipts.ts +23 -0
- package/dist/bundling-sources/sdk/src/tool-calls.ts +21 -1
- package/dist/bundling-sources/sdk/src/types.ts +16 -1
- package/dist/bundling-sources/shared_libs/observability/scheduled-jobs.json +19 -1
- package/dist/bundling-sources/shared_libs/play-data-plane/column-names.ts +17 -3
- package/dist/bundling-sources/shared_libs/play-data-plane/sheet-contract.ts +40 -8
- package/dist/bundling-sources/shared_libs/play-runtime/app-runtime-api.ts +26 -1
- package/dist/bundling-sources/shared_libs/play-runtime/context.ts +144 -14
- package/dist/bundling-sources/shared_libs/play-runtime/ctx-types.ts +2 -1
- package/dist/bundling-sources/shared_libs/play-runtime/daytona-runtime-config.ts +6 -3
- package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-janitor.ts +26 -21
- package/dist/bundling-sources/shared_libs/play-runtime/daytona-sandbox-reaper.ts +23 -21
- package/dist/bundling-sources/shared_libs/play-runtime/deployment/fly-machines.ts +17 -0
- package/dist/bundling-sources/shared_libs/play-runtime/log-context.ts +105 -0
- package/dist/bundling-sources/shared_libs/play-runtime/output-size-limits.ts +8 -1
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/job.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/play-runs/terminal/ports.ts +1 -0
- package/dist/bundling-sources/shared_libs/play-runtime/protocol.ts +9 -0
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger-event-transport.ts +40 -4
- package/dist/bundling-sources/shared_libs/play-runtime/run-ledger.ts +50 -5
- package/dist/bundling-sources/shared_libs/play-runtime/run-lifecycle-policy.ts +3 -1
- package/dist/bundling-sources/shared_libs/play-runtime/run-log-buffer.ts +28 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/index.ts +14 -1
- package/dist/bundling-sources/shared_libs/play-runtime/runner-app/runtime-terminal-push.ts +56 -16
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-control-transport.ts +184 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-launch-transport.ts +327 -0
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-lifecycle.ts +87 -30
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-session-execution.ts +24 -13
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona-start-admission.ts +23 -6
- package/dist/bundling-sources/shared_libs/play-runtime/runner-backends/backends/daytona.ts +76 -33
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/absurd-runtime-worker.ts +90 -35
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-pool.ts +16 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres-progress.ts +112 -50
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/postgres.ts +447 -77
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/receipt-gateway.ts +22 -4
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/run-ledger-outbox-recovery.ts +105 -121
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-event-search.ts +1648 -0
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-queue-health.ts +16 -19
- package/dist/bundling-sources/shared_libs/play-runtime/scheduler-backends/runtime-receipt-value-storage.ts +14 -6
- package/dist/bundling-sources/shared_libs/play-runtime/tool-execute-retry-policy.ts +16 -0
- package/dist/bundling-sources/shared_libs/play-runtime/tool-http-errors.ts +40 -0
- package/dist/bundling-sources/shared_libs/plays/authoring-contract.ts +5 -2
- package/dist/bundling-sources/shared_libs/plays/dataset-summary.ts +11 -7
- package/dist/bundling-sources/shared_libs/plays/static-pipeline-storage-codec.ts +5 -2
- package/dist/bundling-sources/shared_libs/plays/static-pipeline.ts +11 -2
- package/dist/cli/index.js +3504 -2368
- package/dist/cli/index.mjs +3188 -2052
- package/dist/{compiler-manifest-n8ZtPv2h.d.mts → compiler-manifest-DavmLa8y.d.mts} +1 -0
- package/dist/{compiler-manifest-n8ZtPv2h.d.ts → compiler-manifest-DavmLa8y.d.ts} +1 -0
- package/dist/index.d.mts +97 -5
- package/dist/index.d.ts +97 -5
- package/dist/index.js +135 -15
- package/dist/index.mjs +135 -15
- package/dist/install-integrity.json +9 -2
- package/dist/plays/bundle-play-file.d.mts +2 -2
- package/dist/plays/bundle-play-file.d.ts +2 -2
- package/dist/plays/bundle-play-file.mjs +1 -1
- package/dist/release.d.mts +1 -1
- package/dist/release.d.ts +1 -1
- package/dist/release.js +1 -1
- package/dist/release.mjs +1 -1
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type PlayRunnerEvent,
|
|
4
4
|
type PlayRunnerResult,
|
|
5
5
|
} from '../protocol';
|
|
6
|
+
import type { PlayLogContext } from '../log-context';
|
|
6
7
|
import { parseCtxFetchHttpFailureDiagnostic } from '../log-provenance';
|
|
7
8
|
import {
|
|
8
9
|
RUNNER_TERMINAL_PUSH_MAX_BODY_BYTES,
|
|
@@ -170,6 +171,7 @@ function retainArrayWithinJsonByteLimit<T>(
|
|
|
170
171
|
*/
|
|
171
172
|
function retainTerminalLogTailWithRuntimeDiagnostics(logs: readonly string[]): {
|
|
172
173
|
values: string[];
|
|
174
|
+
indexes: number[];
|
|
173
175
|
truncated: boolean;
|
|
174
176
|
} {
|
|
175
177
|
const retainedIndexes = new Set<number>();
|
|
@@ -204,6 +206,7 @@ function retainTerminalLogTailWithRuntimeDiagnostics(logs: readonly string[]): {
|
|
|
204
206
|
const indexes = [...retainedIndexes].sort((left, right) => left - right);
|
|
205
207
|
return {
|
|
206
208
|
values: indexes.map((index) => logs[index]!),
|
|
209
|
+
indexes,
|
|
207
210
|
truncated: indexes.length !== logs.length,
|
|
208
211
|
};
|
|
209
212
|
}
|
|
@@ -231,11 +234,13 @@ function retainRecordWithinJsonByteLimit(
|
|
|
231
234
|
function appendTerminalDiagnosticsMarker(
|
|
232
235
|
logs: string[],
|
|
233
236
|
truncatedFields: string[],
|
|
234
|
-
|
|
237
|
+
logContexts?: Array<PlayLogContext | null>,
|
|
238
|
+
): { logs: string[]; logContexts?: Array<PlayLogContext | null> } {
|
|
235
239
|
const marker =
|
|
236
240
|
`[deepline] terminal diagnostic metadata was bounded (${truncatedFields.join(', ')}); ` +
|
|
237
241
|
'the authored play output is complete.';
|
|
238
242
|
const marked = [marker, ...logs];
|
|
243
|
+
const markedContexts = logContexts ? [null, ...logContexts] : undefined;
|
|
239
244
|
while (
|
|
240
245
|
marked.length > 1 &&
|
|
241
246
|
!fitsJsonByteLimit(marked, TERMINAL_TRANSPORT_LOG_TAIL_MAX_BYTES)
|
|
@@ -247,11 +252,22 @@ function appendTerminalDiagnosticsMarker(
|
|
|
247
252
|
(line, index) =>
|
|
248
253
|
index > 0 && parseCtxFetchHttpFailureDiagnostic(line) === null,
|
|
249
254
|
);
|
|
250
|
-
|
|
255
|
+
const removeAt = removableIndex === -1 ? 1 : removableIndex;
|
|
256
|
+
marked.splice(removeAt, 1);
|
|
257
|
+
markedContexts?.splice(removeAt, 1);
|
|
251
258
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
259
|
+
const logsFit = fitsJsonByteLimit(
|
|
260
|
+
marked,
|
|
261
|
+
TERMINAL_TRANSPORT_LOG_TAIL_MAX_BYTES,
|
|
262
|
+
);
|
|
263
|
+
const retainedLogs = logsFit ? marked : [marker];
|
|
264
|
+
const retainedContexts = logsFit ? markedContexts : [null];
|
|
265
|
+
return {
|
|
266
|
+
logs: retainedLogs,
|
|
267
|
+
...(retainedContexts?.some(Boolean)
|
|
268
|
+
? { logContexts: retainedContexts }
|
|
269
|
+
: {}),
|
|
270
|
+
};
|
|
255
271
|
}
|
|
256
272
|
|
|
257
273
|
/**
|
|
@@ -266,6 +282,15 @@ function boundSettledTerminalTransportDiagnostics(
|
|
|
266
282
|
const logProjection = retainTerminalLogTailWithRuntimeDiagnostics(
|
|
267
283
|
result.logs,
|
|
268
284
|
);
|
|
285
|
+
if (
|
|
286
|
+
result.logContexts !== undefined &&
|
|
287
|
+
result.logContexts.length !== result.logs.length
|
|
288
|
+
) {
|
|
289
|
+
throw new TypeError('Terminal log contexts must align with result logs.');
|
|
290
|
+
}
|
|
291
|
+
const logContexts = result.logContexts?.length
|
|
292
|
+
? logProjection.indexes.map((index) => result.logContexts![index] ?? null)
|
|
293
|
+
: undefined;
|
|
269
294
|
const statsProjection = retainRecordWithinJsonByteLimit(
|
|
270
295
|
result.stats,
|
|
271
296
|
TERMINAL_TRANSPORT_STATS_MAX_BYTES,
|
|
@@ -303,15 +328,21 @@ function boundSettledTerminalTransportDiagnostics(
|
|
|
303
328
|
'head',
|
|
304
329
|
);
|
|
305
330
|
if (warningsProjection.truncated) truncatedFields.push('outputWarnings');
|
|
331
|
+
const boundedLogs =
|
|
332
|
+
truncatedFields.length > 0
|
|
333
|
+
? appendTerminalDiagnosticsMarker(
|
|
334
|
+
logProjection.values,
|
|
335
|
+
truncatedFields,
|
|
336
|
+
logContexts,
|
|
337
|
+
)
|
|
338
|
+
: {
|
|
339
|
+
logs: logProjection.values,
|
|
340
|
+
...(logContexts?.some(Boolean) ? { logContexts } : {}),
|
|
341
|
+
};
|
|
306
342
|
return {
|
|
307
343
|
...result,
|
|
308
|
-
logs:
|
|
309
|
-
|
|
310
|
-
? appendTerminalDiagnosticsMarker(
|
|
311
|
-
logProjection.values,
|
|
312
|
-
truncatedFields,
|
|
313
|
-
)
|
|
314
|
-
: logProjection.values,
|
|
344
|
+
logs: boundedLogs.logs,
|
|
345
|
+
logContexts: boundedLogs.logContexts,
|
|
315
346
|
stats: statsProjection.value,
|
|
316
347
|
steps: stepsProjection.values,
|
|
317
348
|
...(result.outputWarnings
|
|
@@ -337,13 +368,22 @@ function boundSettledTerminalTransportDiagnostics(
|
|
|
337
368
|
);
|
|
338
369
|
if (error.truncated) truncatedFields.push('error');
|
|
339
370
|
if (errorsProjection.truncated) truncatedFields.push('errors');
|
|
371
|
+
const boundedLogs =
|
|
372
|
+
truncatedFields.length > 0
|
|
373
|
+
? appendTerminalDiagnosticsMarker(
|
|
374
|
+
logProjection.values,
|
|
375
|
+
truncatedFields,
|
|
376
|
+
logContexts,
|
|
377
|
+
)
|
|
378
|
+
: {
|
|
379
|
+
logs: logProjection.values,
|
|
380
|
+
...(logContexts?.some(Boolean) ? { logContexts } : {}),
|
|
381
|
+
};
|
|
340
382
|
return {
|
|
341
383
|
...result,
|
|
342
384
|
error: error.value,
|
|
343
|
-
logs:
|
|
344
|
-
|
|
345
|
-
? appendTerminalDiagnosticsMarker(logProjection.values, truncatedFields)
|
|
346
|
-
: logProjection.values,
|
|
385
|
+
logs: boundedLogs.logs,
|
|
386
|
+
logContexts: boundedLogs.logContexts,
|
|
347
387
|
stats: statsProjection.value,
|
|
348
388
|
steps: stepsProjection.values,
|
|
349
389
|
...(result.errors ? { errors: errorsProjection.values } : {}),
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Configuration,
|
|
3
|
+
SandboxApi,
|
|
4
|
+
type SandboxListItem,
|
|
5
|
+
type Sandbox as SandboxData,
|
|
6
|
+
} from '@daytona/api-client';
|
|
7
|
+
import { Daytona, Sandbox } from '@daytonaio/sdk';
|
|
8
|
+
import axios from 'axios';
|
|
9
|
+
import type { DaytonaClientOptions } from '../../daytona-runtime-config';
|
|
10
|
+
|
|
11
|
+
/** Control-plane recovery must not inherit the SDK's 24-hour HTTP timeout. */
|
|
12
|
+
export const DAYTONA_CONTROL_TIMEOUT_MS = 10_000;
|
|
13
|
+
|
|
14
|
+
export class DaytonaControlTimeoutError extends Error {
|
|
15
|
+
readonly code = 'ETIMEDOUT';
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
super('Daytona control request timed out');
|
|
19
|
+
this.name = 'DaytonaControlTimeoutError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* One lookup and its follow-up toolbox/delete requests share a native transport
|
|
25
|
+
* deadline. The caller must dispose the client in finally. This adapter never
|
|
26
|
+
* creates sandboxes or starts customer code; ambiguous starts retain their
|
|
27
|
+
* existing lifecycle owner.
|
|
28
|
+
*/
|
|
29
|
+
export function createDaytonaControlClient(
|
|
30
|
+
options: DaytonaClientOptions,
|
|
31
|
+
timeoutMs = DAYTONA_CONTROL_TIMEOUT_MS,
|
|
32
|
+
) {
|
|
33
|
+
if (!options.apiKey?.trim()) {
|
|
34
|
+
throw new Error('Daytona control requests require the configured API key.');
|
|
35
|
+
}
|
|
36
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
|
|
37
|
+
throw new Error('Daytona control timeout must be positive.');
|
|
38
|
+
}
|
|
39
|
+
const controller = new AbortController();
|
|
40
|
+
const deadlineAtMs = Date.now() + timeoutMs;
|
|
41
|
+
const timer = setTimeout(
|
|
42
|
+
() => controller.abort(new DaytonaControlTimeoutError()),
|
|
43
|
+
timeoutMs,
|
|
44
|
+
);
|
|
45
|
+
timer.unref();
|
|
46
|
+
const configuration = new Configuration({
|
|
47
|
+
basePath:
|
|
48
|
+
options.apiUrl || options.serverUrl || 'https://app.daytona.io/api',
|
|
49
|
+
baseOptions: {
|
|
50
|
+
signal: controller.signal,
|
|
51
|
+
timeout: timeoutMs,
|
|
52
|
+
headers: {
|
|
53
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
54
|
+
'X-Daytona-Source': 'deepline-runtime-control',
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
const createTransport = () => {
|
|
59
|
+
const transport = axios.create({
|
|
60
|
+
signal: controller.signal,
|
|
61
|
+
timeout: timeoutMs,
|
|
62
|
+
});
|
|
63
|
+
transport.interceptors.request.use((request) => {
|
|
64
|
+
if (Date.now() >= deadlineAtMs) {
|
|
65
|
+
controller.abort(new DaytonaControlTimeoutError());
|
|
66
|
+
}
|
|
67
|
+
controller.signal.throwIfAborted();
|
|
68
|
+
return request;
|
|
69
|
+
});
|
|
70
|
+
transport.interceptors.response.use(undefined, (error: unknown) => {
|
|
71
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
72
|
+
throw error;
|
|
73
|
+
});
|
|
74
|
+
return transport;
|
|
75
|
+
};
|
|
76
|
+
// The generated API is CommonJS and owns its Axios declaration/transport.
|
|
77
|
+
// Its public request options carry native cancellation without crossing the
|
|
78
|
+
// incompatible Axios ESM/CJS interceptor types into its constructor.
|
|
79
|
+
const api = new SandboxApi(configuration);
|
|
80
|
+
const sandboxData = new WeakMap<Sandbox, SandboxData>();
|
|
81
|
+
const call = async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
82
|
+
if (Date.now() >= deadlineAtMs) {
|
|
83
|
+
controller.abort(new DaytonaControlTimeoutError());
|
|
84
|
+
}
|
|
85
|
+
controller.signal.throwIfAborted();
|
|
86
|
+
try {
|
|
87
|
+
return await operation();
|
|
88
|
+
} catch (error) {
|
|
89
|
+
if (controller.signal.aborted) throw controller.signal.reason;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
return {
|
|
94
|
+
deadlineAtMs,
|
|
95
|
+
async listPage(query: { labels: Record<string, string> }, cursor?: string) {
|
|
96
|
+
const { data } = await call(() =>
|
|
97
|
+
api.listSandboxes(
|
|
98
|
+
undefined,
|
|
99
|
+
cursor,
|
|
100
|
+
undefined,
|
|
101
|
+
undefined,
|
|
102
|
+
undefined,
|
|
103
|
+
JSON.stringify(query.labels),
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
return data;
|
|
107
|
+
},
|
|
108
|
+
async get(sandboxId: string): Promise<Sandbox> {
|
|
109
|
+
const { data } = await call(() => api.getSandbox(sandboxId));
|
|
110
|
+
// Sandbox sets its configuration/base URL to the provider-returned
|
|
111
|
+
// toolbox endpoint. Keep that separate from the control API's authority.
|
|
112
|
+
const sandbox = new Sandbox(
|
|
113
|
+
data,
|
|
114
|
+
new Configuration({
|
|
115
|
+
basePath: configuration.basePath,
|
|
116
|
+
baseOptions: {
|
|
117
|
+
...configuration.baseOptions,
|
|
118
|
+
headers: { ...configuration.baseOptions.headers },
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
createTransport(),
|
|
122
|
+
api,
|
|
123
|
+
);
|
|
124
|
+
sandboxData.set(sandbox, data);
|
|
125
|
+
return sandbox;
|
|
126
|
+
},
|
|
127
|
+
/**
|
|
128
|
+
* Full captured stdout is a legacy data-plane artifact, not fixed-size
|
|
129
|
+
* metadata. Preserve its SDK file-download timeout independently of this
|
|
130
|
+
* control scope. This facade cannot perform another sandbox lookup.
|
|
131
|
+
*/
|
|
132
|
+
legacyOutputRecovery(sandbox: Sandbox): Pick<Sandbox, 'fs'> {
|
|
133
|
+
const data = sandboxData.get(sandbox);
|
|
134
|
+
if (!data)
|
|
135
|
+
throw new Error(
|
|
136
|
+
"Output recovery requires this control client's sandbox.",
|
|
137
|
+
);
|
|
138
|
+
const recovery = new Sandbox(
|
|
139
|
+
data,
|
|
140
|
+
new Configuration({
|
|
141
|
+
basePath: configuration.basePath,
|
|
142
|
+
baseOptions: { headers: { ...configuration.baseOptions.headers } },
|
|
143
|
+
}),
|
|
144
|
+
Daytona.createAxiosInstance(),
|
|
145
|
+
api,
|
|
146
|
+
);
|
|
147
|
+
return { fs: recovery.fs };
|
|
148
|
+
},
|
|
149
|
+
async delete(sandbox: Pick<Sandbox, 'id'>): Promise<void> {
|
|
150
|
+
// SDK Sandbox.delete starts an unawaited refresh after success. The
|
|
151
|
+
// generated deletion endpoint preserves the same confirmed outcome
|
|
152
|
+
// without starting a new request after this scope finishes.
|
|
153
|
+
await call(() => api.deleteSandbox(sandbox.id));
|
|
154
|
+
},
|
|
155
|
+
dispose(): void {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
controller.abort(new Error('Daytona control request scope finished'));
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Bound each external inventory request, not the sweep. Consume every provider
|
|
164
|
+
* cursor; a failed page throws instead of reporting a partially scanned fleet
|
|
165
|
+
* as success. Release the HTTP scope before the caller processes page items.
|
|
166
|
+
*/
|
|
167
|
+
export async function* listDaytonaControlSandboxes(
|
|
168
|
+
options: DaytonaClientOptions,
|
|
169
|
+
query: { labels: Record<string, string> },
|
|
170
|
+
pageTimeoutMs = DAYTONA_CONTROL_TIMEOUT_MS,
|
|
171
|
+
): AsyncGenerator<SandboxListItem> {
|
|
172
|
+
let cursor: string | undefined;
|
|
173
|
+
do {
|
|
174
|
+
const control = createDaytonaControlClient(options, pageTimeoutMs);
|
|
175
|
+
let page;
|
|
176
|
+
try {
|
|
177
|
+
page = await control.listPage(query, cursor);
|
|
178
|
+
} finally {
|
|
179
|
+
control.dispose();
|
|
180
|
+
}
|
|
181
|
+
yield* page.items;
|
|
182
|
+
cursor = page.nextCursor ?? undefined;
|
|
183
|
+
} while (cursor);
|
|
184
|
+
}
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Configuration,
|
|
3
|
+
SandboxApi,
|
|
4
|
+
type Sandbox as SandboxData,
|
|
5
|
+
} from '@daytona/api-client';
|
|
6
|
+
import {
|
|
7
|
+
Sandbox,
|
|
8
|
+
DaytonaError,
|
|
9
|
+
DaytonaAuthenticationError,
|
|
10
|
+
DaytonaAuthorizationError,
|
|
11
|
+
DaytonaNotFoundError,
|
|
12
|
+
DaytonaConflictError,
|
|
13
|
+
DaytonaRateLimitError,
|
|
14
|
+
DaytonaValidationError,
|
|
15
|
+
type CreateSandboxFromSnapshotParams,
|
|
16
|
+
} from '@daytonaio/sdk';
|
|
17
|
+
import axios, { type InternalAxiosRequestConfig } from 'axios';
|
|
18
|
+
import { Readable } from 'node:stream';
|
|
19
|
+
import type { DaytonaClientOptions } from '../../daytona-runtime-config';
|
|
20
|
+
import {
|
|
21
|
+
DAYTONA_CONTROL_TIMEOUT_MS,
|
|
22
|
+
DaytonaControlTimeoutError,
|
|
23
|
+
} from './daytona-control-transport';
|
|
24
|
+
|
|
25
|
+
/** A sent create may have committed even when no response reached the worker. */
|
|
26
|
+
export class DaytonaLaunchOutcomeUnknownError extends Error {
|
|
27
|
+
constructor(cause: unknown) {
|
|
28
|
+
super(
|
|
29
|
+
'Daytona named create outcome is unknown; reconcile the same operation before another launch.',
|
|
30
|
+
{ cause },
|
|
31
|
+
);
|
|
32
|
+
this.name = 'DaytonaLaunchOutcomeUnknownError';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function providerError(error: unknown): unknown {
|
|
37
|
+
if (!axios.isAxiosError(error)) return error;
|
|
38
|
+
const status = error.response?.status;
|
|
39
|
+
const data = error.response?.data;
|
|
40
|
+
const message =
|
|
41
|
+
typeof data === 'string'
|
|
42
|
+
? data
|
|
43
|
+
: typeof data?.message === 'string'
|
|
44
|
+
? data.message
|
|
45
|
+
: error.message;
|
|
46
|
+
const errorClasses: Record<number, typeof DaytonaError> = {
|
|
47
|
+
400: DaytonaValidationError,
|
|
48
|
+
401: DaytonaAuthenticationError,
|
|
49
|
+
403: DaytonaAuthorizationError,
|
|
50
|
+
404: DaytonaNotFoundError,
|
|
51
|
+
409: DaytonaConflictError,
|
|
52
|
+
429: DaytonaRateLimitError,
|
|
53
|
+
};
|
|
54
|
+
const ErrorClass = errorClasses[status ?? 0] ?? DaytonaError;
|
|
55
|
+
return new ErrorClass(
|
|
56
|
+
message,
|
|
57
|
+
status,
|
|
58
|
+
undefined,
|
|
59
|
+
typeof data?.code === 'string' ? data.code : error.code,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Every SDK request owns its socket deadline, including streamed response bodies. */
|
|
64
|
+
function createTransport(signal: () => AbortSignal | undefined) {
|
|
65
|
+
const active = new WeakMap<
|
|
66
|
+
InternalAxiosRequestConfig,
|
|
67
|
+
{ finish(): void; signal: AbortSignal }
|
|
68
|
+
>();
|
|
69
|
+
const transport = axios.create();
|
|
70
|
+
transport.interceptors.request.use((request) => {
|
|
71
|
+
const controller = new AbortController();
|
|
72
|
+
// Honor the SDK's shorter command/delete windows; upload gets at most the
|
|
73
|
+
// existing 90s staging budget even without an enclosing phase scope.
|
|
74
|
+
const timeoutMs = Math.min(
|
|
75
|
+
request.timeout || DAYTONA_CONTROL_TIMEOUT_MS,
|
|
76
|
+
90_000,
|
|
77
|
+
);
|
|
78
|
+
const timer = setTimeout(
|
|
79
|
+
() => controller.abort(new DaytonaControlTimeoutError()),
|
|
80
|
+
timeoutMs,
|
|
81
|
+
);
|
|
82
|
+
timer.unref();
|
|
83
|
+
const inherited = signal();
|
|
84
|
+
const combined = AbortSignal.any([
|
|
85
|
+
controller.signal,
|
|
86
|
+
...(inherited ? [inherited] : []),
|
|
87
|
+
...(request.signal instanceof AbortSignal ? [request.signal] : []),
|
|
88
|
+
]);
|
|
89
|
+
const finish = () => clearTimeout(timer);
|
|
90
|
+
if (combined.aborted) {
|
|
91
|
+
finish();
|
|
92
|
+
combined.throwIfAborted();
|
|
93
|
+
}
|
|
94
|
+
request.signal = combined;
|
|
95
|
+
active.set(request, { finish, signal: combined });
|
|
96
|
+
return request;
|
|
97
|
+
});
|
|
98
|
+
transport.interceptors.response.use(
|
|
99
|
+
(response) => {
|
|
100
|
+
const scope = active.get(response.config);
|
|
101
|
+
if (scope && response.data instanceof Readable) {
|
|
102
|
+
const body = response.data;
|
|
103
|
+
const abort = () =>
|
|
104
|
+
body.destroy(
|
|
105
|
+
scope.signal.reason instanceof Error
|
|
106
|
+
? scope.signal.reason
|
|
107
|
+
: new Error('Daytona request aborted'),
|
|
108
|
+
);
|
|
109
|
+
const finish = () => {
|
|
110
|
+
scope.finish();
|
|
111
|
+
scope.signal.removeEventListener('abort', abort);
|
|
112
|
+
};
|
|
113
|
+
scope.signal.addEventListener('abort', abort, { once: true });
|
|
114
|
+
body.once('end', finish).once('close', finish).once('error', finish);
|
|
115
|
+
if (scope.signal.aborted) abort();
|
|
116
|
+
} else scope?.finish();
|
|
117
|
+
return response;
|
|
118
|
+
},
|
|
119
|
+
(error: unknown) => {
|
|
120
|
+
const scope =
|
|
121
|
+
axios.isAxiosError(error) && error.config
|
|
122
|
+
? active.get(error.config)
|
|
123
|
+
: undefined;
|
|
124
|
+
scope?.finish();
|
|
125
|
+
if (scope?.signal.aborted) throw scope.signal.reason;
|
|
126
|
+
throw providerError(error);
|
|
127
|
+
},
|
|
128
|
+
);
|
|
129
|
+
return transport;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function deadline(timeoutMs: number, signal?: AbortSignal) {
|
|
133
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
134
|
+
throw new Error('Daytona phase deadline must be positive.');
|
|
135
|
+
const controller = new AbortController();
|
|
136
|
+
const timer = setTimeout(
|
|
137
|
+
() => controller.abort(new DaytonaControlTimeoutError()),
|
|
138
|
+
timeoutMs,
|
|
139
|
+
);
|
|
140
|
+
timer.unref();
|
|
141
|
+
const combined = signal
|
|
142
|
+
? AbortSignal.any([controller.signal, signal])
|
|
143
|
+
: controller.signal;
|
|
144
|
+
const deadlineAtMs = Date.now() + timeoutMs;
|
|
145
|
+
return {
|
|
146
|
+
get signal() {
|
|
147
|
+
if (Date.now() >= deadlineAtMs)
|
|
148
|
+
controller.abort(new DaytonaControlTimeoutError());
|
|
149
|
+
return combined;
|
|
150
|
+
},
|
|
151
|
+
dispose() {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
controller.abort(new Error('Daytona phase finished'));
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Deepline's default-snapshot launch contract; image/build orchestration stays in the SDK. */
|
|
159
|
+
export function createDaytonaLaunchClient(
|
|
160
|
+
options: DaytonaClientOptions,
|
|
161
|
+
cancellation?: AbortSignal,
|
|
162
|
+
) {
|
|
163
|
+
if (!options.apiKey?.trim())
|
|
164
|
+
throw new Error('Daytona launch requires the configured API key.');
|
|
165
|
+
const configuration = new Configuration({
|
|
166
|
+
basePath:
|
|
167
|
+
options.apiUrl || options.serverUrl || 'https://app.daytona.io/api',
|
|
168
|
+
baseOptions: {
|
|
169
|
+
headers: {
|
|
170
|
+
Authorization: `Bearer ${options.apiKey}`,
|
|
171
|
+
'X-Daytona-Source': 'deepline-runtime-launch',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
const dataBySandbox = new WeakMap<Sandbox, SandboxData>();
|
|
176
|
+
const makeSandbox = (
|
|
177
|
+
data: SandboxData,
|
|
178
|
+
signal: () => AbortSignal | undefined,
|
|
179
|
+
): Sandbox => {
|
|
180
|
+
const scopedConfiguration = new Configuration({
|
|
181
|
+
basePath: configuration.basePath,
|
|
182
|
+
baseOptions: {
|
|
183
|
+
...configuration.baseOptions,
|
|
184
|
+
signal: signal(),
|
|
185
|
+
timeout: DAYTONA_CONTROL_TIMEOUT_MS,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
const api = new SandboxApi(scopedConfiguration);
|
|
189
|
+
const sandbox = new Sandbox(
|
|
190
|
+
data,
|
|
191
|
+
new Configuration({
|
|
192
|
+
basePath: configuration.basePath,
|
|
193
|
+
baseOptions: { ...configuration.baseOptions },
|
|
194
|
+
}),
|
|
195
|
+
createTransport(signal),
|
|
196
|
+
api,
|
|
197
|
+
);
|
|
198
|
+
dataBySandbox.set(sandbox, data);
|
|
199
|
+
return sandbox;
|
|
200
|
+
};
|
|
201
|
+
const optionsTarget = options.target;
|
|
202
|
+
return {
|
|
203
|
+
async create(
|
|
204
|
+
params: CreateSandboxFromSnapshotParams,
|
|
205
|
+
options: { timeout?: number } = {},
|
|
206
|
+
) {
|
|
207
|
+
const timeoutMs = (options.timeout ?? 60) * 1_000;
|
|
208
|
+
const phase = deadline(timeoutMs, cancellation);
|
|
209
|
+
let sent = false;
|
|
210
|
+
let data: SandboxData | undefined;
|
|
211
|
+
try {
|
|
212
|
+
phase.signal.throwIfAborted();
|
|
213
|
+
const api = new SandboxApi(
|
|
214
|
+
new Configuration({
|
|
215
|
+
basePath: configuration.basePath,
|
|
216
|
+
baseOptions: { ...configuration.baseOptions, signal: phase.signal },
|
|
217
|
+
}),
|
|
218
|
+
);
|
|
219
|
+
sent = true;
|
|
220
|
+
const response = await api.createSandbox(
|
|
221
|
+
{
|
|
222
|
+
name: params.name,
|
|
223
|
+
snapshot: params.snapshot,
|
|
224
|
+
user: params.user,
|
|
225
|
+
env: params.envVars ?? {},
|
|
226
|
+
labels: {
|
|
227
|
+
...params.labels,
|
|
228
|
+
'code-toolbox-language': params.language ?? 'python',
|
|
229
|
+
},
|
|
230
|
+
public: params.public,
|
|
231
|
+
target: optionsTarget,
|
|
232
|
+
autoStopInterval: params.autoStopInterval,
|
|
233
|
+
autoArchiveInterval: params.autoArchiveInterval,
|
|
234
|
+
autoDeleteInterval: params.ephemeral
|
|
235
|
+
? 0
|
|
236
|
+
: params.autoDeleteInterval,
|
|
237
|
+
volumes: params.volumes,
|
|
238
|
+
networkBlockAll: params.networkBlockAll,
|
|
239
|
+
networkAllowList: params.networkAllowList,
|
|
240
|
+
linkedSandbox: params.linkedSandbox,
|
|
241
|
+
},
|
|
242
|
+
undefined,
|
|
243
|
+
{ timeout: timeoutMs },
|
|
244
|
+
);
|
|
245
|
+
data = response.data;
|
|
246
|
+
const sandbox = makeSandbox(data, () => phase.signal);
|
|
247
|
+
await sandbox.waitUntilStarted(options.timeout ?? 60);
|
|
248
|
+
// Future phases own fresh deadlines; none inherits this create scope.
|
|
249
|
+
return makeSandbox(
|
|
250
|
+
{
|
|
251
|
+
...data,
|
|
252
|
+
state: sandbox.state,
|
|
253
|
+
id: sandbox.id,
|
|
254
|
+
name: sandbox.name,
|
|
255
|
+
organizationId: sandbox.organizationId,
|
|
256
|
+
cpu: sandbox.cpu,
|
|
257
|
+
gpu: sandbox.gpu,
|
|
258
|
+
memory: sandbox.memory,
|
|
259
|
+
disk: sandbox.disk,
|
|
260
|
+
snapshot: sandbox.snapshot,
|
|
261
|
+
labels: sandbox.labels,
|
|
262
|
+
toolboxProxyUrl: sandbox.toolboxProxyUrl,
|
|
263
|
+
},
|
|
264
|
+
() => cancellation,
|
|
265
|
+
);
|
|
266
|
+
} catch (caught) {
|
|
267
|
+
const error = providerError(caught);
|
|
268
|
+
const confirmedRejection =
|
|
269
|
+
!data &&
|
|
270
|
+
error instanceof DaytonaError &&
|
|
271
|
+
[400, 401, 403, 429].includes(error.statusCode ?? 0);
|
|
272
|
+
if (sent && !confirmedRejection)
|
|
273
|
+
throw new DaytonaLaunchOutcomeUnknownError(error);
|
|
274
|
+
throw error;
|
|
275
|
+
} finally {
|
|
276
|
+
phase.dispose();
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
async get(id: string) {
|
|
280
|
+
const phase = deadline(DAYTONA_CONTROL_TIMEOUT_MS, cancellation);
|
|
281
|
+
try {
|
|
282
|
+
phase.signal.throwIfAborted();
|
|
283
|
+
const api = new SandboxApi(configuration);
|
|
284
|
+
const { data } = await api.getSandbox(id, undefined, undefined, {
|
|
285
|
+
signal: phase.signal,
|
|
286
|
+
});
|
|
287
|
+
return makeSandbox(data, () => cancellation);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
throw providerError(error);
|
|
290
|
+
} finally {
|
|
291
|
+
phase.dispose();
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
async delete(sandbox: Pick<Sandbox, 'id'>, timeoutSeconds = 30) {
|
|
295
|
+
// Cleanup must survive execution cancellation and must not trigger the
|
|
296
|
+
// SDK's unawaited refreshDataSafe after a confirmed delete response.
|
|
297
|
+
const phase = deadline(timeoutSeconds * 1_000);
|
|
298
|
+
try {
|
|
299
|
+
const api = new SandboxApi(
|
|
300
|
+
new Configuration({
|
|
301
|
+
basePath: configuration.basePath,
|
|
302
|
+
baseOptions: { ...configuration.baseOptions, signal: phase.signal },
|
|
303
|
+
}),
|
|
304
|
+
);
|
|
305
|
+
await api.deleteSandbox(sandbox.id, undefined, {
|
|
306
|
+
timeout: timeoutSeconds * 1_000,
|
|
307
|
+
});
|
|
308
|
+
} catch (error) {
|
|
309
|
+
throw providerError(error);
|
|
310
|
+
} finally {
|
|
311
|
+
phase.dispose();
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
scope(sandbox: Sandbox, timeoutMs: number) {
|
|
315
|
+
const data = dataBySandbox.get(sandbox);
|
|
316
|
+
if (!data)
|
|
317
|
+
throw new Error(
|
|
318
|
+
'Daytona phase requires a sandbox owned by this launch client.',
|
|
319
|
+
);
|
|
320
|
+
const phase = deadline(timeoutMs, cancellation);
|
|
321
|
+
return {
|
|
322
|
+
sandbox: makeSandbox(data, () => phase.signal),
|
|
323
|
+
dispose: phase.dispose,
|
|
324
|
+
};
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|