@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.
- package/CHANGELOG.md +274 -0
- package/dist/backends/http-worker-client.d.ts +3 -1
- package/dist/backends/http-worker-client.d.ts.map +1 -1
- package/dist/backends/http-worker-client.js +15 -4
- package/dist/backends/http-worker-client.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/local-virtual-computer/client.d.ts +21 -0
- package/dist/local-virtual-computer/client.d.ts.map +1 -0
- package/dist/local-virtual-computer/client.js +503 -0
- package/dist/local-virtual-computer/client.js.map +1 -0
- package/dist/local-virtual-computer/detached-process.d.ts +15 -0
- package/dist/local-virtual-computer/detached-process.d.ts.map +1 -0
- package/dist/local-virtual-computer/detached-process.js +111 -0
- package/dist/local-virtual-computer/detached-process.js.map +1 -0
- package/dist/local-virtual-computer/detached-worker.d.ts +11 -0
- package/dist/local-virtual-computer/detached-worker.d.ts.map +1 -0
- package/dist/local-virtual-computer/detached-worker.js +97 -0
- package/dist/local-virtual-computer/detached-worker.js.map +1 -0
- package/dist/local-virtual-computer/dialect.d.ts +26 -0
- package/dist/local-virtual-computer/dialect.d.ts.map +1 -0
- package/dist/local-virtual-computer/dialect.js +61 -0
- package/dist/local-virtual-computer/dialect.js.map +1 -0
- package/dist/local-virtual-computer/engine.d.ts +63 -0
- package/dist/local-virtual-computer/engine.d.ts.map +1 -0
- package/dist/local-virtual-computer/engine.js +125 -0
- package/dist/local-virtual-computer/engine.js.map +1 -0
- package/dist/local-virtual-computer/guest-stdio.d.ts +15 -0
- package/dist/local-virtual-computer/guest-stdio.d.ts.map +1 -0
- package/dist/local-virtual-computer/guest-stdio.js +330 -0
- package/dist/local-virtual-computer/guest-stdio.js.map +1 -0
- package/dist/local-virtual-computer/index.d.ts +36 -0
- package/dist/local-virtual-computer/index.d.ts.map +1 -0
- package/dist/local-virtual-computer/index.js +341 -0
- package/dist/local-virtual-computer/index.js.map +1 -0
- package/dist/local-virtual-computer/podman-machine.d.ts +17 -0
- package/dist/local-virtual-computer/podman-machine.d.ts.map +1 -0
- package/dist/local-virtual-computer/podman-machine.js +181 -0
- package/dist/local-virtual-computer/podman-machine.js.map +1 -0
- package/dist/local-virtual-computer/setup/contract.d.ts +360 -0
- package/dist/local-virtual-computer/setup/contract.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/contract.js +3 -0
- package/dist/local-virtual-computer/setup/contract.js.map +1 -0
- package/dist/local-virtual-computer/setup/elevated.d.ts +78 -0
- package/dist/local-virtual-computer/setup/elevated.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/elevated.js +548 -0
- package/dist/local-virtual-computer/setup/elevated.js.map +1 -0
- package/dist/local-virtual-computer/setup/host.d.ts +12 -0
- package/dist/local-virtual-computer/setup/host.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/host.js +93 -0
- package/dist/local-virtual-computer/setup/host.js.map +1 -0
- package/dist/local-virtual-computer/setup/idle.d.ts +18 -0
- package/dist/local-virtual-computer/setup/idle.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/idle.js +38 -0
- package/dist/local-virtual-computer/setup/idle.js.map +1 -0
- package/dist/local-virtual-computer/setup/image.d.ts +7 -0
- package/dist/local-virtual-computer/setup/image.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/image.js +124 -0
- package/dist/local-virtual-computer/setup/image.js.map +1 -0
- package/dist/local-virtual-computer/setup/index.d.ts +17 -0
- package/dist/local-virtual-computer/setup/index.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/index.js +17 -0
- package/dist/local-virtual-computer/setup/index.js.map +1 -0
- package/dist/local-virtual-computer/setup/limits.d.ts +30 -0
- package/dist/local-virtual-computer/setup/limits.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/limits.js +58 -0
- package/dist/local-virtual-computer/setup/limits.js.map +1 -0
- package/dist/local-virtual-computer/setup/plan.d.ts +8 -0
- package/dist/local-virtual-computer/setup/plan.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/plan.js +131 -0
- package/dist/local-virtual-computer/setup/plan.js.map +1 -0
- package/dist/local-virtual-computer/setup/readiness.d.ts +11 -0
- package/dist/local-virtual-computer/setup/readiness.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/readiness.js +320 -0
- package/dist/local-virtual-computer/setup/readiness.js.map +1 -0
- package/dist/local-virtual-computer/setup/registry.d.ts +18 -0
- package/dist/local-virtual-computer/setup/registry.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/registry.js +50 -0
- package/dist/local-virtual-computer/setup/registry.js.map +1 -0
- package/dist/local-virtual-computer/setup/restart.d.ts +8 -0
- package/dist/local-virtual-computer/setup/restart.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/restart.js +60 -0
- package/dist/local-virtual-computer/setup/restart.js.map +1 -0
- package/dist/local-virtual-computer/setup/resume.d.ts +14 -0
- package/dist/local-virtual-computer/setup/resume.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/resume.js +58 -0
- package/dist/local-virtual-computer/setup/resume.js.map +1 -0
- package/dist/local-virtual-computer/setup/run.d.ts +8 -0
- package/dist/local-virtual-computer/setup/run.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/run.js +200 -0
- package/dist/local-virtual-computer/setup/run.js.map +1 -0
- package/dist/local-virtual-computer/setup/windows-folder.d.ts +11 -0
- package/dist/local-virtual-computer/setup/windows-folder.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/windows-folder.js +14 -0
- package/dist/local-virtual-computer/setup/windows-folder.js.map +1 -0
- package/dist/local-virtual-computer/setup/wsl-package.d.ts +19 -0
- package/dist/local-virtual-computer/setup/wsl-package.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/wsl-package.js +242 -0
- package/dist/local-virtual-computer/setup/wsl-package.js.map +1 -0
- package/dist/local-virtual-computer/setup/wslc-shared.d.ts +27 -0
- package/dist/local-virtual-computer/setup/wslc-shared.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/wslc-shared.js +137 -0
- package/dist/local-virtual-computer/setup/wslc-shared.js.map +1 -0
- package/dist/local-virtual-computer/ssh-forward.d.ts +19 -0
- package/dist/local-virtual-computer/ssh-forward.d.ts.map +1 -0
- package/dist/local-virtual-computer/ssh-forward.js +179 -0
- package/dist/local-virtual-computer/ssh-forward.js.map +1 -0
- package/dist/local-virtual-computer/wslc-engine.d.ts +27 -0
- package/dist/local-virtual-computer/wslc-engine.d.ts.map +1 -0
- package/dist/local-virtual-computer/wslc-engine.js +98 -0
- package/dist/local-virtual-computer/wslc-engine.js.map +1 -0
- package/local-computer/Dockerfile +99 -0
- package/local-computer/app-catalog.cjs +240 -0
- package/local-computer/app-native-host.cjs +144 -0
- package/local-computer/browser-home.cjs +249 -0
- package/local-computer/build-image.mjs +310 -0
- package/local-computer/desktop-worker.cjs +477 -0
- package/local-computer/entrypoint.sh +161 -0
- package/local-computer/home.html +28 -0
- package/local-computer/new-tab/home.css +255 -0
- package/local-computer/new-tab/home.js +65 -0
- package/local-computer/new-tab/manifest.json +12 -0
- package/local-computer/new-tab/wordmark.svg +33 -0
- package/local-computer/openbox-rc.xml +41 -0
- package/local-computer/prepare-home.cjs +121 -0
- package/local-computer/tint2rc +44 -0
- package/package.json +13 -4
- package/src/backends/http-worker-client.ts +31 -3
- package/src/index.ts +13 -0
- package/src/local-virtual-computer/client.ts +573 -0
- package/src/local-virtual-computer/detached-process.ts +133 -0
- package/src/local-virtual-computer/detached-worker.ts +124 -0
- package/src/local-virtual-computer/dialect.ts +83 -0
- package/src/local-virtual-computer/engine.ts +197 -0
- package/src/local-virtual-computer/guest-stdio.ts +332 -0
- package/src/local-virtual-computer/index.ts +447 -0
- package/src/local-virtual-computer/podman-machine.ts +264 -0
- package/src/local-virtual-computer/setup/contract.ts +395 -0
- package/src/local-virtual-computer/setup/elevated.ts +655 -0
- package/src/local-virtual-computer/setup/host.ts +100 -0
- package/src/local-virtual-computer/setup/idle.ts +39 -0
- package/src/local-virtual-computer/setup/image.ts +159 -0
- package/src/local-virtual-computer/setup/index.ts +16 -0
- package/src/local-virtual-computer/setup/limits.ts +68 -0
- package/src/local-virtual-computer/setup/plan.ts +144 -0
- package/src/local-virtual-computer/setup/readiness.ts +386 -0
- package/src/local-virtual-computer/setup/registry.ts +50 -0
- package/src/local-virtual-computer/setup/restart.ts +64 -0
- package/src/local-virtual-computer/setup/resume.ts +65 -0
- package/src/local-virtual-computer/setup/run.ts +225 -0
- package/src/local-virtual-computer/setup/windows-folder.ts +13 -0
- package/src/local-virtual-computer/setup/wsl-package.ts +242 -0
- package/src/local-virtual-computer/setup/wslc-shared.ts +147 -0
- package/src/local-virtual-computer/ssh-forward.ts +195 -0
- package/src/local-virtual-computer/wslc-engine.ts +124 -0
- 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
|
+
}
|