deepline 0.1.288 → 0.1.289

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.
@@ -155,7 +155,7 @@ export const SDK_RELEASE = {
155
155
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
156
156
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
157
157
  // Operators use the checkout-local deepline-admin binary instead.
158
- version: '0.1.288',
158
+ version: '0.1.289',
159
159
  contracts: {
160
160
  api: {
161
161
  name: 'sdk-http-api',
@@ -27,7 +27,8 @@ const DAYTONA_NETWORK_ALLOW_LIST_ENV = 'DEEPLINE_DAYTONA_NETWORK_ALLOW_LIST';
27
27
 
28
28
  export const DAYTONA_CANCELLED_ERROR = 'Daytona play runner cancelled';
29
29
 
30
- export type DaytonaClient = Pick<Daytona, 'create'>;
30
+ export type DaytonaClient = Pick<Daytona, 'create'> &
31
+ Partial<Pick<Daytona, 'get'>>;
31
32
  export type DaytonaSandbox = Awaited<ReturnType<DaytonaClient['create']>>;
32
33
  export type DaytonaExecutionContext = PlayRunnerExecutionConfig['context'];
33
34
  export type DaytonaStageEmitter = (
@@ -37,6 +38,7 @@ export type DaytonaStageEmitter = (
37
38
 
38
39
  export type AcquiredDaytonaSandbox = {
39
40
  sandbox: DaytonaSandbox;
41
+ daytonaOrganizationId: string;
40
42
  billingStartedAt: number;
41
43
  billingEndedAt?: number;
42
44
  };
@@ -56,6 +58,22 @@ type DaytonaCreateResult = {
56
58
  attemptElapsedMs: number;
57
59
  };
58
60
 
61
+ async function rejectAcquiredSandbox(
62
+ sandbox: DaytonaSandbox,
63
+ reason: string,
64
+ ): Promise<never> {
65
+ try {
66
+ await sandbox.delete(30);
67
+ } catch (error) {
68
+ const cleanupError = error instanceof Error ? error.message : String(error);
69
+ throw new Error(
70
+ `${reason} Defensive deletion of Daytona sandbox ${sandbox.id} also failed: ${cleanupError}`,
71
+ { cause: error },
72
+ );
73
+ }
74
+ throw new Error(reason);
75
+ }
76
+
59
77
  function normalizeLabelValue(value: string | null | undefined): string | null {
60
78
  const trimmed = value?.trim();
61
79
  return trimmed ? trimmed.slice(0, 63) : null;
@@ -233,7 +251,15 @@ async function createRetriedOneShotDaytonaSandbox(input: {
233
251
  } catch (error) {
234
252
  const message = error instanceof Error ? error.message : String(error);
235
253
  errors.push(message);
254
+ input.emitStage('create:attempt_failed', {
255
+ attempt,
256
+ elapsedMs: Date.now() - input.startedAt,
257
+ attemptElapsedMs: Date.now() - attemptStartedAt,
258
+ error: message,
259
+ });
236
260
  console.warn('[play-runner.daytona.create_attempt_failed]', {
261
+ workflowId: input.context.workflowId ?? null,
262
+ runId: input.context.runId ?? null,
237
263
  attempt,
238
264
  error: message,
239
265
  });
@@ -265,11 +291,46 @@ async function acquireOneShotDaytonaSandbox(input: {
265
291
  granted.diskGiB !== DAYTONA_SANDBOX_DISK_GIB ||
266
292
  granted.gpu !== DAYTONA_SANDBOX_GPU
267
293
  ) {
268
- await result.sandbox.delete(30).catch(() => undefined);
269
- throw new Error(
294
+ await rejectAcquiredSandbox(
295
+ result.sandbox,
270
296
  `Daytona sandbox resource boundary mismatch: expected cpu=${DAYTONA_SANDBOX_CPU} memoryGiB=${DAYTONA_SANDBOX_MEMORY_GIB} diskGiB=${DAYTONA_SANDBOX_DISK_GIB} gpu=${DAYTONA_SANDBOX_GPU}, granted cpu=${granted.cpu} memoryGiB=${granted.memoryGiB} diskGiB=${granted.diskGiB} gpu=${granted.gpu}`,
271
297
  );
272
298
  }
299
+ const configuredOrganizationId =
300
+ process.env.DAYTONA_ORGANIZATION_ID?.trim() || null;
301
+ const observedOrganizationId = result.sandbox.organizationId?.trim() || null;
302
+ if (
303
+ configuredOrganizationId &&
304
+ observedOrganizationId &&
305
+ configuredOrganizationId !== observedOrganizationId
306
+ ) {
307
+ await rejectAcquiredSandbox(
308
+ result.sandbox,
309
+ 'Daytona sandbox organization routing mismatch. Refusing to run customer code in a sandbox whose observed organization differs from the configured organization.',
310
+ );
311
+ }
312
+ let lookupOrganizationId: string | null = null;
313
+ if (
314
+ !observedOrganizationId &&
315
+ !configuredOrganizationId &&
316
+ input.daytona.get
317
+ ) {
318
+ try {
319
+ const lookedUpSandbox = await input.daytona.get(result.sandbox.id);
320
+ lookupOrganizationId = lookedUpSandbox.organizationId?.trim() || null;
321
+ } catch {
322
+ // The failure below is intentionally about the invariant, not the
323
+ // provider response. The newly created sandbox is still deleted.
324
+ }
325
+ }
326
+ const daytonaOrganizationId =
327
+ observedOrganizationId ?? configuredOrganizationId ?? lookupOrganizationId;
328
+ if (!daytonaOrganizationId) {
329
+ return await rejectAcquiredSandbox(
330
+ result.sandbox,
331
+ 'Daytona sandbox organization routing identity is missing. Refusing to run customer code without a durable cleanup routing domain.',
332
+ );
333
+ }
273
334
  const billingStartedAt = Date.now();
274
335
  const sandbox = result.sandbox;
275
336
  input.emitStage('create:done', {
@@ -281,7 +342,7 @@ async function acquireOneShotDaytonaSandbox(input: {
281
342
  memoryGiB: granted.memoryGiB,
282
343
  diskGiB: granted.diskGiB,
283
344
  });
284
- return { sandbox, billingStartedAt };
345
+ return { sandbox, daytonaOrganizationId, billingStartedAt };
285
346
  }
286
347
 
287
348
  export function createOneShotDaytonaSandboxLifecycle(input: {
@@ -61,6 +61,7 @@ export type StagedDaytonaPayload = {
61
61
  command: string;
62
62
  outputPath: string;
63
63
  exitCodePath: string;
64
+ runtimeCompletedPath: string;
64
65
  progressEventPath: string;
65
66
  };
66
67
 
@@ -557,6 +558,7 @@ export async function stageDaytonaRunnerPayload(input: {
557
558
  command,
558
559
  outputPath,
559
560
  exitCodePath,
561
+ runtimeCompletedPath,
560
562
  progressEventPath,
561
563
  };
562
564
  }
@@ -289,27 +289,161 @@ export async function inspectDetachedDaytonaRunner(input: {
289
289
  }
290
290
  }
291
291
 
292
- export async function deleteDaytonaSandboxById(input: {
292
+ /** Read the runner's exact customer-code completion fence before terminal GC. */
293
+ export async function readDetachedDaytonaRuntimeCompletion(input: {
294
+ sandboxId: string;
295
+ runtimeCompletedPath: string;
296
+ }): Promise<number | null> {
297
+ try {
298
+ const { clientOptions } = loadDaytonaRequiredConfig();
299
+ const sandbox = (await daytonaSdkClientFactory
300
+ .createFull(clientOptions)
301
+ .get(input.sandboxId)) as DaytonaSandbox;
302
+ const marker = JSON.parse(
303
+ (await sandbox.fs.downloadFile(input.runtimeCompletedPath, 5)).toString(
304
+ 'utf-8',
305
+ ),
306
+ ) as { at?: unknown };
307
+ return typeof marker.at === 'number' && Number.isFinite(marker.at)
308
+ ? marker.at
309
+ : null;
310
+ } catch (error) {
311
+ console.warn(
312
+ '[play-runner.daytona.runtime_completion_marker_unavailable]',
313
+ {
314
+ sandboxId: input.sandboxId,
315
+ // Path is generated per attempt and contains no customer data.
316
+ runtimeCompletedPath: input.runtimeCompletedPath,
317
+ error: error instanceof Error ? error.message : String(error),
318
+ },
319
+ );
320
+ return null;
321
+ }
322
+ }
323
+
324
+ export type DaytonaSandboxDeleteOutcome =
325
+ | {
326
+ kind: 'deleted' | 'already_absent';
327
+ organizationId: string | null;
328
+ }
329
+ | {
330
+ kind: 'timed_out' | 'rate_limited' | 'failed';
331
+ organizationId: string | null;
332
+ code: string;
333
+ detail: string;
334
+ };
335
+
336
+ export async function deleteDaytonaSandboxByIdWithOutcome(input: {
293
337
  sandboxId: string;
294
338
  timeoutSeconds?: number;
295
- }): Promise<boolean> {
339
+ expectedOrganizationId?: string | null;
340
+ allowUnscopedAlreadyAbsent?: boolean;
341
+ }): Promise<DaytonaSandboxDeleteOutcome> {
296
342
  const sandboxId = input.sandboxId?.trim();
297
- if (!sandboxId) return false;
343
+ const expectedOrganizationId = input.expectedOrganizationId?.trim() || null;
344
+ if (!sandboxId) {
345
+ return {
346
+ kind: 'failed',
347
+ organizationId: expectedOrganizationId,
348
+ code: 'invalid_sandbox_id',
349
+ detail: 'Sandbox ID is required.',
350
+ };
351
+ }
298
352
  try {
299
353
  const { clientOptions } = loadDaytonaRequiredConfig();
300
354
  const daytona = daytonaSdkClientFactory.createFull(clientOptions);
301
355
  const sandbox = await daytona.get(sandboxId);
356
+ const observedOrganizationId = sandbox.organizationId?.trim() || null;
357
+ if (
358
+ expectedOrganizationId &&
359
+ observedOrganizationId !== expectedOrganizationId
360
+ ) {
361
+ return {
362
+ kind: 'failed',
363
+ organizationId: observedOrganizationId,
364
+ code: 'wrong_routing_domain',
365
+ detail: 'Sandbox belongs to a different Daytona organization.',
366
+ };
367
+ }
302
368
  await daytona.delete(sandbox, input.timeoutSeconds ?? 30);
303
- return true;
369
+ return {
370
+ kind: 'deleted',
371
+ organizationId: observedOrganizationId,
372
+ };
304
373
  } catch (error) {
374
+ const failure = describeDaytonaLookupFailure(error);
375
+ // Cleanup is an idempotent "ensure absent" operation. Daytona returning
376
+ // not-found means another cleanup owner already satisfied the obligation.
377
+ // The durable expected organization came from the sandbox returned by the
378
+ // same organization-scoped credential at creation time. When an explicit
379
+ // worker organization is configured it must still match that evidence;
380
+ // deployments which rely only on the provider-returned organization retain
381
+ // that durable creation-domain proof. Legacy eager cleanup retains its
382
+ // previous unscoped behavior through the explicit compatibility option.
383
+ if (failure.httpStatus === 404) {
384
+ const configuredOrganizationId =
385
+ process.env.DAYTONA_ORGANIZATION_ID?.trim() || null;
386
+ if (
387
+ !input.allowUnscopedAlreadyAbsent &&
388
+ (!expectedOrganizationId ||
389
+ (configuredOrganizationId &&
390
+ configuredOrganizationId !== expectedOrganizationId))
391
+ ) {
392
+ return {
393
+ kind: 'failed',
394
+ organizationId: configuredOrganizationId,
395
+ code: expectedOrganizationId
396
+ ? 'wrong_routing_domain'
397
+ : 'missing_routing_domain',
398
+ detail:
399
+ 'Daytona returned not-found without an exact creation-domain match.',
400
+ };
401
+ }
402
+ console.info('[play-runner.daytona.reclaim_sandbox_already_absent]', {
403
+ sandboxId,
404
+ });
405
+ return {
406
+ kind: 'already_absent',
407
+ organizationId:
408
+ configuredOrganizationId ?? expectedOrganizationId ?? null,
409
+ };
410
+ }
305
411
  console.warn('[play-runner.daytona.reclaim_sandbox_delete_failed]', {
306
412
  sandboxId,
307
- error: error instanceof Error ? error.message : String(error),
413
+ failure,
308
414
  });
309
- return false;
415
+ const timedOut =
416
+ failure.httpStatus === 408 ||
417
+ /(?:timeout|timed out|ETIMEDOUT)/i.test(
418
+ `${failure.errorCode ?? ''} ${failure.detail}`,
419
+ );
420
+ return {
421
+ kind:
422
+ failure.httpStatus === 429
423
+ ? 'rate_limited'
424
+ : timedOut
425
+ ? 'timed_out'
426
+ : 'failed',
427
+ organizationId: expectedOrganizationId,
428
+ code:
429
+ failure.errorCode ??
430
+ (failure.httpStatus ? `http_${failure.httpStatus}` : 'delete_failed'),
431
+ detail: failure.detail,
432
+ };
310
433
  }
311
434
  }
312
435
 
436
+ export async function deleteDaytonaSandboxById(input: {
437
+ sandboxId: string;
438
+ timeoutSeconds?: number;
439
+ }): Promise<boolean> {
440
+ const outcome = await deleteDaytonaSandboxByIdWithOutcome({
441
+ ...input,
442
+ allowUnscopedAlreadyAbsent: true,
443
+ });
444
+ return outcome.kind === 'deleted' || outcome.kind === 'already_absent';
445
+ }
446
+
313
447
  function formatDaytonaExecutionError(error: unknown): string {
314
448
  const message = formatDaytonaError(error);
315
449
  return isConfiguredDaytonaRuntimeLimit(error)
@@ -630,6 +764,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
630
764
  let cancellationCleanupStarted = false;
631
765
  let activeAcquiredResource: {
632
766
  sandbox: DaytonaSandbox;
767
+ daytonaOrganizationId: string;
633
768
  billingStartedAt: number;
634
769
  billingEndedAt?: number;
635
770
  } | null = null;
@@ -639,6 +774,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
639
774
  const runtimeResourceReportErrors = new Set<unknown>();
640
775
  const reportRuntimeResource = async (acquired: {
641
776
  sandbox: DaytonaSandbox;
777
+ daytonaOrganizationId: string;
642
778
  billingStartedAt: number;
643
779
  billingEndedAt?: number;
644
780
  }) => {
@@ -658,6 +794,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
658
794
  process.env.DEEPLINE_RUNTIME_ENVIRONMENT === 'preview'
659
795
  ? 'preview'
660
796
  : 'production',
797
+ daytonaOrganizationId: acquired.daytonaOrganizationId,
661
798
  billingStartedAt: acquired.billingStartedAt,
662
799
  billingEndedAt,
663
800
  cpu: typeof sandbox.cpu === 'number' ? sandbox.cpu : null,
@@ -673,6 +810,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
673
810
  };
674
811
  const reportRetiringRuntimeResource = async (acquired: {
675
812
  sandbox: DaytonaSandbox;
813
+ daytonaOrganizationId: string;
676
814
  billingStartedAt: number;
677
815
  billingEndedAt?: number;
678
816
  }) => {
@@ -929,6 +1067,7 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
929
1067
  cmdId: start.cmdId,
930
1068
  outputPath: stagedPayload.outputPath,
931
1069
  exitCodePath: stagedPayload.exitCodePath,
1070
+ runtimeCompletedPath: stagedPayload.runtimeCompletedPath,
932
1071
  startedAtMs: Date.now(),
933
1072
  ceilingMs: DAYTONA_DETACHED_CEILING_SECONDS * 1_000,
934
1073
  },
@@ -988,8 +1127,28 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
988
1127
  // Resource persistence belongs to the scheduler control plane. Preserve
989
1128
  // its typed capacity/fence errors so Absurd can defer or fence the
990
1129
  // attempt; converting them into a runner failure would terminally fail a
991
- // play that never began executing customer code.
992
- if (runtimeResourceReportErrors.has(error)) throw error;
1130
+ // play that never began executing customer code. The sandbox exists
1131
+ // before that durable callback can succeed, so synchronously delete it
1132
+ // before returning the control-plane error. The old fire-and-forget
1133
+ // cleanup path was skipped by this rethrow and leaked the acquisition.
1134
+ if (runtimeResourceReportErrors.has(error)) {
1135
+ const sandbox = sandboxCleanup.currentSandbox();
1136
+ if (sandbox) {
1137
+ try {
1138
+ await sandbox.delete(30);
1139
+ console.info(
1140
+ '[play-runner.daytona.resource_report_failure_cleanup_done]',
1141
+ { sandboxId: sandbox.id },
1142
+ );
1143
+ } catch (cleanupError) {
1144
+ throw new AggregateError(
1145
+ [error, cleanupError],
1146
+ `Failed to persist or delete acquired Daytona sandbox ${sandbox.id}.`,
1147
+ );
1148
+ }
1149
+ }
1150
+ throw error;
1151
+ }
993
1152
  emitDaytonaStage(callbacks, config.context, 'execute:error', {
994
1153
  sandboxId: sandboxCleanup.currentSandbox()?.id ?? null,
995
1154
  error: formatDaytonaError(error),
@@ -13,6 +13,8 @@ export type PlayRunnerRuntimeResource = {
13
13
  kind: 'daytona_sandbox';
14
14
  sandboxId: string;
15
15
  daytonaEnvironment?: 'preview' | 'production';
16
+ /** Stable, non-secret provider ownership domain returned by Daytona. */
17
+ daytonaOrganizationId?: string;
16
18
  billingStartedAt: number;
17
19
  billingEndedAt?: number | null;
18
20
  terminalReason?: RuntimeResourceTerminalReason | null;
@@ -39,6 +39,8 @@ export type PlayExecutionSuspension =
39
39
  * timeout-wake salvage verification. */
40
40
  outputPath: string;
41
41
  exitCodePath: string;
42
+ /** Exact customer-code completion fence inside the Daytona sandbox. */
43
+ runtimeCompletedPath?: string;
42
44
  startedAtMs: number;
43
45
  /** Overall run ceiling; the park timeout. */
44
46
  ceilingMs: number;
package/dist/cli/index.js CHANGED
@@ -718,7 +718,7 @@ var SDK_RELEASE = {
718
718
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
719
719
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
720
720
  // Operators use the checkout-local deepline-admin binary instead.
721
- version: "0.1.288",
721
+ version: "0.1.289",
722
722
  contracts: {
723
723
  api: {
724
724
  name: "sdk-http-api",
@@ -703,7 +703,7 @@ var SDK_RELEASE = {
703
703
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
704
704
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
705
705
  // Operators use the checkout-local deepline-admin binary instead.
706
- version: "0.1.288",
706
+ version: "0.1.289",
707
707
  contracts: {
708
708
  api: {
709
709
  name: "sdk-http-api",
package/dist/index.js CHANGED
@@ -438,7 +438,7 @@ var SDK_RELEASE = {
438
438
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
439
439
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
440
440
  // Operators use the checkout-local deepline-admin binary instead.
441
- version: "0.1.288",
441
+ version: "0.1.289",
442
442
  contracts: {
443
443
  api: {
444
444
  name: "sdk-http-api",
package/dist/index.mjs CHANGED
@@ -367,7 +367,7 @@ var SDK_RELEASE = {
367
367
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
368
368
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
369
369
  // Operators use the checkout-local deepline-admin binary instead.
370
- version: "0.1.288",
370
+ version: "0.1.289",
371
371
  contracts: {
372
372
  api: {
373
373
  name: "sdk-http-api",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.1.288",
3
+ "version": "0.1.289",
4
4
  "description": "Deepline SDK + CLI — B2B data enrichment powered by durable cloud execution",
5
5
  "license": "MIT",
6
6
  "repository": {