deepline 0.1.249 → 0.1.251

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.
@@ -80,6 +80,13 @@ import type {
80
80
  import type { PlayStagedFileRef } from './plays/local-file-discovery.js';
81
81
  import type { PlayCompilerManifest } from '../../shared_libs/plays/compiler-manifest.js';
82
82
  import type { EnrichCompiledConfig } from './cli/enrich-play-compiler.js';
83
+ import { RUNTIME_ENVIRONMENT_TOKEN_HEADER } from '../../shared_libs/play-runtime/coordinator-headers.js';
84
+ import {
85
+ normalizePlayRuntimeEnvironment,
86
+ normalizePlayRuntimeNamespace,
87
+ normalizePlayRuntimeSelection,
88
+ type PlayRuntimeSelection,
89
+ } from '../../shared_libs/play-runtime/runtime-environment.js';
83
90
 
84
91
  const TERMINAL_PLAY_STATUSES = new Set(['completed', 'failed', 'cancelled']);
85
92
  const INCLUDE_TOOL_METADATA_HEADER = 'x-deepline-include-tool-metadata';
@@ -109,6 +116,75 @@ function resolvePlayRunIntegrationMode(
109
116
  );
110
117
  }
111
118
 
119
+ function resolvePlayRunRuntimeSelection(
120
+ request: StartPlayRunRequest,
121
+ ): PlayRuntimeSelection | undefined {
122
+ if (request.runtime !== undefined) {
123
+ const runtime = normalizePlayRuntimeSelection(request.runtime);
124
+ if (!runtime) {
125
+ throw new DeeplineError(
126
+ 'runtime must be exactly { environment: "preview", namespace } with namespace matching ^[a-z][a-z0-9-]{0,30}$.',
127
+ undefined,
128
+ 'INVALID_RUNTIME_SELECTION',
129
+ );
130
+ }
131
+ return runtime;
132
+ }
133
+ // These variables select where the app executes a run, not which app the
134
+ // SDK calls. DEEPLINE_API_BASE_URL still chooses the app host. Environment
135
+ // opts into remote preview routing, namespace names the isolated lane, and
136
+ // the token only authorizes that selection. Omitting all three leaves the
137
+ // app free to use its native runtime (local for a local app, prod for prod).
138
+ const configured = process.env.DEEPLINE_RUNTIME_ENVIRONMENT?.trim();
139
+ const configuredNamespace = process.env.DEEPLINE_RUNTIME_NAMESPACE?.trim();
140
+ const configuredToken =
141
+ process.env.DEEPLINE_RUNTIME_ENVIRONMENT_TOKEN?.trim();
142
+ if (!configured) {
143
+ if (configuredNamespace || configuredToken) {
144
+ throw new DeeplineError(
145
+ 'DEEPLINE_RUNTIME_ENVIRONMENT=preview and DEEPLINE_RUNTIME_NAMESPACE are required when runtime selection configuration is present.',
146
+ undefined,
147
+ 'INVALID_RUNTIME_ENVIRONMENT',
148
+ );
149
+ }
150
+ return undefined;
151
+ }
152
+ const environment = normalizePlayRuntimeEnvironment(configured);
153
+ if (!environment) {
154
+ throw new DeeplineError(
155
+ `DEEPLINE_RUNTIME_ENVIRONMENT supports only explicit preview selection. Received "${configured}". Omit it to use the app-native runtime.`,
156
+ undefined,
157
+ 'INVALID_RUNTIME_ENVIRONMENT',
158
+ );
159
+ }
160
+ const namespace = normalizePlayRuntimeNamespace(configuredNamespace);
161
+ if (!namespace) {
162
+ throw new DeeplineError(
163
+ 'DEEPLINE_RUNTIME_NAMESPACE is required for preview and must match ^[a-z][a-z0-9-]{0,30}$.',
164
+ undefined,
165
+ 'INVALID_RUNTIME_NAMESPACE',
166
+ );
167
+ }
168
+ return { environment, namespace };
169
+ }
170
+
171
+ function runtimeSelectionHeaders(
172
+ runtime: PlayRuntimeSelection | undefined,
173
+ ): Record<string, string> | undefined {
174
+ if (!runtime) return undefined;
175
+ // Keep the credential out of the typed runtime payload. The app consumes it
176
+ // at the HTTP boundary and never forwards it to Fly, Absurd, or Daytona.
177
+ const token = process.env.DEEPLINE_RUNTIME_ENVIRONMENT_TOKEN?.trim();
178
+ if (!token) {
179
+ throw new DeeplineError(
180
+ 'DEEPLINE_RUNTIME_ENVIRONMENT_TOKEN is required for explicit preview runtime selection.',
181
+ undefined,
182
+ 'RUNTIME_ENVIRONMENT_TOKEN_REQUIRED',
183
+ );
184
+ }
185
+ return { [RUNTIME_ENVIRONMENT_TOKEN_HEADER]: token };
186
+ }
187
+
112
188
  function normalizeTestPolicyOverrides(
113
189
  value: unknown,
114
190
  source: string,
@@ -1577,6 +1653,7 @@ export class DeeplineClient {
1577
1653
  const testPolicyOverrides =
1578
1654
  request.testPolicyOverrides ?? parseEnvTestPolicyOverrides();
1579
1655
  const forceToolRefresh = request.forceToolRefresh === true;
1656
+ const runtime = resolvePlayRunRuntimeSelection(request);
1580
1657
  const response = await this.http.post<Record<string, unknown>>(
1581
1658
  '/api/v2/plays/run',
1582
1659
  {
@@ -1619,9 +1696,10 @@ export class DeeplineClient {
1619
1696
  // defaults to absurd; callers normally omit this field.
1620
1697
  ...(request.profile ? { profile: request.profile } : {}),
1621
1698
  ...(integrationMode ? { integrationMode } : {}),
1699
+ ...(runtime ? { runtime } : {}),
1622
1700
  ...(testPolicyOverrides ? { testPolicyOverrides } : {}),
1623
1701
  },
1624
- undefined,
1702
+ runtimeSelectionHeaders(runtime),
1625
1703
  // A start can reach the server even when its response is lost or times
1626
1704
  // out. Retrying this mutation without an idempotency key can create a
1627
1705
  // second root run, so callers must resolve ambiguous failures explicitly.
@@ -1649,6 +1727,7 @@ export class DeeplineClient {
1649
1727
  const testPolicyOverrides =
1650
1728
  request.testPolicyOverrides ?? parseEnvTestPolicyOverrides();
1651
1729
  const forceToolRefresh = request.forceToolRefresh === true;
1730
+ const runtime = resolvePlayRunRuntimeSelection(request);
1652
1731
  const body = {
1653
1732
  ...(request.name ? { name: request.name } : {}),
1654
1733
  ...(request.revisionId ? { revisionId: request.revisionId } : {}),
@@ -1687,6 +1766,7 @@ export class DeeplineClient {
1687
1766
  : {}),
1688
1767
  ...(request.profile ? { profile: request.profile } : {}),
1689
1768
  ...(integrationMode ? { integrationMode } : {}),
1769
+ ...(runtime ? { runtime } : {}),
1690
1770
  ...(testPolicyOverrides ? { testPolicyOverrides } : {}),
1691
1771
  };
1692
1772
  for await (const event of this.http.streamSse<PlayLiveEvent>(
@@ -1694,6 +1774,7 @@ export class DeeplineClient {
1694
1774
  {
1695
1775
  method: 'POST',
1696
1776
  body,
1777
+ headers: runtimeSelectionHeaders(runtime),
1697
1778
  signal: options?.signal,
1698
1779
  },
1699
1780
  )) {
@@ -113,7 +113,7 @@ export const SDK_RELEASE = {
113
113
  // runtime intentionally no longer compiles at publish or launch time.
114
114
  // 0.1.242 removes the retired Workers/ESM compiler, generated bundles, and
115
115
  // deploy-time artifact migration. Play authoring now has one CJS contract.
116
- version: '0.1.249',
116
+ version: '0.1.251',
117
117
  apiContract: '2026-07-cjs-absurd-only-play-runtime-hard-cutover',
118
118
  supportPolicy: {
119
119
  minimumSupported: '0.1.53',
@@ -1,4 +1,5 @@
1
1
  import type { PlayCompilerManifest } from '../../shared_libs/plays/compiler-manifest';
2
+ import type { PlayRuntimeSelection } from '../../shared_libs/play-runtime/runtime-environment';
2
3
  import type {
3
4
  DeeplineToolCategory,
4
5
  PlayBootstrapFinderKind,
@@ -1226,6 +1227,8 @@ export interface StartPlayRunRequest {
1226
1227
  profile?: string;
1227
1228
  /** Optional per-run provider execution mode for eval/smoke runs. */
1228
1229
  integrationMode?: 'live' | 'eval_stub' | 'fixture';
1230
+ /** Internal runtime estate selection. The app host remains unchanged. */
1231
+ runtime?: PlayRuntimeSelection;
1229
1232
  /** Internal/dev-only runtime policy overrides for black-box durability tests. */
1230
1233
  testPolicyOverrides?: Record<string, unknown>;
1231
1234
  }
@@ -1542,13 +1542,17 @@ export class PlayContextImpl {
1542
1542
  this.#options.workflowId &&
1543
1543
  this.#options.runId
1544
1544
  ) {
1545
- const response = await fetch(
1546
- `${this.#options.baseUrl.replace(/\/$/, '')}${PLAY_RUNTIME_API_COMPAT_PATH}`,
1547
- {
1545
+ const url = `${this.#options.baseUrl.replace(/\/$/, '')}${PLAY_RUNTIME_API_COMPAT_PATH}`;
1546
+ const requestId = `ctx-secret-${crypto.randomUUID()}`;
1547
+ const startedAt = Date.now();
1548
+ let response: Response;
1549
+ try {
1550
+ response = await fetch(url, {
1548
1551
  method: 'POST',
1549
1552
  headers: {
1550
1553
  Authorization: `Bearer ${this.#options.executorToken}`,
1551
1554
  'Content-Type': 'application/json',
1555
+ 'x-deepline-request-id': requestId,
1552
1556
  ...(await this.vercelProtectionHeaders()),
1553
1557
  },
1554
1558
  body: JSON.stringify({
@@ -1556,11 +1560,89 @@ export class PlayContextImpl {
1556
1560
  name: auth.secret.name,
1557
1561
  playName: this.#options.playName,
1558
1562
  }),
1559
- },
1560
- );
1561
- if (response.ok) {
1562
- const payload = (await response.json()) as { value?: unknown };
1563
- value = typeof payload.value === 'string' ? payload.value : null;
1563
+ });
1564
+ } catch (error) {
1565
+ const diagnostic = describeTransportError(error);
1566
+ this.log(
1567
+ `[runtime.secret_resolution_failure] ${JSON.stringify({
1568
+ stage: 'transport',
1569
+ secret_name: auth.secret.name,
1570
+ gateway_origin: transportGatewayOriginForDiagnostic(url),
1571
+ request_id: requestId,
1572
+ elapsed_ms: Date.now() - startedAt,
1573
+ error: diagnostic,
1574
+ })}`,
1575
+ );
1576
+ throw new Error(
1577
+ `Secret ${auth.secret.name} resolution transport failed (request_id=${requestId}): ${diagnostic.message ?? 'unknown transport error'}`,
1578
+ );
1579
+ }
1580
+
1581
+ const responseDiagnostic = {
1582
+ stage: response.ok ? 'invalid_response' : 'http_response',
1583
+ secret_name: auth.secret.name,
1584
+ gateway_origin: transportGatewayOriginForDiagnostic(url),
1585
+ request_id: requestId,
1586
+ elapsed_ms: Date.now() - startedAt,
1587
+ http_status: response.status,
1588
+ status_text: response.statusText || null,
1589
+ content_type: response.headers.get('content-type'),
1590
+ server: response.headers.get('server'),
1591
+ vercel_request_id:
1592
+ response.headers.get('x-vercel-id') ??
1593
+ response.headers.get('x-vercel-request-id'),
1594
+ response_request_id: response.headers.get('x-deepline-request-id'),
1595
+ error_code: response.headers.get('x-deepline-error-code'),
1596
+ retry_after: response.headers.get('retry-after'),
1597
+ };
1598
+ if (!response.ok) {
1599
+ this.log(
1600
+ `[runtime.secret_resolution_failure] ${JSON.stringify(responseDiagnostic)}`,
1601
+ );
1602
+ throw new Error(
1603
+ `Secret ${auth.secret.name} is not available to this run (resolution status=${response.status}, request_id=${requestId}).`,
1604
+ );
1605
+ }
1606
+
1607
+ let payload: unknown;
1608
+ try {
1609
+ payload = await response.json();
1610
+ } catch (error) {
1611
+ this.log(
1612
+ `[runtime.secret_resolution_failure] ${JSON.stringify({
1613
+ ...responseDiagnostic,
1614
+ response_kind: 'invalid_json',
1615
+ parse_error_name:
1616
+ error instanceof Error ? error.name : typeof error,
1617
+ })}`,
1618
+ );
1619
+ throw new Error(
1620
+ `Secret ${auth.secret.name} is not available to this run (invalid resolution response, request_id=${requestId}).`,
1621
+ );
1622
+ }
1623
+
1624
+ value =
1625
+ payload &&
1626
+ typeof payload === 'object' &&
1627
+ !Array.isArray(payload) &&
1628
+ typeof (payload as { value?: unknown }).value === 'string'
1629
+ ? (payload as { value: string }).value
1630
+ : null;
1631
+ if (!value) {
1632
+ this.log(
1633
+ `[runtime.secret_resolution_failure] ${JSON.stringify({
1634
+ ...responseDiagnostic,
1635
+ response_kind:
1636
+ payload === null
1637
+ ? 'null'
1638
+ : Array.isArray(payload)
1639
+ ? 'array'
1640
+ : typeof payload,
1641
+ })}`,
1642
+ );
1643
+ throw new Error(
1644
+ `Secret ${auth.secret.name} is not available to this run (missing resolution value, request_id=${requestId}).`,
1645
+ );
1564
1646
  }
1565
1647
  } else {
1566
1648
  throw new Error(
@@ -6997,7 +7079,9 @@ export class PlayContextImpl {
6997
7079
  Object.fromEntries(response.headers.entries()),
6998
7080
  ) as Record<string, string>,
6999
7081
  bodyText: redactedBodyText,
7000
- json: this.secretRedactor.redact(parseJsonOrNull(bodyText)),
7082
+ json: this.secretRedactor.redactKnownSecrets(
7083
+ parseJsonOrNull(bodyText),
7084
+ ),
7001
7085
  };
7002
7086
 
7003
7087
  this.checkpoint.resolvedBoundaries = {
@@ -28,6 +28,13 @@ export const WORKER_CALLBACK_URL_OVERRIDE_HEADER =
28
28
  'x-deepline-worker-callback-url';
29
29
  export const RUNTIME_SCHEDULER_SCHEMA_OVERRIDE_HEADER =
30
30
  'x-deepline-runtime-scheduler-schema';
31
+ /**
32
+ * CLI-to-app credential used only to authorize a typed runtime-environment
33
+ * selection. It is deliberately separate from the coordinator credential:
34
+ * selecting an approved environment must not grant access to runtime callbacks.
35
+ */
36
+ export const RUNTIME_ENVIRONMENT_TOKEN_HEADER =
37
+ 'x-deepline-runtime-environment-token';
31
38
  /**
32
39
  * Internal top-level `profile=absurd` launches, including deploy canaries, use
33
40
  * this header to pin an exact candidate release before activation. Public API
@@ -27,10 +27,17 @@ export function daytonaClientOptions(apiKey: string): DaytonaClientOptions {
27
27
  };
28
28
  }
29
29
 
30
- export function loadDaytonaRequiredConfig(): DaytonaRequiredConfig {
31
- const apiKey = process.env.DAYTONA_API_KEY?.trim();
30
+ export function loadDaytonaRequiredConfig(
31
+ options: {
32
+ environment?: 'preview' | 'production' | null;
33
+ } = {},
34
+ ): DaytonaRequiredConfig {
35
+ const environment = options.environment ?? null;
36
+ const keyName =
37
+ environment === 'preview' ? 'DAYTONA_API_KEY_PREVIEW' : 'DAYTONA_API_KEY';
38
+ const apiKey = process.env[keyName]?.trim();
32
39
  if (!apiKey) {
33
- throw new Error('Missing required Daytona configuration: DAYTONA_API_KEY.');
40
+ throw new Error(`Missing required Daytona configuration: ${keyName}.`);
34
41
  }
35
42
  return {
36
43
  apiKey,
@@ -53,6 +53,13 @@ const RUNTIME_RECEIPT_GATEWAY_TRANSPORT_RETRY_PATTERN =
53
53
  /AppRuntimeApiTransportError: App runtime API transport exhausted action=(?:get|claim|mark|complete|fail|heartbeat|release|skip)_runtime_step_receipts?/i;
54
54
  const RUNTIME_API_TRANSPORT_RETRY_PATTERN =
55
55
  /\bRuntime API request to .+ failed before receiving a response:\s*(?:fetch failed|connection (?:terminated|timed out|closed|reset)|ECONNRESET|ETIMEDOUT|ECONNREFUSED)\b/i;
56
+ // The ready fence precedes customer play execution. A failed detached start is
57
+ // safe to retry only when the runner never wrote its ready marker or attempted
58
+ // the start acknowledgement, and Daytona has no exit evidence. This covers a
59
+ // transient Daytona control-plane blind spot where the accepted command cannot
60
+ // be inspected, while keeping post-start and crash failures terminal.
61
+ const RUNTIME_SANDBOX_UNSTARTED_RETRY_PATTERN =
62
+ /DaytonaRunnerInitializationError:\s*RUNTIME_SANDBOX_START_FAILED[\s\S]*Marker diagnosis:\s*ready_marker_unavailable[\s\S]*"readyMarkerObserved"\s*:\s*false[\s\S]*"startAcknowledgementAttempted"\s*:\s*false[\s\S]*"exitCode"\s*:\s*null[\s\S]*"exitCodeFile"\s*:\s*null/i;
56
63
  const DAYTONA_INFRASTRUCTURE_RETRY_PATTERN =
57
64
  /\b(?:Request failed with status code (?:408|429|500|502|503|504)|ECONNRESET|ECONNREFUSED|ETIMEDOUT|UND_ERR_CONNECT_TIMEOUT|fetch failed|socket hang up|Daytona sandbox create failed across|Daytona sandbox create did not complete)\b/i;
58
65
 
@@ -446,6 +453,7 @@ export function retryableRuntimeInitializationFailureReason(
446
453
  | 'runtime_postgres_connect'
447
454
  | 'runtime_api_transport'
448
455
  | 'runtime_receipt_gateway_transport'
456
+ | 'runtime_sandbox_unstarted'
449
457
  | null {
450
458
  if (result.status !== 'failed') return null;
451
459
  if (RUNTIME_POSTGRES_CONNECT_RETRY_PATTERN.test(result.error)) {
@@ -455,6 +463,13 @@ export function retryableRuntimeInitializationFailureReason(
455
463
  return 'runtime_receipt_gateway_transport';
456
464
  }
457
465
  const rowsProcessed = Number(result.stats?.rowsProcessed ?? 0);
466
+ if (
467
+ rowsProcessed === 0 &&
468
+ result.steps.length === 0 &&
469
+ RUNTIME_SANDBOX_UNSTARTED_RETRY_PATTERN.test(result.error)
470
+ ) {
471
+ return 'runtime_sandbox_unstarted';
472
+ }
458
473
  if (
459
474
  rowsProcessed === 0 &&
460
475
  result.steps.length === 0 &&
@@ -624,6 +639,10 @@ export const daytonaPlayRunnerBackend: PlayRunnerBackend = {
624
639
  await callbacks?.onRuntimeResourceAcquired?.({
625
640
  kind: 'daytona_sandbox',
626
641
  sandboxId: sandbox.id,
642
+ daytonaEnvironment:
643
+ process.env.DEEPLINE_RUNTIME_ENVIRONMENT === 'preview'
644
+ ? 'preview'
645
+ : 'production',
627
646
  billingStartedAt: acquired.billingStartedAt,
628
647
  billingEndedAt,
629
648
  cpu: typeof sandbox.cpu === 'number' ? sandbox.cpu : null,
@@ -12,6 +12,7 @@ import type {
12
12
  export type PlayRunnerRuntimeResource = {
13
13
  kind: 'daytona_sandbox';
14
14
  sandboxId: string;
15
+ daytonaEnvironment?: 'preview' | 'production';
15
16
  billingStartedAt: number;
16
17
  billingEndedAt?: number | null;
17
18
  terminalReason?: RuntimeResourceTerminalReason | null;
@@ -0,0 +1,44 @@
1
+ export const PLAY_RUNTIME_ENVIRONMENTS = ['preview'] as const;
2
+
3
+ export type PlayRuntimeEnvironment = (typeof PLAY_RUNTIME_ENVIRONMENTS)[number];
4
+
5
+ export type PlayRuntimeSelection = {
6
+ environment: 'preview';
7
+ /** Caller-named isolation scope inside a remote runtime environment. */
8
+ namespace: string;
9
+ };
10
+
11
+ const PLAY_RUNTIME_NAMESPACE_PATTERN = /^[a-z][a-z0-9-]{0,30}$/;
12
+
13
+ export function normalizePlayRuntimeNamespace(value: unknown): string | null {
14
+ if (typeof value !== 'string') return null;
15
+ const normalized = value.trim();
16
+ return PLAY_RUNTIME_NAMESPACE_PATTERN.test(normalized) ? normalized : null;
17
+ }
18
+
19
+ /** Validate the selector as one value so the SDK and server cannot drift. */
20
+ export function normalizePlayRuntimeSelection(
21
+ value: unknown,
22
+ ): PlayRuntimeSelection | null {
23
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
24
+ const record = value as Record<string, unknown>;
25
+ if (
26
+ Object.keys(record).some(
27
+ (key) => key !== 'environment' && key !== 'namespace',
28
+ ) ||
29
+ record.environment !== 'preview'
30
+ ) {
31
+ return null;
32
+ }
33
+ const namespace = normalizePlayRuntimeNamespace(record.namespace);
34
+ return namespace ? { environment: 'preview', namespace } : null;
35
+ }
36
+
37
+ export function normalizePlayRuntimeEnvironment(
38
+ value: unknown,
39
+ ): PlayRuntimeEnvironment | null {
40
+ return typeof value === 'string' &&
41
+ PLAY_RUNTIME_ENVIRONMENTS.includes(value as PlayRuntimeEnvironment)
42
+ ? (value as PlayRuntimeEnvironment)
43
+ : null;
44
+ }
@@ -76,6 +76,7 @@ export function redactSecretLikeString(value: string): string {
76
76
  export type SecretRedactionContext = {
77
77
  register(value: string): void;
78
78
  redactString(value: string): string;
79
+ redactKnownSecrets<T>(value: T): T;
79
80
  redact<T>(value: T): T;
80
81
  };
81
82
 
@@ -129,9 +130,26 @@ export function createSecretRedactionContext(
129
130
  return value;
130
131
  }
131
132
 
133
+ function redactKnownSecrets<T>(value: T): T {
134
+ if (typeof value === 'string') return redactString(value) as T;
135
+ if (Array.isArray(value)) {
136
+ return value.map((entry) => redactKnownSecrets(entry)) as T;
137
+ }
138
+ if (value && typeof value === 'object') {
139
+ return Object.fromEntries(
140
+ Object.entries(value).map(([key, entry]) => [
141
+ key,
142
+ redactKnownSecrets(entry),
143
+ ]),
144
+ ) as T;
145
+ }
146
+ return value;
147
+ }
148
+
132
149
  return {
133
150
  register,
134
151
  redactString,
152
+ redactKnownSecrets,
135
153
  redact,
136
154
  };
137
155
  }