@namzu/sandbox 7.0.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 (35) hide show
  1. package/CHANGELOG.md +32 -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 +95 -82
  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 +108 -114
  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 +117 -52
  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 +23 -4
  17. package/dist/backends/firecracker/transport.d.ts.map +1 -1
  18. package/dist/backends/firecracker/transport.js +153 -32
  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/remote-execution-controller.d.ts +76 -0
  25. package/dist/backends/remote-execution-controller.d.ts.map +1 -0
  26. package/dist/backends/remote-execution-controller.js +294 -0
  27. package/dist/backends/remote-execution-controller.js.map +1 -0
  28. package/package.json +2 -2
  29. package/src/backends/aci-standby-pool/index.ts +101 -88
  30. package/src/backends/docker/index.ts +101 -132
  31. package/src/backends/firecracker/index.ts +117 -45
  32. package/src/backends/firecracker/protocol.ts +67 -33
  33. package/src/backends/firecracker/transport.ts +214 -39
  34. package/src/backends/http-worker-client.ts +230 -0
  35. package/src/backends/remote-execution-controller.ts +462 -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
+ }