deepline 0.1.312 → 0.1.314

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.
@@ -198,16 +198,12 @@ export type PlayBindings = {
198
198
  /** Stop the run before a billed action would push total run credits above this cap. */
199
199
  maxCreditsPerRun?: number;
200
200
  };
201
- /** Requested sandbox envelope; the server enforces the final allowed limits. */
201
+ /** Requested prebuilt sandbox and runtime deadline. */
202
202
  runtime?: {
203
203
  /** Duration such as `"90m"` or `"2h"`. */
204
204
  timeout?: string;
205
- /** Memory such as `"4GiB"`. */
206
- memory?: string;
207
- /** Whole vCPU count. */
208
- cpu?: number;
209
- /** Disk such as `"10GiB"`. */
210
- disk?: string;
205
+ /** Deepline-managed prebuilt sandbox size. */
206
+ size?: 'standard';
211
207
  };
212
208
  /** Webhook trigger with optional HMAC signature verification. */
213
209
  webhook?: {
@@ -1204,7 +1200,7 @@ export type DefinePlayConfig<TInput, TOutput extends PlayReturnObject> = {
1204
1200
  bindings?: PlayBindings;
1205
1201
  /** Billing options. */
1206
1202
  billing?: PlayBindings['billing'];
1207
- /** Requested sandbox envelope; the server enforces the final allowed limits. */
1203
+ /** Requested prebuilt sandbox and runtime deadline. */
1208
1204
  runtime?: PlayBindings['runtime'];
1209
1205
  /** Runtime compatibility override. Omit for the current typed contract. */
1210
1206
  compatibility?: PlayBindings['compatibility'];
@@ -1367,7 +1363,7 @@ export type DefinedPlay<TInput, TOutput extends PlayReturnObject> = ((
1367
1363
  DeeplineNamedPlay<TInput, TOutput> & {
1368
1364
  /** Optional trigger bindings (cron, webhook). */
1369
1365
  readonly bindings?: PlayBindings;
1370
- /** Requested sandbox envelope. */
1366
+ /** Requested prebuilt sandbox and runtime deadline. */
1371
1367
  readonly runtime?: PlayBindings['runtime'];
1372
1368
  /** Runtime compatibility explicitly selected by the author. */
1373
1369
  readonly compatibility?: PlayBindings['compatibility'];
@@ -157,7 +157,7 @@ export const SDK_RELEASE = {
157
157
  // 0.1.253 makes play-page browser opening opt-in and retires --no-open.
158
158
  // 0.1.254 removes the internal operations tree from the published SDK CLI.
159
159
  // Operators use the checkout-local deepline-admin binary instead.
160
- version: '0.1.312',
160
+ version: '0.1.314',
161
161
  contracts: {
162
162
  api: {
163
163
  name: 'sdk-http-api',
@@ -1291,6 +1291,31 @@ function createPacingResolver(
1291
1291
  export class PlayContextImpl {
1292
1292
  private rowStates = new Map<number, RowState>();
1293
1293
  private toolCallQueue: ToolCallRequest[] = [];
1294
+ private pendingRuntimeToolOwnershipAssertions: Array<{
1295
+ targets: Array<{ receiptKey: string; leaseId: string }>;
1296
+ resolve: () => void;
1297
+ reject: (error: unknown) => void;
1298
+ }> = [];
1299
+ private runtimeToolOwnershipAssertionFlushScheduled = false;
1300
+ /**
1301
+ * Direct durable boundaries (including ctx.fetch) arrive independently from
1302
+ * concurrent map rows. Hold one microtask's worth so they can use the bulk
1303
+ * receipt gateway instead of serial remote claim requests.
1304
+ */
1305
+ private pendingRuntimeReceiptClaims = new Map<
1306
+ string,
1307
+ {
1308
+ scheduled: boolean;
1309
+ requests: Array<{
1310
+ key: string;
1311
+ reclaimRunning: boolean;
1312
+ forceRefresh: boolean;
1313
+ forceFailedRefresh: boolean;
1314
+ resolve: (receipt: RuntimeStepReceipt | null) => void;
1315
+ reject: (error: unknown) => void;
1316
+ }>;
1317
+ }
1318
+ >();
1294
1319
  /**
1295
1320
  * Fixed, non-resetting coalescing deadlines for ready scheduling lanes. A
1296
1321
  * deadline starts when the first request enters an empty batch bucket. New
@@ -2002,6 +2027,14 @@ export class PlayContextImpl {
2002
2027
  forceRefresh = false,
2003
2028
  forceFailedRefresh = false,
2004
2029
  ): Promise<RuntimeStepReceipt | null> {
2030
+ if (this.#options.claimRuntimeStepReceipts) {
2031
+ return await this.enqueueRuntimeStepReceiptClaim({
2032
+ key,
2033
+ reclaimRunning,
2034
+ forceRefresh,
2035
+ forceFailedRefresh,
2036
+ });
2037
+ }
2005
2038
  if (!this.#options.claimRuntimeStepReceipt) {
2006
2039
  return null;
2007
2040
  }
@@ -2025,6 +2058,73 @@ export class PlayContextImpl {
2025
2058
  };
2026
2059
  }
2027
2060
 
2061
+ private async enqueueRuntimeStepReceiptClaim(input: {
2062
+ key: string;
2063
+ reclaimRunning: boolean;
2064
+ forceRefresh: boolean;
2065
+ forceFailedRefresh: boolean;
2066
+ }): Promise<RuntimeStepReceipt | null> {
2067
+ const batchKey = [
2068
+ input.reclaimRunning ? 'reclaim' : 'ordinary',
2069
+ input.forceRefresh ? 'force' : 'cached',
2070
+ input.forceFailedRefresh ? 'failed-force' : 'failed-cached',
2071
+ ].join(':');
2072
+ let batch = this.pendingRuntimeReceiptClaims.get(batchKey);
2073
+ if (!batch) {
2074
+ batch = { scheduled: false, requests: [] };
2075
+ this.pendingRuntimeReceiptClaims.set(batchKey, batch);
2076
+ }
2077
+ return await new Promise<RuntimeStepReceipt | null>((resolve, reject) => {
2078
+ batch!.requests.push({ ...input, resolve, reject });
2079
+ if (batch!.scheduled) return;
2080
+ batch!.scheduled = true;
2081
+ queueMicrotask(
2082
+ () => void this.flushRuntimeStepReceiptClaimBatch(batchKey),
2083
+ );
2084
+ });
2085
+ }
2086
+
2087
+ private async flushRuntimeStepReceiptClaimBatch(
2088
+ batchKey: string,
2089
+ ): Promise<void> {
2090
+ const batch = this.pendingRuntimeReceiptClaims.get(batchKey);
2091
+ if (!batch) return;
2092
+ this.pendingRuntimeReceiptClaims.delete(batchKey);
2093
+ const requests = batch.requests;
2094
+ const first = requests[0];
2095
+ const claimReceipts = this.#options.claimRuntimeStepReceipts;
2096
+ if (!first || !claimReceipts) {
2097
+ for (const request of requests) request.resolve(null);
2098
+ return;
2099
+ }
2100
+ try {
2101
+ const receipts = await this.dispatchChunkedRuntimeReceiptRequest(
2102
+ requests,
2103
+ (chunk) => {
2104
+ const leaseId = `receipt-lease:${crypto.randomUUID()}`;
2105
+ return claimReceipts({
2106
+ keys: chunk.map((request) => request.key),
2107
+ leaseIds: chunk.map(() => leaseId),
2108
+ runId: this.currentReceiptOwnerRunId,
2109
+ runAttempt: this.currentRunAttempt,
2110
+ leaseAware: true,
2111
+ ...(first.reclaimRunning ? { reclaimRunning: true } : {}),
2112
+ ...(first.forceRefresh ? { forceRefresh: true } : {}),
2113
+ ...(first.forceFailedRefresh ? { forceFailedRefresh: true } : {}),
2114
+ });
2115
+ },
2116
+ );
2117
+ for (let index = 0; index < requests.length; index += 1) {
2118
+ const request = requests[index]!;
2119
+ request.resolve(
2120
+ this.normalizeRuntimeStepReceipt(request.key, receipts[index]),
2121
+ );
2122
+ }
2123
+ } catch (error) {
2124
+ for (const request of requests) request.reject(error);
2125
+ }
2126
+ }
2127
+
2028
2128
  private async completeRuntimeStepReceipt(
2029
2129
  key: string,
2030
2130
  _runId: string,
@@ -2111,59 +2211,148 @@ export class PlayContextImpl {
2111
2211
  private async assertRuntimeToolReceiptOwnership(
2112
2212
  requests: ToolCallRequest[],
2113
2213
  ): Promise<void> {
2114
- const targets = requests.flatMap((request) => {
2214
+ const targets = this.runtimeToolReceiptOwnershipTargets(requests);
2215
+ if (targets.length === 0) return;
2216
+
2217
+ return await new Promise<void>((resolve, reject) => {
2218
+ this.pendingRuntimeToolOwnershipAssertions.push({
2219
+ targets,
2220
+ resolve,
2221
+ reject,
2222
+ });
2223
+ if (this.runtimeToolOwnershipAssertionFlushScheduled) return;
2224
+ this.runtimeToolOwnershipAssertionFlushScheduled = true;
2225
+ queueMicrotask(() => void this.flushRuntimeToolOwnershipAssertionBatch());
2226
+ });
2227
+ }
2228
+
2229
+ private runtimeToolReceiptOwnershipTargets(
2230
+ requests: ToolCallRequest[],
2231
+ ): Array<{ receiptKey: string; leaseId: string }> {
2232
+ return requests.flatMap((request) => {
2115
2233
  const receiptKey = request.receiptKey?.trim() || null;
2116
2234
  if (!receiptKey) return [];
2117
2235
  const leaseId = request.receiptLeaseId?.trim() || null;
2118
2236
  if (!leaseId) return [];
2119
2237
  return [{ receiptKey, leaseId }];
2120
2238
  });
2121
- if (targets.length === 0) return;
2239
+ }
2122
2240
 
2241
+ private async renewRuntimeToolReceiptOwnership(
2242
+ requests: ToolCallRequest[],
2243
+ ): Promise<void> {
2244
+ const targets = this.runtimeToolReceiptOwnershipTargets(requests);
2245
+ if (targets.length === 0) return;
2246
+ if (!this.#options.heartbeatRuntimeStepReceipts) {
2247
+ return await this.assertRuntimeToolReceiptOwnership(requests);
2248
+ }
2123
2249
  const byLeaseId = new Map<string, string[]>();
2124
2250
  for (const target of targets) {
2125
2251
  const keys = byLeaseId.get(target.leaseId) ?? [];
2126
2252
  keys.push(target.receiptKey);
2127
2253
  byLeaseId.set(target.leaseId, keys);
2128
2254
  }
2129
-
2130
- if (this.#options.heartbeatRuntimeStepReceipts) {
2131
- for (const [leaseId, keys] of byLeaseId) {
2132
- const receipts = await this.#options.heartbeatRuntimeStepReceipts({
2255
+ await Promise.all(
2256
+ [...byLeaseId].map(async ([leaseId, keys]) => {
2257
+ const receipts = await this.#options.heartbeatRuntimeStepReceipts!({
2133
2258
  runId: this.currentReceiptOwnerRunId,
2134
2259
  runAttempt: this.currentRunAttempt,
2135
2260
  leaseId,
2136
2261
  keys,
2137
2262
  });
2138
2263
  this.assertRuntimeToolReceiptHeartbeatResult(keys, leaseId, receipts);
2264
+ }),
2265
+ );
2266
+ }
2267
+
2268
+ private async flushRuntimeToolOwnershipAssertionBatch(): Promise<void> {
2269
+ this.runtimeToolOwnershipAssertionFlushScheduled = false;
2270
+ const assertions = this.pendingRuntimeToolOwnershipAssertions.splice(0);
2271
+ if (assertions.length === 0) return;
2272
+ const targets = assertions.flatMap((assertion) => assertion.targets);
2273
+
2274
+ if (this.#options.heartbeatRuntimeStepReceipts) {
2275
+ try {
2276
+ const byLeaseId = new Map<string, string[]>();
2277
+ for (const target of targets) {
2278
+ const keys = byLeaseId.get(target.leaseId) ?? [];
2279
+ keys.push(target.receiptKey);
2280
+ byLeaseId.set(target.leaseId, keys);
2281
+ }
2282
+ const renewed = new Map<string, RuntimeStepReceipt | null>();
2283
+ await Promise.all(
2284
+ [...byLeaseId].map(async ([leaseId, keys]) => {
2285
+ const receipts = await this.#options.heartbeatRuntimeStepReceipts!({
2286
+ runId: this.currentReceiptOwnerRunId,
2287
+ runAttempt: this.currentRunAttempt,
2288
+ leaseId,
2289
+ keys,
2290
+ });
2291
+ for (let index = 0; index < keys.length; index += 1) {
2292
+ renewed.set(keys[index]!, receipts[index] ?? null);
2293
+ }
2294
+ }),
2295
+ );
2296
+ for (const assertion of assertions) {
2297
+ const lost = assertion.targets.find((target) => {
2298
+ const receipt = renewed.get(target.receiptKey);
2299
+ return !this.runtimeToolReceiptStillOwned(receipt, target.leaseId);
2300
+ });
2301
+ if (lost) {
2302
+ assertion.reject(
2303
+ new RuntimeReceiptLeaseLostError({
2304
+ receiptKey: lost.receiptKey,
2305
+ runId: this.currentReceiptOwnerRunId,
2306
+ leaseId: lost.leaseId,
2307
+ }),
2308
+ );
2309
+ } else {
2310
+ assertion.resolve();
2311
+ }
2312
+ }
2313
+ } catch (error) {
2314
+ for (const assertion of assertions) assertion.reject(error);
2139
2315
  }
2140
2316
  return;
2141
2317
  }
2142
2318
 
2143
2319
  if (
2144
- !this.#options.getRuntimeStepReceipt &&
2145
- !this.#options.getRuntimeStepReceipts
2320
+ this.#options.getRuntimeStepReceipt ||
2321
+ this.#options.getRuntimeStepReceipts
2146
2322
  ) {
2147
- throw new RuntimeReceiptLeaseLostError({
2148
- receiptKey: targets[0]?.receiptKey ?? 'unknown',
2149
- runId: this.currentReceiptOwnerRunId,
2150
- leaseId: targets[0]?.leaseId ?? 'unknown',
2151
- });
2152
- }
2153
-
2154
- const latest = await this.getRuntimeStepReceipts(
2155
- targets.map((target) => target.receiptKey),
2156
- );
2157
- for (const target of targets) {
2158
- const receipt = latest.get(target.receiptKey);
2159
- if (!this.runtimeToolReceiptStillOwned(receipt, target.leaseId)) {
2160
- throw new RuntimeReceiptLeaseLostError({
2161
- receiptKey: target.receiptKey,
2162
- runId: this.currentReceiptOwnerRunId,
2163
- leaseId: target.leaseId,
2164
- });
2323
+ try {
2324
+ const latest = await this.getRuntimeStepReceipts(
2325
+ targets.map((target) => target.receiptKey),
2326
+ );
2327
+ for (const assertion of assertions) {
2328
+ const lost = assertion.targets.find((target) => {
2329
+ const receipt = latest.get(target.receiptKey);
2330
+ return !this.runtimeToolReceiptStillOwned(receipt, target.leaseId);
2331
+ });
2332
+ if (lost) {
2333
+ assertion.reject(
2334
+ new RuntimeReceiptLeaseLostError({
2335
+ receiptKey: lost.receiptKey,
2336
+ runId: this.currentReceiptOwnerRunId,
2337
+ leaseId: lost.leaseId,
2338
+ }),
2339
+ );
2340
+ } else {
2341
+ assertion.resolve();
2342
+ }
2343
+ }
2344
+ } catch (error) {
2345
+ for (const assertion of assertions) assertion.reject(error);
2165
2346
  }
2347
+ return;
2166
2348
  }
2349
+
2350
+ const error = new RuntimeReceiptLeaseLostError({
2351
+ receiptKey: targets[0]?.receiptKey ?? 'unknown',
2352
+ runId: this.currentReceiptOwnerRunId,
2353
+ leaseId: targets[0]?.leaseId ?? 'unknown',
2354
+ });
2355
+ for (const assertion of assertions) assertion.reject(error);
2167
2356
  }
2168
2357
 
2169
2358
  private assertRuntimeToolReceiptHeartbeatResult(
@@ -2366,22 +2555,23 @@ export class PlayContextImpl {
2366
2555
  if (uniqueKeys.length === 0) return new Map();
2367
2556
  const claimReceipts = this.#options.claimRuntimeStepReceipts;
2368
2557
  const receipts = claimReceipts
2369
- ? await this.dispatchChunkedRuntimeReceiptRequest(uniqueKeys, (chunk) =>
2370
- claimReceipts({
2558
+ ? await this.dispatchChunkedRuntimeReceiptRequest(uniqueKeys, (chunk) => {
2559
+ const leaseId = `receipt-lease:${crypto.randomUUID()}`;
2560
+ return claimReceipts({
2371
2561
  keys: chunk,
2372
2562
  // A transport retry can replay this mutation after the store
2373
2563
  // committed but before the response body was consumed. Stable
2374
2564
  // caller-owned tokens distinguish that replay from a concurrent
2375
2565
  // claimant without weakening the provider-call execution fence.
2376
- leaseIds: chunk.map(() => `receipt-lease:${crypto.randomUUID()}`),
2566
+ leaseIds: chunk.map(() => leaseId),
2377
2567
  runId: this.currentReceiptOwnerRunId,
2378
2568
  runAttempt: this.currentRunAttempt,
2379
2569
  leaseAware: true,
2380
2570
  ...(reclaimRunning ? { reclaimRunning: true } : {}),
2381
2571
  ...(forceRefresh ? { forceRefresh: true } : {}),
2382
2572
  ...(forceFailedRefresh ? { forceFailedRefresh: true } : {}),
2383
- }),
2384
- )
2573
+ });
2574
+ })
2385
2575
  : await Promise.all(
2386
2576
  uniqueKeys.map((key) =>
2387
2577
  this.claimRuntimeStepReceipt(
@@ -8194,7 +8384,7 @@ export class PlayContextImpl {
8194
8384
  executionAuthScopeDigest: owner.executionAuthScopeDigest,
8195
8385
  receiptLeaseExpiresAt: owner.receiptLeaseExpiresAt,
8196
8386
  heartbeatReceipt: () =>
8197
- this.assertRuntimeToolReceiptOwnership([owner]),
8387
+ this.renewRuntimeToolReceiptOwnership([owner]),
8198
8388
  providerIdempotencyKey:
8199
8389
  this.providerIdempotencyKeyForToolCall({
8200
8390
  cacheKey: owner.cacheKey,
@@ -8471,7 +8661,7 @@ export class PlayContextImpl {
8471
8661
  batch.memberRequests,
8472
8662
  ),
8473
8663
  heartbeatReceipt: () =>
8474
- this.assertRuntimeToolReceiptOwnership(
8664
+ this.renewRuntimeToolReceiptOwnership(
8475
8665
  batch.memberRequests,
8476
8666
  ),
8477
8667
  },
@@ -8634,7 +8824,7 @@ export class PlayContextImpl {
8634
8824
  }),
8635
8825
  receiptLeaseExpiresAt: request.receiptLeaseExpiresAt,
8636
8826
  heartbeatReceipt: () =>
8637
- this.assertRuntimeToolReceiptOwnership([request]),
8827
+ this.renewRuntimeToolReceiptOwnership([request]),
8638
8828
  }
8639
8829
  : {}),
8640
8830
  timeoutMs: resolveRuntimeTimeoutMsForClaimedOwners([
@@ -1,11 +1,14 @@
1
- import type { Daytona } from '@daytonaio/sdk';
1
+ import { type Daytona } from '@daytonaio/sdk';
2
2
  import type {
3
3
  PlayRunnerExecutionConfig,
4
4
  PlayRunnerResult,
5
5
  } from '@shared_libs/play-runtime/protocol';
6
6
  import { isIsolatedRuntimeSchedulerSchema } from '@shared_libs/play-runtime/runtime-scheduler-topology';
7
7
  import { PLAY_RUNNER_TIMEOUT_SECONDS } from '@shared_libs/play-runtime/runtime-constants';
8
- import { resolvePlaySandboxRuntimeLimits } from '@shared_libs/play-runtime/sandbox-runtime-limits';
8
+ import {
9
+ STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS,
10
+ validatePlaySandboxRuntimeLimits,
11
+ } from '@shared_libs/play-runtime/sandbox-runtime-limits';
9
12
 
10
13
  const DAYTONA_CREATE_TIMEOUT_SECONDS = 10;
11
14
  const DAYTONA_CREATE_RETRY_DELAYS_MS = [0, 500, 1_500] as const;
@@ -14,11 +17,13 @@ const DAYTONA_CREATE_RETRY_DELAYS_MS = [0, 500, 1_500] as const;
14
17
  // setup time cannot consume the terminal-flush grace.
15
18
  const DAYTONA_AUTO_ARCHIVE_INTERVAL_MINUTES = 15;
16
19
  const DAYTONA_SANDBOX_LABEL_SOURCE = 'deepline-play-runner';
17
- // Daytona's default image is the fast path. Non-standard resources are applied
18
- // during acquisition, before customer code and before the billing clock starts.
19
- export const DAYTONA_SANDBOX_CPU = 1;
20
- export const DAYTONA_SANDBOX_MEMORY_GIB = 1;
21
- export const DAYTONA_SANDBOX_DISK_GIB = 3;
20
+ // Daytona's Deepline-managed default snapshot is the only admitted prebuilt.
21
+ // Resources are verified during acquisition, before customer code or billing.
22
+ export const DAYTONA_SANDBOX_CPU = STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS.cpu;
23
+ export const DAYTONA_SANDBOX_MEMORY_GIB =
24
+ STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS.memoryGiB;
25
+ export const DAYTONA_SANDBOX_DISK_GIB =
26
+ STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS.diskGiB;
22
27
  export const DAYTONA_SANDBOX_GPU = 0;
23
28
  const DAYTONA_NETWORK_ALLOW_LIST_ENV = 'DEEPLINE_DAYTONA_NETWORK_ALLOW_LIST';
24
29
 
@@ -173,15 +178,10 @@ async function createOneShotDaytonaSandbox(input: {
173
178
  orgId: string;
174
179
  context: DaytonaExecutionContext;
175
180
  }): Promise<DaytonaSandbox> {
176
- const limits = resolvePlaySandboxRuntimeLimits(
177
- input.context.sandboxRuntimeLimits
178
- ? {
179
- timeout: `${input.context.sandboxRuntimeLimits.timeoutSeconds / 60}m`,
180
- memory: `${input.context.sandboxRuntimeLimits.memoryGiB}GiB`,
181
- cpu: input.context.sandboxRuntimeLimits.cpu,
182
- disk: `${input.context.sandboxRuntimeLimits.diskGiB}GiB`,
183
- }
184
- : null,
181
+ const limits = validatePlaySandboxRuntimeLimits(
182
+ input.context.sandboxRuntimeLimits ?? {
183
+ ...STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS,
184
+ },
185
185
  );
186
186
  const orgId = normalizeLabelValue(input.orgId);
187
187
  const workflowId = normalizeLabelValue(input.context.workflowId);
@@ -204,26 +204,24 @@ async function createOneShotDaytonaSandbox(input: {
204
204
  runtimeSchedulerSchema: input.context.runtimeSchedulerSchema ?? null,
205
205
  });
206
206
 
207
- // Intentionally omit image/snapshot so Daytona uses its default fast sandbox
208
- // image; custom images would add build/pull tax to one-shot cold starts.
209
- return input.daytona.create(
210
- {
211
- labels,
212
- ephemeral: true,
213
- autoStopInterval: Math.ceil(
214
- (limits.timeoutSeconds + PLAY_RUNNER_TIMEOUT_SECONDS - 30 * 60) / 60,
215
- ),
216
- autoArchiveInterval: DAYTONA_AUTO_ARCHIVE_INTERVAL_MINUTES,
217
- // A non-empty `networkAllowList` IS the "block all egress except these
218
- // CIDRs" control; Daytona rejects create when `networkBlockAll: true` is
219
- // combined with a non-empty allow-list ("networkBlockAll: true cannot be
220
- // combined with a non-empty networkAllowList or domainAllowList"). Pass
221
- // the allow-list alone so the egress restriction holds without the
222
- // contradictory flag.
223
- ...(networkAllowList ? { networkAllowList } : {}),
224
- },
225
- { timeout: DAYTONA_CREATE_TIMEOUT_SECONDS },
226
- );
207
+ const commonParams = {
208
+ labels,
209
+ ephemeral: true,
210
+ autoStopInterval: Math.ceil(
211
+ (limits.timeoutSeconds + PLAY_RUNNER_TIMEOUT_SECONDS - 30 * 60) / 60,
212
+ ),
213
+ autoArchiveInterval: DAYTONA_AUTO_ARCHIVE_INTERVAL_MINUTES,
214
+ // A non-empty `networkAllowList` IS the "block all egress except these
215
+ // CIDRs" control; Daytona rejects create when `networkBlockAll: true` is
216
+ // combined with a non-empty allow-list ("networkBlockAll: true cannot be
217
+ // combined with a non-empty networkAllowList or domainAllowList"). Pass
218
+ // the allow-list alone so the egress restriction holds without the
219
+ // contradictory flag.
220
+ ...(networkAllowList ? { networkAllowList } : {}),
221
+ };
222
+ return input.daytona.create(commonParams, {
223
+ timeout: DAYTONA_CREATE_TIMEOUT_SECONDS,
224
+ });
227
225
  }
228
226
 
229
227
  async function createRetriedOneShotDaytonaSandbox(input: {
@@ -286,15 +284,10 @@ async function acquireOneShotDaytonaSandbox(input: {
286
284
  emitStage: DaytonaStageEmitter;
287
285
  startedAt: number;
288
286
  }): Promise<AcquiredDaytonaSandbox> {
289
- const limits = resolvePlaySandboxRuntimeLimits(
290
- input.context.sandboxRuntimeLimits
291
- ? {
292
- timeout: `${input.context.sandboxRuntimeLimits.timeoutSeconds / 60}m`,
293
- memory: `${input.context.sandboxRuntimeLimits.memoryGiB}GiB`,
294
- cpu: input.context.sandboxRuntimeLimits.cpu,
295
- disk: `${input.context.sandboxRuntimeLimits.diskGiB}GiB`,
296
- }
297
- : null,
287
+ const limits = validatePlaySandboxRuntimeLimits(
288
+ input.context.sandboxRuntimeLimits ?? {
289
+ ...STANDARD_PLAY_SANDBOX_RUNTIME_LIMITS,
290
+ },
298
291
  );
299
292
  input.emitStage('create:start');
300
293
  const result = await createRetriedOneShotDaytonaSandbox(input);
@@ -304,42 +297,6 @@ async function acquireOneShotDaytonaSandbox(input: {
304
297
  diskGiB: result.sandbox.disk,
305
298
  gpu: result.sandbox.gpu ?? 0,
306
299
  };
307
- // Daytona's SDK only accepts resources when a custom image is supplied.
308
- // We deliberately retain the provider's fast default image, then resize
309
- // before any customer code or billing window begins.
310
- if (
311
- typeof result.sandbox.resize === 'function' &&
312
- (granted.cpu !== limits.cpu ||
313
- granted.memoryGiB !== limits.memoryGiB ||
314
- granted.diskGiB !== limits.diskGiB)
315
- ) {
316
- try {
317
- if (granted.diskGiB !== limits.diskGiB) {
318
- await result.sandbox.stop(60);
319
- await result.sandbox.resize({
320
- cpu: limits.cpu,
321
- memory: limits.memoryGiB,
322
- disk: limits.diskGiB,
323
- });
324
- await result.sandbox.start(60);
325
- } else {
326
- await result.sandbox.resize({
327
- cpu: limits.cpu,
328
- memory: limits.memoryGiB,
329
- disk: limits.diskGiB,
330
- });
331
- }
332
- } catch (error) {
333
- const message = error instanceof Error ? error.message : String(error);
334
- return await rejectAcquiredSandbox(
335
- result.sandbox,
336
- `Daytona sandbox resize failed before execution: ${message}.`,
337
- );
338
- }
339
- granted.cpu = result.sandbox.cpu;
340
- granted.memoryGiB = result.sandbox.memory;
341
- granted.diskGiB = result.sandbox.disk;
342
- }
343
300
  if (
344
301
  granted.cpu !== limits.cpu ||
345
302
  granted.memoryGiB !== limits.memoryGiB ||