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.
@@ -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 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")
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 = parseAllowedModels(environment.THREADWIRE_ALLOWED_CODEX_MODELS)
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
- 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")
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("Codex resume state is unavailable")
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("Codex resume state is unavailable") }
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, codexArguments, state, fingerprint, worktreeRoot, launchDeadlineAt})
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("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")
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({runId, provider: "codex", networkId, model: options.preflight.codexArguments.model, brokerAddress, ttlMs: Math.min(operation.remaining(), 3_600_000)})
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: options.preflight.codexArguments.arguments,
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(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")}))
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
- 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")
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
- throw new ClientError(fresh ? "Codex state volume collision" : "Codex resume state is unavailable")
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
- 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
- }
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
- records.push({type: "worker-event", event: {type: "diagnostic", level: "warning", summary: "Worker emitted an unreadable event"}})
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 !== "codex" || typeof value.profile !== "string" || typeof value.repositoryRoot !== "string" || typeof value.cwd !== "string" || !Array.isArray(value.providerArguments) || !value.providerArguments.every((item) => typeof item === "string")) {
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, 256))) {
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, 256))) {
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.sealRun(persisted) !== labels.seal) continue
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.sealRun(identity) !== labels.seal) continue
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 (Object.keys(value).length !== expectedKeys.length || expectedKeys.some((key) => typeof value[key] !== "string")) return undefined
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(["/usr/local/libexec/threadwire/worker-entrypoint"])
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) || config.Env.length !== 13 + (config.Env.some((entry) => entry.startsWith("THREADWIRE_RESUME_SESSION=")) ? 1 : 0)) return false
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,