@namzu/sandbox 23.0.1 → 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 +290 -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,447 @@
1
+ import { createHash, randomBytes, randomUUID } from 'node:crypto'
2
+ import type { PalEnvironmentLease, PalEnvironmentProvider, PalEnvironmentRequest } from '@namzu/sdk'
3
+ import { OperationDeadline } from '../backends/readiness.js'
4
+ import { localComputerClients } from './client.js'
5
+ import {
6
+ containerIdQuery,
7
+ ensureVolume,
8
+ hardeningFlags,
9
+ parsePublishedPorts,
10
+ portsQuery,
11
+ resourceFlags,
12
+ } from './dialect.js'
13
+ import {
14
+ type EngineDialect,
15
+ LocalCommandError,
16
+ type LocalComputerCommandRunner,
17
+ LocalComputerSetupError,
18
+ type LocalEngine,
19
+ type LocalPortForward,
20
+ resolveLocalEngine,
21
+ } from './engine.js'
22
+ import { resolveLocalPodmanEngine } from './podman-machine.js'
23
+ import { LocalForwardCleanupError } from './ssh-forward.js'
24
+ import { mapWslcFailure, resolveLocalWslcEngine, wslcSetupError } from './wslc-engine.js'
25
+
26
+ export type { LocalComputerCommandRunner } from './engine.js'
27
+ export {
28
+ type EngineDialect,
29
+ LocalComputerSetupError,
30
+ type LocalComputerSetupCode,
31
+ } from './engine.js'
32
+
33
+ export const LOCAL_COMPUTER_IMAGE = 'namzu-local-computer:1'
34
+ export const LOCAL_COMPUTER_PROTOCOL_LABEL = 'org.namzu.local-computer.protocol'
35
+ const OWNER_LABEL = 'org.namzu.pal.owner'
36
+
37
+ /** Uses an already installed local engine and an already built guest image. */
38
+ export interface LocalVirtualComputerOptions {
39
+ readonly image?: string
40
+ /** Foreground applications normally belong to the computer after a successful launcher. */
41
+ readonly normalExitPolicy?: 'strict' | 'computer-lifetime'
42
+ /**
43
+ * Docker remains the default. Podman requires an explicitly selected local
44
+ * Windows WSL machine. wslc is Windows' own container CLI for WSL and must be
45
+ * selected explicitly; it is never chosen automatically.
46
+ */
47
+ readonly engine?: EngineDialect
48
+ readonly dockerBinary?: string
49
+ /** Path to wslc.exe; defaults to the WSL install directory, then PATH. */
50
+ readonly wslcBinary?: string
51
+ readonly podmanBinary?: string
52
+ readonly podmanMachine?: string
53
+ readonly podmanConnection?: string
54
+ readonly engineEndpoint?: string
55
+ readonly readyTimeoutMs?: number
56
+ readonly width?: number
57
+ readonly height?: number
58
+ /** Host transport injection for embedders/tests; contains authority, never model input. */
59
+ readonly runner?: LocalComputerCommandRunner
60
+ }
61
+
62
+ function dimension(value: number | undefined, fallback: number): number {
63
+ const result = value ?? fallback
64
+ if (!Number.isSafeInteger(result) || result < 320 || result > 3840)
65
+ throw new Error('The Pal display dimensions must be integers between 320 and 3840')
66
+ return result
67
+ }
68
+
69
+ function ownerFor(palId: string): string {
70
+ return createHash('sha256').update(palId).digest('hex')
71
+ }
72
+
73
+ /**
74
+ * One readiness probe must not outlive its turn: a published-port relay can
75
+ * accept a connection before the guest listens and then never answer it.
76
+ */
77
+ const READINESS_ATTEMPT_MS = 3000
78
+
79
+ async function verifyImage(
80
+ engine: LocalEngine,
81
+ image: string,
82
+ signal?: AbortSignal,
83
+ ): Promise<void> {
84
+ let images: {
85
+ Os?: string
86
+ Config?: { Labels?: Record<string, string>; User?: string }
87
+ }[]
88
+ try {
89
+ images = JSON.parse(await engine.run(['image', 'inspect', image], { signal })) as typeof images
90
+ if (engine.dialect === 'wslc' && images.length === 0) throw wslcSetupError('image-missing')
91
+ } catch (error) {
92
+ signal?.throwIfAborted()
93
+ if (engine.dialect === 'wslc') {
94
+ if (error instanceof LocalComputerSetupError) throw error
95
+ // A missing image makes `image inspect` exit non-zero carrying either
96
+ // the not-found code or no code at all (observed on wslc 3.0.1). Any
97
+ // other code (an HRESULT, a spawn errno) or a non-command failure
98
+ // surfaces as itself, so a crash is not mistaken for missing setup.
99
+ const mapped = mapWslcFailure(error)
100
+ if (mapped) throw mapped
101
+ if (
102
+ error instanceof LocalCommandError &&
103
+ (error.errorCodes.length === 0 || error.errorCodes.includes('WSLC_E_IMAGE_NOT_FOUND'))
104
+ )
105
+ throw wslcSetupError('image-missing', error)
106
+ throw error
107
+ }
108
+ throw new LocalComputerSetupError(
109
+ `The ${image} image is not installed on this local engine. Build packages/sandbox/local-computer/Dockerfile explicitly; Namzu does not pull or build it automatically.`,
110
+ )
111
+ }
112
+ if (
113
+ images[0]?.Os !== 'linux' ||
114
+ images[0]?.Config?.Labels?.[LOCAL_COMPUTER_PROTOCOL_LABEL] !== '1' ||
115
+ images[0]?.Config?.User !== '1001:1001'
116
+ )
117
+ throw new LocalComputerSetupError(
118
+ 'The local Pal computer image must declare the Namzu desktop protocol and non-root guest user; rebuild the shipped Dockerfile',
119
+ )
120
+ }
121
+
122
+ function portFor(
123
+ ports: Record<string, { HostIp?: string; HostPort?: string }[]>,
124
+ port: number,
125
+ ): number {
126
+ const mappings = ports[`${port}/tcp`]
127
+ if (!mappings || mappings.length !== 1 || mappings[0]?.HostIp !== '127.0.0.1')
128
+ throw new Error('The Pal computer control port must be published on IPv4 loopback only')
129
+ const result = Number(mappings[0].HostPort)
130
+ if (!Number.isSafeInteger(result) || result < 1 || result > 65535)
131
+ throw new Error('The Pal computer control port was not allocated')
132
+ return result
133
+ }
134
+
135
+ /**
136
+ * A separate guest desktop in a local Linux container. It does not provision
137
+ * a dedicated-kernel VM and never falls back to the operator's desktop.
138
+ */
139
+ export function createLocalVirtualComputerProvider(
140
+ options: LocalVirtualComputerOptions = {},
141
+ ): PalEnvironmentProvider {
142
+ if (
143
+ options.engine !== undefined &&
144
+ options.engine !== 'docker' &&
145
+ options.engine !== 'podman' &&
146
+ options.engine !== 'wslc'
147
+ )
148
+ throw new Error('Choose the Docker, Podman or wslc local computer engine')
149
+ const image = options.image ?? LOCAL_COMPUTER_IMAGE
150
+ const normalExitPolicy = options.normalExitPolicy ?? 'computer-lifetime'
151
+ if (normalExitPolicy !== 'strict' && normalExitPolicy !== 'computer-lifetime')
152
+ throw new Error('Choose strict or computer-lifetime foreground execution ownership')
153
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_./:@-]*$/.test(image))
154
+ throw new Error('Invalid local Pal image reference')
155
+ const width = dimension(options.width, 1280)
156
+ const height = dimension(options.height, 800)
157
+ const timeout = options.readyTimeoutMs ?? 60_000
158
+ if (!Number.isSafeInteger(timeout) || timeout <= 0 || timeout > 2_147_483_647)
159
+ throw new Error('Invalid Pal computer readiness timeout')
160
+ const holders = new Set<string>()
161
+ const pendingCleanup = new Map<string, (signal?: AbortSignal) => Promise<void>>()
162
+ let generation = 0
163
+ const prepare = async (signal?: AbortSignal) => {
164
+ const engine =
165
+ options.engine === 'podman'
166
+ ? await resolveLocalPodmanEngine(options, signal)
167
+ : options.engine === 'wslc'
168
+ ? await resolveLocalWslcEngine(options, signal)
169
+ : await resolveLocalEngine(options, signal)
170
+ await verifyImage(engine, image, signal)
171
+ return engine
172
+ }
173
+ const dialectOf = (engine: LocalEngine): EngineDialect =>
174
+ engine.dialect ?? options.engine ?? 'docker'
175
+ return {
176
+ async probe() {
177
+ try {
178
+ await new OperationDeadline(timeout, 'local Pal computer probe').run(
179
+ async (signal) => await prepare(signal),
180
+ )
181
+ return { ready: true }
182
+ } catch (error) {
183
+ return {
184
+ ready: false,
185
+ // The code lets an application choose its words by what failed, not
186
+ // by matching the message. A failure that is not a setup error (a
187
+ // timeout, say) carries no code: it is not known to be fixable by setup.
188
+ setupCode: error instanceof LocalComputerSetupError ? error.code : undefined,
189
+ reason:
190
+ error instanceof LocalComputerSetupError
191
+ ? error.message
192
+ : options.engine === 'wslc'
193
+ ? 'Linux container support and the Pal computer image are required. Namzu sets them up; finish the setup from the Pal computer and try again.'
194
+ : `A local ${options.engine === 'podman' ? 'Podman machine' : 'Docker engine'} running Linux containers and the ${image} image are required. Build packages/sandbox/local-computer/Dockerfile explicitly; Namzu does not install or pull it automatically.`,
195
+ }
196
+ }
197
+ },
198
+ async acquire(request: PalEnvironmentRequest): Promise<PalEnvironmentLease> {
199
+ request.signal?.throwIfAborted()
200
+ if (request.pal.paused) throw new Error('This Pal is paused')
201
+ if (!request.pal.id || !request.conversationId)
202
+ throw new Error('A Pal and conversation identity are required')
203
+ const owner = ownerFor(request.pal.id)
204
+ if (holders.has(owner)) {
205
+ const cleanup = pendingCleanup.get(owner)
206
+ if (!cleanup) throw new Error('This Pal computer is already in use')
207
+ try {
208
+ await new OperationDeadline(timeout, 'local Pal computer recovery', request.signal).run(
209
+ async (signal) => await cleanup(signal),
210
+ )
211
+ } catch {
212
+ throw new Error(
213
+ 'This Pal computer is already in use by an unconfirmed allocation; cleanup recovery is still required',
214
+ )
215
+ }
216
+ pendingCleanup.delete(owner)
217
+ holders.delete(owner)
218
+ }
219
+ holders.add(owner)
220
+ const name = `namzu-pal-${owner.slice(0, 24)}`
221
+ const volume = `namzu-pal-data-${owner}`
222
+ const environmentId = `local-container:${owner}`
223
+ const token = randomBytes(32).toString('base64url')
224
+ const allocationId = randomUUID()
225
+ generation = Math.max(Date.now(), generation + 1)
226
+ const leaseGeneration = generation
227
+ let engine: LocalEngine | undefined
228
+ let created = false
229
+ let runAttempted = false
230
+ let containerId: string | undefined
231
+ let forwarding: LocalPortForward | undefined
232
+ let clients: ReturnType<typeof localComputerClients> | undefined
233
+ const stop = async (signal?: AbortSignal) => {
234
+ if (!engine) return
235
+ // Only the name we successfully created is removed. No volume is
236
+ // deleted; profile/files survive the next allocation.
237
+ if (created) {
238
+ // Commit the browser profile before removing this allocation.
239
+ // Forced removal remains the recovery path if graceful stop fails.
240
+ try {
241
+ await engine.run(['stop', '--time', '10', containerId ?? name], { signal })
242
+ } catch {
243
+ signal?.throwIfAborted()
244
+ }
245
+ await engine.run(['rm', '-f', containerId ?? name], { signal })
246
+ created = false
247
+ }
248
+ await forwarding?.close()
249
+ holders.delete(owner)
250
+ }
251
+ try {
252
+ const deadline = new OperationDeadline(
253
+ timeout,
254
+ 'local Pal computer startup',
255
+ request.signal,
256
+ )
257
+ const connected = await deadline.run(async (signal) => {
258
+ engine = await prepare(signal)
259
+ const dialect = dialectOf(engine)
260
+ const occupied = await engine.run(
261
+ containerIdQuery(dialect, `name=^/${name}$`, { full: false }),
262
+ { signal },
263
+ )
264
+ if (occupied.trim())
265
+ throw new Error(
266
+ 'An earlier Pal computer allocation still exists; stop it explicitly before reconnecting',
267
+ )
268
+ await ensureVolume(engine, dialect, volume, `${OWNER_LABEL}=${owner}`, signal)
269
+ const volumes = JSON.parse(
270
+ await engine.run(['volume', 'inspect', volume], { signal }),
271
+ ) as { Labels?: Record<string, string> }[]
272
+ if (volumes[0]?.Labels?.[OWNER_LABEL] !== owner)
273
+ throw new Error('The Pal data volume belongs to a different owner')
274
+ runAttempted = true
275
+ const started = await engine.run(
276
+ [
277
+ 'run',
278
+ '--detach',
279
+ '--pull=never',
280
+ // WSL machines can lack delegated cgroup controllers.
281
+ // This adapter provides no per-container resource quotas.
282
+ ...resourceFlags(dialect),
283
+ '--name',
284
+ name,
285
+ '--label',
286
+ `${OWNER_LABEL}=${owner}`,
287
+ '--label',
288
+ `org.namzu.pal.generation=${leaseGeneration}`,
289
+ '--label',
290
+ `org.namzu.pal.allocation=${allocationId}`,
291
+ '--user',
292
+ '1001:1001',
293
+ ...hardeningFlags(dialect),
294
+ '--network=bridge',
295
+ '--shm-size=256m',
296
+ '--tmpfs',
297
+ '/tmp:rw,nosuid,nodev,size=512m,mode=1777',
298
+ '--tmpfs',
299
+ '/run:rw,nosuid,nodev,size=16m,mode=1777',
300
+ '--mount',
301
+ `type=volume,source=${volume},target=/home/namzu`,
302
+ '--publish',
303
+ '127.0.0.1::2024',
304
+ '--publish',
305
+ '127.0.0.1::2025',
306
+ '--env',
307
+ `NAMZU_SANDBOX_SCREEN_WIDTH=${width}`,
308
+ '--env',
309
+ `NAMZU_SANDBOX_SCREEN_HEIGHT=${height}`,
310
+ '--env',
311
+ 'NAMZU_SANDBOX_TOKEN',
312
+ image,
313
+ ],
314
+ { signal, env: { NAMZU_SANDBOX_TOKEN: token } },
315
+ )
316
+ created = true
317
+ containerId = started.trim()
318
+ if (!/^[0-9a-f]{64}$/.test(containerId))
319
+ throw new Error('The Pal computer engine did not return a valid allocation identifier')
320
+ const ports = parsePublishedPorts(
321
+ dialect,
322
+ await engine.run(portsQuery(dialect, name), { signal }),
323
+ )
324
+ const guestPorts = [portFor(ports, 2024), portFor(ports, 2025)]
325
+ forwarding = await engine.forwardPorts?.(guestPorts, {
326
+ signal,
327
+ onLost() {
328
+ void clients?.sandbox.destroy().catch(() => {
329
+ // Destroy fences calls first; its retained lease can retry recovery.
330
+ })
331
+ },
332
+ })
333
+ const localPorts = forwarding?.ports ?? guestPorts
334
+ const executionUrl = `http://127.0.0.1:${localPorts[0]}`
335
+ const desktopUrl = `http://127.0.0.1:${localPorts[1]}`
336
+ for (;;) {
337
+ signal.throwIfAborted()
338
+ forwarding?.assertAlive()
339
+ const attempt = new AbortController()
340
+ const attemptTimer = setTimeout(() => attempt.abort(), READINESS_ATTEMPT_MS)
341
+ const attemptSignal = AbortSignal.any([signal, attempt.signal])
342
+ try {
343
+ const response = await fetch(`${desktopUrl}/readyz`, {
344
+ headers: { authorization: `Bearer ${token}` },
345
+ signal: attemptSignal,
346
+ })
347
+ const desktop = response.ok
348
+ ? ((await response.json()) as {
349
+ protocol?: number
350
+ width?: number
351
+ height?: number
352
+ browserReady?: boolean
353
+ stream?: { protocol?: unknown }
354
+ heldKeyboard?: { version?: unknown }
355
+ })
356
+ : undefined
357
+ const execution = await fetch(`${executionUrl}/healthz`, { signal: attemptSignal })
358
+ const worker = execution.ok
359
+ ? ((await execution.json()) as { protocolVersion?: number })
360
+ : undefined
361
+ if (
362
+ desktop?.protocol === 1 &&
363
+ desktop.width === width &&
364
+ desktop.height === height &&
365
+ desktop.browserReady === true &&
366
+ worker?.protocolVersion === 2
367
+ )
368
+ return {
369
+ executionUrl,
370
+ desktopUrl,
371
+ screenStream: desktop.stream?.protocol === 'rfb',
372
+ heldKeyboard: desktop.heldKeyboard?.version === 1,
373
+ }
374
+ } catch {
375
+ signal.throwIfAborted()
376
+ } finally {
377
+ clearTimeout(attemptTimer)
378
+ }
379
+ await new Promise<void>((resolve, reject) => {
380
+ const timer = setTimeout(() => {
381
+ signal.removeEventListener('abort', abort)
382
+ resolve()
383
+ }, 200)
384
+ const abort = () => {
385
+ clearTimeout(timer)
386
+ reject(signal.reason)
387
+ }
388
+ signal.addEventListener('abort', abort, { once: true })
389
+ })
390
+ }
391
+ })
392
+ request.signal?.throwIfAborted()
393
+ clients = localComputerClients({
394
+ ...connected,
395
+ normalExitPolicy,
396
+ token,
397
+ geometry: { width, height, scaleFactor: 1 },
398
+ stop,
399
+ })
400
+ const admitted = clients
401
+ return {
402
+ palId: request.pal.id,
403
+ environmentId,
404
+ generation: leaseGeneration,
405
+ ...admitted,
406
+ release: () => admitted.sandbox.destroy(),
407
+ }
408
+ } catch (error) {
409
+ if (error instanceof LocalForwardCleanupError) forwarding = error.forward
410
+ if (runAttempted && engine) {
411
+ const ownedEngine = engine
412
+ const cleanup = async (signal?: AbortSignal) => {
413
+ // Reconcile an unacknowledged start by this attempt's unique
414
+ // label, never by a name another process may have acquired.
415
+ const owned = await ownedEngine.run(
416
+ containerIdQuery(
417
+ dialectOf(ownedEngine),
418
+ `label=org.namzu.pal.allocation=${allocationId}`,
419
+ { full: true },
420
+ ),
421
+ { signal },
422
+ )
423
+ for (const id of owned.trim().split(/\s+/).filter(Boolean)) {
424
+ if (!/^[0-9a-f]{64}$/.test(id)) throw new Error('Invalid owned allocation identifier')
425
+ await ownedEngine.run(['rm', '-f', id], { signal })
426
+ }
427
+ created = false
428
+ await forwarding?.close()
429
+ }
430
+ try {
431
+ await new OperationDeadline(timeout, 'local Pal computer cleanup').run(
432
+ async (signal) => await cleanup(signal),
433
+ )
434
+ } catch {
435
+ pendingCleanup.set(owner, cleanup)
436
+ throw new Error(
437
+ 'Pal computer startup failed and cleanup could not be confirmed; the owned allocation requires operator recovery',
438
+ { cause: error },
439
+ )
440
+ }
441
+ }
442
+ holders.delete(owner)
443
+ throw error
444
+ }
445
+ },
446
+ }
447
+ }