@namzu/sandbox 24.0.0 → 25.0.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 (158) hide show
  1. package/CHANGELOG.md +274 -0
  2. package/dist/backends/http-worker-client.d.ts +3 -1
  3. package/dist/backends/http-worker-client.d.ts.map +1 -1
  4. package/dist/backends/http-worker-client.js +15 -4
  5. package/dist/backends/http-worker-client.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/local-virtual-computer/client.d.ts +21 -0
  11. package/dist/local-virtual-computer/client.d.ts.map +1 -0
  12. package/dist/local-virtual-computer/client.js +503 -0
  13. package/dist/local-virtual-computer/client.js.map +1 -0
  14. package/dist/local-virtual-computer/detached-process.d.ts +15 -0
  15. package/dist/local-virtual-computer/detached-process.d.ts.map +1 -0
  16. package/dist/local-virtual-computer/detached-process.js +111 -0
  17. package/dist/local-virtual-computer/detached-process.js.map +1 -0
  18. package/dist/local-virtual-computer/detached-worker.d.ts +11 -0
  19. package/dist/local-virtual-computer/detached-worker.d.ts.map +1 -0
  20. package/dist/local-virtual-computer/detached-worker.js +97 -0
  21. package/dist/local-virtual-computer/detached-worker.js.map +1 -0
  22. package/dist/local-virtual-computer/dialect.d.ts +26 -0
  23. package/dist/local-virtual-computer/dialect.d.ts.map +1 -0
  24. package/dist/local-virtual-computer/dialect.js +61 -0
  25. package/dist/local-virtual-computer/dialect.js.map +1 -0
  26. package/dist/local-virtual-computer/engine.d.ts +63 -0
  27. package/dist/local-virtual-computer/engine.d.ts.map +1 -0
  28. package/dist/local-virtual-computer/engine.js +125 -0
  29. package/dist/local-virtual-computer/engine.js.map +1 -0
  30. package/dist/local-virtual-computer/guest-stdio.d.ts +15 -0
  31. package/dist/local-virtual-computer/guest-stdio.d.ts.map +1 -0
  32. package/dist/local-virtual-computer/guest-stdio.js +330 -0
  33. package/dist/local-virtual-computer/guest-stdio.js.map +1 -0
  34. package/dist/local-virtual-computer/index.d.ts +36 -0
  35. package/dist/local-virtual-computer/index.d.ts.map +1 -0
  36. package/dist/local-virtual-computer/index.js +341 -0
  37. package/dist/local-virtual-computer/index.js.map +1 -0
  38. package/dist/local-virtual-computer/podman-machine.d.ts +17 -0
  39. package/dist/local-virtual-computer/podman-machine.d.ts.map +1 -0
  40. package/dist/local-virtual-computer/podman-machine.js +181 -0
  41. package/dist/local-virtual-computer/podman-machine.js.map +1 -0
  42. package/dist/local-virtual-computer/setup/contract.d.ts +360 -0
  43. package/dist/local-virtual-computer/setup/contract.d.ts.map +1 -0
  44. package/dist/local-virtual-computer/setup/contract.js +3 -0
  45. package/dist/local-virtual-computer/setup/contract.js.map +1 -0
  46. package/dist/local-virtual-computer/setup/elevated.d.ts +78 -0
  47. package/dist/local-virtual-computer/setup/elevated.d.ts.map +1 -0
  48. package/dist/local-virtual-computer/setup/elevated.js +548 -0
  49. package/dist/local-virtual-computer/setup/elevated.js.map +1 -0
  50. package/dist/local-virtual-computer/setup/host.d.ts +12 -0
  51. package/dist/local-virtual-computer/setup/host.d.ts.map +1 -0
  52. package/dist/local-virtual-computer/setup/host.js +93 -0
  53. package/dist/local-virtual-computer/setup/host.js.map +1 -0
  54. package/dist/local-virtual-computer/setup/idle.d.ts +18 -0
  55. package/dist/local-virtual-computer/setup/idle.d.ts.map +1 -0
  56. package/dist/local-virtual-computer/setup/idle.js +38 -0
  57. package/dist/local-virtual-computer/setup/idle.js.map +1 -0
  58. package/dist/local-virtual-computer/setup/image.d.ts +7 -0
  59. package/dist/local-virtual-computer/setup/image.d.ts.map +1 -0
  60. package/dist/local-virtual-computer/setup/image.js +124 -0
  61. package/dist/local-virtual-computer/setup/image.js.map +1 -0
  62. package/dist/local-virtual-computer/setup/index.d.ts +17 -0
  63. package/dist/local-virtual-computer/setup/index.d.ts.map +1 -0
  64. package/dist/local-virtual-computer/setup/index.js +17 -0
  65. package/dist/local-virtual-computer/setup/index.js.map +1 -0
  66. package/dist/local-virtual-computer/setup/limits.d.ts +30 -0
  67. package/dist/local-virtual-computer/setup/limits.d.ts.map +1 -0
  68. package/dist/local-virtual-computer/setup/limits.js +58 -0
  69. package/dist/local-virtual-computer/setup/limits.js.map +1 -0
  70. package/dist/local-virtual-computer/setup/plan.d.ts +8 -0
  71. package/dist/local-virtual-computer/setup/plan.d.ts.map +1 -0
  72. package/dist/local-virtual-computer/setup/plan.js +131 -0
  73. package/dist/local-virtual-computer/setup/plan.js.map +1 -0
  74. package/dist/local-virtual-computer/setup/readiness.d.ts +11 -0
  75. package/dist/local-virtual-computer/setup/readiness.d.ts.map +1 -0
  76. package/dist/local-virtual-computer/setup/readiness.js +320 -0
  77. package/dist/local-virtual-computer/setup/readiness.js.map +1 -0
  78. package/dist/local-virtual-computer/setup/registry.d.ts +18 -0
  79. package/dist/local-virtual-computer/setup/registry.d.ts.map +1 -0
  80. package/dist/local-virtual-computer/setup/registry.js +50 -0
  81. package/dist/local-virtual-computer/setup/registry.js.map +1 -0
  82. package/dist/local-virtual-computer/setup/restart.d.ts +8 -0
  83. package/dist/local-virtual-computer/setup/restart.d.ts.map +1 -0
  84. package/dist/local-virtual-computer/setup/restart.js +60 -0
  85. package/dist/local-virtual-computer/setup/restart.js.map +1 -0
  86. package/dist/local-virtual-computer/setup/resume.d.ts +14 -0
  87. package/dist/local-virtual-computer/setup/resume.d.ts.map +1 -0
  88. package/dist/local-virtual-computer/setup/resume.js +58 -0
  89. package/dist/local-virtual-computer/setup/resume.js.map +1 -0
  90. package/dist/local-virtual-computer/setup/run.d.ts +8 -0
  91. package/dist/local-virtual-computer/setup/run.d.ts.map +1 -0
  92. package/dist/local-virtual-computer/setup/run.js +200 -0
  93. package/dist/local-virtual-computer/setup/run.js.map +1 -0
  94. package/dist/local-virtual-computer/setup/windows-folder.d.ts +11 -0
  95. package/dist/local-virtual-computer/setup/windows-folder.d.ts.map +1 -0
  96. package/dist/local-virtual-computer/setup/windows-folder.js +14 -0
  97. package/dist/local-virtual-computer/setup/windows-folder.js.map +1 -0
  98. package/dist/local-virtual-computer/setup/wsl-package.d.ts +19 -0
  99. package/dist/local-virtual-computer/setup/wsl-package.d.ts.map +1 -0
  100. package/dist/local-virtual-computer/setup/wsl-package.js +242 -0
  101. package/dist/local-virtual-computer/setup/wsl-package.js.map +1 -0
  102. package/dist/local-virtual-computer/setup/wslc-shared.d.ts +27 -0
  103. package/dist/local-virtual-computer/setup/wslc-shared.d.ts.map +1 -0
  104. package/dist/local-virtual-computer/setup/wslc-shared.js +137 -0
  105. package/dist/local-virtual-computer/setup/wslc-shared.js.map +1 -0
  106. package/dist/local-virtual-computer/ssh-forward.d.ts +19 -0
  107. package/dist/local-virtual-computer/ssh-forward.d.ts.map +1 -0
  108. package/dist/local-virtual-computer/ssh-forward.js +179 -0
  109. package/dist/local-virtual-computer/ssh-forward.js.map +1 -0
  110. package/dist/local-virtual-computer/wslc-engine.d.ts +27 -0
  111. package/dist/local-virtual-computer/wslc-engine.d.ts.map +1 -0
  112. package/dist/local-virtual-computer/wslc-engine.js +98 -0
  113. package/dist/local-virtual-computer/wslc-engine.js.map +1 -0
  114. package/local-computer/Dockerfile +99 -0
  115. package/local-computer/app-catalog.cjs +240 -0
  116. package/local-computer/app-native-host.cjs +144 -0
  117. package/local-computer/browser-home.cjs +249 -0
  118. package/local-computer/build-image.mjs +310 -0
  119. package/local-computer/desktop-worker.cjs +477 -0
  120. package/local-computer/entrypoint.sh +161 -0
  121. package/local-computer/home.html +28 -0
  122. package/local-computer/new-tab/home.css +255 -0
  123. package/local-computer/new-tab/home.js +65 -0
  124. package/local-computer/new-tab/manifest.json +12 -0
  125. package/local-computer/new-tab/wordmark.svg +33 -0
  126. package/local-computer/openbox-rc.xml +41 -0
  127. package/local-computer/prepare-home.cjs +121 -0
  128. package/local-computer/tint2rc +44 -0
  129. package/package.json +13 -4
  130. package/src/backends/http-worker-client.ts +31 -3
  131. package/src/index.ts +13 -0
  132. package/src/local-virtual-computer/client.ts +573 -0
  133. package/src/local-virtual-computer/detached-process.ts +133 -0
  134. package/src/local-virtual-computer/detached-worker.ts +124 -0
  135. package/src/local-virtual-computer/dialect.ts +83 -0
  136. package/src/local-virtual-computer/engine.ts +197 -0
  137. package/src/local-virtual-computer/guest-stdio.ts +332 -0
  138. package/src/local-virtual-computer/index.ts +447 -0
  139. package/src/local-virtual-computer/podman-machine.ts +264 -0
  140. package/src/local-virtual-computer/setup/contract.ts +395 -0
  141. package/src/local-virtual-computer/setup/elevated.ts +655 -0
  142. package/src/local-virtual-computer/setup/host.ts +100 -0
  143. package/src/local-virtual-computer/setup/idle.ts +39 -0
  144. package/src/local-virtual-computer/setup/image.ts +159 -0
  145. package/src/local-virtual-computer/setup/index.ts +16 -0
  146. package/src/local-virtual-computer/setup/limits.ts +68 -0
  147. package/src/local-virtual-computer/setup/plan.ts +144 -0
  148. package/src/local-virtual-computer/setup/readiness.ts +386 -0
  149. package/src/local-virtual-computer/setup/registry.ts +50 -0
  150. package/src/local-virtual-computer/setup/restart.ts +64 -0
  151. package/src/local-virtual-computer/setup/resume.ts +65 -0
  152. package/src/local-virtual-computer/setup/run.ts +225 -0
  153. package/src/local-virtual-computer/setup/windows-folder.ts +13 -0
  154. package/src/local-virtual-computer/setup/wsl-package.ts +242 -0
  155. package/src/local-virtual-computer/setup/wslc-shared.ts +147 -0
  156. package/src/local-virtual-computer/ssh-forward.ts +195 -0
  157. package/src/local-virtual-computer/wslc-engine.ts +124 -0
  158. package/worker/server.js +1499 -0
@@ -0,0 +1,332 @@
1
+ import type {
2
+ SandboxStdioChannel,
3
+ SandboxStdioEvent,
4
+ SandboxStdioOperation,
5
+ SandboxStdioOptions,
6
+ } from '@namzu/sdk'
7
+ import { workerAuthorization } from '../backends/http-worker-client.js'
8
+
9
+ interface GuestStdioHost {
10
+ readonly executionUrl: string
11
+ readonly token: string
12
+ readonly acquire: (signal?: AbortSignal) => Promise<() => void>
13
+ readonly uncertain: () => void
14
+ }
15
+
16
+ const MAX_FRAME_BYTES = 2 * 1024 * 1024
17
+ const MAX_QUEUED_BYTES = 4 * 1024 * 1024
18
+
19
+ export interface OwnedGuestStdioChannel extends SandboxStdioChannel {
20
+ /** Host-only: the owning engine has positively confirmed allocation removal. */
21
+ allocationEnded(): void
22
+ }
23
+
24
+ /** Private allocation token stays inside this host channel, never in guest argv/env. */
25
+ export async function openGuestStdio(
26
+ host: GuestStdioHost,
27
+ command: string,
28
+ args: readonly string[],
29
+ options?: SandboxStdioOptions,
30
+ ): Promise<OwnedGuestStdioChannel> {
31
+ if (!command || typeof command !== 'string' || args.some((arg) => typeof arg !== 'string'))
32
+ throw new Error('Guest stdio requires a command and literal argv')
33
+ const headers = { 'content-type': 'application/json', ...workerAuthorization(host.token) }
34
+ const post = async (route: string, body: unknown, signal?: AbortSignal) => {
35
+ const response = await fetch(`${host.executionUrl}${route}`, {
36
+ method: 'POST',
37
+ headers,
38
+ body: JSON.stringify(body),
39
+ signal: signal
40
+ ? AbortSignal.any([signal, AbortSignal.timeout(30_000)])
41
+ : AbortSignal.timeout(30_000),
42
+ })
43
+ if (!response.ok) throw new Error(`Guest stdio request refused (${response.status})`)
44
+ return (await response.json()) as Record<string, unknown>
45
+ }
46
+ const reservation = await post('/executions/reserve', {}, options?.signal)
47
+ const executionId = reservation.executionId
48
+ if (typeof executionId !== 'string' || !/^exec_[0-9a-f-]{36}$/.test(executionId))
49
+ throw new Error('Guest did not return an execution reservation')
50
+ const cancel = async () => {
51
+ const result = await post('/cancel', { executionId })
52
+ if (result.ok !== true || !['completed', 'cancelled', 'failed'].includes(String(result.state)))
53
+ throw new Error('Guest stdio process-group termination was not confirmed')
54
+ }
55
+ const capability = reservation.stdio as { version?: unknown; maxLifetimeMs?: unknown } | undefined
56
+ if (
57
+ capability?.version !== 1 ||
58
+ !Number.isSafeInteger(capability.maxLifetimeMs) ||
59
+ typeof capability.maxLifetimeMs !== 'number' ||
60
+ capability.maxLifetimeMs < 1 ||
61
+ capability.maxLifetimeMs > 2_147_000_000
62
+ ) {
63
+ await cancel()
64
+ throw new Error(
65
+ 'This guest worker does not support owned interactive stdio; rebuild the Pal image',
66
+ )
67
+ }
68
+ const controller = new AbortController()
69
+ let confirmed = false
70
+ let closing: Promise<void> | undefined
71
+ let terminal = false
72
+ let failure: Error | undefined
73
+ let resolveClosed!: () => void
74
+ let rejectClosed!: (error: Error) => void
75
+ const closed = new Promise<void>((resolve, reject) => {
76
+ resolveClosed = resolve
77
+ rejectClosed = reject
78
+ })
79
+ // An embedding transport may attach only after connect resolves.
80
+ void closed.catch(() => {})
81
+ const operations = new Set<{ unknown: () => void }>()
82
+ const unknown = () => {
83
+ host.uncertain()
84
+ for (const operation of operations) operation.unknown()
85
+ }
86
+ const queue: SandboxStdioEvent[] = []
87
+ let queuedBytes = 0
88
+ let consumerWake: (() => void) | undefined
89
+ let producerWake: (() => void) | undefined
90
+ let claimed = false
91
+ const finish = (error?: Error) => {
92
+ if (terminal) return
93
+ terminal = true
94
+ failure = error
95
+ options?.signal?.removeEventListener('abort', onAbort)
96
+ consumerWake?.()
97
+ producerWake?.()
98
+ if (error) rejectClosed(error)
99
+ else resolveClosed()
100
+ }
101
+ const close = (): Promise<void> => {
102
+ if (confirmed) return Promise.resolve()
103
+ if (closing) return closing
104
+ // Closing MCP does not prove an external editor command stopped.
105
+ if (operations.size) unknown()
106
+ closing = cancel()
107
+ .then(() => {
108
+ confirmed = true
109
+ controller.abort()
110
+ finish()
111
+ })
112
+ .catch((error: unknown) => {
113
+ unknown()
114
+ closing = undefined
115
+ throw error
116
+ })
117
+ return closing
118
+ }
119
+ const onAbort = () => {
120
+ void close().catch(() => {})
121
+ }
122
+ let response: Response
123
+ const handshakeTimeout = setTimeout(
124
+ () => controller.abort(new Error('Guest stdio startup timed out')),
125
+ 30_000,
126
+ )
127
+ try {
128
+ options?.signal?.throwIfAborted()
129
+ await options?.assertExecutionAllowed?.()
130
+ options?.signal?.throwIfAborted()
131
+ response = await fetch(`${host.executionUrl}/execute`, {
132
+ method: 'POST',
133
+ headers,
134
+ body: JSON.stringify({
135
+ executionId,
136
+ command,
137
+ args,
138
+ cwd: options?.cwd,
139
+ env: options?.env,
140
+ stdio: true,
141
+ timeoutMs: capability.maxLifetimeMs,
142
+ normalExitPolicy: 'strict',
143
+ }),
144
+ signal: AbortSignal.any([controller.signal, ...(options?.signal ? [options.signal] : [])]),
145
+ })
146
+ if (!response.ok || !response.body) throw new Error('Guest stdio admission was not confirmed')
147
+ } catch (error) {
148
+ clearTimeout(handshakeTimeout)
149
+ try {
150
+ await close()
151
+ } catch {
152
+ host.uncertain()
153
+ }
154
+ throw error
155
+ }
156
+ const frames = readFrames(response.body)
157
+ try {
158
+ const first = await frames.next()
159
+ if (first.done || first.value.type !== 'stdio_started' || first.value.version !== 1)
160
+ throw new Error('Guest stdio startup acknowledgement is missing')
161
+ } catch (error) {
162
+ try {
163
+ await close()
164
+ } catch {
165
+ host.uncertain()
166
+ }
167
+ throw error
168
+ } finally {
169
+ clearTimeout(handshakeTimeout)
170
+ }
171
+ options?.signal?.addEventListener('abort', onAbort, { once: true })
172
+ if (options?.signal?.aborted) onAbort()
173
+ void (async () => {
174
+ try {
175
+ for await (const frame of frames) {
176
+ if (frame.type === 'stdio_heartbeat') {
177
+ if (frame.version !== 1 || Object.keys(frame).length !== 2)
178
+ throw new Error('Invalid guest stdio heartbeat')
179
+ // Transport liveness is not stdout, an RPC reply or device admission.
180
+ continue
181
+ }
182
+ if (frame.type === 'result') {
183
+ confirmed = true
184
+ // The consumer may still have an exact reply in its queue. Its RPC
185
+ // adapter settles that reply before classifying remaining EOF calls.
186
+ finish()
187
+ return
188
+ }
189
+ if (
190
+ frame.type !== 'stdio_data' ||
191
+ !['stdout', 'stderr'].includes(String(frame.stream)) ||
192
+ typeof frame.data !== 'string'
193
+ )
194
+ throw new Error('Invalid guest stdio stream frame')
195
+ const bytes = Buffer.from(frame.data, 'base64')
196
+ if (bytes.toString('base64') !== frame.data)
197
+ throw new Error('Invalid guest stdio stream bytes')
198
+ while (!terminal && queuedBytes >= MAX_QUEUED_BYTES)
199
+ await new Promise<void>((resolve) => {
200
+ producerWake = resolve
201
+ })
202
+ if (terminal) return
203
+ queue.push({ stream: frame.stream as 'stdout' | 'stderr', data: bytes })
204
+ queuedBytes += bytes.length
205
+ consumerWake?.()
206
+ }
207
+ if (!confirmed) throw new Error('Guest stdio stream ended without process-group confirmation')
208
+ } catch (error) {
209
+ if (confirmed) return
210
+ if (operations.size) unknown()
211
+ try {
212
+ await cancel()
213
+ confirmed = true
214
+ } catch {
215
+ unknown()
216
+ }
217
+ finish(error instanceof Error ? error : new Error('Guest stdio stream failed'))
218
+ }
219
+ })()
220
+ return {
221
+ closed,
222
+ close,
223
+ allocationEnded() {
224
+ confirmed = true
225
+ controller.abort()
226
+ finish()
227
+ },
228
+ events: {
229
+ async *[Symbol.asyncIterator]() {
230
+ if (claimed) throw new Error('Guest stdio supports one stream consumer')
231
+ claimed = true
232
+ while (true) {
233
+ if (queue.length) {
234
+ const event = queue.shift()
235
+ if (!event) continue
236
+ queuedBytes -= event.data.length
237
+ producerWake?.()
238
+ yield event
239
+ } else if (terminal) {
240
+ if (failure) throw failure
241
+ return
242
+ } else
243
+ await new Promise<void>((resolve) => {
244
+ consumerWake = resolve
245
+ })
246
+ }
247
+ },
248
+ },
249
+ async beginOperation(signal): Promise<SandboxStdioOperation> {
250
+ if (terminal || closing) throw new Error('Guest stdio channel has ended')
251
+ const release = await host.acquire(signal)
252
+ if (terminal || closing) {
253
+ release()
254
+ throw new Error('Guest stdio channel has ended')
255
+ }
256
+ let ended = false
257
+ const operation = {
258
+ unknown: () => {
259
+ if (!ended) {
260
+ ended = true
261
+ host.uncertain()
262
+ operations.delete(operation)
263
+ }
264
+ },
265
+ }
266
+ operations.add(operation)
267
+ return {
268
+ complete() {
269
+ if (!ended) {
270
+ ended = true
271
+ operations.delete(operation)
272
+ release()
273
+ }
274
+ },
275
+ outcomeUnknown: operation.unknown,
276
+ }
277
+ },
278
+ async write(data, signal) {
279
+ if (terminal || closing) throw new Error('Guest stdio channel has ended')
280
+ const bytes = Buffer.from(data)
281
+ if (bytes.length > 1024 * 1024)
282
+ throw new Error('Guest stdio write exceeds 1 MiB; split the byte stream')
283
+ const release = await host.acquire(signal)
284
+ let issued = false
285
+ try {
286
+ if (terminal || closing) throw new Error('Guest stdio channel has ended')
287
+ signal?.throwIfAborted()
288
+ issued = true
289
+ const result = await post(
290
+ '/executions/write',
291
+ { executionId, data: bytes.toString('base64') },
292
+ signal,
293
+ )
294
+ if (result.ok !== true || result.bytesWritten !== bytes.length)
295
+ throw new Error('Guest stdin delivery was not confirmed')
296
+ } catch (error) {
297
+ if (issued) unknown()
298
+ throw error
299
+ } finally {
300
+ release()
301
+ }
302
+ },
303
+ }
304
+ }
305
+
306
+ async function* readFrames(
307
+ body: ReadableStream<Uint8Array>,
308
+ ): AsyncGenerator<Record<string, unknown>> {
309
+ const reader = body.getReader()
310
+ let pending = ''
311
+ const decoder = new TextDecoder()
312
+ try {
313
+ while (true) {
314
+ const next = await reader.read()
315
+ if (next.done) break
316
+ pending += decoder.decode(next.value, { stream: true })
317
+ let newline = pending.indexOf('\n')
318
+ while (newline >= 0) {
319
+ if (newline > MAX_FRAME_BYTES) throw new Error('Guest stdio frame exceeds its bound')
320
+ const frame = JSON.parse(pending.slice(0, newline)) as Record<string, unknown>
321
+ pending = pending.slice(newline + 1)
322
+ newline = pending.indexOf('\n')
323
+ if (!frame || typeof frame !== 'object') throw new Error('Invalid guest stdio frame')
324
+ yield frame
325
+ }
326
+ if (pending.length > MAX_FRAME_BYTES) throw new Error('Guest stdio frame exceeds its bound')
327
+ }
328
+ if (pending.trim()) throw new Error('Truncated guest stdio frame')
329
+ } finally {
330
+ reader.releaseLock()
331
+ }
332
+ }