threadwire 0.1.6 → 0.1.8
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 +40 -0
- package/README.md +13 -2
- package/TELEGRAM-INGRESS.md +8 -2
- package/bin/isolated-runtime.js +5 -0
- package/bin/model-broker.js +5 -0
- package/docs/isolated-provider-runtime.md +137 -0
- package/package.json +5 -1
- package/scripts/provider-shims/front-door.sh.template +18 -0
- package/scripts/verify-package.js +15 -0
- package/src/absolute-deadline.js +94 -0
- package/src/cli.js +120 -21
- package/src/delegated-result-admission.js +1 -1
- package/src/docker-api.js +131 -0
- package/src/isolated-runtime-client.js +149 -0
- package/src/isolated-runtime.js +982 -0
- package/src/isolated-state.js +409 -0
- package/src/isolated-worker.js +123 -0
- package/src/model-broker-policy.js +139 -0
- package/src/model-broker.js +313 -0
- package/src/mount-policy.js +28 -0
- package/src/normalized-output.js +68 -0
- package/src/relay-write.js +44 -0
- package/src/telegram-ingress/config.js +7 -0
- package/src/telegram-ingress/core.js +40 -0
- package/src/telegram-webhook.js +6 -0
|
@@ -0,0 +1,982 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/* eslint-disable jsdoc/require-jsdoc */
|
|
3
|
+
|
|
4
|
+
import {randomBytes} from "node:crypto"
|
|
5
|
+
import {lstat, realpath} from "node:fs/promises"
|
|
6
|
+
import {createServer} from "node:http"
|
|
7
|
+
import {isAbsolute, join, normalize, relative} from "node:path"
|
|
8
|
+
import {DockerApi} from "./docker-api.js"
|
|
9
|
+
import {buildWorkerContainerSpec, isDigestImage} from "./isolated-worker.js"
|
|
10
|
+
import {validateRelayWriteProviderArguments} from "./relay-write.js"
|
|
11
|
+
import {codexSessionId, parseCodexEvent} from "./providers/codex.js"
|
|
12
|
+
import {openStateRegistry, validateStateVolume} from "./isolated-state.js"
|
|
13
|
+
import {assertNoNestedMounts} from "./mount-policy.js"
|
|
14
|
+
import {AbsoluteDeadline, deadlineAfter} from "./absolute-deadline.js"
|
|
15
|
+
|
|
16
|
+
const MAX_PREFLIGHT_BYTES = 32_768
|
|
17
|
+
const MAX_RUN_BYTES = 1_048_576
|
|
18
|
+
const PREFLIGHT_TTL_MS = 60_000
|
|
19
|
+
const PREFLIGHT_CAPACITY = 128
|
|
20
|
+
const PREFLIGHT_TASK_CAPACITY = 8
|
|
21
|
+
const GC_INTERVAL_MS = 5_000
|
|
22
|
+
const ACTIVE_RUN_CAPACITY = 16
|
|
23
|
+
const ACTIVE_TASK_CAPACITY = 2
|
|
24
|
+
const WORKER_TIMEOUT_MS = 3_600_000
|
|
25
|
+
const PREFLIGHT_TIMEOUT_MS = 30_000
|
|
26
|
+
const MAX_RAW_OUTPUT_BYTES = 1_048_576
|
|
27
|
+
|
|
28
|
+
/** @param {{environment?: NodeJS.ProcessEnv, docker?: DockerApi, fetchImplementation?: typeof fetch, now?: () => number}} [options] */
|
|
29
|
+
export async function startIsolatedRuntime(options = {}) {
|
|
30
|
+
const environment = options.environment ?? process.env
|
|
31
|
+
const host = environment.THREADWIRE_ISOLATED_RUNTIME_HOST ?? "0.0.0.0"
|
|
32
|
+
const port = portValue(environment.THREADWIRE_ISOLATED_RUNTIME_PORT, 8790)
|
|
33
|
+
const controlToken = safeSetting(environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN, "THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN")
|
|
34
|
+
const workerImage = safeSetting(environment.THREADWIRE_RELAY_WORKER_IMAGE, "THREADWIRE_RELAY_WORKER_IMAGE")
|
|
35
|
+
if (!isDigestImage(workerImage)) throw new Error("THREADWIRE_RELAY_WORKER_IMAGE must use an immutable digest")
|
|
36
|
+
const brokerUrl = normalizedHttpUrl(environment.THREADWIRE_MODEL_BROKER_URL, "THREADWIRE_MODEL_BROKER_URL")
|
|
37
|
+
const brokerAdminToken = safeSetting(environment.THREADWIRE_MODEL_BROKER_ADMIN_TOKEN, "THREADWIRE_MODEL_BROKER_ADMIN_TOKEN")
|
|
38
|
+
const brokerContainer = safeSetting(environment.THREADWIRE_MODEL_BROKER_CONTAINER, "THREADWIRE_MODEL_BROKER_CONTAINER")
|
|
39
|
+
const allowedRoots = parseAllowedRoots(environment.THREADWIRE_ALLOWED_WORKTREE_ROOTS)
|
|
40
|
+
const worktreeVolume = environment.THREADWIRE_WORKTREE_VOLUME
|
|
41
|
+
const sessionTtlMs = positiveDuration(environment.THREADWIRE_SESSION_TTL_MS, 86_400_000)
|
|
42
|
+
const workerTimeoutMs = positiveDuration(environment.THREADWIRE_WORKER_TIMEOUT_MS, WORKER_TIMEOUT_MS)
|
|
43
|
+
const preflightTimeoutMs = positiveDuration(environment.THREADWIRE_PREFLIGHT_TIMEOUT_MS, PREFLIGHT_TIMEOUT_MS)
|
|
44
|
+
const docker = options.docker ?? new DockerApi()
|
|
45
|
+
const fetchImplementation = options.fetchImplementation ?? fetch
|
|
46
|
+
const now = options.now ?? Date.now
|
|
47
|
+
const allowedModels = parseAllowedModels(environment.THREADWIRE_ALLOWED_CODEX_MODELS)
|
|
48
|
+
const stateRegistry = await openStateRegistry({
|
|
49
|
+
file: safeSetting(environment.THREADWIRE_STATE_REGISTRY_FILE, "THREADWIRE_STATE_REGISTRY_FILE"),
|
|
50
|
+
key: safeSetting(environment.THREADWIRE_STATE_AUTH_KEY, "THREADWIRE_STATE_AUTH_KEY"),
|
|
51
|
+
namespace: safeSetting(environment.THREADWIRE_STATE_NAMESPACE, "THREADWIRE_STATE_NAMESPACE"),
|
|
52
|
+
now
|
|
53
|
+
})
|
|
54
|
+
const preflights = new PreflightStore({
|
|
55
|
+
now,
|
|
56
|
+
ttlMs: options.preflightTtlMs ?? PREFLIGHT_TTL_MS,
|
|
57
|
+
capacity: options.preflightCapacity ?? PREFLIGHT_CAPACITY,
|
|
58
|
+
taskCapacity: options.preflightTaskCapacity ?? PREFLIGHT_TASK_CAPACITY
|
|
59
|
+
})
|
|
60
|
+
const activeLineages = new LineageReservations()
|
|
61
|
+
const activeRuns = new ActiveRunAdmission({
|
|
62
|
+
capacity: positiveInteger(environment.THREADWIRE_ACTIVE_RUN_CAPACITY, ACTIVE_RUN_CAPACITY),
|
|
63
|
+
taskCapacity: positiveInteger(environment.THREADWIRE_ACTIVE_TASK_CAPACITY, ACTIVE_TASK_CAPACITY)
|
|
64
|
+
})
|
|
65
|
+
const incomingRequests = new ActiveRunAdmission({capacity: ACTIVE_RUN_CAPACITY, taskCapacity: ACTIVE_RUN_CAPACITY})
|
|
66
|
+
const shutdown = new AbortController()
|
|
67
|
+
const sockets = new Set()
|
|
68
|
+
let stopping = false
|
|
69
|
+
let gcPromise = Promise.resolve()
|
|
70
|
+
const collectState = () => {
|
|
71
|
+
gcPromise = gcPromise.then(async () => {
|
|
72
|
+
if (shutdown.signal.aborted) return
|
|
73
|
+
const collection = deadlineAfter(Math.min(30_000, workerTimeoutMs), {signal: shutdown.signal, timeoutMessage: "Isolated runtime collection timeout"})
|
|
74
|
+
const releaseCollection = activeLineages.beginCollection()
|
|
75
|
+
try {
|
|
76
|
+
const active = new Set(activeLineages.values())
|
|
77
|
+
const current = {
|
|
78
|
+
*[Symbol.iterator]() { yield * active },
|
|
79
|
+
has(lineage) { return active.has(lineage) || activeLineages.has(lineage) }
|
|
80
|
+
}
|
|
81
|
+
for (const lineage of await reconcileDockerResources(docker, brokerContainer, stateRegistry, current, collection.options())) active.add(lineage)
|
|
82
|
+
await stateRegistry.collect(docker, {has: (lineage) => active.has(lineage) || activeLineages.has(lineage)}, collection.options())
|
|
83
|
+
} finally {
|
|
84
|
+
collection.close()
|
|
85
|
+
releaseCollection()
|
|
86
|
+
}
|
|
87
|
+
}).catch(() => {})
|
|
88
|
+
return gcPromise
|
|
89
|
+
}
|
|
90
|
+
const startupActive = await reconcileDockerResources(docker, brokerContainer, stateRegistry, new Set())
|
|
91
|
+
await stateRegistry.collect(docker, new Set(startupActive))
|
|
92
|
+
const gcTimer = setInterval(collectState, Math.min(GC_INTERVAL_MS, sessionTtlMs))
|
|
93
|
+
gcTimer.unref()
|
|
94
|
+
|
|
95
|
+
const server = createServer(async (request, response) => {
|
|
96
|
+
try {
|
|
97
|
+
if (stopping) throw new Error("Isolated runtime is shutting down")
|
|
98
|
+
requireControl(request.headers.authorization, controlToken)
|
|
99
|
+
if (request.method === "GET" && request.url === "/healthz") {
|
|
100
|
+
sendJson(response, 200, {ok: true})
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
if (request.method === "POST" && request.url === "/preflight") {
|
|
104
|
+
const releaseIncoming = incomingRequests.acquire("preflight")
|
|
105
|
+
const launchDeadlineAt = requestedDeadline(request.headers["x-threadwire-launch-deadline"], workerTimeoutMs)
|
|
106
|
+
const operation = new AbsoluteDeadline(Math.min(Date.now() + preflightTimeoutMs, launchDeadlineAt), {
|
|
107
|
+
signal: shutdown.signal, request, response, timeoutMessage: "Isolated runtime operation timed out", disconnectMessage: "Isolated runtime caller disconnected"
|
|
108
|
+
})
|
|
109
|
+
try {
|
|
110
|
+
const body = await readJson(request, MAX_PREFLIGHT_BYTES, operation.signal)
|
|
111
|
+
const selection = parsePreflight(body)
|
|
112
|
+
const task = taskIdentity(selection)
|
|
113
|
+
preflights.assertCapacity(task)
|
|
114
|
+
let codexArguments
|
|
115
|
+
try {
|
|
116
|
+
codexArguments = validateRelayWriteProviderArguments(selection.providerArguments)
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new ClientError(error instanceof Error ? error.message : "Provider argument is unavailable in relay write mode")
|
|
119
|
+
}
|
|
120
|
+
if (!allowedModels.has(codexArguments.model)) throw new ClientError("Codex model is not allowed")
|
|
121
|
+
const {fingerprint, root: worktreeRoot} = await verifyPrerequisites({
|
|
122
|
+
docker, workerImage, selection, allowedRoots, brokerUrl, brokerAdminToken, fetchImplementation, operation
|
|
123
|
+
})
|
|
124
|
+
let state
|
|
125
|
+
if (selection.resumeSession !== undefined) {
|
|
126
|
+
state = stateRegistry.lookup(selection.resumeSession)
|
|
127
|
+
if (!state || state.task !== task) throw new ClientError("Codex resume state is unavailable")
|
|
128
|
+
const volume = await docker.inspectVolume(state.volume, operation.options()).catch(() => undefined)
|
|
129
|
+
try { validateStateVolume(volume, state.volume, state.labels) } catch { throw new ClientError("Codex resume state is unavailable") }
|
|
130
|
+
} else state = stateRegistry.allocate(task)
|
|
131
|
+
const preflightId = randomBytes(24).toString("base64url")
|
|
132
|
+
preflights.add(preflightId, task, {...selection, codexArguments, state, fingerprint, worktreeRoot, launchDeadlineAt})
|
|
133
|
+
sendJson(response, 200, {ok: true, preflightId, launchDeadlineAt})
|
|
134
|
+
} finally {
|
|
135
|
+
operation.close()
|
|
136
|
+
releaseIncoming()
|
|
137
|
+
}
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if (request.method === "POST" && request.url === "/run") {
|
|
141
|
+
const releaseIncoming = incomingRequests.acquire("run")
|
|
142
|
+
const bodyOperation = new AbsoluteDeadline(Date.now() + preflightTimeoutMs, {signal: shutdown.signal, request, response, timeoutMessage: "Isolated runtime operation timed out"})
|
|
143
|
+
let body
|
|
144
|
+
try {
|
|
145
|
+
body = parseRun(await readJson(request, MAX_RUN_BYTES, bodyOperation.signal))
|
|
146
|
+
} finally {
|
|
147
|
+
bodyOperation.close()
|
|
148
|
+
releaseIncoming()
|
|
149
|
+
}
|
|
150
|
+
const preflight = preflights.consume(body.preflightId)
|
|
151
|
+
if (!preflight) throw new ClientError("Isolated runtime preflight expired")
|
|
152
|
+
const operation = new AbsoluteDeadline(Math.min(preflight.launchDeadlineAt,
|
|
153
|
+
requestedDeadline(request.headers["x-threadwire-launch-deadline"], workerTimeoutMs)), {
|
|
154
|
+
signal: shutdown.signal, request, response, timeoutMessage: "Isolated runtime launch timed out", disconnectMessage: "Isolated runtime caller disconnected"
|
|
155
|
+
})
|
|
156
|
+
const task = taskIdentity(preflight)
|
|
157
|
+
let releaseRun
|
|
158
|
+
let releaseLineage
|
|
159
|
+
try {
|
|
160
|
+
releaseRun = activeRuns.acquire(task)
|
|
161
|
+
releaseLineage = activeLineages.acquire(preflight.state.lineage)
|
|
162
|
+
if (body.resumeSession !== preflight.resumeSession) throw new ClientError("Codex resume state does not match preflight")
|
|
163
|
+
if (JSON.stringify(validateRelayWriteProviderArguments(body.providerArguments)) !== JSON.stringify(preflight.codexArguments)) {
|
|
164
|
+
throw new ClientError("Codex arguments do not match preflight")
|
|
165
|
+
}
|
|
166
|
+
await revalidateFingerprint(preflight.fingerprint)
|
|
167
|
+
operation.throwIfAborted()
|
|
168
|
+
const result = await runIsolatedWorker({
|
|
169
|
+
docker, workerImage, brokerUrl, brokerAdminToken, brokerContainer,
|
|
170
|
+
fetchImplementation, preflight, ...body, task, now, stateRegistry, sessionTtlMs, operation,
|
|
171
|
+
...(worktreeVolume === undefined ? {} : {worktreeVolume})
|
|
172
|
+
})
|
|
173
|
+
sendJson(response, 200, result)
|
|
174
|
+
} finally {
|
|
175
|
+
operation.close()
|
|
176
|
+
releaseLineage?.()
|
|
177
|
+
releaseRun?.()
|
|
178
|
+
void collectState()
|
|
179
|
+
}
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
response.writeHead(404)
|
|
183
|
+
response.end()
|
|
184
|
+
} catch (error) {
|
|
185
|
+
sendJson(response, error instanceof ClientError ? 400 : 503, {
|
|
186
|
+
error: error instanceof ClientError ? error.message : "Isolated runtime unavailable"
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
})
|
|
190
|
+
server.on("connection", (socket) => {
|
|
191
|
+
sockets.add(socket)
|
|
192
|
+
socket.once("close", () => sockets.delete(socket))
|
|
193
|
+
})
|
|
194
|
+
await listen(server, port, host)
|
|
195
|
+
return {
|
|
196
|
+
server,
|
|
197
|
+
close: async () => {
|
|
198
|
+
stopping = true
|
|
199
|
+
clearInterval(gcTimer)
|
|
200
|
+
preflights.close()
|
|
201
|
+
shutdown.abort(new Error("Isolated runtime is shutting down"))
|
|
202
|
+
for (const socket of sockets) socket.destroy()
|
|
203
|
+
await boundedClose(server, 1000)
|
|
204
|
+
await Promise.race([gcPromise, new Promise((resolve) => setTimeout(resolve, 1000))])
|
|
205
|
+
},
|
|
206
|
+
config: {host, port}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function verifyPrerequisites(options) {
|
|
211
|
+
const {docker, workerImage, selection, allowedRoots, brokerUrl, brokerAdminToken, fetchImplementation, operation} = options
|
|
212
|
+
const version = /** @type {{ApiVersion?: unknown}} */ (await docker.version(operation.options()))
|
|
213
|
+
if (typeof version.ApiVersion !== "string") throw new Error("Docker API unavailable")
|
|
214
|
+
const image = /** @type {{Id?: unknown}} */ (await docker.inspectImage(workerImage, operation.options()))
|
|
215
|
+
if (typeof image.Id !== "string" || !isDigestImage(image.Id)) throw new Error("Worker image unavailable")
|
|
216
|
+
const validatedWorktree = await validateWorktreeSelection(selection.repositoryRoot, selection.cwd, allowedRoots)
|
|
217
|
+
const health = await abortable(fetchImplementation(new URL("/healthz", brokerUrl), {
|
|
218
|
+
method: "GET",
|
|
219
|
+
redirect: "error",
|
|
220
|
+
signal: operation.signal,
|
|
221
|
+
headers: {authorization: `Bearer ${brokerAdminToken}`}
|
|
222
|
+
}), operation.signal)
|
|
223
|
+
if (!health.ok) throw new Error("Model broker unavailable")
|
|
224
|
+
return validatedWorktree
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export async function validateWorktree(repositoryRoot, cwd, allowedRoots) {
|
|
228
|
+
return (await validateWorktreeSelection(repositoryRoot, cwd, allowedRoots)).fingerprint
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function validateWorktreeSelection(repositoryRoot, cwd, allowedRoots) {
|
|
232
|
+
const root = allowedRoots
|
|
233
|
+
.filter((allowed) => within(allowed, repositoryRoot))
|
|
234
|
+
.sort((first, second) => second.length - first.length)[0]
|
|
235
|
+
if (root === undefined) throw new ClientError("Configured worktree is unavailable")
|
|
236
|
+
const paths = ancestry(root, repositoryRoot)
|
|
237
|
+
for (const path of ancestry(repositoryRoot, cwd).slice(1)) if (!paths.includes(path)) paths.push(path)
|
|
238
|
+
/** @type {{path: string, dev: string, ino: string}[]} */
|
|
239
|
+
const fingerprint = []
|
|
240
|
+
for (const path of paths) {
|
|
241
|
+
if (!isAbsolute(path) || normalize(path) !== path || path === "/") throw new ClientError("Configured worktree is unavailable")
|
|
242
|
+
const metadata = await lstat(path, {bigint: true})
|
|
243
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink() || await realpath(path) !== path) {
|
|
244
|
+
throw new ClientError("Configured worktree is unavailable")
|
|
245
|
+
}
|
|
246
|
+
fingerprint.push({path, dev: String(metadata.dev), ino: String(metadata.ino)})
|
|
247
|
+
}
|
|
248
|
+
if (!within(repositoryRoot, cwd) || !allowedRoots.some((allowed) => within(allowed, repositoryRoot))) {
|
|
249
|
+
throw new ClientError("Configured worktree is unavailable")
|
|
250
|
+
}
|
|
251
|
+
await assertNoNestedMounts(repositoryRoot)
|
|
252
|
+
try {
|
|
253
|
+
const gitEntry = await lstat(join(repositoryRoot, ".git"))
|
|
254
|
+
if (gitEntry.isDirectory()) throw new ClientError("Common Git directories cannot enter the worker")
|
|
255
|
+
if (!gitEntry.isFile() || gitEntry.isSymbolicLink()) throw new ClientError("Configured worktree is unavailable")
|
|
256
|
+
} catch (error) {
|
|
257
|
+
if (error instanceof ClientError) throw error
|
|
258
|
+
if (/** @type {NodeJS.ErrnoException} */ (error).code !== "ENOENT") throw error
|
|
259
|
+
}
|
|
260
|
+
return {fingerprint, root}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export async function revalidateFingerprint(fingerprint) {
|
|
264
|
+
if (!Array.isArray(fingerprint)) throw new ClientError("Configured worktree changed after preflight")
|
|
265
|
+
for (const expected of fingerprint) {
|
|
266
|
+
const metadata = await lstat(expected.path, {bigint: true}).catch(() => undefined)
|
|
267
|
+
if (!metadata?.isDirectory() || metadata.isSymbolicLink() || String(metadata.dev) !== expected.dev || String(metadata.ino) !== expected.ino || await realpath(expected.path) !== expected.path) {
|
|
268
|
+
throw new ClientError("Configured worktree changed after preflight")
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export async function runIsolatedWorker(options) {
|
|
274
|
+
const runId = `tw-${randomBytes(12).toString("hex")}`
|
|
275
|
+
const networkName = `${runId}-net`
|
|
276
|
+
const state = options.preflight.state
|
|
277
|
+
const freshState = options.resumeSession === undefined
|
|
278
|
+
let keepFreshState = false
|
|
279
|
+
let networkId
|
|
280
|
+
let containerId
|
|
281
|
+
let grantToken
|
|
282
|
+
let runTracked = false
|
|
283
|
+
const operation = options.operation
|
|
284
|
+
const worktreeSubpath = options.worktreeVolume === undefined
|
|
285
|
+
? ""
|
|
286
|
+
: deriveVolumeWorktreeSubpath(options.preflight.worktreeRoot, options.preflight.repositoryRoot)
|
|
287
|
+
try {
|
|
288
|
+
operation.throwIfAborted()
|
|
289
|
+
await prepareStateVolume(options.docker, state, freshState, operation.options())
|
|
290
|
+
if (freshState) await options.stateRegistry.track(state, operation.signal)
|
|
291
|
+
const runIdentity = expectedRunIdentity(options, runId, networkName, state, worktreeSubpath)
|
|
292
|
+
const runSeal = options.stateRegistry.sealRun(runIdentity)
|
|
293
|
+
await options.stateRegistry.trackRun(runIdentity, operation.signal)
|
|
294
|
+
runTracked = true
|
|
295
|
+
const resourceLabels = {
|
|
296
|
+
"org.threadwire.namespace": options.stateRegistry.namespace,
|
|
297
|
+
"org.threadwire.run": runId,
|
|
298
|
+
"org.threadwire.lineage": state.lineage,
|
|
299
|
+
"org.threadwire.task": state.labels["org.threadwire.task"],
|
|
300
|
+
"org.threadwire.state-volume": state.volume,
|
|
301
|
+
"org.threadwire.run-seal": runSeal
|
|
302
|
+
}
|
|
303
|
+
const network = /** @type {{Id?: unknown}} */ (await options.docker.createNetwork(networkName, resourceLabels, operation.options()))
|
|
304
|
+
if (typeof network.Id !== "string") throw new Error("Docker network creation failed")
|
|
305
|
+
networkId = network.Id
|
|
306
|
+
await options.docker.connectNetwork(networkId, options.brokerContainer, [], operation.options())
|
|
307
|
+
const networkInspection = await options.docker.inspectNetwork(networkId, operation.options())
|
|
308
|
+
const brokerAddress = brokerNetworkAddress(networkInspection, options.brokerContainer)
|
|
309
|
+
const grantResponse = await abortable(options.fetchImplementation(new URL("/admin/grants", options.brokerUrl), {
|
|
310
|
+
method: "POST",
|
|
311
|
+
redirect: "error",
|
|
312
|
+
signal: operation.signal,
|
|
313
|
+
headers: {authorization: `Bearer ${options.brokerAdminToken}`, "content-type": "application/json"},
|
|
314
|
+
body: JSON.stringify({runId, provider: "codex", networkId, model: options.preflight.codexArguments.model, brokerAddress, ttlMs: Math.min(operation.remaining(), 3_600_000)})
|
|
315
|
+
}), operation.signal)
|
|
316
|
+
if (!grantResponse.ok) throw new Error("Model broker grant failed")
|
|
317
|
+
const grant = await grantResponse.json()
|
|
318
|
+
if (!isRecord(grant) || typeof grant.token !== "string" || !Number.isSafeInteger(grant.port)) throw new Error("Model broker grant failed")
|
|
319
|
+
grantToken = grant.token
|
|
320
|
+
const repository = /** @type {{path: string, ino: string}[]} */ (options.preflight.fingerprint)
|
|
321
|
+
const worktreeMetadata = repository.find((entry) => entry.path === options.preflight.repositoryRoot)
|
|
322
|
+
const cwdMetadata = repository.find((entry) => entry.path === options.preflight.cwd)
|
|
323
|
+
if (worktreeMetadata === undefined || cwdMetadata === undefined) throw new Error("Worktree fingerprint unavailable")
|
|
324
|
+
const spec = buildWorkerContainerSpec({
|
|
325
|
+
image: options.workerImage,
|
|
326
|
+
worktree: options.preflight.repositoryRoot,
|
|
327
|
+
networkName,
|
|
328
|
+
brokerToken: grantToken,
|
|
329
|
+
brokerUrl: `http://${brokerAddress}:${grant.port}/v1`,
|
|
330
|
+
prompt: options.prompt,
|
|
331
|
+
providerArguments: options.preflight.codexArguments.arguments,
|
|
332
|
+
stateVolume: state.volume,
|
|
333
|
+
workingDirectory: `/worktree${relative(options.preflight.repositoryRoot, options.preflight.cwd) === "" ? "" : `/${relative(options.preflight.repositoryRoot, options.preflight.cwd)}`}`,
|
|
334
|
+
worktreeInode: worktreeMetadata.ino,
|
|
335
|
+
cwdInode: cwdMetadata.ino,
|
|
336
|
+
lineage: state.lineage,
|
|
337
|
+
namespace: options.stateRegistry.namespace,
|
|
338
|
+
runId,
|
|
339
|
+
taskHash: state.labels["org.threadwire.task"],
|
|
340
|
+
runSeal,
|
|
341
|
+
...(options.worktreeVolume === undefined
|
|
342
|
+
? {}
|
|
343
|
+
: {
|
|
344
|
+
worktreeVolume: {
|
|
345
|
+
name: options.worktreeVolume,
|
|
346
|
+
subpath: worktreeSubpath
|
|
347
|
+
}
|
|
348
|
+
}),
|
|
349
|
+
...(options.resumeSession === undefined ? {} : {resumeSession: options.resumeSession})
|
|
350
|
+
})
|
|
351
|
+
const container = /** @type {{Id?: unknown}} */ (await options.docker.createContainer(`${runId}-worker`, spec, operation.options()))
|
|
352
|
+
if (typeof container.Id !== "string") throw new Error("Docker worker creation failed")
|
|
353
|
+
containerId = container.Id
|
|
354
|
+
await revalidateFingerprint(options.preflight.fingerprint)
|
|
355
|
+
await validateWorkerMountInventory(await options.docker.inspectContainer(containerId, operation.options()), spec.HostConfig.Mounts)
|
|
356
|
+
const activationResponse = await abortable(options.fetchImplementation(
|
|
357
|
+
new URL(`/admin/grants/${encodeURIComponent(grantToken)}/activate`, options.brokerUrl), {
|
|
358
|
+
method: "POST",
|
|
359
|
+
redirect: "error",
|
|
360
|
+
signal: operation.signal,
|
|
361
|
+
headers: {authorization: `Bearer ${options.brokerAdminToken}`}
|
|
362
|
+
}), operation.signal)
|
|
363
|
+
if (!activationResponse.ok) throw new Error("Model broker grant activation failed")
|
|
364
|
+
await options.docker.startContainer(containerId, operation.options())
|
|
365
|
+
const wait = /** @type {{StatusCode?: unknown}} */ (await options.docker.waitContainer(containerId, operation.options()))
|
|
366
|
+
const logs = /** @type {Buffer} */ (await options.docker.logs(containerId, operation.options()))
|
|
367
|
+
const rawChunks = demultiplexDockerLogChunks(logs)
|
|
368
|
+
const result = {
|
|
369
|
+
exitCode: typeof wait.StatusCode === "number" ? wait.StatusCode : 1,
|
|
370
|
+
records: parseWorkerRecords(Buffer.concat(rawChunks.filter((chunk) => chunk.channel === "stdout").map((chunk) => chunk.data)).toString("utf8")),
|
|
371
|
+
rawChunks: rawChunks.map((chunk) => ({channel: chunk.channel, data: chunk.data.toString("base64")}))
|
|
372
|
+
}
|
|
373
|
+
if (freshState && result.exitCode === 0) {
|
|
374
|
+
const sessionIds = [...new Set(result.records.filter((record) => record.type === "session").map((record) => record.sessionId))]
|
|
375
|
+
if (sessionIds.length === 0) throw new Error("Codex session state was not established")
|
|
376
|
+
await options.stateRegistry.register(sessionIds, state, options.sessionTtlMs, operation.signal)
|
|
377
|
+
keepFreshState = true
|
|
378
|
+
}
|
|
379
|
+
return result
|
|
380
|
+
} finally {
|
|
381
|
+
let resourcesRemoved = true
|
|
382
|
+
// A caller/deadline abort cannot authorize more work, but exact force
|
|
383
|
+
// teardown must remain possible. This supervisor-only emergency budget is
|
|
384
|
+
// bounded and is used solely for already-recorded resource identifiers.
|
|
385
|
+
const emergencyCleanup = operation.signal.aborted
|
|
386
|
+
? deadlineAfter(5_000, {timeoutMessage: "Isolated runtime emergency cleanup timeout"})
|
|
387
|
+
: undefined
|
|
388
|
+
const cleanupOperation = emergencyCleanup ?? operation
|
|
389
|
+
const cleanupOptions = () => cleanupOperation.options()
|
|
390
|
+
if (grantToken !== undefined) {
|
|
391
|
+
await abortable(options.fetchImplementation(new URL(`/admin/grants/${encodeURIComponent(grantToken)}`, options.brokerUrl), {
|
|
392
|
+
method: "DELETE",
|
|
393
|
+
signal: cleanupOperation.signal,
|
|
394
|
+
headers: {authorization: `Bearer ${options.brokerAdminToken}`}
|
|
395
|
+
}), cleanupOperation.signal).catch(() => process.stderr.write("threadwire-isolated-runtime: grant cleanup failed\n"))
|
|
396
|
+
}
|
|
397
|
+
if (containerId !== undefined) await cleanupDocker(() => options.docker.removeContainer(containerId, cleanupOptions())).catch(() => {
|
|
398
|
+
resourcesRemoved = false
|
|
399
|
+
process.stderr.write("threadwire-isolated-runtime: worker cleanup failed\n")
|
|
400
|
+
})
|
|
401
|
+
if (networkId !== undefined) {
|
|
402
|
+
await cleanupDocker(() => options.docker.disconnectNetwork(networkId, options.brokerContainer, cleanupOptions())).catch(() => {})
|
|
403
|
+
await cleanupDocker(() => options.docker.removeNetwork(networkId, cleanupOptions())).catch(() => {
|
|
404
|
+
resourcesRemoved = false
|
|
405
|
+
process.stderr.write("threadwire-isolated-runtime: network cleanup failed\n")
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
if (freshState && !keepFreshState) await cleanupDocker(() => options.docker.removeVolume(state.volume, cleanupOptions())).catch(() => { resourcesRemoved = false })
|
|
409
|
+
if (runTracked && resourcesRemoved) await options.stateRegistry.completeRun(runId, cleanupOperation.signal).catch(() => {})
|
|
410
|
+
emergencyCleanup?.close()
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function cleanupDocker(operation) {
|
|
415
|
+
return operation()
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export async function prepareStateVolume(docker, state, fresh, requestOptions) {
|
|
419
|
+
if (fresh) await docker.createVolume(state.volume, state.labels, requestOptions)
|
|
420
|
+
const volume = await docker.inspectVolume(state.volume, requestOptions).catch(() => undefined)
|
|
421
|
+
try {
|
|
422
|
+
validateStateVolume(volume, state.volume, state.labels)
|
|
423
|
+
} catch {
|
|
424
|
+
throw new ClientError(fresh ? "Codex state volume collision" : "Codex resume state is unavailable")
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
export function validateWorkerMountInventory(container, expected) {
|
|
429
|
+
if (!isRecord(container) || !Array.isArray(container.Mounts) || container.Mounts.length !== expected.length) {
|
|
430
|
+
throw new Error("Worker mount topology unavailable")
|
|
431
|
+
}
|
|
432
|
+
for (const mount of expected) {
|
|
433
|
+
const actual = container.Mounts.find((entry) => isRecord(entry) && entry.Destination === mount.Target)
|
|
434
|
+
const sourceMatches = mount.Type === "volume" ? actual?.Name === mount.Source : actual?.Source === mount.Source
|
|
435
|
+
if (!actual || actual.Type !== mount.Type || !sourceMatches || actual.RW !== !mount.ReadOnly) {
|
|
436
|
+
throw new Error("Worker mount topology unavailable")
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function parseWorkerRecords(text) {
|
|
442
|
+
const records = []
|
|
443
|
+
for (const line of text.split("\n")) {
|
|
444
|
+
if (line.trim().length === 0) continue
|
|
445
|
+
try {
|
|
446
|
+
const record = JSON.parse(line)
|
|
447
|
+
if (isRecord(record)) {
|
|
448
|
+
records.push(record)
|
|
449
|
+
if (record.type !== "worker-event") {
|
|
450
|
+
const sessionId = codexSessionId(record)
|
|
451
|
+
if (sessionId !== undefined) records.push({type: "session", sessionId})
|
|
452
|
+
for (const event of parseCodexEvent(record)) records.push({type: "worker-event", event})
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
records.push({type: "worker-event", event: {type: "diagnostic", level: "warning", summary: "Worker emitted an unreadable event"}})
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return records
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
export function demultiplexDockerLogs(buffer) {
|
|
463
|
+
return Buffer.concat(demultiplexDockerLogChunks(buffer).map((chunk) => chunk.data)).toString("utf8")
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function demultiplexDockerLogChunks(buffer) {
|
|
467
|
+
let offset = 0
|
|
468
|
+
const output = []
|
|
469
|
+
let bytes = 0
|
|
470
|
+
while (offset + 8 <= buffer.length) {
|
|
471
|
+
const stream = buffer[offset]
|
|
472
|
+
const length = buffer.readUInt32BE(offset + 4)
|
|
473
|
+
if ((stream !== 1 && stream !== 2) || offset + 8 + length > buffer.length) throw new Error("Worker emitted invalid Docker logs")
|
|
474
|
+
bytes += length
|
|
475
|
+
if (bytes > MAX_RAW_OUTPUT_BYTES) throw new Error("Worker output exceeded capacity")
|
|
476
|
+
output.push({channel: stream === 1 ? "stdout" : "stderr", data: buffer.subarray(offset + 8, offset + 8 + length)})
|
|
477
|
+
offset += 8 + length
|
|
478
|
+
}
|
|
479
|
+
if (offset === buffer.length && output.length > 0) return output
|
|
480
|
+
if (buffer.length > MAX_RAW_OUTPUT_BYTES) throw new Error("Worker output exceeded capacity")
|
|
481
|
+
return [{channel: "stdout", data: buffer}]
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
export function parsePreflight(value) {
|
|
485
|
+
if (!isRecord(value) || value.provider !== "codex" || typeof value.profile !== "string" || typeof value.repositoryRoot !== "string" || typeof value.cwd !== "string" || !Array.isArray(value.providerArguments) || !value.providerArguments.every((item) => typeof item === "string")) {
|
|
486
|
+
throw new ClientError("Invalid isolated runtime preflight")
|
|
487
|
+
}
|
|
488
|
+
if (!bounded(value.profile, 128) || !bounded(value.repositoryRoot, 4096) || !bounded(value.cwd, 4096)
|
|
489
|
+
|| value.providerArguments.length > 16 || value.providerArguments.some((item) => !bounded(item, 512))
|
|
490
|
+
|| (value.resumeSession !== undefined && !bounded(value.resumeSession, 256))) {
|
|
491
|
+
throw new ClientError("Invalid isolated runtime preflight")
|
|
492
|
+
}
|
|
493
|
+
if (value.resumeSession !== undefined && (typeof value.resumeSession !== "string" || /[\r\n]/u.test(value.resumeSession))) throw new ClientError("Invalid isolated runtime preflight")
|
|
494
|
+
return {
|
|
495
|
+
profile: value.profile,
|
|
496
|
+
repositoryRoot: value.repositoryRoot,
|
|
497
|
+
cwd: value.cwd,
|
|
498
|
+
providerArguments: value.providerArguments,
|
|
499
|
+
...(value.resumeSession === undefined ? {} : {resumeSession: value.resumeSession})
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export function parseRun(value) {
|
|
504
|
+
if (!isRecord(value) || typeof value.preflightId !== "string" || typeof value.prompt !== "string" || value.prompt.trim().length === 0 || !Array.isArray(value.providerArguments) || !value.providerArguments.every((item) => typeof item === "string")) {
|
|
505
|
+
throw new ClientError("Invalid isolated runtime run")
|
|
506
|
+
}
|
|
507
|
+
if (!bounded(value.preflightId, 64) || !bounded(value.prompt, 524_288)
|
|
508
|
+
|| value.providerArguments.length > 16 || value.providerArguments.some((item) => !bounded(item, 512))
|
|
509
|
+
|| (value.resumeSession !== undefined && !bounded(value.resumeSession, 256))) {
|
|
510
|
+
throw new ClientError("Invalid isolated runtime run")
|
|
511
|
+
}
|
|
512
|
+
try {
|
|
513
|
+
validateRelayWriteProviderArguments(value.providerArguments)
|
|
514
|
+
} catch (error) {
|
|
515
|
+
throw new ClientError(error instanceof Error ? error.message : "Provider argument is unavailable in relay write mode")
|
|
516
|
+
}
|
|
517
|
+
if (value.resumeSession !== undefined && (typeof value.resumeSession !== "string" || /[\r\n]/u.test(value.resumeSession))) throw new ClientError("Invalid isolated runtime run")
|
|
518
|
+
return {
|
|
519
|
+
preflightId: value.preflightId,
|
|
520
|
+
prompt: value.prompt,
|
|
521
|
+
providerArguments: value.providerArguments,
|
|
522
|
+
...(value.resumeSession === undefined ? {} : {resumeSession: value.resumeSession})
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
function requestedDeadline(value, maximumMs) {
|
|
527
|
+
const parsed = Number(value)
|
|
528
|
+
const maximum = Date.now() + maximumMs
|
|
529
|
+
return Number.isSafeInteger(parsed) && parsed > Date.now() ? Math.min(parsed, maximum) : maximum
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function parseAllowedRoots(value) {
|
|
533
|
+
let roots
|
|
534
|
+
try {
|
|
535
|
+
roots = JSON.parse(safeSetting(value, "THREADWIRE_ALLOWED_WORKTREE_ROOTS"))
|
|
536
|
+
} catch {
|
|
537
|
+
throw new Error("THREADWIRE_ALLOWED_WORKTREE_ROOTS must be a JSON array")
|
|
538
|
+
}
|
|
539
|
+
if (!Array.isArray(roots) || roots.length === 0 || !roots.every((root) => typeof root === "string" && isAbsolute(root) && normalize(root) === root && root !== "/")) {
|
|
540
|
+
throw new Error("THREADWIRE_ALLOWED_WORKTREE_ROOTS must contain normalized absolute paths")
|
|
541
|
+
}
|
|
542
|
+
return roots
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function within(root, child) {
|
|
546
|
+
const path = relative(root, child)
|
|
547
|
+
return path === "" || (!path.startsWith("..") && !isAbsolute(path))
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function ancestry(root, child) {
|
|
551
|
+
const remainder = relative(root, child)
|
|
552
|
+
const paths = [root]
|
|
553
|
+
let current = root
|
|
554
|
+
if (remainder === "") return paths
|
|
555
|
+
for (const component of remainder.split("/")) {
|
|
556
|
+
current = `${current}/${component}`
|
|
557
|
+
paths.push(current)
|
|
558
|
+
}
|
|
559
|
+
return paths
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function taskIdentity(preflight) {
|
|
563
|
+
return `${preflight.profile}\0${preflight.repositoryRoot}`
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function brokerNetworkAddress(value, containerName) {
|
|
567
|
+
if (!isRecord(value) || !isRecord(value.Containers)) throw new Error("Broker network identity unavailable")
|
|
568
|
+
for (const [id, endpoint] of Object.entries(value.Containers)) {
|
|
569
|
+
if (!isRecord(endpoint)) continue
|
|
570
|
+
if (id === containerName || endpoint.Name === containerName) {
|
|
571
|
+
const address = endpoint.IPv4Address
|
|
572
|
+
if (typeof address !== "string" || !/^(?:\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/u.test(address)) break
|
|
573
|
+
return address.split("/")[0]
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
throw new Error("Broker network identity unavailable")
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function readJson(request, capacity, signal) {
|
|
580
|
+
const chunks = []
|
|
581
|
+
let bytes = 0
|
|
582
|
+
for await (const chunk of request) {
|
|
583
|
+
if (signal?.aborted) throw signal.reason
|
|
584
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
585
|
+
bytes += buffer.length
|
|
586
|
+
if (bytes > capacity) throw new ClientError("Request exceeded capacity")
|
|
587
|
+
chunks.push(buffer)
|
|
588
|
+
}
|
|
589
|
+
if (signal?.aborted) throw signal.reason
|
|
590
|
+
try {
|
|
591
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"))
|
|
592
|
+
} catch {
|
|
593
|
+
throw new ClientError("Invalid request")
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function requireControl(value, expected) {
|
|
598
|
+
if (value !== `Bearer ${expected}`) throw new ClientError("Request denied")
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function normalizedHttpUrl(value, name) {
|
|
602
|
+
const url = new URL(safeSetting(value, name))
|
|
603
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error(`${name} is invalid`)
|
|
604
|
+
return url
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function safeSetting(value, name) {
|
|
608
|
+
if (typeof value !== "string" || value.length === 0 || /[\r\n]/u.test(value)) throw new Error(`${name} is required`)
|
|
609
|
+
return value
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function portValue(value, fallback) {
|
|
613
|
+
if (value === undefined) return fallback
|
|
614
|
+
const number = Number(value)
|
|
615
|
+
if (!/^\d+$/u.test(value) || !Number.isSafeInteger(number) || number < 1 || number > 65535) throw new Error("Invalid isolated runtime port")
|
|
616
|
+
return number
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function positiveDuration(value, fallback) {
|
|
620
|
+
if (value === undefined) return fallback
|
|
621
|
+
const number = Number(value)
|
|
622
|
+
if (!/^\d+$/u.test(value) || !Number.isSafeInteger(number) || number < 1 || number > 604_800_000) throw new Error("Invalid session TTL")
|
|
623
|
+
return number
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function positiveInteger(value, fallback) {
|
|
627
|
+
if (value === undefined) return fallback
|
|
628
|
+
const number = Number(value)
|
|
629
|
+
if (!/^\d+$/u.test(value) || !Number.isSafeInteger(number) || number < 1 || number > 1024) throw new Error("Invalid isolated runtime capacity")
|
|
630
|
+
return number
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function parseAllowedModels(value) {
|
|
634
|
+
let models
|
|
635
|
+
try {
|
|
636
|
+
models = JSON.parse(safeSetting(value, "THREADWIRE_ALLOWED_CODEX_MODELS"))
|
|
637
|
+
} catch {
|
|
638
|
+
throw new Error("THREADWIRE_ALLOWED_CODEX_MODELS must be a JSON array")
|
|
639
|
+
}
|
|
640
|
+
if (!Array.isArray(models) || models.length === 0 || !models.every((model) => typeof model === "string" && /^[A-Za-z0-9._-]{1,128}$/u.test(model))) {
|
|
641
|
+
throw new Error("THREADWIRE_ALLOWED_CODEX_MODELS must contain model identifiers")
|
|
642
|
+
}
|
|
643
|
+
return new Set(models)
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function sendJson(response, status, value) {
|
|
647
|
+
response.writeHead(status, {"content-type": "application/json"})
|
|
648
|
+
response.end(JSON.stringify(value))
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function isRecord(value) {
|
|
652
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function abortable(promise, signal) {
|
|
656
|
+
if (signal.aborted) return Promise.reject(signal.reason)
|
|
657
|
+
return new Promise((resolve, reject) => {
|
|
658
|
+
const abort = () => reject(signal.reason instanceof Error ? signal.reason : new Error("Isolated runtime operation aborted"))
|
|
659
|
+
signal.addEventListener("abort", abort, {once: true})
|
|
660
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort))
|
|
661
|
+
})
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function listen(server, port, host) {
|
|
665
|
+
return new Promise((resolve, reject) => {
|
|
666
|
+
server.once("error", reject)
|
|
667
|
+
server.listen(port, host, () => {
|
|
668
|
+
server.removeListener("error", reject)
|
|
669
|
+
resolve(undefined)
|
|
670
|
+
})
|
|
671
|
+
})
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
class ClientError extends Error {}
|
|
675
|
+
|
|
676
|
+
function bounded(value, maximum) {
|
|
677
|
+
return typeof value === "string" && value.length > 0 && value.length <= maximum && !/[\0\r\n]/u.test(value)
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export class PreflightStore {
|
|
681
|
+
constructor({now = Date.now, ttlMs = PREFLIGHT_TTL_MS, capacity = PREFLIGHT_CAPACITY, taskCapacity = PREFLIGHT_TASK_CAPACITY} = {}) {
|
|
682
|
+
this.now = now
|
|
683
|
+
this.ttlMs = ttlMs
|
|
684
|
+
this.capacity = capacity
|
|
685
|
+
this.taskCapacity = taskCapacity
|
|
686
|
+
this.entries = new Map()
|
|
687
|
+
this.timer = setInterval(() => this.sweep(), Math.max(10, Math.min(ttlMs, 1000)))
|
|
688
|
+
this.timer.unref()
|
|
689
|
+
}
|
|
690
|
+
sweep() {
|
|
691
|
+
const current = this.now()
|
|
692
|
+
for (const [id, entry] of this.entries) if (entry.expiresAt <= current) this.entries.delete(id)
|
|
693
|
+
}
|
|
694
|
+
add(id, task, value) {
|
|
695
|
+
this.sweep()
|
|
696
|
+
this.assertCapacity(task)
|
|
697
|
+
this.entries.set(id, {task, expiresAt: this.now() + this.ttlMs, value})
|
|
698
|
+
}
|
|
699
|
+
assertCapacity(task) {
|
|
700
|
+
this.sweep()
|
|
701
|
+
let taskCount = 0
|
|
702
|
+
for (const entry of this.entries.values()) if (entry.task === task) taskCount++
|
|
703
|
+
if (this.entries.size >= this.capacity || taskCount >= this.taskCapacity) throw new ClientError("Isolated runtime preflight capacity exhausted")
|
|
704
|
+
}
|
|
705
|
+
consume(id) {
|
|
706
|
+
this.sweep()
|
|
707
|
+
const entry = this.entries.get(id)
|
|
708
|
+
this.entries.delete(id)
|
|
709
|
+
return entry?.value
|
|
710
|
+
}
|
|
711
|
+
get size() { return this.entries.size }
|
|
712
|
+
close() {
|
|
713
|
+
clearInterval(this.timer)
|
|
714
|
+
this.entries.clear()
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export class LineageReservations {
|
|
719
|
+
constructor() {
|
|
720
|
+
this.active = new Set()
|
|
721
|
+
this.collecting = false
|
|
722
|
+
}
|
|
723
|
+
acquire(lineage) {
|
|
724
|
+
if (this.active.has(lineage)) throw new ClientError("Codex resume state is already active")
|
|
725
|
+
this.active.add(lineage)
|
|
726
|
+
let released = false
|
|
727
|
+
return () => {
|
|
728
|
+
if (released) return
|
|
729
|
+
released = true
|
|
730
|
+
this.active.delete(lineage)
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
beginCollection() {
|
|
734
|
+
if (this.collecting) throw new Error("Codex state collection overlap")
|
|
735
|
+
this.collecting = true
|
|
736
|
+
return () => { this.collecting = false }
|
|
737
|
+
}
|
|
738
|
+
values() { return this.active.values() }
|
|
739
|
+
has(lineage) { return this.active.has(lineage) }
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
export class ActiveRunAdmission {
|
|
743
|
+
constructor({capacity = ACTIVE_RUN_CAPACITY, taskCapacity = ACTIVE_TASK_CAPACITY} = {}) {
|
|
744
|
+
this.capacity = capacity
|
|
745
|
+
this.taskCapacity = taskCapacity
|
|
746
|
+
this.active = new Map()
|
|
747
|
+
}
|
|
748
|
+
acquire(task) {
|
|
749
|
+
const taskCount = this.active.get(task) ?? 0
|
|
750
|
+
let total = 0
|
|
751
|
+
for (const count of this.active.values()) total += count
|
|
752
|
+
if (total >= this.capacity || taskCount >= this.taskCapacity) throw new ClientError("Isolated runtime active-run capacity exhausted")
|
|
753
|
+
this.active.set(task, taskCount + 1)
|
|
754
|
+
let released = false
|
|
755
|
+
return () => {
|
|
756
|
+
if (released) return
|
|
757
|
+
released = true
|
|
758
|
+
const remaining = (this.active.get(task) ?? 1) - 1
|
|
759
|
+
if (remaining === 0) this.active.delete(task)
|
|
760
|
+
else this.active.set(task, remaining)
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export class OperationDeadline extends AbsoluteDeadline {
|
|
766
|
+
constructor(timeoutMs, request, response, now = Date.now) {
|
|
767
|
+
super(now() + timeoutMs, {request, response, now, timeoutMessage: "Isolated runtime operation timed out", disconnectMessage: "Isolated runtime caller disconnected"})
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function boundedClose(server, timeoutMs) {
|
|
772
|
+
return Promise.race([
|
|
773
|
+
new Promise((resolve) => server.close(() => resolve(undefined))),
|
|
774
|
+
new Promise((resolve) => {
|
|
775
|
+
const timer = setTimeout(resolve, timeoutMs)
|
|
776
|
+
timer.unref()
|
|
777
|
+
})
|
|
778
|
+
])
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
export async function reconcileDockerResources(docker, brokerContainer, stateRegistry, inProcessLineages = new Set(), requestOptions) {
|
|
782
|
+
const namespace = stateRegistry.namespace
|
|
783
|
+
const activeLineages = new Set(inProcessLineages)
|
|
784
|
+
const activeRuns = new Set()
|
|
785
|
+
const ownedIdentities = new Map(stateRegistry.runRecords().map((identity) => [identity.run, identity]))
|
|
786
|
+
const cleanupFailed = new Set()
|
|
787
|
+
const containers = await docker.listContainers({"org.threadwire.owner": "isolated-runtime"}, true, requestOptions)
|
|
788
|
+
for (const container of Array.isArray(containers) ? containers : []) {
|
|
789
|
+
const labels = validResourceLabels(container?.Labels, namespace, true)
|
|
790
|
+
if (!labels) continue
|
|
791
|
+
const persisted = ownedIdentities.get(labels.run)
|
|
792
|
+
if (!persisted || stateRegistry.sealRun(persisted) !== labels.seal) continue
|
|
793
|
+
const inspection = await docker.inspectContainer(container.Id, requestOptions).catch(() => undefined)
|
|
794
|
+
const identity = ownedContainerIdentity(inspection, labels)
|
|
795
|
+
if (!identity || !stateRegistry.ownsRun(identity, labels.seal)) {
|
|
796
|
+
cleanupFailed.add(labels.run)
|
|
797
|
+
continue
|
|
798
|
+
}
|
|
799
|
+
if (inProcessLineages.has(labels.lineage)) {
|
|
800
|
+
activeLineages.add(labels.lineage)
|
|
801
|
+
activeRuns.add(labels.run)
|
|
802
|
+
continue
|
|
803
|
+
}
|
|
804
|
+
if (inProcessLineages.has(labels.lineage)) {
|
|
805
|
+
activeLineages.add(labels.lineage)
|
|
806
|
+
activeRuns.add(labels.run)
|
|
807
|
+
continue
|
|
808
|
+
}
|
|
809
|
+
await docker.removeContainer(container.Id, requestOptions).catch(() => cleanupFailed.add(labels.run))
|
|
810
|
+
}
|
|
811
|
+
const networks = await docker.listNetworks({"org.threadwire.owner": "isolated-runtime"}, requestOptions)
|
|
812
|
+
for (const network of Array.isArray(networks) ? networks : []) {
|
|
813
|
+
const labels = validResourceLabels(network?.Labels, namespace, false)
|
|
814
|
+
if (!labels || activeRuns.has(labels.run) || cleanupFailed.has(labels.run)) continue
|
|
815
|
+
const identity = ownedIdentities.get(labels.run)
|
|
816
|
+
if (!identity || stateRegistry.sealRun(identity) !== labels.seal) continue
|
|
817
|
+
const inspection = await docker.inspectNetwork(network.Id, requestOptions).catch(() => undefined)
|
|
818
|
+
if (!identity || !ownedNetworkIdentity(inspection, labels, identity, brokerContainer)
|
|
819
|
+
|| !stateRegistry.ownsRun(identity, labels.seal)) {
|
|
820
|
+
cleanupFailed.add(labels.run)
|
|
821
|
+
continue
|
|
822
|
+
}
|
|
823
|
+
if (inProcessLineages.has(labels.lineage)) {
|
|
824
|
+
activeLineages.add(labels.lineage)
|
|
825
|
+
activeRuns.add(labels.run)
|
|
826
|
+
continue
|
|
827
|
+
}
|
|
828
|
+
const brokerEndpoint = Object.entries(inspection.Containers ?? {}).find(([, endpoint]) => endpoint?.Name === brokerContainer)
|
|
829
|
+
if (brokerEndpoint) await docker.disconnectNetwork(network.Id, brokerContainer, requestOptions).catch(() => {})
|
|
830
|
+
await docker.removeNetwork(network.Id, requestOptions).catch(() => cleanupFailed.add(labels.run))
|
|
831
|
+
}
|
|
832
|
+
for (const identity of ownedIdentities.values()) {
|
|
833
|
+
if (activeRuns.has(identity.run) || cleanupFailed.has(identity.run)) {
|
|
834
|
+
activeLineages.add(identity.lineage)
|
|
835
|
+
continue
|
|
836
|
+
}
|
|
837
|
+
if (inProcessLineages.has(identity.lineage)) {
|
|
838
|
+
activeLineages.add(identity.lineage)
|
|
839
|
+
continue
|
|
840
|
+
}
|
|
841
|
+
await stateRegistry.completeRun(identity.run, requestOptions?.signal).catch(() => activeLineages.add(identity.lineage))
|
|
842
|
+
}
|
|
843
|
+
return [...activeLineages]
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
function validResourceLabels(value, namespace, container) {
|
|
847
|
+
if (!isRecord(value)) return undefined
|
|
848
|
+
const expectedKeys = [
|
|
849
|
+
"org.threadwire.owner", "org.threadwire.namespace", "org.threadwire.run",
|
|
850
|
+
"org.threadwire.lineage", "org.threadwire.task", "org.threadwire.state-volume",
|
|
851
|
+
"org.threadwire.run-seal",
|
|
852
|
+
...(container ? ["org.threadwire.network"] : [])
|
|
853
|
+
]
|
|
854
|
+
if (Object.keys(value).length !== expectedKeys.length || expectedKeys.some((key) => typeof value[key] !== "string")) return undefined
|
|
855
|
+
const run = value["org.threadwire.run"]
|
|
856
|
+
const lineage = value["org.threadwire.lineage"]
|
|
857
|
+
const volume = value["org.threadwire.state-volume"]
|
|
858
|
+
const seal = value["org.threadwire.run-seal"]
|
|
859
|
+
if (value["org.threadwire.owner"] !== "isolated-runtime" || value["org.threadwire.namespace"] !== namespace
|
|
860
|
+
|| !/^tw-[0-9a-f]{24}$/u.test(run) || !/^[0-9a-f]{48}$/u.test(lineage)
|
|
861
|
+
|| !/^[0-9a-f]{64}$/u.test(value["org.threadwire.task"])
|
|
862
|
+
|| !/^[0-9a-f]{64}$/u.test(seal)
|
|
863
|
+
|| volume !== `threadwire-state-${namespace}-${lineage}`
|
|
864
|
+
|| (container && value["org.threadwire.network"] !== `${run}-net`)) return undefined
|
|
865
|
+
return {run, lineage, volume, seal, task: value["org.threadwire.task"], namespace}
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function ownedContainerIdentity(container, labels) {
|
|
869
|
+
if (!isRecord(container) || container.Name !== `/${labels.run}-worker`
|
|
870
|
+
|| !isRecord(container.Config) || !isRecord(container.HostConfig)
|
|
871
|
+
|| container.Config.Image !== container.Image || !isDigestImage(container.Config.Image)
|
|
872
|
+
|| JSON.stringify(container.Config.Entrypoint) !== JSON.stringify(["/usr/local/libexec/threadwire/worker-entrypoint"])
|
|
873
|
+
|| ![null, undefined].includes(container.Config.Cmd) || container.Config.User !== "10002:10002"
|
|
874
|
+
|| typeof container.Config.WorkingDir !== "string" || !within("/worktree", container.Config.WorkingDir)
|
|
875
|
+
|| container.HostConfig.NetworkMode !== `${labels.run}-net`
|
|
876
|
+
|| !validWorkerSecurity(container.Config, container.HostConfig)
|
|
877
|
+
|| !Array.isArray(container.HostConfig.Mounts) || container.HostConfig.Mounts.length !== 2) return undefined
|
|
878
|
+
const inspectedLabels = validResourceLabels(container.Config.Labels, labels.namespace, true)
|
|
879
|
+
if (!inspectedLabels || JSON.stringify(inspectedLabels) !== JSON.stringify(labels)) return undefined
|
|
880
|
+
const state = container.HostConfig.Mounts.find((mount) => mount?.Target === "/home/worker")
|
|
881
|
+
const worktree = container.HostConfig.Mounts.find((mount) => mount?.Target === "/worktree")
|
|
882
|
+
if (state?.Type !== "volume" || state.Source !== labels.volume || state.ReadOnly === true
|
|
883
|
+
|| Object.keys(state).some((key) => !["Type", "Source", "Target", "ReadOnly"].includes(key))) return undefined
|
|
884
|
+
if (!validWorktreeMount(worktree)) return undefined
|
|
885
|
+
return runIdentityFrom(labels, container.Config.Image, worktree)
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
function validWorkerSecurity(config, host) {
|
|
889
|
+
if (host.AutoRemove !== false || host.ReadonlyRootfs !== true
|
|
890
|
+
|| host.Privileged === true || JSON.stringify(host.CapDrop) !== JSON.stringify(["ALL"])
|
|
891
|
+
|| JSON.stringify(host.CapAdd ?? null) !== "null"
|
|
892
|
+
|| JSON.stringify(host.SecurityOpt) !== JSON.stringify(["no-new-privileges"])
|
|
893
|
+
|| host.PidsLimit !== 128 || host.Memory !== 1_073_741_824 || host.NanoCpus !== 2_000_000_000
|
|
894
|
+
|| !isRecord(host.Tmpfs) || Object.keys(host.Tmpfs).length !== 2
|
|
895
|
+
|| host.Tmpfs["/tmp"] !== "rw,noexec,nosuid,nodev,size=67108864,uid=10002,gid=10002"
|
|
896
|
+
|| host.Tmpfs["/run"] !== "rw,noexec,nosuid,nodev,size=1048576,uid=10002,gid=10002"
|
|
897
|
+
|| !Array.isArray(host.Ulimits) || host.Ulimits.length !== 3
|
|
898
|
+
|| ![
|
|
899
|
+
["nofile", 1024, 1024], ["core", 0, 0], ["fsize", 1_073_741_824, 1_073_741_824]
|
|
900
|
+
].every(([name, soft, hard], index) => host.Ulimits[index]?.Name === name
|
|
901
|
+
&& host.Ulimits[index]?.Soft === soft && host.Ulimits[index]?.Hard === hard)) return false
|
|
902
|
+
const forbidden = {
|
|
903
|
+
Binds: [null, []], Devices: [null, []], DeviceRequests: [null, []], PortBindings: [null, {}],
|
|
904
|
+
Links: [null, []], ExtraHosts: [null, []], VolumesFrom: [null, []], Dns: [null, []],
|
|
905
|
+
DnsSearch: [null, []], Sysctls: [null, {}]
|
|
906
|
+
}
|
|
907
|
+
for (const [key, defaults] of Object.entries(forbidden)) {
|
|
908
|
+
if (!defaults.some((value) => JSON.stringify(host[key] ?? null) === JSON.stringify(value))) return false
|
|
909
|
+
}
|
|
910
|
+
if (![undefined, "", "private"].includes(host.PidMode) || ![undefined, "", "private"].includes(host.IpcMode)
|
|
911
|
+
|| ![undefined, ""].includes(host.UTSMode) || ![undefined, ""].includes(host.UsernsMode)
|
|
912
|
+
|| ![undefined, "", "private"].includes(host.CgroupnsMode) || host.PublishAllPorts === true) return false
|
|
913
|
+
if (!Array.isArray(config.Env) || config.Env.length !== 13 + (config.Env.some((entry) => entry.startsWith("THREADWIRE_RESUME_SESSION=")) ? 1 : 0)) return false
|
|
914
|
+
const names = config.Env.map((entry) => entry.split("=", 1)[0])
|
|
915
|
+
const expected = [
|
|
916
|
+
"HOME", "CODEX_HOME", "TMPDIR", "OPENAI_BASE_URL", "OPENAI_API_KEY", "CODEX_API_KEY",
|
|
917
|
+
"THREADWIRE_PROMPT", "THREADWIRE_CODEX_ARGUMENTS", "THREADWIRE_WORKTREE_INODE", "THREADWIRE_CWD_INODE"
|
|
918
|
+
]
|
|
919
|
+
return expected.every((name) => names.filter((entry) => entry === name).length === 1)
|
|
920
|
+
&& config.Env.includes("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin")
|
|
921
|
+
&& config.Env.includes("NODE_VERSION=22.17.0") && config.Env.includes("YARN_VERSION=1.22.22")
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function ownedNetworkIdentity(network, labels, identity, brokerContainer) {
|
|
925
|
+
if (!isRecord(network) || network.Name !== `${labels.run}-net` || network.Internal !== true
|
|
926
|
+
|| network.Driver !== "bridge" || network.Scope !== "local"
|
|
927
|
+
|| network.EnableIPv6 !== false || network.Attachable !== false || network.Ingress !== false
|
|
928
|
+
|| network.ConfigOnly !== false || !isRecord(network.Options) || Object.keys(network.Options).length !== 0
|
|
929
|
+
|| !isRecord(network.Containers)) return undefined
|
|
930
|
+
const inspectedLabels = validResourceLabels(network.Labels, labels.namespace, false)
|
|
931
|
+
if (!inspectedLabels || JSON.stringify(inspectedLabels) !== JSON.stringify(labels)) return undefined
|
|
932
|
+
const endpoints = Object.values(network.Containers)
|
|
933
|
+
if (endpoints.some((endpoint) => !isRecord(endpoint)
|
|
934
|
+
|| ![identity.container, brokerContainer].includes(endpoint.Name))) return undefined
|
|
935
|
+
return true
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
function validWorktreeMount(mount) {
|
|
939
|
+
if (!isRecord(mount) || mount.Target !== "/worktree" || mount.ReadOnly === true) return false
|
|
940
|
+
if (mount.Type === "bind") {
|
|
941
|
+
return Object.keys(mount).every((key) => ["Type", "Source", "Target", "ReadOnly", "BindOptions"].includes(key))
|
|
942
|
+
&& typeof mount.Source === "string" && isAbsolute(mount.Source)
|
|
943
|
+
&& isRecord(mount.BindOptions) && Object.keys(mount.BindOptions).length === 1
|
|
944
|
+
&& mount.BindOptions.Propagation === "rprivate"
|
|
945
|
+
}
|
|
946
|
+
return mount.Type === "volume" && typeof mount.Source === "string"
|
|
947
|
+
&& Object.keys(mount).every((key) => ["Type", "Source", "Target", "ReadOnly", "VolumeOptions"].includes(key))
|
|
948
|
+
&& isRecord(mount.VolumeOptions) && Object.keys(mount.VolumeOptions).length === 1
|
|
949
|
+
&& typeof mount.VolumeOptions.Subpath === "string" && mount.VolumeOptions.Subpath.length > 0
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function runIdentityFrom(labels, image, worktree) {
|
|
953
|
+
return {
|
|
954
|
+
namespace: labels.namespace, run: labels.run, lineage: labels.lineage, task: labels.task,
|
|
955
|
+
volume: labels.volume, network: `${labels.run}-net`, container: `${labels.run}-worker`,
|
|
956
|
+
image, worktreeType: worktree.Type, worktreeSource: worktree.Source,
|
|
957
|
+
worktreeSubpath: worktree.Type === "volume" ? worktree.VolumeOptions.Subpath : ""
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function expectedRunIdentity(options, run, network, state, worktreeSubpath) {
|
|
962
|
+
const volumeMode = options.worktreeVolume !== undefined
|
|
963
|
+
return {
|
|
964
|
+
namespace: options.stateRegistry.namespace, run, lineage: state.lineage,
|
|
965
|
+
task: state.labels["org.threadwire.task"], volume: state.volume, network,
|
|
966
|
+
container: `${run}-worker`, image: options.workerImage,
|
|
967
|
+
worktreeType: volumeMode ? "volume" : "bind",
|
|
968
|
+
worktreeSource: volumeMode ? options.worktreeVolume : options.preflight.repositoryRoot,
|
|
969
|
+
worktreeSubpath: volumeMode ? worktreeSubpath : ""
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
export function deriveVolumeWorktreeSubpath(worktreeRoot, repositoryRoot) {
|
|
974
|
+
if (typeof worktreeRoot !== "string" || typeof repositoryRoot !== "string" || !within(worktreeRoot, repositoryRoot)) {
|
|
975
|
+
throw new ClientError("Configured worktree has an invalid volume subpath")
|
|
976
|
+
}
|
|
977
|
+
const subpath = relative(worktreeRoot, repositoryRoot)
|
|
978
|
+
if (subpath === "" || isAbsolute(subpath) || normalize(subpath) !== subpath || subpath.split("/").includes("..")) {
|
|
979
|
+
throw new ClientError("Configured worktree has an invalid volume subpath")
|
|
980
|
+
}
|
|
981
|
+
return subpath
|
|
982
|
+
}
|