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