threadwire 0.1.6 → 0.1.9

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.
@@ -0,0 +1,470 @@
1
+ // @ts-nocheck
2
+ /* eslint-disable jsdoc/require-jsdoc */
3
+
4
+ import {createHmac, randomBytes, timingSafeEqual} from "node:crypto"
5
+ import {constants} from "node:fs"
6
+ import {chmod, lstat, mkdir, open, readFile, readdir, rename, unlink} from "node:fs/promises"
7
+ import {dirname, join} from "node:path"
8
+
9
+ const OWNER = "threadwire-isolated-runtime"
10
+
11
+ /**
12
+ * Authenticated registry of both public session aliases and private volume
13
+ * ownership. Lineage ownership deliberately outlives an expired alias until
14
+ * Docker confirms that the volume was deleted.
15
+ * @param {{file: string, key: string, namespace: string, provider?: "codex" | "kimi", now?: () => number, random?: (size: number) => Buffer}} options
16
+ */
17
+ export async function openStateRegistry(options) {
18
+ const provider = options.provider ?? "codex"
19
+ if (!/^\/.+/u.test(options.file) || options.key.length < 32 || !/^[A-Za-z0-9_.-]{1,64}$/u.test(options.namespace)
20
+ || (provider !== "codex" && provider !== "kimi")) {
21
+ throw new Error("Invalid isolated state configuration")
22
+ }
23
+ const now = options.now ?? Date.now
24
+ const random = options.random ?? randomBytes
25
+ const durabilityObserver = options.durabilityObserver
26
+ await mkdir(dirname(options.file), {recursive: true, mode: 0o700})
27
+ await chmod(dirname(options.file), 0o700)
28
+ await requirePrivateDirectory(dirname(options.file))
29
+ const sessions = new Map()
30
+ const lineages = new Map()
31
+ const runs = new Map()
32
+ let serial = Promise.resolve()
33
+ try {
34
+ const document = JSON.parse(await readFile(options.file, "utf8"))
35
+ const payload = JSON.stringify(document.payload)
36
+ if (![1, 2, 3, 4].includes(document.version) || typeof document.mac !== "string" || !equalMac(document.mac, mac(options.key, payload))) throw new Error()
37
+ if (document.version < 4 && provider !== "codex") throw new Error()
38
+ if (!Array.isArray(document.payload?.sessions)) throw new Error()
39
+ for (const rawEntry of document.payload.sessions) {
40
+ if (!validSession(rawEntry, document.version === 4)) throw new Error()
41
+ const entry = document.version === 4 ? rawEntry : {...rawEntry, provider: "codex"}
42
+ if (entry.provider !== provider) throw new Error()
43
+ sessions.set(entry.sessionId, without(entry, "sessionId"))
44
+ lineages.set(entry.lineage, lineageFromSession(options.namespace, entry))
45
+ }
46
+ if (document.version === 2) {
47
+ if (!Array.isArray(document.payload.lineages)) throw new Error()
48
+ for (const entry of document.payload.lineages) {
49
+ if (!validLineage(entry, options.namespace, false)) throw new Error()
50
+ lineages.set(entry.lineage, {...entry, provider: "codex"})
51
+ }
52
+ }
53
+ if (document.version === 3 || document.version === 4) {
54
+ if (!Array.isArray(document.payload.lineages) || !Array.isArray(document.payload.runs)) throw new Error()
55
+ for (const rawEntry of document.payload.lineages) {
56
+ if (!validLineage(rawEntry, options.namespace, document.version === 4)) throw new Error()
57
+ const entry = document.version === 4 ? rawEntry : {...rawEntry, provider: "codex"}
58
+ if (entry.provider !== provider) throw new Error()
59
+ lineages.set(entry.lineage, entry)
60
+ }
61
+ for (const rawEntry of document.payload.runs) {
62
+ canonicalRunIdentity(rawEntry)
63
+ const entry = document.version === 4 ? rawEntry : {...rawEntry, provider: "codex", sealVersion: 3}
64
+ if (entry.provider !== provider) throw new Error()
65
+ runs.set(entry.run, entry)
66
+ }
67
+ }
68
+ } catch (error) {
69
+ if (/** @type {NodeJS.ErrnoException} */ (error).code !== "ENOENT") throw new Error("Isolated state registry authentication failed", {cause: error})
70
+ }
71
+ await removeAuthenticatedTemps(dirname(options.file), options.key, options.namespace)
72
+
73
+ const persist = async (sessionState = sessions, lineageState = lineages, runState = runs, signal) => {
74
+ throwIfAborted(signal)
75
+ const payload = {
76
+ sessions: [...sessionState.entries()].map(([sessionId, entry]) => ({sessionId, ...entry})),
77
+ lineages: [...lineageState.values()],
78
+ runs: [...runState.values()]
79
+ }
80
+ const serializedPayload = JSON.stringify(payload)
81
+ const document = JSON.stringify({version: 4, payload, mac: mac(options.key, serializedPayload)})
82
+ const temporary = join(dirname(options.file), `.sessions-${random(12).toString("hex")}.tmp`)
83
+ let handle
84
+ try {
85
+ handle = await open(temporary, "wx", 0o600)
86
+ throwIfAborted(signal)
87
+ await handle.writeFile(document)
88
+ await durabilityObserver?.("before-file-sync")
89
+ await handle.sync()
90
+ await durabilityObserver?.("after-file-sync")
91
+ await handle.close()
92
+ handle = undefined
93
+ throwIfAborted(signal)
94
+ await durabilityObserver?.("before-rename")
95
+ await rename(temporary, options.file)
96
+ await durabilityObserver?.("after-rename")
97
+ const directory = await open(dirname(options.file), "r")
98
+ try {
99
+ await durabilityObserver?.("before-directory-sync")
100
+ await directory.sync()
101
+ await durabilityObserver?.("after-directory-sync")
102
+ } finally {
103
+ await directory.close()
104
+ }
105
+ } catch (error) {
106
+ await handle?.close().catch(() => {})
107
+ throw error
108
+ }
109
+ }
110
+ const transact = (operation) => {
111
+ const result = serial.then(operation)
112
+ serial = result.catch(() => {})
113
+ return result
114
+ }
115
+ const allocationRecord = (allocation) => ({
116
+ provider: allocation.provider,
117
+ task: allocation.task,
118
+ lineage: allocation.lineage,
119
+ volume: allocation.volume,
120
+ labels: allocation.labels
121
+ })
122
+ const ownsRunSeal = (identity, seal, legacyLabels = false) => {
123
+ const persisted = runs.get(identity.run)
124
+ if (!persisted || typeof seal !== "string") return false
125
+ try {
126
+ if (canonicalRunIdentity(persisted) !== canonicalRunIdentity(identity)) return false
127
+ const legacySeal = persisted.sealVersion === 3
128
+ if (legacyLabels !== legacySeal) return false
129
+ const canonical = legacySeal ? legacyCanonicalRunIdentity(persisted) : canonicalRunIdentity(persisted)
130
+ return equalMac(seal, mac(options.key, canonical))
131
+ } catch {
132
+ return false
133
+ }
134
+ }
135
+
136
+ return {
137
+ namespace: options.namespace,
138
+ provider,
139
+ allocate(task) {
140
+ const lineage = random(24).toString("hex")
141
+ return {
142
+ provider,
143
+ task,
144
+ lineage,
145
+ volume: `threadwire-state-${options.namespace}-${lineage}`,
146
+ labels: stateLabels(options.namespace, task, lineage)
147
+ }
148
+ },
149
+ sealRun(identity) {
150
+ if (identity.sealVersion !== undefined) throw new Error("Invalid isolated run identity")
151
+ return mac(options.key, canonicalRunIdentity(identity))
152
+ },
153
+ ownsRunSeal(identity, seal, legacyLabels = false) {
154
+ return ownsRunSeal(identity, seal, legacyLabels)
155
+ },
156
+ ownsRun(identity, seal, legacyLabels = false) {
157
+ const lineage = lineages.get(identity.lineage)
158
+ if (!lineage || lineage.provider !== (identity.provider ?? "codex")
159
+ || lineage.volume !== identity.volume || lineage.labels["org.threadwire.task"] !== identity.task) return false
160
+ return ownsRunSeal(identity, seal, legacyLabels)
161
+ },
162
+ async trackRun(identity, signal) {
163
+ await transact(async () => {
164
+ throwIfAborted(signal)
165
+ const normalized = {...identity, provider: identity.provider ?? provider}
166
+ if (normalized.sealVersion !== undefined) throw new Error("Invalid isolated run identity")
167
+ canonicalRunIdentity(normalized)
168
+ if (normalized.provider !== provider || !lineages.has(normalized.lineage)) throw new Error(`${provider === "kimi" ? "Kimi" : "Codex"} run lineage is unavailable`)
169
+ const existing = runs.get(normalized.run)
170
+ if (existing && canonicalRunIdentity(existing) !== canonicalRunIdentity(normalized)) throw new Error(`${provider === "kimi" ? "Kimi" : "Codex"} run ownership collision`)
171
+ const nextRuns = new Map(runs)
172
+ nextRuns.set(normalized.run, existing ?? normalized)
173
+ await persist(sessions, lineages, nextRuns, signal)
174
+ replace(runs, nextRuns)
175
+ throwIfAborted(signal)
176
+ })
177
+ },
178
+ async completeRun(run, signal) {
179
+ await transact(async () => {
180
+ throwIfAborted(signal)
181
+ if (!runs.has(run)) return
182
+ const nextRuns = new Map(runs)
183
+ nextRuns.delete(run)
184
+ await persist(sessions, lineages, nextRuns, signal)
185
+ replace(runs, nextRuns)
186
+ throwIfAborted(signal)
187
+ })
188
+ },
189
+ runRecords() {
190
+ return [...runs.values()].map((entry) => ({...entry}))
191
+ },
192
+ lookup(sessionId, expectedProvider = provider) {
193
+ const entry = sessions.get(sessionId)
194
+ if (!entry || entry.provider !== expectedProvider || entry.expiresAt <= now()) return undefined
195
+ return {...entry, labels: stateLabels(options.namespace, entry.task, entry.lineage)}
196
+ },
197
+ async track(allocation, signal) {
198
+ await transact(async () => {
199
+ throwIfAborted(signal)
200
+ const normalized = providerAllocation(allocation, provider)
201
+ const existing = lineages.get(normalized.lineage)
202
+ const candidate = allocationRecord(normalized)
203
+ if (existing && JSON.stringify(existing) !== JSON.stringify(candidate)) throw new Error("Codex lineage ownership collision")
204
+ const nextLineages = new Map(lineages)
205
+ nextLineages.set(normalized.lineage, candidate)
206
+ await persist(sessions, nextLineages, runs, signal)
207
+ replace(lineages, nextLineages)
208
+ throwIfAborted(signal)
209
+ })
210
+ },
211
+ async register(sessionIds, allocation, ttlMs, signal) {
212
+ await transact(async () => {
213
+ throwIfAborted(signal)
214
+ const normalized = providerAllocation(allocation, provider)
215
+ const candidate = allocationRecord(normalized)
216
+ const owned = lineages.get(normalized.lineage)
217
+ if (owned && JSON.stringify(owned) !== JSON.stringify(candidate)) throw new Error("Codex lineage ownership collision")
218
+ const nextSessions = new Map(sessions)
219
+ const nextLineages = new Map(lineages)
220
+ for (const sessionId of sessionIds) {
221
+ const existing = nextSessions.get(sessionId)
222
+ if (existing && (existing.provider !== normalized.provider || existing.task !== normalized.task || existing.lineage !== normalized.lineage || existing.volume !== normalized.volume)) {
223
+ throw new Error("Codex session lineage collision")
224
+ }
225
+ nextSessions.set(sessionId, {
226
+ provider: normalized.provider, task: normalized.task, lineage: normalized.lineage,
227
+ volume: normalized.volume, expiresAt: now() + ttlMs
228
+ })
229
+ }
230
+ nextLineages.set(normalized.lineage, candidate)
231
+ await persist(nextSessions, nextLineages, runs, signal)
232
+ replace(sessions, nextSessions)
233
+ replace(lineages, nextLineages)
234
+ throwIfAborted(signal)
235
+ })
236
+ },
237
+ /**
238
+ * Sweeps aliases, adopts only exactly labelled namespace-owned orphan
239
+ * volumes, and retries failed deletions without dropping ownership.
240
+ */
241
+ async collect(docker, activeLineages = new Set(), requestOptions = {}) {
242
+ throwIfAborted(requestOptions.signal)
243
+ const listed = await docker.listVolumes({
244
+ "org.threadwire.owner": OWNER,
245
+ "org.threadwire.namespace": options.namespace
246
+ }, requestOptions)
247
+ const plan = await transact(async () => {
248
+ throwIfAborted(requestOptions.signal)
249
+ const nextSessions = new Map(sessions)
250
+ const nextLineages = new Map(lineages)
251
+ for (const [sessionId, entry] of nextSessions) if (entry.expiresAt <= now()) nextSessions.delete(sessionId)
252
+ for (const volume of Array.isArray(listed?.Volumes) ? listed.Volumes : []) {
253
+ const orphan = ownedVolumeRecord(volume, options.namespace, provider)
254
+ if (orphan && !nextLineages.has(orphan.lineage)) nextLineages.set(orphan.lineage, orphan)
255
+ }
256
+ // Persist removals and adopted cleanup evidence before touching Docker.
257
+ await persist(nextSessions, nextLineages, runs, requestOptions.signal)
258
+ replace(sessions, nextSessions)
259
+ replace(lineages, nextLineages)
260
+ throwIfAborted(requestOptions.signal)
261
+ const live = new Set([...nextSessions.values()].map((entry) => entry.lineage))
262
+ return [...nextLineages].filter(([lineage]) => !live.has(lineage) && !activeLineages.has(lineage))
263
+ })
264
+ const removedCandidates = []
265
+ for (const [lineage, entry] of plan) {
266
+ throwIfAborted(requestOptions.signal)
267
+ try {
268
+ const volume = await docker.inspectVolume(entry.volume, requestOptions)
269
+ validateStateVolume(volume, entry.volume, entry.labels)
270
+ if (activeLineages.has(lineage)) continue
271
+ await docker.removeVolume(entry.volume, requestOptions)
272
+ removedCandidates.push([lineage, entry])
273
+ } catch (error) {
274
+ if (isDockerMissing(error)) removedCandidates.push([lineage, entry])
275
+ }
276
+ }
277
+ return transact(async () => {
278
+ throwIfAborted(requestOptions.signal)
279
+ const nextLineages = new Map(lineages)
280
+ let removed = 0
281
+ const live = new Set([...sessions.values()].filter((entry) => entry.expiresAt > now()).map((entry) => entry.lineage))
282
+ for (const [lineage, expected] of removedCandidates) {
283
+ if (live.has(lineage) || activeLineages.has(lineage)) continue
284
+ if (JSON.stringify(nextLineages.get(lineage)) !== JSON.stringify(expected)) continue
285
+ nextLineages.delete(lineage)
286
+ removed++
287
+ }
288
+ await persist(sessions, nextLineages, runs, requestOptions.signal)
289
+ replace(lineages, nextLineages)
290
+ throwIfAborted(requestOptions.signal)
291
+ return {sessions: sessions.size, lineages: lineages.size, removed}
292
+ })
293
+ },
294
+ counts() {
295
+ return {sessions: sessions.size, lineages: lineages.size, runs: runs.size}
296
+ }
297
+ }
298
+ }
299
+
300
+ function throwIfAborted(signal) {
301
+ if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error("Isolated state operation aborted")
302
+ }
303
+
304
+ export function stateLabels(namespace, task, lineage) {
305
+ return {
306
+ "org.threadwire.owner": OWNER,
307
+ "org.threadwire.namespace": namespace,
308
+ "org.threadwire.task": createHmac("sha256", namespace).update(task).digest("hex"),
309
+ "org.threadwire.lineage": lineage
310
+ }
311
+ }
312
+
313
+ export function validateStateVolume(volume, name, labels) {
314
+ if (!record(volume) || volume.Name !== name || !record(volume.Labels)) throw new Error("Isolated state volume collision")
315
+ const actual = volume.Labels
316
+ if (Object.keys(actual).length !== Object.keys(labels).length) throw new Error("Isolated state volume collision")
317
+ for (const [key, value] of Object.entries(labels)) if (actual[key] !== value) throw new Error("Isolated state volume collision")
318
+ }
319
+
320
+ function ownedVolumeRecord(volume, namespace, provider) {
321
+ if (!record(volume) || !record(volume.Labels)) return undefined
322
+ const labels = volume.Labels
323
+ const lineage = labels["org.threadwire.lineage"]
324
+ const name = volume.Name
325
+ if (Object.keys(labels).length !== 4
326
+ || labels["org.threadwire.owner"] !== OWNER
327
+ || labels["org.threadwire.namespace"] !== namespace
328
+ || !/^[0-9a-f]{64}$/u.test(labels["org.threadwire.task"])
329
+ || !/^[0-9a-f]{48}$/u.test(lineage)
330
+ || name !== `threadwire-state-${namespace}-${lineage}`) return undefined
331
+ return {provider, task: "", lineage, volume: name, labels}
332
+ }
333
+
334
+ function providerAllocation(allocation, expectedProvider) {
335
+ const actualProvider = allocation.provider ?? "codex"
336
+ if (actualProvider !== expectedProvider) throw new Error("Isolated state provider mismatch")
337
+ return {...allocation, provider: actualProvider}
338
+ }
339
+
340
+ function lineageFromSession(namespace, entry) {
341
+ return {
342
+ provider: entry.provider, task: entry.task, lineage: entry.lineage, volume: entry.volume,
343
+ labels: stateLabels(namespace, entry.task, entry.lineage)
344
+ }
345
+ }
346
+
347
+ function validSession(value, requireProvider = true) {
348
+ return record(value) && (!requireProvider || value.provider === "codex" || value.provider === "kimi")
349
+ && typeof value.sessionId === "string" && typeof value.task === "string"
350
+ && /^[0-9a-f]{48}$/u.test(value.lineage)
351
+ && /^threadwire-state-[A-Za-z0-9_.-]+-[0-9a-f]{48}$/u.test(value.volume)
352
+ && Number.isSafeInteger(value.expiresAt)
353
+ }
354
+
355
+ function validLineage(value, namespace, requireProvider = true) {
356
+ return record(value) && (!requireProvider || value.provider === "codex" || value.provider === "kimi")
357
+ && typeof value.task === "string" && /^[0-9a-f]{48}$/u.test(value.lineage)
358
+ && value.volume === `threadwire-state-${namespace}-${value.lineage}` && record(value.labels)
359
+ && Object.keys(value.labels).length === 4
360
+ && value.labels["org.threadwire.owner"] === OWNER
361
+ && value.labels["org.threadwire.namespace"] === namespace
362
+ && value.labels["org.threadwire.lineage"] === value.lineage
363
+ && /^[0-9a-f]{64}$/u.test(value.labels["org.threadwire.task"])
364
+ }
365
+
366
+ function isDockerMissing(error) {
367
+ return error instanceof Error && /\(404\)/u.test(error.message)
368
+ }
369
+
370
+ function without(value, key) {
371
+ const copy = {...value}
372
+ delete copy[key]
373
+ return copy
374
+ }
375
+
376
+ function replace(target, source) {
377
+ target.clear()
378
+ for (const [key, value] of source) target.set(key, value)
379
+ }
380
+
381
+ async function removeAuthenticatedTemps(directory, key, namespace) {
382
+ const names = await readdir(directory)
383
+ let removed = false
384
+ for (const name of names) {
385
+ if (!/^\.sessions-[0-9a-f]{24}\.tmp$/u.test(name)) continue
386
+ let handle
387
+ try {
388
+ const path = join(directory, name)
389
+ handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW)
390
+ const before = await handle.stat({bigint: true})
391
+ if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1n
392
+ || before.uid !== BigInt(process.getuid?.() ?? -1)
393
+ || before.size < 1n || before.size > 4_194_304n || (before.mode & 0o077n) !== 0n) continue
394
+ const document = JSON.parse(await handle.readFile("utf8"))
395
+ if (!authenticatedDocument(document, key, namespace)) continue
396
+ const after = await lstat(path, {bigint: true})
397
+ if (after.dev !== before.dev || after.ino !== before.ino || after.isSymbolicLink()) continue
398
+ await unlink(path)
399
+ removed = true
400
+ } catch {
401
+ // Malformed, unreadable, and foreign temp files are deliberately left untouched.
402
+ } finally {
403
+ await handle?.close().catch(() => {})
404
+ }
405
+ }
406
+ if (!removed) return
407
+ const handle = await open(directory, "r")
408
+ try { await handle.sync() } finally { await handle.close() }
409
+ }
410
+
411
+ function authenticatedDocument(document, key, namespace) {
412
+ if (!record(document) || ![1, 2, 3, 4].includes(document.version) || typeof document.mac !== "string"
413
+ || !record(document.payload) || !Array.isArray(document.payload.sessions)) return false
414
+ const payload = JSON.stringify(document.payload)
415
+ if (!equalMac(document.mac, mac(key, payload))) return false
416
+ const current = document.version === 4
417
+ if (!document.payload.sessions.every((entry) => validSession(entry, current))) return false
418
+ if (document.version === 1) return true
419
+ if (!Array.isArray(document.payload.lineages)
420
+ || !document.payload.lineages.every((entry) => validLineage(entry, namespace, current))) return false
421
+ return document.version === 2 || (Array.isArray(document.payload.runs)
422
+ && document.payload.runs.every((entry) => {
423
+ try { canonicalRunIdentity(entry); return true } catch { return false }
424
+ }))
425
+ }
426
+
427
+ function canonicalRunIdentity(identity) {
428
+ const keys = [
429
+ "provider", "namespace", "run", "lineage", "task", "volume", "network", "container",
430
+ "image", "worktreeType", "worktreeSource", "worktreeSubpath"
431
+ ]
432
+ if (!record(identity)) throw new Error("Invalid isolated run identity")
433
+ const normalized = {...identity, provider: identity.provider ?? "codex"}
434
+ const legacySeal = normalized.sealVersion === 3 && normalized.provider === "codex"
435
+ const expectedKeys = legacySeal ? [...keys, "sealVersion"] : keys
436
+ if (Object.keys(normalized).length !== expectedKeys.length
437
+ || expectedKeys.some((key) => !Object.prototype.hasOwnProperty.call(normalized, key))
438
+ || (normalized.provider !== "codex" && normalized.provider !== "kimi")
439
+ || keys.some((key) => typeof normalized[key] !== "string")) throw new Error("Invalid isolated run identity")
440
+ return JSON.stringify(Object.fromEntries(keys.map((key) => [key, normalized[key]])))
441
+ }
442
+
443
+ function legacyCanonicalRunIdentity(identity) {
444
+ const keys = [
445
+ "namespace", "run", "lineage", "task", "volume", "network", "container",
446
+ "image", "worktreeType", "worktreeSource", "worktreeSubpath"
447
+ ]
448
+ if (!record(identity) || identity.provider !== "codex" || identity.sealVersion !== 3
449
+ || Object.keys(identity).length !== keys.length + 2
450
+ || keys.some((key) => typeof identity[key] !== "string")) throw new Error("Invalid legacy Codex run identity")
451
+ return JSON.stringify(Object.fromEntries(keys.map((key) => [key, identity[key]])))
452
+ }
453
+
454
+ async function requirePrivateDirectory(path) {
455
+ const stats = await lstat(path)
456
+ if (!stats.isDirectory() || stats.isSymbolicLink() || (stats.mode & 0o077) !== 0) throw new Error("Invalid isolated state directory")
457
+ }
458
+
459
+ function mac(key, payload) {
460
+ return createHmac("sha256", key).update(payload).digest("hex")
461
+ }
462
+
463
+ function equalMac(left, right) {
464
+ if (!/^[0-9a-f]{64}$/u.test(left)) return false
465
+ return timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"))
466
+ }
467
+
468
+ function record(value) {
469
+ return typeof value === "object" && value !== null && !Array.isArray(value)
470
+ }
@@ -0,0 +1,144 @@
1
+ // @ts-check
2
+
3
+ import {isAbsolute, normalize} from "node:path"
4
+
5
+ const DIGEST_IMAGE = /^(?:[^@\s]+@)?sha256:[0-9a-f]{64}$/u
6
+ const SAFE_VALUE = /^[^\0\r\n]+$/u
7
+
8
+ /**
9
+ * @typedef {{
10
+ * provider?: "codex" | "kimi",
11
+ * image: string,
12
+ * worktree: string,
13
+ * networkName: string,
14
+ * brokerToken: string,
15
+ * prompt: string,
16
+ * resumeSession?: string,
17
+ * brokerUrl?: string
18
+ * worktreeVolume?: {name: string, subpath: string}
19
+ * workingDirectory: string,
20
+ * providerArguments: string[],
21
+ * model?: string,
22
+ * stateVolume: string,
23
+ * worktreeInode: string,
24
+ * cwdInode: string
25
+ * lineage?: string
26
+ * namespace?: string
27
+ * runId?: string
28
+ * taskHash?: string
29
+ * runSeal?: string
30
+ * }} WorkerSpecOptions
31
+ */
32
+
33
+ /** @param {WorkerSpecOptions} options */
34
+ export function buildWorkerContainerSpec(options) {
35
+ const provider = options.provider ?? "codex"
36
+ if (provider !== "codex" && provider !== "kimi") throw new Error("Worker provider is invalid")
37
+ if (!DIGEST_IMAGE.test(options.image)) throw new Error("Worker image must use an immutable digest")
38
+ if (!isAbsolute(options.worktree) || normalize(options.worktree) !== options.worktree || options.worktree === "/") {
39
+ throw new Error("Worker worktree must be a normalized non-root absolute path")
40
+ }
41
+ for (const [name, value] of /** @type {[string, string][]} */ ([["network", options.networkName], ["capability", options.brokerToken], ["prompt", options.prompt]])) {
42
+ if (!SAFE_VALUE.test(value)) throw new Error(`Worker ${name} is invalid`)
43
+ }
44
+ if (options.resumeSession !== undefined && !SAFE_VALUE.test(options.resumeSession)) throw new Error("Worker resume session is invalid")
45
+ if (!isAbsolute(options.workingDirectory) || !withinWorktree(options.workingDirectory)) throw new Error("Worker cwd is invalid")
46
+ if (provider === "kimi" && (typeof options.model !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u.test(options.model))) {
47
+ throw new Error("Worker Kimi model is invalid")
48
+ }
49
+ const commonEnvironment = [
50
+ "HOME=/home/worker",
51
+ "TMPDIR=/tmp",
52
+ `THREADWIRE_PROMPT=${options.prompt}`,
53
+ `THREADWIRE_WORKTREE_INODE=${options.worktreeInode}`,
54
+ `THREADWIRE_CWD_INODE=${options.cwdInode}`,
55
+ ...(options.resumeSession === undefined ? [] : [`THREADWIRE_RESUME_SESSION=${options.resumeSession}`])
56
+ ]
57
+ const environment = provider === "kimi"
58
+ ? [
59
+ ...commonEnvironment,
60
+ "KIMI_CODE_HOME=/home/worker/.kimi-code",
61
+ `THREADWIRE_KIMI_MODEL=${options.model}`,
62
+ `THREADWIRE_KIMI_BROKER_URL=${options.brokerUrl}`,
63
+ `THREADWIRE_KIMI_BROKER_TOKEN=${options.brokerToken}`
64
+ ]
65
+ : [
66
+ ...commonEnvironment,
67
+ "CODEX_HOME=/home/worker/.codex",
68
+ `OPENAI_BASE_URL=${options.brokerUrl ?? "http://model-broker:8789/v1"}`,
69
+ `OPENAI_API_KEY=${options.brokerToken}`,
70
+ `CODEX_API_KEY=${options.brokerToken}`,
71
+ `THREADWIRE_CODEX_ARGUMENTS=${JSON.stringify(options.providerArguments)}`
72
+ ]
73
+ const worktreeMount = options.worktreeVolume === undefined
74
+ ? {
75
+ Type: "bind",
76
+ Source: options.worktree,
77
+ Target: "/worktree",
78
+ ReadOnly: false,
79
+ BindOptions: {Propagation: "rprivate"}
80
+ }
81
+ : {
82
+ Type: "volume",
83
+ Source: options.worktreeVolume.name,
84
+ Target: "/worktree",
85
+ ReadOnly: false,
86
+ VolumeOptions: {Subpath: options.worktreeVolume.subpath}
87
+ }
88
+ return {
89
+ Image: options.image,
90
+ Labels: {
91
+ "org.threadwire.owner": "isolated-runtime",
92
+ ...(options.lineage === undefined ? {} : {
93
+ "org.threadwire.namespace": options.namespace,
94
+ "org.threadwire.provider": provider,
95
+ "org.threadwire.run": options.runId,
96
+ "org.threadwire.lineage": options.lineage,
97
+ "org.threadwire.task": options.taskHash,
98
+ "org.threadwire.network": options.networkName,
99
+ "org.threadwire.state-volume": options.stateVolume,
100
+ "org.threadwire.run-seal": options.runSeal
101
+ })
102
+ },
103
+ Entrypoint: provider === "kimi"
104
+ ? ["node", "/opt/threadwire/docker/kimi-worker-entrypoint.mjs"]
105
+ : ["/usr/local/libexec/threadwire/worker-entrypoint"],
106
+ Env: environment,
107
+ WorkingDir: options.workingDirectory,
108
+ User: "10002:10002",
109
+ NetworkDisabled: false,
110
+ HostConfig: {
111
+ AutoRemove: false,
112
+ Mounts: [
113
+ worktreeMount,
114
+ {Type: "volume", Source: options.stateVolume, Target: "/home/worker", ReadOnly: false}
115
+ ],
116
+ CapDrop: ["ALL"],
117
+ NetworkMode: options.networkName,
118
+ ReadonlyRootfs: true,
119
+ SecurityOpt: ["no-new-privileges"],
120
+ PidsLimit: 128,
121
+ Memory: 1_073_741_824,
122
+ NanoCpus: 2_000_000_000,
123
+ Tmpfs: {
124
+ "/tmp": "rw,noexec,nosuid,nodev,size=67108864,uid=10002,gid=10002",
125
+ "/run": "rw,noexec,nosuid,nodev,size=1048576,uid=10002,gid=10002"
126
+ },
127
+ Ulimits: [
128
+ {Name: "nofile", Soft: 1024, Hard: 1024},
129
+ {Name: "core", Soft: 0, Hard: 0},
130
+ {Name: "fsize", Soft: 1_073_741_824, Hard: 1_073_741_824}
131
+ ]
132
+ }
133
+ }
134
+ }
135
+
136
+ /** @param {string} path */
137
+ function withinWorktree(path) {
138
+ return path === "/worktree" || path.startsWith("/worktree/")
139
+ }
140
+
141
+ /** @param {string} value */
142
+ export function isDigestImage(value) {
143
+ return DIGEST_IMAGE.test(value)
144
+ }