threadwire 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +3 -3
- package/TELEGRAM-INGRESS.md +8 -4
- package/bin/kimi-model-broker.js +5 -0
- package/docs/container-runtime.md +22 -0
- package/docs/isolated-provider-runtime.md +95 -0
- package/package.json +6 -3
- package/scripts/verify-package.js +8 -1
- package/src/activity-log.js +3 -3
- package/src/cli.js +20 -14
- package/src/isolated-runtime-client.js +9 -0
- package/src/isolated-runtime.js +199 -64
- package/src/isolated-state.js +105 -44
- package/src/isolated-worker.js +28 -7
- package/src/kimi-model-broker-policy.js +209 -0
- package/src/kimi-model-broker.js +325 -0
- package/src/kimi-oauth-store.js +267 -0
- package/src/providers/index.js +8 -3
- package/src/providers/kimi.js +165 -0
- package/src/telegram-ingress/command.js +4 -4
- package/src/telegram-ingress/config.js +11 -0
- package/src/telegram-ingress/core.js +33 -10
- package/src/telegram-ingress/http.js +2 -1
- package/src/telegram-webhook.js +8 -3
- package/src/workspace-profile.js +3 -3
- package/threadwire.workspace-profiles.json +1 -1
package/src/isolated-runtime.js
CHANGED
|
@@ -9,6 +9,8 @@ import {DockerApi} from "./docker-api.js"
|
|
|
9
9
|
import {buildWorkerContainerSpec, isDigestImage} from "./isolated-worker.js"
|
|
10
10
|
import {validateRelayWriteProviderArguments} from "./relay-write.js"
|
|
11
11
|
import {codexSessionId, parseCodexEvent} from "./providers/codex.js"
|
|
12
|
+
import {kimiSessionEnvelopeId, validateKimiProviderArguments} from "./providers/kimi.js"
|
|
13
|
+
import {parseApprovedKimiModels} from "./kimi-model-broker-policy.js"
|
|
12
14
|
import {openStateRegistry, validateStateVolume} from "./isolated-state.js"
|
|
13
15
|
import {assertNoNestedMounts} from "./mount-policy.js"
|
|
14
16
|
import {AbsoluteDeadline, deadlineAfter} from "./absolute-deadline.js"
|
|
@@ -30,12 +32,21 @@ export async function startIsolatedRuntime(options = {}) {
|
|
|
30
32
|
const environment = options.environment ?? process.env
|
|
31
33
|
const host = environment.THREADWIRE_ISOLATED_RUNTIME_HOST ?? "0.0.0.0"
|
|
32
34
|
const port = portValue(environment.THREADWIRE_ISOLATED_RUNTIME_PORT, 8790)
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
const
|
|
35
|
+
const provider = isolatedProvider(environment.THREADWIRE_ISOLATED_PROVIDER)
|
|
36
|
+
const controlToken = provider === "kimi"
|
|
37
|
+
? authoritySetting(environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN, "THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN")
|
|
38
|
+
: safeSetting(environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN, "THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN")
|
|
39
|
+
const workerImageName = provider === "kimi" ? "THREADWIRE_KIMI_RELAY_WORKER_IMAGE" : "THREADWIRE_RELAY_WORKER_IMAGE"
|
|
40
|
+
const workerImage = safeSetting(environment[workerImageName], workerImageName)
|
|
41
|
+
if (!isDigestImage(workerImage)) throw new Error(`${workerImageName} must use an immutable digest`)
|
|
42
|
+
const brokerUrlName = provider === "kimi" ? "THREADWIRE_KIMI_MODEL_BROKER_URL" : "THREADWIRE_MODEL_BROKER_URL"
|
|
43
|
+
const brokerAdminName = provider === "kimi" ? "THREADWIRE_KIMI_MODEL_BROKER_ADMIN_TOKEN" : "THREADWIRE_MODEL_BROKER_ADMIN_TOKEN"
|
|
44
|
+
const brokerContainerName = provider === "kimi" ? "THREADWIRE_KIMI_MODEL_BROKER_CONTAINER" : "THREADWIRE_MODEL_BROKER_CONTAINER"
|
|
45
|
+
const brokerUrl = normalizedHttpUrl(environment[brokerUrlName], brokerUrlName)
|
|
46
|
+
const brokerAdminToken = provider === "kimi"
|
|
47
|
+
? authoritySetting(environment[brokerAdminName], brokerAdminName)
|
|
48
|
+
: safeSetting(environment[brokerAdminName], brokerAdminName)
|
|
49
|
+
const brokerContainer = safeSetting(environment[brokerContainerName], brokerContainerName)
|
|
39
50
|
const allowedRoots = parseAllowedRoots(environment.THREADWIRE_ALLOWED_WORKTREE_ROOTS)
|
|
40
51
|
const worktreeVolume = environment.THREADWIRE_WORKTREE_VOLUME
|
|
41
52
|
const sessionTtlMs = positiveDuration(environment.THREADWIRE_SESSION_TTL_MS, 86_400_000)
|
|
@@ -44,11 +55,14 @@ export async function startIsolatedRuntime(options = {}) {
|
|
|
44
55
|
const docker = options.docker ?? new DockerApi()
|
|
45
56
|
const fetchImplementation = options.fetchImplementation ?? fetch
|
|
46
57
|
const now = options.now ?? Date.now
|
|
47
|
-
const allowedModels =
|
|
58
|
+
const allowedModels = provider === "kimi"
|
|
59
|
+
? parseApprovedKimiModels(environment.THREADWIRE_ALLOWED_KIMI_MODELS)
|
|
60
|
+
: parseAllowedModels(environment.THREADWIRE_ALLOWED_CODEX_MODELS)
|
|
48
61
|
const stateRegistry = await openStateRegistry({
|
|
49
62
|
file: safeSetting(environment.THREADWIRE_STATE_REGISTRY_FILE, "THREADWIRE_STATE_REGISTRY_FILE"),
|
|
50
63
|
key: safeSetting(environment.THREADWIRE_STATE_AUTH_KEY, "THREADWIRE_STATE_AUTH_KEY"),
|
|
51
64
|
namespace: safeSetting(environment.THREADWIRE_STATE_NAMESPACE, "THREADWIRE_STATE_NAMESPACE"),
|
|
65
|
+
provider,
|
|
52
66
|
now
|
|
53
67
|
})
|
|
54
68
|
const preflights = new PreflightStore({
|
|
@@ -108,28 +122,22 @@ export async function startIsolatedRuntime(options = {}) {
|
|
|
108
122
|
})
|
|
109
123
|
try {
|
|
110
124
|
const body = await readJson(request, MAX_PREFLIGHT_BYTES, operation.signal)
|
|
111
|
-
const selection = parsePreflight(body)
|
|
125
|
+
const selection = parsePreflight(body, provider)
|
|
112
126
|
const task = taskIdentity(selection)
|
|
113
127
|
preflights.assertCapacity(task)
|
|
114
|
-
|
|
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")
|
|
128
|
+
const providerSelection = runtimeProviderSelection(provider, selection.providerArguments, allowedModels)
|
|
121
129
|
const {fingerprint, root: worktreeRoot} = await verifyPrerequisites({
|
|
122
130
|
docker, workerImage, selection, allowedRoots, brokerUrl, brokerAdminToken, fetchImplementation, operation
|
|
123
131
|
})
|
|
124
132
|
let state
|
|
125
133
|
if (selection.resumeSession !== undefined) {
|
|
126
|
-
state = stateRegistry.lookup(selection.resumeSession)
|
|
127
|
-
if (!state || state.task !== task) throw new ClientError(
|
|
134
|
+
state = stateRegistry.lookup(selection.resumeSession, provider)
|
|
135
|
+
if (!state || state.provider !== provider || state.task !== task) throw new ClientError(`${providerName(provider)} resume state is unavailable`)
|
|
128
136
|
const volume = await docker.inspectVolume(state.volume, operation.options()).catch(() => undefined)
|
|
129
|
-
try { validateStateVolume(volume, state.volume, state.labels) } catch { throw new ClientError(
|
|
137
|
+
try { validateStateVolume(volume, state.volume, state.labels) } catch { throw new ClientError(`${providerName(provider)} resume state is unavailable`) }
|
|
130
138
|
} else state = stateRegistry.allocate(task)
|
|
131
139
|
const preflightId = randomBytes(24).toString("base64url")
|
|
132
|
-
preflights.add(preflightId, task, {...selection,
|
|
140
|
+
preflights.add(preflightId, task, {...selection, providerSelection, state, fingerprint, worktreeRoot, launchDeadlineAt})
|
|
133
141
|
sendJson(response, 200, {ok: true, preflightId, launchDeadlineAt})
|
|
134
142
|
} finally {
|
|
135
143
|
operation.close()
|
|
@@ -159,9 +167,9 @@ export async function startIsolatedRuntime(options = {}) {
|
|
|
159
167
|
try {
|
|
160
168
|
releaseRun = activeRuns.acquire(task)
|
|
161
169
|
releaseLineage = activeLineages.acquire(preflight.state.lineage)
|
|
162
|
-
if (body.resumeSession !== preflight.resumeSession) throw new ClientError(
|
|
163
|
-
if (JSON.stringify(
|
|
164
|
-
throw new ClientError(
|
|
170
|
+
if (body.resumeSession !== preflight.resumeSession) throw new ClientError(`${providerName(provider)} resume state does not match preflight`)
|
|
171
|
+
if (JSON.stringify(runtimeProviderSelection(provider, body.providerArguments, allowedModels)) !== JSON.stringify(preflight.providerSelection)) {
|
|
172
|
+
throw new ClientError(`${providerName(provider)} arguments do not match preflight`)
|
|
165
173
|
}
|
|
166
174
|
await revalidateFingerprint(preflight.fingerprint)
|
|
167
175
|
operation.throwIfAborted()
|
|
@@ -274,6 +282,9 @@ export async function runIsolatedWorker(options) {
|
|
|
274
282
|
const runId = `tw-${randomBytes(12).toString("hex")}`
|
|
275
283
|
const networkName = `${runId}-net`
|
|
276
284
|
const state = options.preflight.state
|
|
285
|
+
const provider = options.preflight.provider ?? "codex"
|
|
286
|
+
const providerSelection = options.preflight.providerSelection ?? options.preflight.codexArguments
|
|
287
|
+
if (providerSelection === undefined) throw new Error("Provider selection unavailable")
|
|
277
288
|
const freshState = options.resumeSession === undefined
|
|
278
289
|
let keepFreshState = false
|
|
279
290
|
let networkId
|
|
@@ -294,6 +305,7 @@ export async function runIsolatedWorker(options) {
|
|
|
294
305
|
runTracked = true
|
|
295
306
|
const resourceLabels = {
|
|
296
307
|
"org.threadwire.namespace": options.stateRegistry.namespace,
|
|
308
|
+
"org.threadwire.provider": provider,
|
|
297
309
|
"org.threadwire.run": runId,
|
|
298
310
|
"org.threadwire.lineage": state.lineage,
|
|
299
311
|
"org.threadwire.task": state.labels["org.threadwire.task"],
|
|
@@ -311,7 +323,14 @@ export async function runIsolatedWorker(options) {
|
|
|
311
323
|
redirect: "error",
|
|
312
324
|
signal: operation.signal,
|
|
313
325
|
headers: {authorization: `Bearer ${options.brokerAdminToken}`, "content-type": "application/json"},
|
|
314
|
-
body: JSON.stringify(
|
|
326
|
+
body: JSON.stringify(provider === "kimi"
|
|
327
|
+
? {
|
|
328
|
+
runId, provider, networkId, modelAlias: providerSelection.modelAlias, brokerAddress,
|
|
329
|
+
taskId: state.labels["org.threadwire.task"],
|
|
330
|
+
sessionId: options.resumeSession ?? `pending:${state.lineage}`,
|
|
331
|
+
ttlMs: Math.min(operation.remaining(), 3_600_000)
|
|
332
|
+
}
|
|
333
|
+
: {runId, provider, networkId, model: providerSelection.model, brokerAddress, ttlMs: Math.min(operation.remaining(), 3_600_000)})
|
|
315
334
|
}), operation.signal)
|
|
316
335
|
if (!grantResponse.ok) throw new Error("Model broker grant failed")
|
|
317
336
|
const grant = await grantResponse.json()
|
|
@@ -322,13 +341,15 @@ export async function runIsolatedWorker(options) {
|
|
|
322
341
|
const cwdMetadata = repository.find((entry) => entry.path === options.preflight.cwd)
|
|
323
342
|
if (worktreeMetadata === undefined || cwdMetadata === undefined) throw new Error("Worktree fingerprint unavailable")
|
|
324
343
|
const spec = buildWorkerContainerSpec({
|
|
344
|
+
provider,
|
|
325
345
|
image: options.workerImage,
|
|
326
346
|
worktree: options.preflight.repositoryRoot,
|
|
327
347
|
networkName,
|
|
328
348
|
brokerToken: grantToken,
|
|
329
349
|
brokerUrl: `http://${brokerAddress}:${grant.port}/v1`,
|
|
330
350
|
prompt: options.prompt,
|
|
331
|
-
providerArguments:
|
|
351
|
+
providerArguments: providerSelection.arguments,
|
|
352
|
+
...(provider === "kimi" ? {model: providerSelection.model} : {}),
|
|
332
353
|
stateVolume: state.volume,
|
|
333
354
|
workingDirectory: `/worktree${relative(options.preflight.repositoryRoot, options.preflight.cwd) === "" ? "" : `/${relative(options.preflight.repositoryRoot, options.preflight.cwd)}`}`,
|
|
334
355
|
worktreeInode: worktreeMetadata.ino,
|
|
@@ -367,12 +388,27 @@ export async function runIsolatedWorker(options) {
|
|
|
367
388
|
const rawChunks = demultiplexDockerLogChunks(logs)
|
|
368
389
|
const result = {
|
|
369
390
|
exitCode: typeof wait.StatusCode === "number" ? wait.StatusCode : 1,
|
|
370
|
-
records: parseWorkerRecords(
|
|
371
|
-
|
|
391
|
+
records: parseWorkerRecords(
|
|
392
|
+
Buffer.concat(rawChunks.filter((chunk) => chunk.channel === "stdout").map((chunk) => chunk.data)).toString("utf8"),
|
|
393
|
+
provider
|
|
394
|
+
),
|
|
395
|
+
rawChunks: provider === "kimi"
|
|
396
|
+
? []
|
|
397
|
+
: rawChunks.map((chunk) => ({channel: chunk.channel, data: chunk.data.toString("base64")}))
|
|
398
|
+
}
|
|
399
|
+
let sessionIds = [...new Set(result.records.filter((record) => record.type === "session").map((record) => record.sessionId))]
|
|
400
|
+
if (provider === "kimi" && result.exitCode !== 0) {
|
|
401
|
+
result.records = result.records.filter((record) => record.type !== "session")
|
|
402
|
+
sessionIds = []
|
|
403
|
+
}
|
|
404
|
+
if (provider === "kimi" && result.exitCode === 0) {
|
|
405
|
+
if (sessionIds.length !== 1) throw new Error("Kimi worker did not establish exactly one native session")
|
|
406
|
+
if (options.resumeSession !== undefined && sessionIds[0] !== options.resumeSession) {
|
|
407
|
+
throw new Error("Kimi worker did not preserve the exact resume session")
|
|
408
|
+
}
|
|
372
409
|
}
|
|
373
410
|
if (freshState && result.exitCode === 0) {
|
|
374
|
-
|
|
375
|
-
if (sessionIds.length === 0) throw new Error("Codex session state was not established")
|
|
411
|
+
if (sessionIds.length === 0) throw new Error(`${providerName(provider)} session state was not established`)
|
|
376
412
|
await options.stateRegistry.register(sessionIds, state, options.sessionTtlMs, operation.signal)
|
|
377
413
|
keepFreshState = true
|
|
378
414
|
}
|
|
@@ -421,7 +457,8 @@ export async function prepareStateVolume(docker, state, fresh, requestOptions) {
|
|
|
421
457
|
try {
|
|
422
458
|
validateStateVolume(volume, state.volume, state.labels)
|
|
423
459
|
} catch {
|
|
424
|
-
|
|
460
|
+
const provider = state.provider ?? "codex"
|
|
461
|
+
throw new ClientError(fresh ? `${providerName(provider)} state volume collision` : `${providerName(provider)} resume state is unavailable`)
|
|
425
462
|
}
|
|
426
463
|
}
|
|
427
464
|
|
|
@@ -438,27 +475,70 @@ export function validateWorkerMountInventory(container, expected) {
|
|
|
438
475
|
}
|
|
439
476
|
}
|
|
440
477
|
|
|
441
|
-
function parseWorkerRecords(text) {
|
|
478
|
+
function parseWorkerRecords(text, provider = "codex") {
|
|
442
479
|
const records = []
|
|
480
|
+
let kimiSessionId
|
|
481
|
+
let conflictingKimiSession = false
|
|
443
482
|
for (const line of text.split("\n")) {
|
|
444
483
|
if (line.trim().length === 0) continue
|
|
445
484
|
try {
|
|
446
485
|
const record = JSON.parse(line)
|
|
447
|
-
if (isRecord(record))
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
if (
|
|
452
|
-
|
|
453
|
-
|
|
486
|
+
if (!isRecord(record)) continue
|
|
487
|
+
if (provider === "kimi") {
|
|
488
|
+
const envelope = validatedKimiWorkerEnvelope(record)
|
|
489
|
+
if (envelope?.type === "session") {
|
|
490
|
+
if (kimiSessionId === undefined) {
|
|
491
|
+
kimiSessionId = envelope.sessionId
|
|
492
|
+
records.push(envelope)
|
|
493
|
+
} else if (kimiSessionId !== envelope.sessionId) conflictingKimiSession = true
|
|
494
|
+
} else if (envelope !== undefined) records.push(envelope)
|
|
495
|
+
continue
|
|
496
|
+
}
|
|
497
|
+
records.push(record)
|
|
498
|
+
if (record.type !== "worker-event") {
|
|
499
|
+
const sessionId = codexSessionId(record)
|
|
500
|
+
if (sessionId !== undefined) records.push({type: "session", sessionId})
|
|
501
|
+
for (const event of parseCodexEvent(record)) records.push({type: "worker-event", event})
|
|
454
502
|
}
|
|
455
503
|
} catch {
|
|
456
|
-
|
|
504
|
+
if (provider === "codex") {
|
|
505
|
+
records.push({type: "worker-event", event: {type: "diagnostic", level: "warning", summary: "Worker emitted an unreadable event"}})
|
|
506
|
+
}
|
|
457
507
|
}
|
|
458
508
|
}
|
|
509
|
+
if (conflictingKimiSession) throw new Error("Kimi worker emitted conflicting native sessions")
|
|
459
510
|
return records
|
|
460
511
|
}
|
|
461
512
|
|
|
513
|
+
function validatedKimiWorkerEnvelope(record) {
|
|
514
|
+
const sessionId = kimiSessionEnvelopeId(record)
|
|
515
|
+
if (sessionId !== undefined) return {type: "session", sessionId}
|
|
516
|
+
if (!exactKeys(record, ["type", "event"]) || record.type !== "worker-event" || !isRecord(record.event)) return undefined
|
|
517
|
+
const event = record.event
|
|
518
|
+
if (exactKeys(event, ["type", "text", "streamId"]) && event.type === "text-delta"
|
|
519
|
+
&& typeof event.text === "string" && event.text.length <= 1_048_576 && event.streamId === "kimi:assistant") {
|
|
520
|
+
return {type: "worker-event", event: {type: "text-delta", text: event.text, streamId: "kimi:assistant"}}
|
|
521
|
+
}
|
|
522
|
+
if (exactKeys(event, ["type", "phase", "name", "key"]) && event.type === "tool"
|
|
523
|
+
&& (event.phase === "started" || event.phase === "finished")
|
|
524
|
+
&& typeof event.name === "string" && /^[A-Za-z_][A-Za-z0-9_.:-]{0,127}$/u.test(event.name)
|
|
525
|
+
&& typeof event.key === "string" && /^tool:[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u.test(event.key)) {
|
|
526
|
+
return {type: "worker-event", event: {type: "tool", phase: event.phase, name: event.name, key: event.key}}
|
|
527
|
+
}
|
|
528
|
+
if (exactKeys(event, ["type", "phase", "summary"]) && event.type === "lifecycle") {
|
|
529
|
+
const summaries = {
|
|
530
|
+
started: "Kimi worker started",
|
|
531
|
+
completed: "Kimi worker completed",
|
|
532
|
+
failed: "Kimi worker failed",
|
|
533
|
+
cancelled: "Kimi worker cancelled"
|
|
534
|
+
}
|
|
535
|
+
if (summaries[event.phase] === event.summary) {
|
|
536
|
+
return {type: "worker-event", event: {type: "lifecycle", phase: event.phase, summary: event.summary}}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return undefined
|
|
540
|
+
}
|
|
541
|
+
|
|
462
542
|
export function demultiplexDockerLogs(buffer) {
|
|
463
543
|
return Buffer.concat(demultiplexDockerLogChunks(buffer).map((chunk) => chunk.data)).toString("utf8")
|
|
464
544
|
}
|
|
@@ -481,17 +561,20 @@ export function demultiplexDockerLogChunks(buffer) {
|
|
|
481
561
|
return [{channel: "stdout", data: buffer}]
|
|
482
562
|
}
|
|
483
563
|
|
|
484
|
-
export function parsePreflight(value) {
|
|
485
|
-
if (!isRecord(value) || value.provider !==
|
|
564
|
+
export function parsePreflight(value, expectedProvider = "codex") {
|
|
565
|
+
if (!isRecord(value) || value.provider !== expectedProvider || (expectedProvider !== "codex" && expectedProvider !== "kimi")
|
|
566
|
+
|| typeof value.profile !== "string" || typeof value.repositoryRoot !== "string" || typeof value.cwd !== "string"
|
|
567
|
+
|| !Array.isArray(value.providerArguments) || !value.providerArguments.every((item) => typeof item === "string")) {
|
|
486
568
|
throw new ClientError("Invalid isolated runtime preflight")
|
|
487
569
|
}
|
|
488
570
|
if (!bounded(value.profile, 128) || !bounded(value.repositoryRoot, 4096) || !bounded(value.cwd, 4096)
|
|
489
571
|
|| value.providerArguments.length > 16 || value.providerArguments.some((item) => !bounded(item, 512))
|
|
490
|
-
|| (value.resumeSession !== undefined && !bounded(value.resumeSession,
|
|
572
|
+
|| (value.resumeSession !== undefined && !bounded(value.resumeSession, 512))) {
|
|
491
573
|
throw new ClientError("Invalid isolated runtime preflight")
|
|
492
574
|
}
|
|
493
575
|
if (value.resumeSession !== undefined && (typeof value.resumeSession !== "string" || /[\r\n]/u.test(value.resumeSession))) throw new ClientError("Invalid isolated runtime preflight")
|
|
494
576
|
return {
|
|
577
|
+
provider: expectedProvider,
|
|
495
578
|
profile: value.profile,
|
|
496
579
|
repositoryRoot: value.repositoryRoot,
|
|
497
580
|
cwd: value.cwd,
|
|
@@ -506,14 +589,9 @@ export function parseRun(value) {
|
|
|
506
589
|
}
|
|
507
590
|
if (!bounded(value.preflightId, 64) || !bounded(value.prompt, 524_288)
|
|
508
591
|
|| value.providerArguments.length > 16 || value.providerArguments.some((item) => !bounded(item, 512))
|
|
509
|
-
|| (value.resumeSession !== undefined && !bounded(value.resumeSession,
|
|
592
|
+
|| (value.resumeSession !== undefined && !bounded(value.resumeSession, 512))) {
|
|
510
593
|
throw new ClientError("Invalid isolated runtime run")
|
|
511
594
|
}
|
|
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
595
|
if (value.resumeSession !== undefined && (typeof value.resumeSession !== "string" || /[\r\n]/u.test(value.resumeSession))) throw new ClientError("Invalid isolated runtime run")
|
|
518
596
|
return {
|
|
519
597
|
preflightId: value.preflightId,
|
|
@@ -523,6 +601,32 @@ export function parseRun(value) {
|
|
|
523
601
|
}
|
|
524
602
|
}
|
|
525
603
|
|
|
604
|
+
function runtimeProviderSelection(provider, providerArguments, allowedModels) {
|
|
605
|
+
if (provider === "kimi") {
|
|
606
|
+
let selection
|
|
607
|
+
try { selection = validateKimiProviderArguments(providerArguments) } catch (error) {
|
|
608
|
+
throw new ClientError(error instanceof Error ? error.message : "Kimi provider arguments are unavailable")
|
|
609
|
+
}
|
|
610
|
+
const approved = allowedModels.get(selection.modelAlias)
|
|
611
|
+
if (approved === undefined) throw new ClientError("Kimi model is not allowed")
|
|
612
|
+
return {modelAlias: approved.alias, model: approved.model, arguments: selection.arguments}
|
|
613
|
+
}
|
|
614
|
+
let selection
|
|
615
|
+
try { selection = validateRelayWriteProviderArguments(providerArguments) } catch (error) {
|
|
616
|
+
throw new ClientError(error instanceof Error ? error.message : "Provider argument is unavailable in relay write mode")
|
|
617
|
+
}
|
|
618
|
+
if (!allowedModels.has(selection.model)) throw new ClientError("Codex model is not allowed")
|
|
619
|
+
return selection
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function isolatedProvider(value) {
|
|
623
|
+
if (value === undefined || value === "codex") return "codex"
|
|
624
|
+
if (value === "kimi") return "kimi"
|
|
625
|
+
throw new Error("THREADWIRE_ISOLATED_PROVIDER must be codex or kimi")
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function providerName(provider) { return provider === "kimi" ? "Kimi" : "Codex" }
|
|
629
|
+
|
|
526
630
|
function requestedDeadline(value, maximumMs) {
|
|
527
631
|
const parsed = Number(value)
|
|
528
632
|
const maximum = Date.now() + maximumMs
|
|
@@ -560,7 +664,7 @@ function ancestry(root, child) {
|
|
|
560
664
|
}
|
|
561
665
|
|
|
562
666
|
function taskIdentity(preflight) {
|
|
563
|
-
return `${preflight.profile}\0${preflight.repositoryRoot}`
|
|
667
|
+
return `${preflight.provider ?? "codex"}\0${preflight.profile}\0${preflight.repositoryRoot}`
|
|
564
668
|
}
|
|
565
669
|
|
|
566
670
|
function brokerNetworkAddress(value, containerName) {
|
|
@@ -609,6 +713,12 @@ function safeSetting(value, name) {
|
|
|
609
713
|
return value
|
|
610
714
|
}
|
|
611
715
|
|
|
716
|
+
function authoritySetting(value, name) {
|
|
717
|
+
const authority = safeSetting(value, name)
|
|
718
|
+
if (authority.length < 32 || authority.length > 512) throw new Error(`${name} must be at least 32 characters`)
|
|
719
|
+
return authority
|
|
720
|
+
}
|
|
721
|
+
|
|
612
722
|
function portValue(value, fallback) {
|
|
613
723
|
if (value === undefined) return fallback
|
|
614
724
|
const number = Number(value)
|
|
@@ -652,6 +762,12 @@ function isRecord(value) {
|
|
|
652
762
|
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
653
763
|
}
|
|
654
764
|
|
|
765
|
+
function exactKeys(value, expected) {
|
|
766
|
+
const actual = Object.keys(value).sort()
|
|
767
|
+
const wanted = [...expected].sort()
|
|
768
|
+
return actual.length === wanted.length && actual.every((key, index) => key === wanted[index])
|
|
769
|
+
}
|
|
770
|
+
|
|
655
771
|
function abortable(promise, signal) {
|
|
656
772
|
if (signal.aborted) return Promise.reject(signal.reason)
|
|
657
773
|
return new Promise((resolve, reject) => {
|
|
@@ -786,13 +902,13 @@ export async function reconcileDockerResources(docker, brokerContainer, stateReg
|
|
|
786
902
|
const cleanupFailed = new Set()
|
|
787
903
|
const containers = await docker.listContainers({"org.threadwire.owner": "isolated-runtime"}, true, requestOptions)
|
|
788
904
|
for (const container of Array.isArray(containers) ? containers : []) {
|
|
789
|
-
const labels = validResourceLabels(container?.Labels, namespace, true)
|
|
905
|
+
const labels = validResourceLabels(container?.Labels, namespace, true, stateRegistry.provider ?? "codex")
|
|
790
906
|
if (!labels) continue
|
|
791
907
|
const persisted = ownedIdentities.get(labels.run)
|
|
792
|
-
if (!persisted || stateRegistry.
|
|
908
|
+
if (!persisted || !stateRegistry.ownsRunSeal(persisted, labels.seal, labels.legacy)) continue
|
|
793
909
|
const inspection = await docker.inspectContainer(container.Id, requestOptions).catch(() => undefined)
|
|
794
910
|
const identity = ownedContainerIdentity(inspection, labels)
|
|
795
|
-
if (!identity || !stateRegistry.ownsRun(identity, labels.seal)) {
|
|
911
|
+
if (!identity || !stateRegistry.ownsRun(identity, labels.seal, labels.legacy)) {
|
|
796
912
|
cleanupFailed.add(labels.run)
|
|
797
913
|
continue
|
|
798
914
|
}
|
|
@@ -810,13 +926,13 @@ export async function reconcileDockerResources(docker, brokerContainer, stateReg
|
|
|
810
926
|
}
|
|
811
927
|
const networks = await docker.listNetworks({"org.threadwire.owner": "isolated-runtime"}, requestOptions)
|
|
812
928
|
for (const network of Array.isArray(networks) ? networks : []) {
|
|
813
|
-
const labels = validResourceLabels(network?.Labels, namespace, false)
|
|
929
|
+
const labels = validResourceLabels(network?.Labels, namespace, false, stateRegistry.provider ?? "codex")
|
|
814
930
|
if (!labels || activeRuns.has(labels.run) || cleanupFailed.has(labels.run)) continue
|
|
815
931
|
const identity = ownedIdentities.get(labels.run)
|
|
816
|
-
if (!identity || stateRegistry.
|
|
932
|
+
if (!identity || !stateRegistry.ownsRunSeal(identity, labels.seal, labels.legacy)) continue
|
|
817
933
|
const inspection = await docker.inspectNetwork(network.Id, requestOptions).catch(() => undefined)
|
|
818
934
|
if (!identity || !ownedNetworkIdentity(inspection, labels, identity, brokerContainer)
|
|
819
|
-
|| !stateRegistry.ownsRun(identity, labels.seal)) {
|
|
935
|
+
|| !stateRegistry.ownsRun(identity, labels.seal, labels.legacy)) {
|
|
820
936
|
cleanupFailed.add(labels.run)
|
|
821
937
|
continue
|
|
822
938
|
}
|
|
@@ -843,15 +959,19 @@ export async function reconcileDockerResources(docker, brokerContainer, stateReg
|
|
|
843
959
|
return [...activeLineages]
|
|
844
960
|
}
|
|
845
961
|
|
|
846
|
-
function validResourceLabels(value, namespace, container) {
|
|
962
|
+
function validResourceLabels(value, namespace, container, expectedProvider = "codex") {
|
|
847
963
|
if (!isRecord(value)) return undefined
|
|
964
|
+
const provider = value["org.threadwire.provider"] ?? "codex"
|
|
965
|
+
const legacy = value["org.threadwire.provider"] === undefined
|
|
848
966
|
const expectedKeys = [
|
|
849
967
|
"org.threadwire.owner", "org.threadwire.namespace", "org.threadwire.run",
|
|
850
968
|
"org.threadwire.lineage", "org.threadwire.task", "org.threadwire.state-volume",
|
|
851
969
|
"org.threadwire.run-seal",
|
|
970
|
+
...(legacy ? [] : ["org.threadwire.provider"]),
|
|
852
971
|
...(container ? ["org.threadwire.network"] : [])
|
|
853
972
|
]
|
|
854
|
-
if (
|
|
973
|
+
if (provider !== expectedProvider || (legacy && provider !== "codex")
|
|
974
|
+
|| Object.keys(value).length !== expectedKeys.length || expectedKeys.some((key) => typeof value[key] !== "string")) return undefined
|
|
855
975
|
const run = value["org.threadwire.run"]
|
|
856
976
|
const lineage = value["org.threadwire.lineage"]
|
|
857
977
|
const volume = value["org.threadwire.state-volume"]
|
|
@@ -862,20 +982,22 @@ function validResourceLabels(value, namespace, container) {
|
|
|
862
982
|
|| !/^[0-9a-f]{64}$/u.test(seal)
|
|
863
983
|
|| volume !== `threadwire-state-${namespace}-${lineage}`
|
|
864
984
|
|| (container && value["org.threadwire.network"] !== `${run}-net`)) return undefined
|
|
865
|
-
return {run, lineage, volume, seal, task: value["org.threadwire.task"], namespace}
|
|
985
|
+
return {provider, legacy, run, lineage, volume, seal, task: value["org.threadwire.task"], namespace}
|
|
866
986
|
}
|
|
867
987
|
|
|
868
988
|
function ownedContainerIdentity(container, labels) {
|
|
869
989
|
if (!isRecord(container) || container.Name !== `/${labels.run}-worker`
|
|
870
990
|
|| !isRecord(container.Config) || !isRecord(container.HostConfig)
|
|
871
991
|
|| container.Config.Image !== container.Image || !isDigestImage(container.Config.Image)
|
|
872
|
-
|| JSON.stringify(container.Config.Entrypoint) !== JSON.stringify(
|
|
992
|
+
|| JSON.stringify(container.Config.Entrypoint) !== JSON.stringify(labels.provider === "kimi"
|
|
993
|
+
? ["node", "/opt/threadwire/docker/kimi-worker-entrypoint.mjs"]
|
|
994
|
+
: ["/usr/local/libexec/threadwire/worker-entrypoint"])
|
|
873
995
|
|| ![null, undefined].includes(container.Config.Cmd) || container.Config.User !== "10002:10002"
|
|
874
996
|
|| typeof container.Config.WorkingDir !== "string" || !within("/worktree", container.Config.WorkingDir)
|
|
875
997
|
|| container.HostConfig.NetworkMode !== `${labels.run}-net`
|
|
876
|
-
|| !validWorkerSecurity(container.Config, container.HostConfig)
|
|
998
|
+
|| !validWorkerSecurity(container.Config, container.HostConfig, labels.provider)
|
|
877
999
|
|| !Array.isArray(container.HostConfig.Mounts) || container.HostConfig.Mounts.length !== 2) return undefined
|
|
878
|
-
const inspectedLabels = validResourceLabels(container.Config.Labels, labels.namespace, true)
|
|
1000
|
+
const inspectedLabels = validResourceLabels(container.Config.Labels, labels.namespace, true, labels.provider)
|
|
879
1001
|
if (!inspectedLabels || JSON.stringify(inspectedLabels) !== JSON.stringify(labels)) return undefined
|
|
880
1002
|
const state = container.HostConfig.Mounts.find((mount) => mount?.Target === "/home/worker")
|
|
881
1003
|
const worktree = container.HostConfig.Mounts.find((mount) => mount?.Target === "/worktree")
|
|
@@ -885,7 +1007,7 @@ function ownedContainerIdentity(container, labels) {
|
|
|
885
1007
|
return runIdentityFrom(labels, container.Config.Image, worktree)
|
|
886
1008
|
}
|
|
887
1009
|
|
|
888
|
-
function validWorkerSecurity(config, host) {
|
|
1010
|
+
function validWorkerSecurity(config, host, provider = "codex") {
|
|
889
1011
|
if (host.AutoRemove !== false || host.ReadonlyRootfs !== true
|
|
890
1012
|
|| host.Privileged === true || JSON.stringify(host.CapDrop) !== JSON.stringify(["ALL"])
|
|
891
1013
|
|| JSON.stringify(host.CapAdd ?? null) !== "null"
|
|
@@ -910,8 +1032,20 @@ function validWorkerSecurity(config, host) {
|
|
|
910
1032
|
if (![undefined, "", "private"].includes(host.PidMode) || ![undefined, "", "private"].includes(host.IpcMode)
|
|
911
1033
|
|| ![undefined, ""].includes(host.UTSMode) || ![undefined, ""].includes(host.UsernsMode)
|
|
912
1034
|
|| ![undefined, "", "private"].includes(host.CgroupnsMode) || host.PublishAllPorts === true) return false
|
|
913
|
-
if (!Array.isArray(config.Env)
|
|
1035
|
+
if (!Array.isArray(config.Env)) return false
|
|
914
1036
|
const names = config.Env.map((entry) => entry.split("=", 1)[0])
|
|
1037
|
+
const resumeCount = names.filter((name) => name === "THREADWIRE_RESUME_SESSION").length
|
|
1038
|
+
if (resumeCount > 1) return false
|
|
1039
|
+
if (provider === "kimi") {
|
|
1040
|
+
const expected = [
|
|
1041
|
+
"HOME", "TMPDIR", "THREADWIRE_PROMPT", "THREADWIRE_WORKTREE_INODE", "THREADWIRE_CWD_INODE",
|
|
1042
|
+
"KIMI_CODE_HOME", "THREADWIRE_KIMI_MODEL", "THREADWIRE_KIMI_BROKER_URL", "THREADWIRE_KIMI_BROKER_TOKEN"
|
|
1043
|
+
]
|
|
1044
|
+
return names.length === expected.length + resumeCount
|
|
1045
|
+
&& expected.every((name) => names.filter((entry) => entry === name).length === 1)
|
|
1046
|
+
&& !names.some((name) => /^(?:CODEX|OPENAI|TELEGRAM|THREADWIRE_KIMI_(?:OAUTH|CONTROL))/u.test(name))
|
|
1047
|
+
}
|
|
1048
|
+
if (config.Env.length !== 13 + resumeCount) return false
|
|
915
1049
|
const expected = [
|
|
916
1050
|
"HOME", "CODEX_HOME", "TMPDIR", "OPENAI_BASE_URL", "OPENAI_API_KEY", "CODEX_API_KEY",
|
|
917
1051
|
"THREADWIRE_PROMPT", "THREADWIRE_CODEX_ARGUMENTS", "THREADWIRE_WORKTREE_INODE", "THREADWIRE_CWD_INODE"
|
|
@@ -927,7 +1061,7 @@ function ownedNetworkIdentity(network, labels, identity, brokerContainer) {
|
|
|
927
1061
|
|| network.EnableIPv6 !== false || network.Attachable !== false || network.Ingress !== false
|
|
928
1062
|
|| network.ConfigOnly !== false || !isRecord(network.Options) || Object.keys(network.Options).length !== 0
|
|
929
1063
|
|| !isRecord(network.Containers)) return undefined
|
|
930
|
-
const inspectedLabels = validResourceLabels(network.Labels, labels.namespace, false)
|
|
1064
|
+
const inspectedLabels = validResourceLabels(network.Labels, labels.namespace, false, labels.provider)
|
|
931
1065
|
if (!inspectedLabels || JSON.stringify(inspectedLabels) !== JSON.stringify(labels)) return undefined
|
|
932
1066
|
const endpoints = Object.values(network.Containers)
|
|
933
1067
|
if (endpoints.some((endpoint) => !isRecord(endpoint)
|
|
@@ -951,7 +1085,7 @@ function validWorktreeMount(mount) {
|
|
|
951
1085
|
|
|
952
1086
|
function runIdentityFrom(labels, image, worktree) {
|
|
953
1087
|
return {
|
|
954
|
-
namespace: labels.namespace, run: labels.run, lineage: labels.lineage, task: labels.task,
|
|
1088
|
+
provider: labels.provider, namespace: labels.namespace, run: labels.run, lineage: labels.lineage, task: labels.task,
|
|
955
1089
|
volume: labels.volume, network: `${labels.run}-net`, container: `${labels.run}-worker`,
|
|
956
1090
|
image, worktreeType: worktree.Type, worktreeSource: worktree.Source,
|
|
957
1091
|
worktreeSubpath: worktree.Type === "volume" ? worktree.VolumeOptions.Subpath : ""
|
|
@@ -961,6 +1095,7 @@ function runIdentityFrom(labels, image, worktree) {
|
|
|
961
1095
|
function expectedRunIdentity(options, run, network, state, worktreeSubpath) {
|
|
962
1096
|
const volumeMode = options.worktreeVolume !== undefined
|
|
963
1097
|
return {
|
|
1098
|
+
provider: options.preflight.provider ?? "codex",
|
|
964
1099
|
namespace: options.stateRegistry.namespace, run, lineage: state.lineage,
|
|
965
1100
|
task: state.labels["org.threadwire.task"], volume: state.volume, network,
|
|
966
1101
|
container: `${run}-worker`, image: options.workerImage,
|