withruntime 0.9.0 → 0.11.0

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.
Files changed (124) hide show
  1. package/BLAXEL.md +4 -4
  2. package/DAYTONA.md +52 -40
  3. package/E2B.md +65 -62
  4. package/README.md +15 -7
  5. package/README.src.md +15 -7
  6. package/VERCEL.md +34 -22
  7. package/compatibility-lock.json +16 -16
  8. package/dist/blaxel/errors.js +1 -1
  9. package/dist/blaxel/lookup.d.ts +3 -2
  10. package/dist/blaxel/lookup.js +3 -2
  11. package/dist/blaxel/pagination.d.ts +2 -2
  12. package/dist/blaxel/pagination.js +45 -17
  13. package/dist/blaxel/preview.d.ts +2 -1
  14. package/dist/blaxel/preview.js +15 -13
  15. package/dist/blaxel/process.js +56 -10
  16. package/dist/blaxel/sandbox.d.ts +2 -0
  17. package/dist/blaxel/sandbox.js +2 -0
  18. package/dist/blaxel/snapshot.js +3 -5
  19. package/dist/cli.d.ts +2 -0
  20. package/dist/cli.js +262 -56
  21. package/dist/client.d.ts +41 -3
  22. package/dist/client.js +67 -11
  23. package/dist/cloudflare/identity.d.ts +7 -0
  24. package/dist/cloudflare/identity.js +13 -0
  25. package/dist/cloudflare/index.d.ts +9 -0
  26. package/dist/cloudflare/index.js +47 -9
  27. package/dist/cloudflare/worker.d.ts +4 -0
  28. package/dist/cloudflare/worker.js +166 -16
  29. package/dist/codesandbox/guest-service.js +57 -20
  30. package/dist/codesandbox/index.js +42 -8
  31. package/dist/compat/core.d.ts +9 -8
  32. package/dist/compat/core.js +60 -21
  33. package/dist/compat/files.js +22 -4
  34. package/dist/compat/lease.d.ts +36 -0
  35. package/dist/compat/lease.js +87 -0
  36. package/dist/credentials.d.ts +6 -1
  37. package/dist/credentials.js +107 -38
  38. package/dist/daytona/computer-use.d.ts +162 -0
  39. package/dist/daytona/computer-use.js +324 -0
  40. package/dist/daytona/context.d.ts +10 -0
  41. package/dist/daytona/context.js +24 -0
  42. package/dist/daytona/daytona.d.ts +20 -0
  43. package/dist/daytona/daytona.js +134 -20
  44. package/dist/daytona/filesystem.d.ts +17 -4
  45. package/dist/daytona/filesystem.js +129 -38
  46. package/dist/daytona/index.d.ts +3 -27
  47. package/dist/daytona/index.js +2 -7
  48. package/dist/daytona/process.d.ts +63 -6
  49. package/dist/daytona/process.js +224 -23
  50. package/dist/daytona/sandbox.d.ts +20 -6
  51. package/dist/daytona/sandbox.js +55 -32
  52. package/dist/e2b/client.d.ts +5 -0
  53. package/dist/e2b/client.js +6 -2
  54. package/dist/e2b/commands.d.ts +43 -12
  55. package/dist/e2b/commands.js +217 -64
  56. package/dist/e2b/errors.d.ts +7 -0
  57. package/dist/e2b/errors.js +20 -0
  58. package/dist/e2b/filesystem.d.ts +2 -1
  59. package/dist/e2b/filesystem.js +43 -6
  60. package/dist/e2b/index.d.ts +3 -2
  61. package/dist/e2b/index.js +2 -1
  62. package/dist/e2b/pty.d.ts +24 -0
  63. package/dist/e2b/pty.js +83 -0
  64. package/dist/e2b/sandbox.d.ts +17 -6
  65. package/dist/e2b/sandbox.js +239 -43
  66. package/dist/e2b/users.d.ts +30 -0
  67. package/dist/e2b/users.js +185 -0
  68. package/dist/file-source.d.ts +16 -0
  69. package/dist/file-source.js +162 -0
  70. package/dist/freestyle/index.d.ts +9 -1
  71. package/dist/freestyle/index.js +38 -5
  72. package/dist/index.d.ts +3 -1
  73. package/dist/login.js +16 -10
  74. package/dist/mcp.d.ts +2 -1
  75. package/dist/mcp.js +6 -1
  76. package/dist/modal/index.js +2 -1
  77. package/dist/network-cli.js +139 -43
  78. package/dist/openai-agents/index.js +9 -45
  79. package/dist/page.d.ts +1 -1
  80. package/dist/page.js +2 -2
  81. package/dist/products/browser.d.ts +60 -0
  82. package/dist/products/browser.js +37 -0
  83. package/dist/products/desktop.d.ts +2 -2
  84. package/dist/products/desktop.js +2 -2
  85. package/dist/products/images.d.ts +3 -1
  86. package/dist/products/images.js +14 -2
  87. package/dist/products/index.d.ts +14 -0
  88. package/dist/products/index.js +12 -0
  89. package/dist/products/network-products.d.ts +7 -0
  90. package/dist/products/network-products.js +3 -0
  91. package/dist/products/observability.d.ts +44 -0
  92. package/dist/products/observability.js +6 -0
  93. package/dist/products/previews.d.ts +8 -2
  94. package/dist/products/previews.js +11 -3
  95. package/dist/products/private-network.d.ts +22 -0
  96. package/dist/products/private-network.js +10 -0
  97. package/dist/products/volumes.d.ts +42 -0
  98. package/dist/products/volumes.js +36 -0
  99. package/dist/products/watch.js +61 -8
  100. package/dist/proxy.js +3 -1
  101. package/dist/runloop/index.d.ts +13 -27
  102. package/dist/runloop/index.js +149 -52
  103. package/dist/sandbox.d.ts +59 -8
  104. package/dist/sandbox.js +479 -68
  105. package/dist/snapshots.d.ts +9 -4
  106. package/dist/sprites/index.js +69 -33
  107. package/dist/tar.d.ts +3 -1
  108. package/dist/tar.js +140 -14
  109. package/dist/transport.d.ts +4 -7
  110. package/dist/transport.js +205 -122
  111. package/dist/types.d.ts +5 -2
  112. package/dist/vercel/command.d.ts +1 -1
  113. package/dist/vercel/command.js +3 -2
  114. package/dist/vercel/errors.d.ts +1 -1
  115. package/dist/vercel/errors.js +7 -1
  116. package/dist/vercel/index.d.ts +1 -7
  117. package/dist/vercel/index.js +1 -3
  118. package/dist/vercel/sandbox-user.d.ts +70 -0
  119. package/dist/vercel/sandbox-user.js +206 -0
  120. package/dist/vercel/sandbox.d.ts +55 -20
  121. package/dist/vercel/sandbox.js +353 -99
  122. package/dist/vercel/snapshot.d.ts +3 -2
  123. package/dist/vercel/snapshot.js +9 -5
  124. package/package.json +2 -1
@@ -30,15 +30,35 @@ def settled(run):
30
30
  # Keep terminal failure readable over the socket even when storage is full.
31
31
  run['exitCode']=125
32
32
  run['error']=(run.get('error','')+'; ' if run.get('error') else '')+'Cannot persist task result: '+str(e)
33
+ def exited(proc):
34
+ if proc.returncode is not None: return True
35
+ # Leave the leader waitable: its PID cannot be reused before group cleanup.
36
+ try: return os.waitid(os.P_PID,proc.pid,os.WEXITED|os.WNOHANG|os.WNOWAIT) is not None
37
+ except ChildProcessError: return proc.returncode is not None
38
+ def kill_group(proc,signum):
39
+ # Callers hold the run lock, so no other thread can reap this owned leader.
40
+ if proc.returncode is not None: return
41
+ try: os.killpg(proc.pid,signum)
42
+ except ProcessLookupError: pass
43
+ except PermissionError:
44
+ # Darwin reports EPERM for a group containing only zombies. Keep real
45
+ # permission failures observable; only accept a verified empty/live-free
46
+ # group while our unreaped leader still prevents group-ID reuse.
47
+ if sys.platform!='darwin' or not exited(proc): raise
48
+ states=subprocess.run(['ps','-o','stat=','-g',str(proc.pid)],capture_output=True,text=True,timeout=2)
49
+ if states.returncode not in (0,1) or any(not state.startswith('Z') for state in states.stdout.split()): raise
33
50
  def complete(run):
34
51
  fd=run['fd']
35
52
  failed=False
36
53
  try:
37
54
  with open(os.path.join(root,run['id']+'.log'),'ab',buffering=0) as out:
38
55
  while True:
56
+ with run['_lock']:
57
+ done=exited(run['process'])
58
+ if done: kill_group(run['process'],signal.SIGKILL)
39
59
  ready,_,_=select.select([fd],[],[],0.05)
40
60
  if not ready:
41
- if run['process'].poll() is not None: break
61
+ if done: break
42
62
  continue
43
63
  try: data=os.read(fd,65536)
44
64
  except OSError as e:
@@ -49,27 +69,35 @@ def complete(run):
49
69
  except BaseException as e:
50
70
  failed=True
51
71
  with run['_lock']: run['error']=str(e)
52
- try: os.killpg(run['process'].pid,signal.SIGKILL)
53
- except ProcessLookupError: pass
72
+ with run['_lock']: kill_group(run['process'],signal.SIGKILL)
54
73
  finally:
55
- code=run['process'].wait()
56
- with run['_lock']:
57
- try: os.close(fd)
58
- except OSError: pass
59
- run['fd']=None
60
- run['exitCode']=125 if failed else code
61
- run['state']='exited'
62
- settled(run)
74
+ # A terminal can report EIO as its leader exits, before the polling branch
75
+ # observes that exit. Cleanup must also precede this final reap. Do not hold
76
+ # the run lock while a still-running leader waits, so stop remains usable.
77
+ while True:
78
+ with run['_lock']:
79
+ if exited(run['process']):
80
+ kill_group(run['process'],signal.SIGKILL)
81
+ code=run['process'].wait()
82
+ try: os.close(fd)
83
+ except OSError: pass
84
+ run['fd']=None
85
+ run['exitCode']=125 if failed else code
86
+ run['state']='exited'
87
+ settled(run)
88
+ break
89
+ time.sleep(0.02)
63
90
  def stop(run):
64
91
  if run.get('state')!='running': return
65
92
  proc=run.get('process')
66
- if proc is not None and proc.poll() is None:
67
- try: os.killpg(proc.pid,signal.SIGTERM)
68
- except ProcessLookupError: pass
69
- try: proc.wait(timeout=2)
70
- except subprocess.TimeoutExpired:
71
- try: os.killpg(proc.pid,signal.SIGKILL)
72
- except ProcessLookupError: pass
93
+ if proc is not None:
94
+ with run['_lock']:
95
+ kill_group(proc,signal.SIGTERM)
96
+ deadline=time.monotonic()+2
97
+ while not exited(proc) and time.monotonic()<deadline: time.sleep(0.02)
98
+ # The leader can exit on TERM while a descendant ignores it. Kill the
99
+ # remaining group before reaping, even when the leader already exited.
100
+ kill_group(proc,signal.SIGKILL)
73
101
  proc.wait()
74
102
  elif proc is None and run.get('pid') and run.get('identity') and identity(run['pid'])==run['identity']:
75
103
  # On supervisor recovery, fence against PID reuse before touching a process.
@@ -235,10 +263,19 @@ def boot():
235
263
  while True:
236
264
  try:
237
265
  with open(progress) as f: result=json.load(f)
238
- except FileNotFoundError: return
266
+ except FileNotFoundError: raise RuntimeError('Setup exited without terminal progress')
239
267
  if result.get('state')!='IN_PROGRESS': break
268
+ # A second runner may have won the setup lock. Wait only while it owns
269
+ # that lock; an exited runner with stale progress cannot ever finish.
270
+ with open(os.path.join(setup_root,'lock'),'a') as active:
271
+ try: fcntl.flock(active,fcntl.LOCK_EX|fcntl.LOCK_NB)
272
+ except BlockingIOError: pass
273
+ else:
274
+ with open(progress) as f: result=json.load(f)
275
+ if result.get('state')=='IN_PROGRESS': raise RuntimeError('Setup exited without terminal progress')
276
+ break
240
277
  time.sleep(0.1)
241
- if result.get('state')!='FINISHED' or result.get('config')!=setup['config']: return
278
+ if result.get('state')!='FINISHED' or result.get('config')!=setup['config']: raise RuntimeError('Setup exited without matching terminal progress')
242
279
  # The service owns automatic tasks independently of SDK connections.
243
280
  with mutex:
244
281
  for task,definition in cfg['tasks'].items():
@@ -58,6 +58,10 @@ const metadata = (s) => ({
58
58
  : (s.info.labels["cs.privacy"] ?? "private"),
59
59
  tags: JSON.parse(s.info.labels["cs.tags"] ?? "[]"),
60
60
  });
61
+ function validateHibernationTimeout(seconds) {
62
+ if (seconds !== undefined && (!Number.isSafeInteger(seconds) || seconds < 0 || seconds > 86_400))
63
+ throw new RangeError("hibernationTimeoutSeconds must be an integer from 0 to 86400.");
64
+ }
61
65
  export class Sandboxes {
62
66
  runtime;
63
67
  constructor(runtime) {
@@ -73,6 +77,7 @@ export class Sandboxes {
73
77
  "vmTier",
74
78
  "hibernationTimeoutSeconds",
75
79
  ]);
80
+ validateHibernationTimeout(options.hibernationTimeoutSeconds);
76
81
  const labels = {
77
82
  "compat.provider": "codesandbox",
78
83
  "cs.tags": JSON.stringify([...new Set([...(options.tags ?? []), "sdk"])]),
@@ -119,6 +124,7 @@ export class Sandboxes {
119
124
  }
120
125
  async restart(id, options = {}) {
121
126
  only("CodeSandbox restart", options, ["hibernationTimeoutSeconds"]);
127
+ validateHibernationTimeout(options.hibernationTimeoutSeconds);
122
128
  const s = await this.runtime.sandboxes.get(id);
123
129
  await s.stop();
124
130
  await s.restart();
@@ -143,19 +149,37 @@ export class Sandboxes {
143
149
  ]);
144
150
  if (options.orderBy === "updated_at")
145
151
  throw new CompatibilityError("CodeSandbox", "sorting by updated_at");
146
- let sandboxes = (await all(this.runtime, "codesandbox"))
152
+ const limit = options.limit ?? 50;
153
+ let currentPage = options.pagination?.page ?? 1;
154
+ const pageSize = options.pagination?.pageSize ?? limit;
155
+ for (const [name, value] of [
156
+ ["limit", limit],
157
+ ["page", currentPage],
158
+ ["pageSize", pageSize],
159
+ ])
160
+ if (!Number.isSafeInteger(value) || value <= 0)
161
+ throw new RangeError(`${name} must be a positive safe integer.`);
162
+ const sandboxes = (await all(this.runtime, "codesandbox"))
147
163
  .filter((s) => (!options.status || s.state === "running") &&
148
164
  (options.tags ?? []).every((tag) => metadata(s).tags.includes(tag)))
149
165
  .map(metadata);
150
166
  sandboxes.sort((a, b) => (a.createdAt.getTime() - b.createdAt.getTime()) * (options.direction === "desc" ? -1 : 1));
151
- const totalCount = sandboxes.length, page = options.pagination?.page ?? 1, pageSize = options.pagination?.pageSize ?? options.limit ?? 50;
152
- sandboxes = sandboxes.slice((page - 1) * pageSize, page * pageSize);
153
- const hasMore = page * pageSize < totalCount;
167
+ const totalCount = sandboxes.length;
168
+ const selected = [];
169
+ let nextPage;
170
+ for (;;) {
171
+ const offset = (currentPage - 1) * pageSize;
172
+ selected.push(...sandboxes.slice(offset, offset + pageSize));
173
+ nextPage = offset + pageSize < totalCount ? currentPage + 1 : null;
174
+ if (nextPage === null || selected.length >= limit)
175
+ break;
176
+ currentPage = nextPage;
177
+ }
154
178
  return {
155
- sandboxes,
179
+ sandboxes: selected,
156
180
  totalCount,
157
- hasMore,
158
- pagination: { currentPage: page, nextPage: hasMore ? page + 1 : null, pageSize },
181
+ hasMore: totalCount > selected.length,
182
+ pagination: { currentPage, nextPage, pageSize },
159
183
  };
160
184
  }
161
185
  }
@@ -475,14 +499,22 @@ export class SandboxClient {
475
499
  };
476
500
  this.ports = {
477
501
  get: async (port) => {
502
+ this.assertConnected();
478
503
  if (native.info.labels["cs.privacy"] === "private")
479
504
  throw new CompatibilityError("CodeSandbox", "private port host authentication; use Runtime signed preview URLs");
480
505
  const p = await native.previews.create(port, { visibility: "public" });
481
506
  this.previewURLs.set(port, p.urlWithToken ?? p.url);
482
507
  return { port, host: new URL(p.urlWithToken ?? p.url).host };
483
508
  },
484
- getAll: async () => (await native.previews.list()).map((p) => ({ port: p.port, host: new URL(p.url).host })),
509
+ getAll: async () => {
510
+ this.assertConnected();
511
+ return (await native.previews.list()).map((p) => ({
512
+ port: p.port,
513
+ host: new URL(p.url).host,
514
+ }));
515
+ },
485
516
  waitForPort: async (port, options = {}) => {
517
+ this.assertConnected();
486
518
  if (!Number.isInteger(port) || port < 1 || port > 65535)
487
519
  throw new TypeError("Invalid port");
488
520
  await native.exec([
@@ -586,6 +618,8 @@ export class Tasks {
586
618
  this.managed = new ManagedTasks(native);
587
619
  }
588
620
  getAll() {
621
+ if (this.client.state !== "CONNECTED")
622
+ return Promise.reject(new Error("Sandbox client is disconnected."));
589
623
  return (this.cached ??= this.load().catch((error) => {
590
624
  this.cached = undefined;
591
625
  throw error;
@@ -1,6 +1,7 @@
1
- import { Runtime, type RuntimeOptions } from "../client.js";
1
+ import { Runtime, type CreateOptions, type RuntimeOptions } from "../client.js";
2
2
  import type { Sandbox } from "../sandbox.js";
3
3
  import type { CreateSandbox, ExecOptions } from "../types.js";
4
+ import type { RequestOptions } from "../transport.js";
4
5
  export type ClientOptions = RuntimeOptions & {
5
6
  client?: Runtime;
6
7
  };
@@ -16,14 +17,14 @@ export declare function only(provider: string, value: object, keys: readonly str
16
17
  export declare function region(provider: string, value?: string | null): string | undefined;
17
18
  export declare const quote: (value: string) => string;
18
19
  export declare const pathAt: (path: string, cwd?: string) => string;
19
- export declare function environment(sandbox: Sandbox): Promise<Record<string, string>>;
20
- export declare function saveEnvironment(sandbox: Sandbox, env: Record<string, string>): Promise<void>;
20
+ export declare function environment(sandbox: Sandbox, options?: RequestOptions): Promise<Record<string, string>>;
21
+ export declare function saveEnvironment(sandbox: Sandbox, env: Record<string, string>, options?: RequestOptions): Promise<void>;
21
22
  export declare function validateEnvironment(env: Record<string, string>): void;
22
- export declare function execOptions(sandbox: Sandbox, options?: ExecOptions): Promise<ExecOptions>;
23
- export declare function create(runtime: Runtime, provider: string, input: CreateSandbox, env?: Record<string, string> | undefined, setup?: (sandbox: Sandbox) => Promise<void>): Promise<Sandbox>;
24
- export declare function destroy(sandbox: Sandbox): Promise<void>;
23
+ export declare function execOptions(sandbox: Sandbox, options?: ExecOptions, request?: RequestOptions): Promise<ExecOptions>;
24
+ export declare function create(runtime: Runtime, provider: string, input: CreateSandbox, env?: Record<string, string> | undefined, setup?: (sandbox: Sandbox) => Promise<void>, options?: CreateOptions): Promise<Sandbox>;
25
+ export declare function destroy(sandbox: Sandbox, options?: RequestOptions): Promise<void>;
25
26
  export declare function ready(sandbox: Sandbox): Promise<Sandbox>;
26
- export declare function lookup(runtime: Runtime, id: string): Promise<Sandbox>;
27
+ export declare function lookup(runtime: Runtime, id: string, options?: RequestOptions): Promise<Sandbox>;
27
28
  export declare function all(runtime: Runtime, provider: string): Promise<Sandbox[]>;
28
29
  export declare function image(runtime: Runtime, ref: string): Promise<string>;
29
30
  export declare function checked(sandbox: Sandbox, argv: string[], options?: ExecOptions): Promise<void>;
@@ -34,6 +35,6 @@ export declare function shellCommand(sandbox: Sandbox, command: string, name?: s
34
35
  env?: Record<string, string>;
35
36
  cwd?: string;
36
37
  persist?: boolean;
37
- }): Promise<string | string[]>;
38
+ }, request?: RequestOptions): Promise<string | string[]>;
38
39
  export declare function closeShell(sandbox: Sandbox, name: string): Promise<void>;
39
40
  export declare function shellRoot(name: string): Promise<string>;
@@ -40,9 +40,12 @@ export function region(provider, value) {
40
40
  export const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
41
41
  export const pathAt = (path, cwd = "/workspace") => path.startsWith("/") ? path : `${cwd.replace(/\/$/, "")}/${path}`;
42
42
  const ENV_PATH = "/workspace/.runtime-compat/environment.json";
43
- export async function environment(sandbox) {
43
+ export async function environment(sandbox, options = {}) {
44
+ options.signal?.throwIfAborted();
44
45
  try {
45
- return JSON.parse(await sandbox.files.readText(ENV_PATH));
46
+ const text = await sandbox.files.readText(ENV_PATH, options);
47
+ options.signal?.throwIfAborted();
48
+ return JSON.parse(text);
46
49
  }
47
50
  catch (error) {
48
51
  if (error instanceof NotFoundError)
@@ -50,9 +53,14 @@ export async function environment(sandbox) {
50
53
  throw error;
51
54
  }
52
55
  }
53
- export async function saveEnvironment(sandbox, env) {
56
+ export async function saveEnvironment(sandbox, env, options = {}) {
57
+ options.signal?.throwIfAborted();
54
58
  validateEnvironment(env);
55
- await sandbox.files.write(ENV_PATH, JSON.stringify(env), { mode: 0o600 });
59
+ await sandbox.files.write(ENV_PATH, JSON.stringify(env), {
60
+ mode: 0o600,
61
+ signal: options.signal,
62
+ timeoutMs: options.timeoutMs,
63
+ });
56
64
  }
57
65
  export function validateEnvironment(env) {
58
66
  for (const key of Object.keys(env))
@@ -62,23 +70,39 @@ export function validateEnvironment(env) {
62
70
  if (typeof value !== "string" || value.includes("\0"))
63
71
  throw new TypeError("Environment values must be strings without NUL bytes");
64
72
  }
65
- export async function execOptions(sandbox, options = {}) {
66
- return { ...options, env: { ...(await environment(sandbox)), ...options.env } };
73
+ export async function execOptions(sandbox, options = {}, request = {}) {
74
+ const requestOptions = { ...request, signal: request.signal ?? options.signal };
75
+ return { ...options, env: { ...(await environment(sandbox, requestOptions)), ...options.env } };
67
76
  }
68
- export async function create(runtime, provider, input, env = undefined, setup) {
77
+ export async function create(runtime, provider, input, env = undefined, setup, options = {}) {
78
+ options.signal?.throwIfAborted();
69
79
  if (env !== undefined)
70
80
  validateEnvironment(env);
71
81
  const sandbox = await runtime.sandboxes.create({
72
82
  ...input,
73
83
  labels: { ...input.labels, "compat.provider": provider },
74
- });
84
+ }, options);
75
85
  try {
86
+ options.signal?.throwIfAborted();
76
87
  if (env !== undefined || !input.snapshot)
77
- await saveEnvironment(sandbox, env ?? {});
88
+ await saveEnvironment(sandbox, env ?? {}, options);
89
+ options.signal?.throwIfAborted();
78
90
  await setup?.(sandbox);
91
+ options.signal?.throwIfAborted();
79
92
  return sandbox;
80
93
  }
81
94
  catch (error) {
95
+ // Cancellation cannot authorize deleting a remote resource: the response
96
+ // may be a retry/reuse of a sandbox that already has somebody's work.
97
+ // Legacy failed-initialization rollback is limited to an ordinary create
98
+ // with a new SDK-generated mutation key and no replay/reuse marker.
99
+ if (options.signal?.aborted ||
100
+ (error instanceof RuntimeError && error.code === "timeout") ||
101
+ input.getOrCreate === true ||
102
+ options.idempotencyKey !== undefined ||
103
+ sandbox.info.replayed === true ||
104
+ sandbox.info.reused === true)
105
+ throw error;
82
106
  try {
83
107
  await destroy(sandbox);
84
108
  }
@@ -88,13 +112,16 @@ export async function create(runtime, provider, input, env = undefined, setup) {
88
112
  throw error;
89
113
  }
90
114
  }
91
- export async function destroy(sandbox) {
115
+ export async function destroy(sandbox, options = {}) {
92
116
  // Stop before releasing persistence: a paused sandbox cannot change its disk
93
117
  // meter, and must never be woken solely to delete it. The stopped update queues
94
118
  // disk destruction; see ARCHITECTURE.md section 10 and migration 0181.
95
- await sandbox.stop();
119
+ options.signal?.throwIfAborted();
120
+ if (options.idempotencyKey !== undefined)
121
+ throw new CompatibilityError("Runtime", "caller idempotency keys for compound sandbox deletion");
122
+ await sandbox.stop(options);
96
123
  if (sandbox.info.persistent)
97
- await sandbox.update({ persistent: false });
124
+ await sandbox.update({ persistent: false }, options);
98
125
  }
99
126
  export async function ready(sandbox) {
100
127
  if (sandbox.state === "paused")
@@ -105,15 +132,18 @@ export async function ready(sandbox) {
105
132
  await sandbox.waitFor("running");
106
133
  return sandbox;
107
134
  }
108
- export async function lookup(runtime, id) {
135
+ export async function lookup(runtime, id, options = {}) {
136
+ options.signal?.throwIfAborted();
109
137
  if (/^[a-f0-9-]{36}$/i.test(id))
110
- return runtime.sandboxes.get(id);
111
- const page = await runtime.sandboxes.list({ name: id, includeStopped: true });
112
- for await (const sandbox of page)
138
+ return runtime.sandboxes.get(id, options);
139
+ const page = await runtime.sandboxes.list({ name: id, includeStopped: true }, options);
140
+ for await (const sandbox of page) {
141
+ options.signal?.throwIfAborted();
113
142
  if (sandbox.info.name === id)
114
143
  return sandbox;
144
+ }
115
145
  // IDs are not necessarily UUIDs in local or self-hosted servers.
116
- return runtime.sandboxes.get(id);
146
+ return runtime.sandboxes.get(id, options);
117
147
  }
118
148
  export async function all(runtime, provider) {
119
149
  const result = [];
@@ -141,14 +171,23 @@ export function missing(path) {
141
171
  }
142
172
  /** Foreground commands share a resident Bash process. Background commands start
143
173
  * from the last completed exported environment/cwd snapshot without blocking it. */
144
- export async function shellCommand(sandbox, command, name, overrides = {}) {
174
+ export async function shellCommand(sandbox, command, name, overrides = {}, request = {}) {
175
+ request.signal?.throwIfAborted();
145
176
  if (!name)
146
177
  return command;
147
178
  const root = await shellRoot(name);
148
- await sandbox.files.mkdir(root, { parents: true });
179
+ await sandbox.files.mkdir(root, {
180
+ parents: true,
181
+ signal: request.signal,
182
+ timeoutMs: request.timeoutMs,
183
+ });
149
184
  if (overrides.persist !== false) {
150
185
  const script = "/workspace/.runtime-compat/shell.py";
151
- await sandbox.files.write(script, SHELL_BROKER, { mode: 0o600 });
186
+ await sandbox.files.write(script, SHELL_BROKER, {
187
+ mode: 0o600,
188
+ signal: request.signal,
189
+ timeoutMs: request.timeoutMs,
190
+ });
152
191
  return [
153
192
  "python3",
154
193
  script,
@@ -158,7 +197,7 @@ export async function shellCommand(sandbox, command, name, overrides = {}) {
158
197
  command,
159
198
  env: overrides.env ?? {},
160
199
  cwd: overrides.cwd,
161
- baseEnv: await environment(sandbox),
200
+ baseEnv: await environment(sandbox, request),
162
201
  }),
163
202
  ];
164
203
  }
@@ -25,8 +25,8 @@ export class GuestFiles {
25
25
  return;
26
26
  }
27
27
  const staging = `/workspace/.runtime-compat/write-${crypto.randomUUID()}`;
28
- await sandbox.files.write(staging, bytes, { mode: 0o600, signal: options.signal });
29
28
  try {
29
+ await sandbox.files.write(staging, bytes, { mode: 0o600, signal: options.signal });
30
30
  await checked(sandbox, [
31
31
  "bash",
32
32
  "-c",
@@ -39,7 +39,9 @@ export class GuestFiles {
39
39
  ], { signal: options.signal });
40
40
  }
41
41
  finally {
42
- await sandbox.files.remove(staging);
42
+ // Cleanup must preserve the write's outcome, including its original
43
+ // failure; a successful destination write must not invite a replay.
44
+ await sandbox.files.remove(staging).catch(() => undefined);
43
45
  }
44
46
  }
45
47
  writeTextFile(path, content) {
@@ -58,6 +60,21 @@ export class GuestFiles {
58
60
  const sandbox = await this.get();
59
61
  if (!overwrite && (await sandbox.files.exists(this.path(to))))
60
62
  throw new Error(`Destination exists: ${to}`);
63
+ if (!overwrite) {
64
+ // cp -n alone reports success when it skips a raced destination. Stage
65
+ // privately, then publish without replacement and verify it moved.
66
+ await checked(sandbox, [
67
+ "bash",
68
+ "-c",
69
+ 'set -e; parent=$(dirname -- "$2"); stage="$parent/.runtime-copy-$4"; mkdir -m 700 -- "$stage"; trap \'rm -rf -- "$stage"\' EXIT; if [ "$3" = true ]; then cp -R -n -- "$1" "$stage/item"; else cp -n -- "$1" "$stage/item"; fi; mv -n -T -- "$stage/item" "$2"; if [ -e "$stage/item" ] || [ -L "$stage/item" ]; then printf "Destination exists: %s\\n" "$2" >&2; exit 73; fi',
70
+ "bash",
71
+ this.path(from),
72
+ this.path(to),
73
+ String(recursive),
74
+ crypto.randomUUID(),
75
+ ]);
76
+ return;
77
+ }
61
78
  await checked(sandbox, [
62
79
  "cp",
63
80
  ...(recursive ? ["-R"] : []),
@@ -97,8 +114,8 @@ export class GuestFiles {
97
114
  return;
98
115
  }
99
116
  const staging = `/workspace/.runtime-compat/append-${crypto.randomUUID()}`;
100
- await sandbox.files.write(staging, bytes, { mode: 0o600 });
101
117
  try {
118
+ await sandbox.files.write(staging, bytes, { mode: 0o600 });
102
119
  await checked(sandbox, [
103
120
  "bash",
104
121
  "-c",
@@ -109,7 +126,8 @@ export class GuestFiles {
109
126
  ]);
110
127
  }
111
128
  finally {
112
- await sandbox.files.remove(staging);
129
+ // A cleanup failure after success must not invite appending twice.
130
+ await sandbox.files.remove(staging).catch(() => undefined);
113
131
  }
114
132
  }
115
133
  }
@@ -0,0 +1,36 @@
1
+ /** The most one lease runs ahead of now. */
2
+ export declare const LEASE_MAX_SECONDS = 3600;
3
+ /** What the keeper needs of a Runtime sandbox. */
4
+ export interface Leased {
5
+ readonly state: string;
6
+ readonly info: {
7
+ expiresAt: string;
8
+ };
9
+ extend(seconds: number): Promise<unknown>;
10
+ refresh(): Promise<unknown>;
11
+ }
12
+ /** Whole seconds to add to a lease ending at `expiresAt` (epoch ms) so it
13
+ * reaches toward `until`, at most an hour ahead of `now`; 0 when more than
14
+ * `marginMs` is left or there is nothing to add. */
15
+ export declare function extensionSeconds(expiresAt: number, until: number, now: number, marginMs: number): number;
16
+ /** Keeps a running sandbox's lease reaching toward `until()`: on `check()`,
17
+ * and once a minute on a timer that never keeps a Node or Bun process alive.
18
+ * When the process ends, the lease runs out within the hour and the sandbox
19
+ * pauses or stops as it was told to at create. A paused or stopped sandbox is
20
+ * left alone; the adapter wakes it on its next call. */
21
+ export declare class LeaseKeeper {
22
+ #private;
23
+ constructor(options: {
24
+ sandbox: () => Leased;
25
+ /** Epoch ms the lease should reach; Infinity for as long as this lives. */
26
+ until: () => number;
27
+ /** Extend only when less than this is left. */
28
+ marginMs: () => number;
29
+ everyMs?: number;
30
+ });
31
+ /** Extends the lease if it is near its end and should go further; `now`
32
+ * extends it as far as it may go whatever is left. One call at a time. */
33
+ check(now?: boolean): Promise<void>;
34
+ /** Stops extending: after stop() or delete(). */
35
+ end(): void;
36
+ }
@@ -0,0 +1,87 @@
1
+ import { RuntimeError } from "../errors.js";
2
+ /* A rival's sandbox may be asked to live longer than one Runtime lease (an
3
+ hour at most, ARCHITECTURE.md section 10): Vercel's `timeout` over an hour,
4
+ Daytona's `autoStopInterval` over an hour or 0. The drop-ins keep the lease
5
+ reaching toward the time asked for while their sandbox object lives, one
6
+ extension at a time, never past that time. */
7
+ /** The most one lease runs ahead of now. */
8
+ export const LEASE_MAX_SECONDS = 3600;
9
+ /** Whole seconds to add to a lease ending at `expiresAt` (epoch ms) so it
10
+ * reaches toward `until`, at most an hour ahead of `now`; 0 when more than
11
+ * `marginMs` is left or there is nothing to add. */
12
+ export function extensionSeconds(expiresAt, until, now, marginMs) {
13
+ // A lease with no readable end (one that never ends) is left alone.
14
+ if (!Number.isFinite(expiresAt) || expiresAt - now > marginMs)
15
+ return 0;
16
+ const target = Math.min(until, now + LEASE_MAX_SECONDS * 1000);
17
+ return Math.max(0, Math.floor((target - expiresAt) / 1000));
18
+ }
19
+ /** Keeps a running sandbox's lease reaching toward `until()`: on `check()`,
20
+ * and once a minute on a timer that never keeps a Node or Bun process alive.
21
+ * When the process ends, the lease runs out within the hour and the sandbox
22
+ * pauses or stops as it was told to at create. A paused or stopped sandbox is
23
+ * left alone; the adapter wakes it on its next call. */
24
+ export class LeaseKeeper {
25
+ #sandbox;
26
+ #until;
27
+ #marginMs;
28
+ #everyMs;
29
+ #timer;
30
+ #pending;
31
+ #ended = false;
32
+ constructor(options) {
33
+ this.#sandbox = options.sandbox;
34
+ this.#until = options.until;
35
+ this.#marginMs = options.marginMs;
36
+ this.#everyMs = options.everyMs ?? 60_000;
37
+ }
38
+ /** Starts the timer when the lease must outlive the next extension. */
39
+ #schedule() {
40
+ if (this.#ended || this.#timer)
41
+ return;
42
+ const expiresAt = Date.parse(this.#sandbox().info.expiresAt);
43
+ if (!Number.isFinite(expiresAt) || this.#until() <= expiresAt)
44
+ return;
45
+ this.#timer = setTimeout(() => {
46
+ this.#timer = undefined;
47
+ void this.check().catch(() => undefined);
48
+ }, this.#everyMs);
49
+ this.#timer.unref?.();
50
+ }
51
+ /** Extends the lease if it is near its end and should go further; `now`
52
+ * extends it as far as it may go whatever is left. One call at a time. */
53
+ check(now = false) {
54
+ if (this.#ended)
55
+ return Promise.resolve();
56
+ this.#pending ??= this.#extend(now).finally(() => {
57
+ this.#pending = undefined;
58
+ this.#schedule();
59
+ });
60
+ return this.#pending;
61
+ }
62
+ async #extend(now) {
63
+ const sandbox = this.#sandbox();
64
+ if (sandbox.state !== "running")
65
+ return;
66
+ const seconds = extensionSeconds(Date.parse(sandbox.info.expiresAt), this.#until(), Date.now(), now ? Infinity : this.#marginMs());
67
+ if (seconds < 1)
68
+ return;
69
+ try {
70
+ await sandbox.extend(seconds);
71
+ }
72
+ catch (error) {
73
+ // The lease may have ended since the sandbox was last read; the next
74
+ // call through the adapter says what state it is in.
75
+ if (!(error instanceof RuntimeError && error.status === 409))
76
+ throw error;
77
+ await sandbox.refresh().catch(() => undefined);
78
+ }
79
+ }
80
+ /** Stops extending: after stop() or delete(). */
81
+ end() {
82
+ this.#ended = true;
83
+ if (this.#timer)
84
+ clearTimeout(this.#timer);
85
+ this.#timer = undefined;
86
+ }
87
+ }
@@ -14,6 +14,9 @@ export declare function connectionOrigins(env: NodeJS.ProcessEnv): {
14
14
  api: string;
15
15
  auth: string;
16
16
  };
17
+ /** Browser-issued production credentials must only reach the production API.
18
+ * CLI and MCP share this boundary, including resumed device requests. */
19
+ export declare function checkBrowserOrigins(origins: ReturnType<typeof connectionOrigins>): void;
17
20
  /** Private user storage, outside project files. Nothing reading this module
18
21
  * returns a secret to CLI output. Store names bind credentials to both origins. */
19
22
  export declare function connectionStore(env: NodeJS.ProcessEnv, directory?: string): {
@@ -27,7 +30,9 @@ export declare function connectionStore(env: NodeJS.ProcessEnv, directory?: stri
27
30
  /** Use another saved account's connection, by account id or name. The one
28
31
  * in use moves to the saved accounts. Null when none matches. */
29
32
  use(which: string): Promise<SavedConnection | null>;
30
- remove(): Promise<void>;
33
+ /** Forget the expected credential after a remote revoke or refusal. A
34
+ * concurrent login may have saved another connection during that request. */
35
+ remove(expectedKey?: string): Promise<void>;
31
36
  /** A browser approval that was started and not yet answered, so a command
32
37
  * that was stopped while it waited picks up the same link and code rather
33
38
  * than asking the person to approve a new one. Unreadable or stale ones