threadwire 0.1.11 → 0.1.12

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.
@@ -33,12 +33,17 @@ export async function openStateRegistry(options) {
33
33
  try {
34
34
  const document = JSON.parse(await readFile(options.file, "utf8"))
35
35
  const payload = JSON.stringify(document.payload)
36
- if (![1, 2, 3, 4].includes(document.version) || typeof document.mac !== "string" || !equalMac(document.mac, mac(options.key, payload))) throw new Error()
37
- if (document.version < 4 && provider !== "codex") throw new Error()
36
+ if (![1, 2, 3, 4, 5, 6, 7].includes(document.version) || typeof document.mac !== "string" || !equalMac(document.mac, mac(options.key, payload))) throw new Error()
37
+ // Binding-v1 Kimi never adopts a host-worktree registry document, the
38
+ // relay-sealed Kimi run identity (v6) never adopts a pre-relay v5 document,
39
+ // and the task/revision-sealed Kimi identity (v7) never adopts a v6 one.
40
+ if (document.version < 5 && provider !== "codex") throw new Error()
41
+ if (document.version < 6 && provider === "kimi") throw new Error()
42
+ if (document.version < 7 && provider === "kimi") throw new Error()
38
43
  if (!Array.isArray(document.payload?.sessions)) throw new Error()
39
44
  for (const rawEntry of document.payload.sessions) {
40
- if (!validSession(rawEntry, document.version === 4)) throw new Error()
41
- const entry = document.version === 4 ? rawEntry : {...rawEntry, provider: "codex"}
45
+ if (!validSession(rawEntry, document.version >= 4)) throw new Error()
46
+ const entry = document.version >= 4 ? rawEntry : {...rawEntry, provider: "codex"}
42
47
  if (entry.provider !== provider) throw new Error()
43
48
  sessions.set(entry.sessionId, without(entry, "sessionId"))
44
49
  lineages.set(entry.lineage, lineageFromSession(options.namespace, entry))
@@ -50,17 +55,17 @@ export async function openStateRegistry(options) {
50
55
  lineages.set(entry.lineage, {...entry, provider: "codex"})
51
56
  }
52
57
  }
53
- if (document.version === 3 || document.version === 4) {
58
+ if (document.version === 3 || document.version === 4 || document.version === 5 || document.version === 6 || document.version === 7) {
54
59
  if (!Array.isArray(document.payload.lineages) || !Array.isArray(document.payload.runs)) throw new Error()
55
60
  for (const rawEntry of document.payload.lineages) {
56
- if (!validLineage(rawEntry, options.namespace, document.version === 4)) throw new Error()
57
- const entry = document.version === 4 ? rawEntry : {...rawEntry, provider: "codex"}
61
+ if (!validLineage(rawEntry, options.namespace, document.version >= 4)) throw new Error()
62
+ const entry = document.version >= 4 ? rawEntry : {...rawEntry, provider: "codex"}
58
63
  if (entry.provider !== provider) throw new Error()
59
64
  lineages.set(entry.lineage, entry)
60
65
  }
61
66
  for (const rawEntry of document.payload.runs) {
62
67
  canonicalRunIdentity(rawEntry)
63
- const entry = document.version === 4 ? rawEntry : {...rawEntry, provider: "codex", sealVersion: 3}
68
+ const entry = document.version >= 4 ? rawEntry : {...rawEntry, provider: "codex", sealVersion: 3}
64
69
  if (entry.provider !== provider) throw new Error()
65
70
  runs.set(entry.run, entry)
66
71
  }
@@ -78,7 +83,7 @@ export async function openStateRegistry(options) {
78
83
  runs: [...runState.values()]
79
84
  }
80
85
  const serializedPayload = JSON.stringify(payload)
81
- const document = JSON.stringify({version: 4, payload, mac: mac(options.key, serializedPayload)})
86
+ const document = JSON.stringify({version: 7, payload, mac: mac(options.key, serializedPayload)})
82
87
  const temporary = join(dirname(options.file), `.sessions-${random(12).toString("hex")}.tmp`)
83
88
  let handle
84
89
  try {
@@ -234,6 +239,32 @@ export async function openStateRegistry(options) {
234
239
  throwIfAborted(signal)
235
240
  })
236
241
  },
242
+ /**
243
+ * Rolls back exactly the aliases a just-completed `register` call added
244
+ * for this allocation. An alias is removed only while it still points at
245
+ * the same lineage and volume; unrelated sessions and the lineage
246
+ * ownership record itself are left untouched.
247
+ */
248
+ async unregister(sessionIds, allocation, signal) {
249
+ await transact(async () => {
250
+ throwIfAborted(signal)
251
+ const normalized = providerAllocation(allocation, provider)
252
+ const nextSessions = new Map(sessions)
253
+ let changed = false
254
+ for (const sessionId of sessionIds) {
255
+ const existing = nextSessions.get(sessionId)
256
+ if (!existing) continue
257
+ if (existing.provider !== normalized.provider || existing.task !== normalized.task
258
+ || existing.lineage !== normalized.lineage || existing.volume !== normalized.volume) continue
259
+ nextSessions.delete(sessionId)
260
+ changed = true
261
+ }
262
+ if (!changed) return
263
+ await persist(nextSessions, lineages, runs, signal)
264
+ replace(sessions, nextSessions)
265
+ throwIfAborted(signal)
266
+ })
267
+ },
237
268
  /**
238
269
  * Sweeps aliases, adopts only exactly labelled namespace-owned orphan
239
270
  * volumes, and retries failed deletions without dropping ownership.
@@ -409,11 +440,11 @@ async function removeAuthenticatedTemps(directory, key, namespace) {
409
440
  }
410
441
 
411
442
  function authenticatedDocument(document, key, namespace) {
412
- if (!record(document) || ![1, 2, 3, 4].includes(document.version) || typeof document.mac !== "string"
443
+ if (!record(document) || ![1, 2, 3, 4, 5, 6, 7].includes(document.version) || typeof document.mac !== "string"
413
444
  || !record(document.payload) || !Array.isArray(document.payload.sessions)) return false
414
445
  const payload = JSON.stringify(document.payload)
415
446
  if (!equalMac(document.mac, mac(key, payload))) return false
416
- const current = document.version === 4
447
+ const current = document.version >= 4
417
448
  if (!document.payload.sessions.every((entry) => validSession(entry, current))) return false
418
449
  if (document.version === 1) return true
419
450
  if (!Array.isArray(document.payload.lineages)
@@ -425,6 +456,7 @@ function authenticatedDocument(document, key, namespace) {
425
456
  }
426
457
 
427
458
  function canonicalRunIdentity(identity) {
459
+ if (record(identity) && identity.provider === "kimi") return canonicalKimiRunIdentity(identity)
428
460
  const keys = [
429
461
  "provider", "namespace", "run", "lineage", "task", "volume", "network", "container",
430
462
  "image", "worktreeType", "worktreeSource", "worktreeSubpath"
@@ -440,6 +472,25 @@ function canonicalRunIdentity(identity) {
440
472
  return JSON.stringify(Object.fromEntries(keys.map((key) => [key, normalized[key]])))
441
473
  }
442
474
 
475
+ function canonicalKimiRunIdentity(identity) {
476
+ const keys = [
477
+ "provider", "namespace", "run", "lineage", "task", "volume", "network", "egressNetwork", "validator", "worker", "relay", "image", "relayImage",
478
+ "endpoint", "bindingDigest", "sourceVolume", "sourceTarget", "sourceReadOnly", "contextVolume", "contextTarget", "contextManifestDigest",
479
+ "contextContentDigest", "contextImageId", "runtimeUid", "runtimeGid", "workdir", "taskId", "expectedRevision"
480
+ ]
481
+ if (!record(identity) || Object.keys(identity).length !== keys.length || keys.some((key) => typeof identity[key] !== "string")
482
+ || identity.provider !== "kimi" || !/^(?:true|false)$/u.test(identity.sourceReadOnly)
483
+ || !/^sha256:[0-9a-f]{64}$/u.test(identity.bindingDigest)
484
+ || !/^sha256:[0-9a-f]{64}$/u.test(identity.contextManifestDigest)
485
+ || !/^sha256:[0-9a-f]{64}$/u.test(identity.contextContentDigest)
486
+ || !/^sha256:[0-9a-f]{64}$/u.test(identity.contextImageId)
487
+ || !/^sha256:[0-9a-f]{64}$/u.test(identity.relayImage)
488
+ || !/^\d+$/u.test(identity.runtimeUid) || !/^\d+$/u.test(identity.runtimeGid)
489
+ || !/^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/u.test(identity.taskId)
490
+ || !/^[0-9a-f]{40}$/u.test(identity.expectedRevision)) throw new Error("Invalid Kimi binding run identity")
491
+ return JSON.stringify(Object.fromEntries(keys.map((key) => [key, identity[key]])))
492
+ }
493
+
443
494
  function legacyCanonicalRunIdentity(identity) {
444
495
  const keys = [
445
496
  "namespace", "run", "lineage", "task", "volume", "network", "container",
@@ -1,9 +1,11 @@
1
1
  // @ts-check
2
2
 
3
3
  import {isAbsolute, normalize} from "node:path"
4
+ import {parseThreadwireBinding} from "./threadwire-binding.js"
4
5
 
5
6
  const DIGEST_IMAGE = /^(?:[^@\s]+@)?sha256:[0-9a-f]{64}$/u
6
7
  const SAFE_VALUE = /^[^\0\r\n]+$/u
8
+ const MAX_PROMPT_LENGTH = 524_288
7
9
 
8
10
  /**
9
11
  * @typedef {{
@@ -22,6 +24,7 @@ const SAFE_VALUE = /^[^\0\r\n]+$/u
22
24
  * stateVolume: string,
23
25
  * worktreeInode: string,
24
26
  * cwdInode: string
27
+ * binding?: unknown
25
28
  * lineage?: string
26
29
  * namespace?: string
27
30
  * runId?: string
@@ -34,18 +37,17 @@ const SAFE_VALUE = /^[^\0\r\n]+$/u
34
37
  export function buildWorkerContainerSpec(options) {
35
38
  const provider = options.provider ?? "codex"
36
39
  if (provider !== "codex" && provider !== "kimi") throw new Error("Worker provider is invalid")
40
+ if (provider === "kimi") return buildKimiWorkerContainerSpec(options)
37
41
  if (!DIGEST_IMAGE.test(options.image)) throw new Error("Worker image must use an immutable digest")
38
42
  if (!isAbsolute(options.worktree) || normalize(options.worktree) !== options.worktree || options.worktree === "/") {
39
43
  throw new Error("Worker worktree must be a normalized non-root absolute path")
40
44
  }
41
- for (const [name, value] of /** @type {[string, string][]} */ ([["network", options.networkName], ["capability", options.brokerToken], ["prompt", options.prompt]])) {
45
+ for (const [name, value] of /** @type {[string, string][]} */ ([["network", options.networkName], ["capability", options.brokerToken]])) {
42
46
  if (!SAFE_VALUE.test(value)) throw new Error(`Worker ${name} is invalid`)
43
47
  }
48
+ if (!validPrompt(options.prompt)) throw new Error("Worker prompt is invalid")
44
49
  if (options.resumeSession !== undefined && !SAFE_VALUE.test(options.resumeSession)) throw new Error("Worker resume session is invalid")
45
50
  if (!isAbsolute(options.workingDirectory) || !withinWorktree(options.workingDirectory)) throw new Error("Worker cwd is invalid")
46
- if (provider === "kimi" && (typeof options.model !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u.test(options.model))) {
47
- throw new Error("Worker Kimi model is invalid")
48
- }
49
51
  const commonEnvironment = [
50
52
  "HOME=/home/worker",
51
53
  "TMPDIR=/tmp",
@@ -54,22 +56,14 @@ export function buildWorkerContainerSpec(options) {
54
56
  `THREADWIRE_CWD_INODE=${options.cwdInode}`,
55
57
  ...(options.resumeSession === undefined ? [] : [`THREADWIRE_RESUME_SESSION=${options.resumeSession}`])
56
58
  ]
57
- const environment = provider === "kimi"
58
- ? [
59
- ...commonEnvironment,
60
- "KIMI_CODE_HOME=/home/worker/.kimi-code",
61
- `THREADWIRE_KIMI_MODEL=${options.model}`,
62
- `THREADWIRE_KIMI_BROKER_URL=${options.brokerUrl}`,
63
- `THREADWIRE_KIMI_BROKER_TOKEN=${options.brokerToken}`
64
- ]
65
- : [
66
- ...commonEnvironment,
67
- "CODEX_HOME=/home/worker/.codex",
68
- `OPENAI_BASE_URL=${options.brokerUrl ?? "http://model-broker:8789/v1"}`,
69
- `OPENAI_API_KEY=${options.brokerToken}`,
70
- `CODEX_API_KEY=${options.brokerToken}`,
71
- `THREADWIRE_CODEX_ARGUMENTS=${JSON.stringify(options.providerArguments)}`
72
- ]
59
+ const environment = [
60
+ ...commonEnvironment,
61
+ "CODEX_HOME=/home/worker/.codex",
62
+ `OPENAI_BASE_URL=${options.brokerUrl ?? "http://model-broker:8789/v1"}`,
63
+ `OPENAI_API_KEY=${options.brokerToken}`,
64
+ `CODEX_API_KEY=${options.brokerToken}`,
65
+ `THREADWIRE_CODEX_ARGUMENTS=${JSON.stringify(options.providerArguments)}`
66
+ ]
73
67
  const worktreeMount = options.worktreeVolume === undefined
74
68
  ? {
75
69
  Type: "bind",
@@ -100,9 +94,7 @@ export function buildWorkerContainerSpec(options) {
100
94
  "org.threadwire.run-seal": options.runSeal
101
95
  })
102
96
  },
103
- Entrypoint: provider === "kimi"
104
- ? ["node", "/opt/threadwire/docker/kimi-worker-entrypoint.mjs"]
105
- : ["/usr/local/libexec/threadwire/worker-entrypoint"],
97
+ Entrypoint: ["/usr/local/libexec/threadwire/worker-entrypoint"],
106
98
  Env: environment,
107
99
  WorkingDir: options.workingDirectory,
108
100
  User: "10002:10002",
@@ -133,6 +125,222 @@ export function buildWorkerContainerSpec(options) {
133
125
  }
134
126
  }
135
127
 
128
+ /**
129
+ * Validate an opaque multiline task prompt for Docker environment transport.
130
+ * @param {unknown} value
131
+ */
132
+ function validPrompt(value) {
133
+ return typeof value === "string" && value.trim().length > 0 && value.length <= MAX_PROMPT_LENGTH && !value.includes("\0")
134
+ }
135
+
136
+ /**
137
+ * Build the Kimi-only worker shape. Kimi deliberately does not inherit the
138
+ * Codex bind-worktree contract: its source and immutable context are exact
139
+ * named volumes resolved by the supervisor binding admission.
140
+ * @param {WorkerSpecOptions} options
141
+ */
142
+ export function buildKimiWorkerContainerSpec(options) {
143
+ if (!DIGEST_IMAGE.test(options.image)) throw new Error("Worker image must use an immutable digest")
144
+ const binding = parseThreadwireBinding(options.binding)
145
+ for (const [name, value] of /** @type {[string, unknown][]} */ ([
146
+ ["network", options.networkName], ["capability", options.brokerToken], ["broker URL", options.brokerUrl]
147
+ ])) if (typeof value !== "string" || !SAFE_VALUE.test(value)) throw new Error(`Worker ${name} is invalid`)
148
+ if (!validPrompt(options.prompt)) throw new Error("Worker prompt is invalid")
149
+ if (typeof options.model !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/u.test(options.model)) throw new Error("Worker Kimi model is invalid")
150
+ if (options.resumeSession !== undefined && !SAFE_VALUE.test(options.resumeSession)) throw new Error("Worker resume session is invalid")
151
+ const user = `${binding.runtime.uid}:${binding.runtime.gid}`
152
+ const uid = binding.runtime.uid
153
+ const gid = binding.runtime.gid
154
+ const environment = [
155
+ "PATH=/usr/local/bin:/usr/bin:/bin",
156
+ "NODE_VERSION=",
157
+ "YARN_VERSION=",
158
+ "HOME=/state/.threadwire-homes/home",
159
+ "KIMI_CODE_HOME=/state/.threadwire-homes/home/.kimi-code",
160
+ "TMPDIR=/tmp",
161
+ `THREADWIRE_PROMPT=${options.prompt}`,
162
+ `THREADWIRE_KIMI_MODEL=${options.model}`,
163
+ `THREADWIRE_KIMI_BROKER_URL=${options.brokerUrl}`,
164
+ `THREADWIRE_KIMI_BROKER_TOKEN=${options.brokerToken}`,
165
+ `THREADWIRE_SOURCE_TARGET=${binding.source.target}`,
166
+ `THREADWIRE_CONTEXT_TARGET=${binding.context.target}`,
167
+ `THREADWIRE_TASK_ID=${binding.taskId}`,
168
+ `THREADWIRE_SOURCE_READ_ONLY=${binding.source.readOnly}`,
169
+ `THREADWIRE_EXPECTED_REVISION=${binding.source.revision}`,
170
+ `THREADWIRE_CONTEXT_MANIFEST_DIGEST=${binding.context.digests.manifest}`,
171
+ `THREADWIRE_CONTEXT_CONTENT_DIGEST=${binding.context.digests.content}`,
172
+ `THREADWIRE_CONTEXT_IMAGE_ID=${binding.context.imageId}`,
173
+ ...(options.resumeSession === undefined ? [] : [`THREADWIRE_RESUME_SESSION=${options.resumeSession}`])
174
+ ]
175
+ return {
176
+ Image: options.image,
177
+ Labels: workerLabels(options, "kimi", "worker"),
178
+ Entrypoint: ["node", "/opt/threadwire/docker/kimi-worker-entrypoint.mjs"],
179
+ Env: environment,
180
+ WorkingDir: binding.runtime.workdir,
181
+ User: user,
182
+ NetworkDisabled: false,
183
+ HostConfig: {
184
+ ...workerHostConfig(options.networkName, uid, gid),
185
+ Mounts: [
186
+ {Type: "volume", Source: binding.source.volume, Target: binding.source.target, ReadOnly: binding.source.readOnly},
187
+ {Type: "volume", Source: binding.context.volume, Target: binding.context.target, ReadOnly: true},
188
+ {Type: "volume", Source: options.stateVolume, Target: "/state", ReadOnly: false}
189
+ ]
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * A credential-free, offline validator run before a Kimi broker grant exists.
196
+ * @param {{image: string, binding: unknown, labels: Record<string, string>}} options
197
+ */
198
+ export function buildKimiBindingValidatorSpec(options) {
199
+ if (!DIGEST_IMAGE.test(options.image)) throw new Error("Worker image must use an immutable digest")
200
+ const binding = parseThreadwireBinding(options.binding)
201
+ if (!plainStringRecord(options.labels)) throw new Error("Kimi validator labels are invalid")
202
+ const uid = binding.runtime.uid
203
+ const gid = binding.runtime.gid
204
+ return {
205
+ Image: options.image,
206
+ Labels: {...options.labels},
207
+ Entrypoint: ["node", "/opt/threadwire/docker/kimi-binding-validator.mjs"],
208
+ Env: [
209
+ `THREADWIRE_TASK_ID=${binding.taskId}`,
210
+ `THREADWIRE_EXPECTED_REVISION=${binding.source.revision}`,
211
+ `THREADWIRE_CONTEXT_MANIFEST_DIGEST=${binding.context.digests.manifest}`,
212
+ `THREADWIRE_CONTEXT_CONTENT_DIGEST=${binding.context.digests.content}`,
213
+ `THREADWIRE_CONTEXT_IMAGE_ID=${binding.context.imageId}`
214
+ ],
215
+ WorkingDir: binding.runtime.workdir,
216
+ User: `${uid}:${gid}`,
217
+ NetworkDisabled: true,
218
+ HostConfig: {
219
+ AutoRemove: false,
220
+ Mounts: [
221
+ {Type: "volume", Source: binding.source.volume, Target: binding.source.target, ReadOnly: true},
222
+ {Type: "volume", Source: binding.context.volume, Target: binding.context.target, ReadOnly: true}
223
+ ],
224
+ CapDrop: ["ALL"],
225
+ NetworkMode: "none",
226
+ ReadonlyRootfs: true,
227
+ SecurityOpt: ["no-new-privileges"],
228
+ PidsLimit: 64,
229
+ Memory: 268_435_456,
230
+ NanoCpus: 500_000_000,
231
+ Tmpfs: {"/tmp": `rw,noexec,nosuid,nodev,size=16777216,uid=${uid},gid=${gid}`},
232
+ Ulimits: [{Name: "nofile", Soft: 256, Hard: 256}, {Name: "core", Soft: 0, Hard: 0}, {Name: "fsize", Soft: 67_108_864, Hard: 67_108_864}]
233
+ }
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Role-aware container identity. Codex workers carry the network label; Kimi
239
+ * containers carry an exact role label (validator/relay/worker) instead, which
240
+ * is what restart reconciliation requires. The network label is never emitted
241
+ * for Kimi.
242
+ * @param {WorkerSpecOptions} options @param {"codex" | "kimi"} provider @param {"validator" | "relay" | "worker"} [role]
243
+ */
244
+ function workerLabels(options, provider, role) {
245
+ return {
246
+ "org.threadwire.owner": "isolated-runtime",
247
+ ...(options.lineage === undefined ? {} : {
248
+ "org.threadwire.namespace": options.namespace,
249
+ "org.threadwire.provider": provider,
250
+ "org.threadwire.run": options.runId,
251
+ "org.threadwire.lineage": options.lineage,
252
+ "org.threadwire.task": options.taskHash,
253
+ ...(role === undefined ? {"org.threadwire.network": options.networkName} : {}),
254
+ "org.threadwire.state-volume": options.stateVolume,
255
+ "org.threadwire.run-seal": options.runSeal,
256
+ ...(role === undefined ? {} : {"org.threadwire.role": role})
257
+ })
258
+ }
259
+ }
260
+
261
+ /** @param {string} networkName @param {number} uid @param {number} gid */
262
+ function workerHostConfig(networkName, uid, gid) {
263
+ return {
264
+ AutoRemove: false,
265
+ CapDrop: ["ALL"],
266
+ NetworkMode: networkName,
267
+ ReadonlyRootfs: true,
268
+ SecurityOpt: ["no-new-privileges"],
269
+ PidsLimit: 128,
270
+ Memory: 1_073_741_824,
271
+ NanoCpus: 2_000_000_000,
272
+ Tmpfs: {
273
+ "/tmp": `rw,noexec,nosuid,nodev,size=67108864,uid=${uid},gid=${gid}`,
274
+ "/run": `rw,noexec,nosuid,nodev,size=1048576,uid=${uid},gid=${gid}`
275
+ },
276
+ Ulimits: [
277
+ {Name: "nofile", Soft: 1024, Hard: 1024},
278
+ {Name: "core", Soft: 0, Hard: 0},
279
+ {Name: "fsize", Soft: 1_073_741_824, Hard: 1_073_741_824}
280
+ ]
281
+ }
282
+ }
283
+
284
+ /** @param {unknown} value */
285
+ function plainStringRecord(value) {
286
+ return typeof value === "object" && value !== null && !Array.isArray(value)
287
+ && Object.values(value).every((entry) => typeof entry === "string" && SAFE_VALUE.test(entry))
288
+ }
289
+
290
+ /**
291
+ * A credential-free, per-run Kimi model relay. The relay is a dumb forwarder:
292
+ * it knows only the fixed central broker worker URL and its own listen port.
293
+ * It never receives a grant token, OAuth material, admin token, binding,
294
+ * source/context/state mount, or Docker socket, so it runs with no mounts and
295
+ * an empty credential surface on the task daemon.
296
+ * @param {{image: string, upstreamUrl: string, listenPort: number, networkName: string, labels?: Record<string, string>}} options
297
+ */
298
+ export function buildKimiRelayContainerSpec(options) {
299
+ if (!DIGEST_IMAGE.test(options.image)) throw new Error("Kimi relay image must use an immutable digest")
300
+ if (typeof options.upstreamUrl !== "string" || !SAFE_VALUE.test(options.upstreamUrl)) throw new Error("Kimi relay upstream URL is invalid")
301
+ if (!Number.isSafeInteger(options.listenPort) || options.listenPort < 1 || options.listenPort > 65535) throw new Error("Kimi relay listen port is invalid")
302
+ if (typeof options.networkName !== "string" || !SAFE_VALUE.test(options.networkName)) throw new Error("Kimi relay network is invalid")
303
+ if (options.labels !== undefined && !plainStringRecord(options.labels)) throw new Error("Kimi relay labels are invalid")
304
+ const uid = 10003
305
+ const gid = 10003
306
+ return {
307
+ Image: options.image,
308
+ Labels: {
309
+ "org.threadwire.owner": "isolated-runtime",
310
+ ...(options.labels === undefined ? {} : {...options.labels})
311
+ },
312
+ Entrypoint: ["node", "/opt/threadwire/docker/kimi-model-relay-entrypoint.mjs"],
313
+ Env: [
314
+ "PATH=/usr/local/bin:/usr/bin:/bin",
315
+ "NODE_VERSION=",
316
+ "YARN_VERSION=",
317
+ `THREADWIRE_KIMI_RELAY_UPSTREAM_URL=${options.upstreamUrl}`,
318
+ `THREADWIRE_KIMI_RELAY_LISTEN_PORT=${options.listenPort}`
319
+ ],
320
+ User: `${uid}:${gid}`,
321
+ NetworkDisabled: false,
322
+ HostConfig: {
323
+ AutoRemove: false,
324
+ Mounts: [],
325
+ CapDrop: ["ALL"],
326
+ NetworkMode: options.networkName,
327
+ ReadonlyRootfs: true,
328
+ SecurityOpt: ["no-new-privileges"],
329
+ PidsLimit: 64,
330
+ Memory: 268_435_456,
331
+ NanoCpus: 500_000_000,
332
+ Tmpfs: {
333
+ "/tmp": `rw,noexec,nosuid,nodev,size=16777216,uid=${uid},gid=${gid}`
334
+ },
335
+ Ulimits: [
336
+ {Name: "nofile", Soft: 256, Hard: 256},
337
+ {Name: "core", Soft: 0, Hard: 0},
338
+ {Name: "fsize", Soft: 67_108_864, Hard: 67_108_864}
339
+ ]
340
+ }
341
+ }
342
+ }
343
+
136
344
  /** @param {string} path */
137
345
  function withinWorktree(path) {
138
346
  return path === "/worktree" || path.startsWith("/worktree/")
@@ -69,6 +69,15 @@ export function createKimiGrantStore(options = {}) {
69
69
  if (!grant.active) throw new Error("Kimi broker grant pending")
70
70
  return {...grant}
71
71
  },
72
+ /** @param {string} token @param {number} ttlMs */
73
+ renew(token, ttlMs) {
74
+ if (!Number.isSafeInteger(ttlMs) || ttlMs < 1 || ttlMs > 3_600_000) throw new Error("Invalid Kimi broker grant")
75
+ const grant = grants.get(token)
76
+ if (!grant) throw new Error("Kimi broker grant unknown token")
77
+ if (grant.expiresAt < now()) throw new Error("Kimi broker grant expired")
78
+ grant.expiresAt = now() + ttlMs
79
+ return {...grant}
80
+ },
72
81
  /** @param {string} token */
73
82
  revoke(token) { grants.delete(token) },
74
83
  clear() { grants.clear() }
@@ -83,10 +92,7 @@ export function createKimiGrantStore(options = {}) {
83
92
  function currentGrant(grants, token, now) {
84
93
  const grant = grants.get(token)
85
94
  if (!grant) throw new Error("Kimi broker grant unknown token")
86
- if (grant.expiresAt < now()) {
87
- grants.delete(token)
88
- throw new Error("Kimi broker grant expired")
89
- }
95
+ if (grant.expiresAt < now()) throw new Error("Kimi broker grant expired")
90
96
  return grant
91
97
  }
92
98
 
@@ -107,7 +113,12 @@ export function validateKimiChatBody(body, allowedModel) {
107
113
  if (topLevelKeyCount(text, "model") !== 1) throw denied()
108
114
  let value
109
115
  try { value = JSON.parse(text) } catch { throw denied() }
110
- if (!record(value) || Object.keys(value).some((key) => !BODY_KEYS.has(key))) throw denied()
116
+ if (!record(value)) throw denied()
117
+ const unknownKeys = Object.keys(value).filter((key) => !BODY_KEYS.has(key))
118
+ if (unknownKeys.length > 0) {
119
+ const safeKeys = unknownKeys.every((key) => /^[a-z][a-z0-9_]{0,63}$/u.test(key)) ? unknownKeys.sort().join(",") : "unsafe"
120
+ throw new Error(`Kimi broker request denied body keys: ${safeKeys}`)
121
+ }
111
122
  if (value.model !== allowedModel || !Array.isArray(value.messages) || value.stream !== true) throw denied()
112
123
  if (value.messages.length === 0 || value.messages.length > 4096 || !value.messages.every(validMessage)) throw denied()
113
124
  if (value.tools !== undefined && (!Array.isArray(value.tools) || value.tools.length > 512 || !value.tools.every(record))) throw denied()