@namzu/sandbox 6.1.0 → 7.1.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 (49) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +29 -1
  3. package/dist/backends/aci-standby-pool/index.d.ts.map +1 -1
  4. package/dist/backends/aci-standby-pool/index.js +163 -111
  5. package/dist/backends/aci-standby-pool/index.js.map +1 -1
  6. package/dist/backends/docker/index.d.ts.map +1 -1
  7. package/dist/backends/docker/index.js +212 -149
  8. package/dist/backends/docker/index.js.map +1 -1
  9. package/dist/backends/firecracker/index.d.ts.map +1 -1
  10. package/dist/backends/firecracker/index.js +154 -71
  11. package/dist/backends/firecracker/index.js.map +1 -1
  12. package/dist/backends/firecracker/protocol.d.ts +19 -27
  13. package/dist/backends/firecracker/protocol.d.ts.map +1 -1
  14. package/dist/backends/firecracker/protocol.js +57 -32
  15. package/dist/backends/firecracker/protocol.js.map +1 -1
  16. package/dist/backends/firecracker/transport.d.ts +26 -7
  17. package/dist/backends/firecracker/transport.d.ts.map +1 -1
  18. package/dist/backends/firecracker/transport.js +235 -72
  19. package/dist/backends/firecracker/transport.js.map +1 -1
  20. package/dist/backends/http-worker-client.d.ts +17 -0
  21. package/dist/backends/http-worker-client.d.ts.map +1 -0
  22. package/dist/backends/http-worker-client.js +173 -0
  23. package/dist/backends/http-worker-client.js.map +1 -0
  24. package/dist/backends/readiness.d.ts +43 -0
  25. package/dist/backends/readiness.d.ts.map +1 -0
  26. package/dist/backends/readiness.js +150 -0
  27. package/dist/backends/readiness.js.map +1 -0
  28. package/dist/backends/remote-execution-controller.d.ts +76 -0
  29. package/dist/backends/remote-execution-controller.d.ts.map +1 -0
  30. package/dist/backends/remote-execution-controller.js +294 -0
  31. package/dist/backends/remote-execution-controller.js.map +1 -0
  32. package/dist/egress/proxy.d.ts.map +1 -1
  33. package/dist/egress/proxy.js +20 -3
  34. package/dist/egress/proxy.js.map +1 -1
  35. package/dist/index.d.ts +18 -0
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +9 -0
  38. package/dist/index.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/backends/aci-standby-pool/index.ts +199 -117
  41. package/src/backends/docker/index.ts +235 -175
  42. package/src/backends/firecracker/index.ts +163 -63
  43. package/src/backends/firecracker/protocol.ts +67 -33
  44. package/src/backends/firecracker/transport.ts +301 -77
  45. package/src/backends/http-worker-client.ts +230 -0
  46. package/src/backends/readiness.ts +173 -0
  47. package/src/backends/remote-execution-controller.ts +462 -0
  48. package/src/egress/proxy.ts +18 -3
  49. package/src/index.ts +27 -0
@@ -0,0 +1,462 @@
1
+ import type { SandboxExecOptions, SandboxExecResult } from '@namzu/sdk'
2
+
3
+ const CONTROL_REQUEST_TIMEOUT_MS = 2_000
4
+ const CANCEL_CONFIRM_TIMEOUT_MS = 8_000
5
+ const RESULT_DRAIN_TIMEOUT_MS = 1_500
6
+ const DEFAULT_EXECUTION_TIMEOUT_MS = 5 * 60 * 1_000
7
+ const EXECUTION_OBSERVATION_GRACE_MS = 500
8
+ const MAX_TIMER_DELAY_MS = 2_147_483_647
9
+ const EXECUTION_ID_PATTERN =
10
+ /^exec_[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
11
+
12
+ export interface RemoteReservation {
13
+ readonly ok: true
14
+ readonly protocolVersion: 2
15
+ readonly executionId: string
16
+ readonly leaseExpiresAt: number
17
+ }
18
+
19
+ export interface RemoteTerminalMetadata {
20
+ readonly exitCode: number
21
+ readonly timedOut: boolean
22
+ readonly durationMs: number
23
+ readonly signal?: string
24
+ readonly stdoutTruncated?: boolean
25
+ readonly stderrTruncated?: boolean
26
+ }
27
+
28
+ export interface RemoteCancellationAcknowledgement {
29
+ readonly ok: true
30
+ readonly state: 'cancelled' | 'completed' | 'failed'
31
+ readonly started: boolean
32
+ readonly result?: RemoteTerminalMetadata
33
+ readonly error?: string
34
+ }
35
+
36
+ export class RemoteCommandError extends Error {}
37
+
38
+ export class RemoteProtocolError extends Error {}
39
+
40
+ export class RemoteCancellationUnsupportedError extends Error {}
41
+
42
+ export interface SandboxRetirementObservation {
43
+ readonly accepted: boolean
44
+ readonly error?: Error
45
+ }
46
+
47
+ export class RemoteCancellationUnknownError extends Error {
48
+ retirement?: SandboxRetirementObservation
49
+
50
+ constructor(message: string, options?: ErrorOptions) {
51
+ super(message, options)
52
+ this.name = 'RemoteCancellationUnknownError'
53
+ }
54
+ }
55
+
56
+ export class RemoteResultIncompleteError extends Error {
57
+ constructor(
58
+ message: string,
59
+ readonly acknowledgement: RemoteCancellationAcknowledgement,
60
+ options?: ErrorOptions,
61
+ ) {
62
+ super(message, options)
63
+ this.name = 'RemoteResultIncompleteError'
64
+ }
65
+ }
66
+
67
+ export interface RemoteExecutionAdapter<Context = undefined> {
68
+ readonly label: string
69
+ reserve(signal: AbortSignal): Promise<unknown>
70
+ cancel(executionId: string, signal: AbortSignal): Promise<unknown>
71
+ execute(
72
+ executionId: string | undefined,
73
+ command: string,
74
+ argv: string[] | undefined,
75
+ opts: SandboxExecOptions | undefined,
76
+ transportSignal: AbortSignal,
77
+ context: Context | undefined,
78
+ ): Promise<SandboxExecResult>
79
+ }
80
+
81
+ export interface RemoteExecutionControllerOptions {
82
+ readonly controlRequestTimeoutMs?: number
83
+ readonly cancelConfirmTimeoutMs?: number
84
+ readonly resultDrainTimeoutMs?: number
85
+ readonly executionObservationGraceMs?: number
86
+ readonly defaultExecutionTimeoutMs?: number
87
+ }
88
+
89
+ function isTerminalMetadata(value: unknown): value is RemoteTerminalMetadata {
90
+ if (!value || typeof value !== 'object') return false
91
+ const metadata = value as Record<string, unknown>
92
+ return (
93
+ Number.isFinite(metadata.exitCode) &&
94
+ typeof metadata.timedOut === 'boolean' &&
95
+ Number.isFinite(metadata.durationMs) &&
96
+ (metadata.signal === undefined || typeof metadata.signal === 'string') &&
97
+ (metadata.stdoutTruncated === undefined || typeof metadata.stdoutTruncated === 'boolean') &&
98
+ (metadata.stderrTruncated === undefined || typeof metadata.stderrTruncated === 'boolean')
99
+ )
100
+ }
101
+
102
+ function parseReservation(value: unknown): RemoteReservation {
103
+ if (!value || typeof value !== 'object') {
104
+ throw new RemoteProtocolError('remote sandbox returned an invalid execution reservation')
105
+ }
106
+ const reservation = value as Record<string, unknown>
107
+ if (
108
+ reservation.ok !== true ||
109
+ reservation.protocolVersion !== 2 ||
110
+ !EXECUTION_ID_PATTERN.test(String(reservation.executionId ?? '')) ||
111
+ !Number.isFinite(reservation.leaseExpiresAt)
112
+ ) {
113
+ throw new RemoteProtocolError('remote sandbox returned an invalid execution reservation')
114
+ }
115
+ return reservation as unknown as RemoteReservation
116
+ }
117
+
118
+ function parseCancellation(value: unknown): RemoteCancellationAcknowledgement {
119
+ if (!value || typeof value !== 'object') {
120
+ throw new RemoteProtocolError('remote sandbox returned an invalid cancellation acknowledgement')
121
+ }
122
+ const acknowledgement = value as Record<string, unknown>
123
+ if (
124
+ acknowledgement.ok !== true ||
125
+ !['cancelled', 'completed', 'failed'].includes(String(acknowledgement.state ?? '')) ||
126
+ typeof acknowledgement.started !== 'boolean' ||
127
+ (acknowledgement.result !== undefined && !isTerminalMetadata(acknowledgement.result)) ||
128
+ (acknowledgement.error !== undefined && typeof acknowledgement.error !== 'string')
129
+ ) {
130
+ throw new RemoteProtocolError('remote sandbox returned an invalid cancellation acknowledgement')
131
+ }
132
+ return acknowledgement as unknown as RemoteCancellationAcknowledgement
133
+ }
134
+
135
+ function cancelledBeforeStart(startedAt: number): SandboxExecResult {
136
+ return {
137
+ exitCode: 1,
138
+ stdout: '',
139
+ stderr: '',
140
+ timedOut: false,
141
+ durationMs: Math.max(0, Date.now() - startedAt),
142
+ stdoutTruncated: false,
143
+ stderrTruncated: false,
144
+ }
145
+ }
146
+
147
+ function resultFromAcknowledgement(
148
+ acknowledgement: RemoteCancellationAcknowledgement,
149
+ startedAt: number,
150
+ ): SandboxExecResult {
151
+ const result = acknowledgement.result
152
+ if (!result) return cancelledBeforeStart(startedAt)
153
+ return { ...result, stdout: '', stderr: '' }
154
+ }
155
+
156
+ function resultIncomplete(
157
+ error: unknown,
158
+ acknowledgement: RemoteCancellationAcknowledgement,
159
+ ): RemoteResultIncompleteError {
160
+ const message = error instanceof Error ? error.message : String(error)
161
+ return new RemoteResultIncompleteError(
162
+ `Remote sandbox termination was confirmed (${acknowledgement.state}), but the terminal result/output stream was incomplete: ${message}. Do not infer complete output.`,
163
+ acknowledgement,
164
+ { cause: error },
165
+ )
166
+ }
167
+
168
+ function cancellationUnknown(error: unknown): RemoteCancellationUnknownError {
169
+ const message = error instanceof Error ? error.message : String(error)
170
+ return new RemoteCancellationUnknownError(
171
+ `Remote sandbox cancellation could not be confirmed: ${message}. The remote outcome is unknown; do not automatically retry the command.`,
172
+ { cause: error },
173
+ )
174
+ }
175
+
176
+ function observationDeadlineMs(
177
+ timeout: number | undefined,
178
+ defaultTimeoutMs: number,
179
+ graceMs: number,
180
+ ): number {
181
+ const requested =
182
+ typeof timeout === 'number' && Number.isFinite(timeout) && timeout > 0
183
+ ? timeout
184
+ : defaultTimeoutMs
185
+ return Math.min(MAX_TIMER_DELAY_MS, requested + graceMs)
186
+ }
187
+
188
+ async function bounded<T>(
189
+ label: string,
190
+ timeoutMs: number,
191
+ operation: (signal: AbortSignal) => Promise<T>,
192
+ callerSignal?: AbortSignal,
193
+ ): Promise<T> {
194
+ callerSignal?.throwIfAborted()
195
+ const controller = new AbortController()
196
+ let timer: ReturnType<typeof setTimeout> | undefined
197
+ let removeAbort: (() => void) | undefined
198
+ const timeout = new Promise<never>((_, reject) => {
199
+ timer = setTimeout(() => {
200
+ const error = new Error(`${label} exceeded ${timeoutMs}ms`)
201
+ controller.abort(error)
202
+ reject(error)
203
+ }, timeoutMs)
204
+ })
205
+ const callerAbort = callerSignal
206
+ ? new Promise<never>((_, reject) => {
207
+ const abort = () => {
208
+ controller.abort(callerSignal.reason)
209
+ reject(callerSignal.reason)
210
+ }
211
+ removeAbort = () => callerSignal.removeEventListener('abort', abort)
212
+ if (callerSignal.aborted) abort()
213
+ else callerSignal.addEventListener('abort', abort, { once: true })
214
+ })
215
+ : undefined
216
+ try {
217
+ const pending = operation(controller.signal)
218
+ return await Promise.race([pending, timeout, ...(callerAbort ? [callerAbort] : [])])
219
+ } finally {
220
+ if (timer) clearTimeout(timer)
221
+ removeAbort?.()
222
+ controller.abort()
223
+ }
224
+ }
225
+
226
+ type Capability = 'unknown' | 'supported' | 'unsupported'
227
+
228
+ /**
229
+ * One state machine for remote command ownership. A v2 peer reserves every
230
+ * command, even when the caller supplied no signal, so transport loss can be
231
+ * reconciled by identity. Legacy execution is entered only after an explicit
232
+ * unsupported response from the peer and is fenced as unknown on any failure.
233
+ */
234
+ export class RemoteExecutionController<Context = undefined> {
235
+ private capability: Capability = 'unknown'
236
+ private unsupportedError: RemoteCancellationUnsupportedError | undefined
237
+ private readonly controlRequestTimeoutMs: number
238
+ private readonly cancelConfirmTimeoutMs: number
239
+ private readonly resultDrainTimeoutMs: number
240
+ private readonly executionObservationGraceMs: number
241
+ private readonly defaultExecutionTimeoutMs: number
242
+
243
+ constructor(
244
+ private readonly adapter: RemoteExecutionAdapter<Context>,
245
+ options: RemoteExecutionControllerOptions = {},
246
+ ) {
247
+ this.controlRequestTimeoutMs = options.controlRequestTimeoutMs ?? CONTROL_REQUEST_TIMEOUT_MS
248
+ this.cancelConfirmTimeoutMs = options.cancelConfirmTimeoutMs ?? CANCEL_CONFIRM_TIMEOUT_MS
249
+ this.resultDrainTimeoutMs = options.resultDrainTimeoutMs ?? RESULT_DRAIN_TIMEOUT_MS
250
+ this.executionObservationGraceMs =
251
+ options.executionObservationGraceMs ?? EXECUTION_OBSERVATION_GRACE_MS
252
+ this.defaultExecutionTimeoutMs =
253
+ options.defaultExecutionTimeoutMs ?? DEFAULT_EXECUTION_TIMEOUT_MS
254
+ }
255
+
256
+ async exec(
257
+ command: string,
258
+ argv: string[] | undefined,
259
+ opts: SandboxExecOptions | undefined,
260
+ context?: Context,
261
+ ): Promise<SandboxExecResult> {
262
+ const startedAt = Date.now()
263
+ if (opts?.signal?.aborted) return cancelledBeforeStart(startedAt)
264
+ if (this.capability === 'unsupported') {
265
+ if (opts?.signal) {
266
+ throw (
267
+ this.unsupportedError ??
268
+ new RemoteCancellationUnsupportedError(
269
+ `${this.adapter.label} was classified as cancellation-unsupported`,
270
+ )
271
+ )
272
+ }
273
+ return await this.executeLegacy(command, argv, opts, context)
274
+ }
275
+
276
+ let reservation: RemoteReservation
277
+ try {
278
+ reservation = parseReservation(
279
+ await bounded(
280
+ `${this.adapter.label} execution reservation`,
281
+ this.controlRequestTimeoutMs,
282
+ (signal) => this.adapter.reserve(signal),
283
+ opts?.signal,
284
+ ),
285
+ )
286
+ this.capability = 'supported'
287
+ } catch (error) {
288
+ if (opts?.signal?.aborted) return cancelledBeforeStart(startedAt)
289
+ if (error instanceof RemoteCancellationUnsupportedError) {
290
+ this.capability = 'unsupported'
291
+ this.unsupportedError = error
292
+ if (opts?.signal) throw error
293
+ return await this.executeLegacy(command, argv, opts, context)
294
+ }
295
+ throw error
296
+ }
297
+
298
+ return await this.executeReserved(reservation, command, argv, opts, startedAt, context)
299
+ }
300
+
301
+ private async executeLegacy(
302
+ command: string,
303
+ argv: string[] | undefined,
304
+ opts: SandboxExecOptions | undefined,
305
+ context: Context | undefined,
306
+ ): Promise<SandboxExecResult> {
307
+ const timeoutMs = observationDeadlineMs(
308
+ opts?.timeout,
309
+ this.defaultExecutionTimeoutMs,
310
+ this.executionObservationGraceMs,
311
+ )
312
+ try {
313
+ return await bounded(
314
+ `${this.adapter.label} legacy execution observation`,
315
+ timeoutMs,
316
+ (signal) => this.adapter.execute(undefined, command, argv, opts, signal, context),
317
+ )
318
+ } catch (error) {
319
+ throw new RemoteCancellationUnknownError(
320
+ `The legacy ${this.adapter.label} execution failed after admission without an execution id: ${error instanceof Error ? error.message : String(error)}. The remote outcome is unknown and the sandbox must not be reused.`,
321
+ { cause: error },
322
+ )
323
+ }
324
+ }
325
+
326
+ private async requestCancellation(
327
+ executionId: string,
328
+ ): Promise<RemoteCancellationAcknowledgement> {
329
+ const deadlineAt = Date.now() + this.cancelConfirmTimeoutMs
330
+ let lastError: unknown = new Error('no cancellation attempt completed')
331
+ while (Date.now() < deadlineAt) {
332
+ const remaining = deadlineAt - Date.now()
333
+ try {
334
+ return parseCancellation(
335
+ await bounded(
336
+ `${this.adapter.label} cancellation attempt`,
337
+ Math.min(this.controlRequestTimeoutMs, remaining),
338
+ (signal) => this.adapter.cancel(executionId, signal),
339
+ ),
340
+ )
341
+ } catch (error) {
342
+ lastError = error
343
+ const delayMs = Math.min(50, deadlineAt - Date.now())
344
+ if (delayMs > 0) await new Promise((resolve) => setTimeout(resolve, delayMs))
345
+ }
346
+ }
347
+ throw cancellationUnknown(lastError)
348
+ }
349
+
350
+ private async drainConfirmedExecution(
351
+ pending: Promise<SandboxExecResult>,
352
+ cancelled: RemoteCancellationAcknowledgement,
353
+ startedAt: number,
354
+ ): Promise<SandboxExecResult> {
355
+ if (!cancelled.started) return resultFromAcknowledgement(cancelled, startedAt)
356
+ try {
357
+ return await bounded(
358
+ `${this.adapter.label} terminal-result drain`,
359
+ this.resultDrainTimeoutMs,
360
+ async () => await pending,
361
+ )
362
+ } catch (error) {
363
+ if (error instanceof RemoteCommandError && cancelled.state === 'failed') throw error
364
+ throw resultIncomplete(error, cancelled)
365
+ }
366
+ }
367
+
368
+ private async executeReserved(
369
+ reservation: RemoteReservation,
370
+ command: string,
371
+ argv: string[] | undefined,
372
+ opts: SandboxExecOptions | undefined,
373
+ startedAt: number,
374
+ context: Context | undefined,
375
+ ): Promise<SandboxExecResult> {
376
+ if (opts?.signal?.aborted) {
377
+ const cancelled = await this.requestCancellation(reservation.executionId)
378
+ if (cancelled.started) {
379
+ throw resultIncomplete(
380
+ new Error('the peer reported an admitted command before execute was sent'),
381
+ cancelled,
382
+ )
383
+ }
384
+ return resultFromAcknowledgement(cancelled, startedAt)
385
+ }
386
+
387
+ type CancellationReason = 'caller' | 'observation-timeout' | 'reconcile'
388
+ let cancellationReason: CancellationReason | undefined
389
+ let startCancellation: ((reason: CancellationReason) => void) | undefined
390
+ const cancellation = new Promise<RemoteCancellationAcknowledgement>((resolve, reject) => {
391
+ let started = false
392
+ startCancellation = (reason) => {
393
+ if (started) return
394
+ started = true
395
+ cancellationReason = reason
396
+ void this.requestCancellation(reservation.executionId).then(resolve, reject)
397
+ }
398
+ })
399
+ const onAbort = () => startCancellation?.('caller')
400
+ opts?.signal?.addEventListener('abort', onAbort, { once: true })
401
+ if (opts?.signal?.aborted) onAbort()
402
+ const observationTimer = setTimeout(
403
+ () => startCancellation?.('observation-timeout'),
404
+ observationDeadlineMs(
405
+ opts?.timeout,
406
+ this.defaultExecutionTimeoutMs,
407
+ this.executionObservationGraceMs,
408
+ ),
409
+ )
410
+ observationTimer.unref?.()
411
+
412
+ const executionTransport = new AbortController()
413
+ const execution = this.adapter.execute(
414
+ reservation.executionId,
415
+ command,
416
+ argv,
417
+ opts,
418
+ executionTransport.signal,
419
+ context,
420
+ )
421
+ const observedExecution = execution.then(
422
+ (result) => ({ kind: 'result' as const, result }),
423
+ (error: unknown) => ({ kind: 'error' as const, error }),
424
+ )
425
+ const observedCancellation = cancellation.then(
426
+ (result) => ({ kind: 'cancelled' as const, result }),
427
+ (error: unknown) => ({ kind: 'cancel-error' as const, error }),
428
+ )
429
+
430
+ try {
431
+ const first = await Promise.race([observedExecution, observedCancellation])
432
+ if (first.kind === 'result') return first.result
433
+ if (first.kind === 'cancel-error') throw first.error
434
+ if (first.kind === 'cancelled') {
435
+ const result = await this.drainConfirmedExecution(execution, first.result, startedAt)
436
+ return cancellationReason === 'observation-timeout' && first.result.state === 'cancelled'
437
+ ? { ...result, timedOut: true }
438
+ : result
439
+ }
440
+
441
+ startCancellation?.('reconcile')
442
+ let confirmed: RemoteCancellationAcknowledgement
443
+ try {
444
+ confirmed = await cancellation
445
+ } catch (cancelError) {
446
+ throw cancellationUnknown(cancelError)
447
+ }
448
+ if (first.error instanceof RemoteCommandError && confirmed.state === 'failed') {
449
+ throw first.error
450
+ }
451
+ if (opts?.signal?.aborted && confirmed.state === 'cancelled' && !confirmed.started) {
452
+ return resultFromAcknowledgement(confirmed, startedAt)
453
+ }
454
+ if (!confirmed.started) throw first.error
455
+ throw resultIncomplete(first.error, confirmed)
456
+ } finally {
457
+ clearTimeout(observationTimer)
458
+ opts?.signal?.removeEventListener('abort', onAbort)
459
+ executionTransport.abort(new Error('remote sandbox execution observation finished'))
460
+ }
461
+ }
462
+ }
@@ -121,6 +121,11 @@ export class EgressProxy {
121
121
  const server = createServer((req, res) => {
122
122
  void this.handleRequest(req, res)
123
123
  })
124
+ const connections = new Set<Duplex>()
125
+ server.on('connection', (socket) => {
126
+ connections.add(socket)
127
+ socket.once('close', () => connections.delete(socket))
128
+ })
124
129
  server.on('connect', (req, socket, head) => {
125
130
  void this.handleConnect(req, socket, head)
126
131
  })
@@ -144,6 +149,7 @@ export class EgressProxy {
144
149
  // process runs out. Found by a test that hung rather than failed,
145
150
  // which is the shape this failure takes in production too.
146
151
  this.selfPort = boundPort
152
+ let closePromise: Promise<void> | undefined
147
153
 
148
154
  return {
149
155
  port: boundPort,
@@ -151,10 +157,16 @@ export class EgressProxy {
151
157
  setAllowedHosts: (resolve) => {
152
158
  this.resolveAllowed = resolve
153
159
  },
154
- close: () =>
155
- new Promise<void>((resolve) => {
160
+ close: () => {
161
+ closePromise ??= new Promise<void>((resolve) => {
162
+ // `server.close()` waits for existing connections. A readiness
163
+ // failure has no returned Sandbox handle through which those
164
+ // sockets can later be retired, so teardown owns them here.
165
+ for (const socket of connections) socket.destroy()
156
166
  server.close(() => resolve())
157
- }),
167
+ })
168
+ return closePromise
169
+ },
158
170
  }
159
171
  }
160
172
 
@@ -346,6 +358,9 @@ export class EgressProxy {
346
358
  socket.on('error', () => {
347
359
  upstream.destroy()
348
360
  })
361
+ socket.once('close', () => {
362
+ upstream.destroy()
363
+ })
349
364
  }
350
365
 
351
366
  /** Whether a target names this proxy. See the loop guard in `listen`. */
package/src/index.ts CHANGED
@@ -155,7 +155,9 @@ export interface ACIStandbyPoolBackendConfig {
155
155
  */
156
156
  readonly getArmToken: () => Promise<string>
157
157
  readonly subnetId?: string
158
+ /** Delay between IP / health probes. Default 500ms. */
158
159
  readonly readyPollIntervalMs?: number
160
+ /** Total deadline across IP publication and worker health. Default 60000ms. */
159
161
  readonly readyTimeoutMs?: number
160
162
  readonly workerPort?: number
161
163
  readonly armApiVersion?: string
@@ -218,6 +220,18 @@ export interface ContainerBackendConfig {
218
220
  * which is checked against this network rather than trusted.
219
221
  */
220
222
  readonly network?: 'none' | 'bridge' | string
223
+ /**
224
+ * Maximum time spent waiting for the container worker's `/healthz`
225
+ * readiness probe. Must be a positive integer within Node's timer range.
226
+ * Default 30000ms.
227
+ */
228
+ readonly readyTimeoutMs?: number
229
+ /**
230
+ * Delay between worker readiness probes. Must be a positive integer within
231
+ * Node's timer range; the final delay is capped by `readyTimeoutMs`.
232
+ * Default 100ms.
233
+ */
234
+ readonly readyPollIntervalMs?: number
221
235
  /**
222
236
  * Allowlisted hosts permitted to resolve to an inward address anyway.
223
237
  *
@@ -297,7 +311,9 @@ export type MicroVMBackendConfig = {
297
311
  readonly agentSnapshot?: AgentSnapshotRef
298
312
  /** Fixed guest AF_VSOCK port the in-VM agent listens on. */
299
313
  readonly agentVsockPort?: number
314
+ /** Total guest-agent health deadline after the orchestrator claim. Default 60000ms. */
300
315
  readonly readyTimeoutMs?: number
316
+ /** Delay between guest-agent health probes. Default 250ms. */
301
317
  readonly readyPollIntervalMs?: number
302
318
  /**
303
319
  * NETWORK-mode mTLS client material (ses_051 P4 client-proxy
@@ -453,6 +469,8 @@ export interface SandboxBackend {
453
469
  * shape.
454
470
  */
455
471
  export interface SandboxBackendOptions {
472
+ /** Run authority for allocation/readiness. See `SandboxCreateConfig.signal`. */
473
+ readonly signal?: AbortSignal
456
474
  readonly workingDirectory: string
457
475
  readonly egress?: EgressPolicy
458
476
  readonly timeoutMs?: number
@@ -531,6 +549,7 @@ export function createSandboxProvider(config: SandboxProviderConfig): SandboxPro
531
549
  environment: 'basic',
532
550
  async create(perCall?: SandboxCreateConfig): Promise<Sandbox> {
533
551
  return await backend.create({
552
+ ...(perCall?.signal !== undefined ? { signal: perCall.signal } : {}),
534
553
  workingDirectory: perCall?.workingDirectory ?? '/workspace',
535
554
  ...(config.defaultEgress !== undefined ? { egress: config.defaultEgress } : {}),
536
555
  ...(perCall?.timeoutMs !== undefined
@@ -569,6 +588,10 @@ function pickBackend(config: SandboxProviderConfig): SandboxBackend {
569
588
  return buildDockerBackend({
570
589
  image: backend.image,
571
590
  layout: resolved,
591
+ ...(backend.readyTimeoutMs !== undefined ? { readyTimeoutMs: backend.readyTimeoutMs } : {}),
592
+ ...(backend.readyPollIntervalMs !== undefined
593
+ ? { readyPollIntervalMs: backend.readyPollIntervalMs }
594
+ : {}),
572
595
  ...(backend.hostReachability !== undefined
573
596
  ? { hostReachability: backend.hostReachability }
574
597
  : {}),
@@ -620,6 +643,10 @@ function pickBackend(config: SandboxProviderConfig): SandboxBackend {
620
643
  image: backend.image,
621
644
  layout: resolved,
622
645
  runtime: 'runsc',
646
+ ...(backend.readyTimeoutMs !== undefined ? { readyTimeoutMs: backend.readyTimeoutMs } : {}),
647
+ ...(backend.readyPollIntervalMs !== undefined
648
+ ? { readyPollIntervalMs: backend.readyPollIntervalMs }
649
+ : {}),
623
650
  ...(backend.hostReachability !== undefined
624
651
  ? { hostReachability: backend.hostReachability }
625
652
  : {}),