threadwire 0.1.10 → 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.
@@ -21,12 +21,20 @@ const UPSTREAM_URL = "https://api.kimi.com/coding/v1/chat/completions"
21
21
  /** @typedef {ReturnType<typeof createKimiGrantStore>} KimiGrantStore */
22
22
  /** @typedef {Awaited<ReturnType<typeof openKimiOAuthStore>>} KimiOAuthStore */
23
23
 
24
- /** @param {{environment?: NodeJS.ProcessEnv, fetchImplementation?: typeof fetch, oauthFetchImplementation?: typeof fetch}} [options] */
24
+ /** @param {{environment?: NodeJS.ProcessEnv, fetchImplementation?: typeof fetch, oauthFetchImplementation?: typeof fetch, onWorkerRequestAuthorized?: (token: string) => void}} [options] */
25
25
  export async function startKimiModelBroker(options = {}) {
26
26
  const environment = options.environment ?? process.env
27
27
  const host = safeHost(environment.THREADWIRE_KIMI_MODEL_BROKER_HOST ?? "0.0.0.0")
28
28
  const port = portValue(environment.THREADWIRE_KIMI_MODEL_BROKER_PORT, 8791)
29
+ const workerHost = safeHost(environment.THREADWIRE_KIMI_MODEL_BROKER_WORKER_HOST ?? "0.0.0.0")
30
+ const workerPort = portValue(environment.THREADWIRE_KIMI_MODEL_BROKER_WORKER_PORT, 8792)
29
31
  const adminToken = authoritySetting(environment.THREADWIRE_KIMI_MODEL_BROKER_ADMIN_TOKEN, "THREADWIRE_KIMI_MODEL_BROKER_ADMIN_TOKEN")
32
+ // The published worker bind is an explicit deployment decision: Compose
33
+ // renders a non-task-reachable loopback sentinel only so an inactive Kimi
34
+ // profile loads, and passes the raw setting through. Startup requires a
35
+ // nonempty supported bind before either listener opens, so a missing or
36
+ // invalid bind exits instead of publishing on every interface.
37
+ const workerBind = bindSetting(environment.THREADWIRE_KIMI_MODEL_BROKER_WORKER_BIND, "THREADWIRE_KIMI_MODEL_BROKER_WORKER_BIND")
30
38
  const approvedModels = parseApprovedKimiModels(environment.THREADWIRE_ALLOWED_KIMI_MODELS)
31
39
  const oauth = await openKimiOAuthStore({
32
40
  file: safeSetting(environment.THREADWIRE_KIMI_OAUTH_FILE, "THREADWIRE_KIMI_OAUTH_FILE"),
@@ -34,27 +42,48 @@ export async function startKimiModelBroker(options = {}) {
34
42
  })
35
43
  const fetchImplementation = options.fetchImplementation ?? fetch
36
44
  const grants = createKimiGrantStore()
37
- /** @type {Map<string, import("node:http").Server>} */
38
- const listeners = new Map()
39
- /** @type {Map<string, {controller: AbortController, timer: NodeJS.Timeout, sockets: Set<import("node:net").Socket>}>} */
45
+ /** @type {Map<string, {controller: AbortController, timer: NodeJS.Timeout, grant: KimiGrant}>} */
40
46
  const runtimes = new Map()
41
47
  /** @type {Set<import("node:net").Socket>} */
42
48
  const controlSockets = new Set()
49
+ /** @type {Set<import("node:net").Socket>} */
50
+ const workerSockets = new Set()
43
51
  let stopping = false
44
52
 
45
53
  /** @param {string} token */
46
- const revokeGrant = async (token) => {
54
+ const revokeGrant = (token) => {
47
55
  grants.revoke(token)
48
56
  const runtime = runtimes.get(token)
49
57
  runtimes.delete(token)
50
- if (runtime !== undefined) {
51
- clearTimeout(runtime.timer)
52
- runtime.controller.abort(new Error("Kimi broker grant revoked"))
53
- for (const socket of runtime.sockets) socket.destroy()
54
- runtime.sockets.clear()
55
- }
56
- await closeListener(listeners, token)
58
+ if (runtime === undefined) return
59
+ clearTimeout(runtime.timer)
60
+ runtime.controller.abort(new Error("Kimi broker grant revoked"))
57
61
  }
62
+ /** @param {string} token @param {number} expiresAt */
63
+ const scheduleExpiry = (token, expiresAt) => {
64
+ const timer = setTimeout(() => revokeGrant(token), Math.max(1, expiresAt - Date.now()))
65
+ timer.unref()
66
+ return timer
67
+ }
68
+
69
+ const workerServer = createServer((request, response) => {
70
+ /** @type {{grants: KimiGrantStore, runtimes: Map<string, {controller: AbortController, timer: NodeJS.Timeout, grant: KimiGrant}>, oauth: KimiOAuthStore, fetchImplementation: typeof fetch, onWorkerRequestAuthorized?: (token: string) => void}} */
71
+ const dependencies = {grants, runtimes, oauth, fetchImplementation}
72
+ if (options.onWorkerRequestAuthorized !== undefined) dependencies.onWorkerRequestAuthorized = options.onWorkerRequestAuthorized
73
+ proxyWorkerRequest(request, response, dependencies).catch((error) => {
74
+ if (error instanceof Error && /^Kimi broker request denied (?:header: [a-z0-9-]+|body keys: (?:unsafe|[a-z0-9_,]+))$/u.test(error.message)) {
75
+ process.stderr.write(`threadwire-kimi-broker: ${error.message}\n`)
76
+ }
77
+ if (response.destroyed) return
78
+ const status = clientError(error) ? 403 : 502
79
+ sendJson(response, status, {error: status === 403 ? "request denied" : "Kimi model broker unavailable"})
80
+ })
81
+ })
82
+ workerServer.on("connection", (socket) => {
83
+ if (stopping) return socket.destroy()
84
+ workerSockets.add(socket)
85
+ socket.once("close", () => workerSockets.delete(socket))
86
+ })
58
87
 
59
88
  const server = createServer(async (request, response) => {
60
89
  try {
@@ -70,37 +99,23 @@ export async function startKimiModelBroker(options = {}) {
70
99
  if (request.method === "POST" && request.url === "/admin/grants") {
71
100
  const health = await oauth.health()
72
101
  if (!health.ready) throw new Error("Kimi OAuth login required")
73
- const body = await readJson(request)
74
- const selection = parseGrant(body, approvedModels)
75
- const token = grants.issue(selection.grant)
102
+ const grant = parseGrant(await readJson(request), approvedModels)
103
+ const token = grants.issue(grant)
76
104
  const controller = new AbortController()
77
- /** @type {Set<import("node:net").Socket>} */
78
- const sockets = new Set()
79
- const listener = createServer((workerRequest, workerResponse) => {
80
- proxyWorkerRequest(workerRequest, workerResponse, {
81
- token, grant: selection.grant, grants, oauth, fetchImplementation, signal: controller.signal
82
- }).catch((error) => {
83
- if (workerResponse.destroyed) return
84
- const status = clientError(error) ? 403 : 502
85
- sendJson(workerResponse, status, {error: status === 403 ? "request denied" : "Kimi model broker unavailable"})
86
- })
87
- })
88
- listener.on("connection", (socket) => {
89
- if (controller.signal.aborted) return socket.destroy()
90
- sockets.add(socket)
91
- socket.once("close", () => sockets.delete(socket))
92
- })
93
- try { await listen(listener, 0, selection.brokerAddress) } catch (error) {
94
- grants.revoke(token)
95
- throw error
96
- }
97
- listeners.set(token, listener)
98
- const timer = setTimeout(() => { void revokeGrant(token) }, selection.grant.ttlMs)
99
- timer.unref()
100
- runtimes.set(token, {controller, timer, sockets})
101
- const address = listener.address()
102
- if (address === null || typeof address === "string") throw new Error("Kimi broker listener unavailable")
103
- sendJson(response, 201, {token, port: address.port})
105
+ runtimes.set(token, {controller, timer: scheduleExpiry(token, Date.now() + grant.ttlMs), grant})
106
+ sendJson(response, 201, {token})
107
+ return
108
+ }
109
+ if (request.method === "POST" && request.url?.startsWith("/admin/grants/") && request.url.endsWith("/renew")) {
110
+ const token = decodeURIComponent(request.url.slice("/admin/grants/".length, -"/renew".length))
111
+ const body = await readJson(request)
112
+ if (!record(body) || typeof body.ttlMs !== "number") throw new Error("Invalid Kimi broker grant")
113
+ const renewed = grants.renew(token, body.ttlMs)
114
+ const runtime = runtimes.get(token)
115
+ if (runtime === undefined) throw new Error("Kimi broker grant unknown token")
116
+ clearTimeout(runtime.timer)
117
+ runtime.timer = scheduleExpiry(token, renewed.expiresAt)
118
+ sendJson(response, 200, {ok: true})
104
119
  return
105
120
  }
106
121
  if (request.method === "POST" && request.url?.startsWith("/admin/grants/") && request.url.endsWith("/activate")) {
@@ -111,7 +126,7 @@ export async function startKimiModelBroker(options = {}) {
111
126
  }
112
127
  if (request.method === "DELETE" && request.url?.startsWith("/admin/grants/")) {
113
128
  const token = decodeURIComponent(request.url.slice("/admin/grants/".length))
114
- await revokeGrant(token)
129
+ revokeGrant(token)
115
130
  response.writeHead(204)
116
131
  response.end()
117
132
  return
@@ -123,65 +138,91 @@ export async function startKimiModelBroker(options = {}) {
123
138
  }
124
139
  })
125
140
  server.on("connection", (socket) => {
141
+ if (stopping) return socket.destroy()
126
142
  controlSockets.add(socket)
127
143
  socket.once("close", () => controlSockets.delete(socket))
128
144
  })
129
- await listen(server, port, host)
145
+ await listen(workerServer, workerPort, workerHost)
146
+ try { await listen(server, port, host) } catch (error) {
147
+ await boundedClose(workerServer)
148
+ throw error
149
+ }
130
150
  return {
131
151
  server,
132
- activeListeners: () => listeners.size,
152
+ workerServer,
153
+ activeListeners: () => runtimes.size,
133
154
  close: async () => {
134
155
  if (stopping) return
135
156
  stopping = true
157
+ for (const token of [...runtimes.keys()]) revokeGrant(token)
136
158
  for (const socket of controlSockets) socket.destroy()
137
- await Promise.all([...listeners.keys()].map(revokeGrant))
138
- await boundedClose(server)
159
+ for (const socket of workerSockets) socket.destroy()
160
+ await Promise.all([boundedClose(server), boundedClose(workerServer)])
139
161
  },
140
- config: {host, port, models: [...approvedModels.keys()]}
162
+ config: {host, port, workerHost, workerPort, workerBind, models: [...approvedModels.keys()]}
141
163
  }
142
164
  }
143
165
 
144
166
  /**
145
167
  * @param {import("node:http").IncomingMessage} request
146
168
  * @param {import("node:http").ServerResponse} response
147
- * @param {{token: string, grant: KimiGrant, grants: KimiGrantStore, oauth: KimiOAuthStore, fetchImplementation: typeof fetch, signal: AbortSignal}} options
169
+ * @param {{grants: KimiGrantStore, runtimes: Map<string, {controller: AbortController, timer: NodeJS.Timeout, grant: KimiGrant}>, oauth: KimiOAuthStore, fetchImplementation: typeof fetch, onWorkerRequestAuthorized?: (token: string) => void}} options
148
170
  */
149
171
  async function proxyWorkerRequest(request, response, options) {
150
- throwIfAborted(options.signal)
151
172
  const policyHeaders = {...request.headers}
152
173
  delete policyHeaders.host
153
174
  delete policyHeaders.connection
154
175
  delete policyHeaders["transfer-encoding"]
155
176
  validateKimiBrokerRequest({method: request.method ?? "", path: request.url ?? "", headers: policyHeaders})
156
177
  const token = kimiBearerToken(request.headers.authorization)
157
- if (token !== options.token) throw new Error("Kimi broker request denied")
178
+ const runtime = options.runtimes.get(token)
179
+ if (runtime === undefined) throw new Error("Kimi broker grant unknown token")
180
+ const signal = runtime.controller.signal
181
+ throwIfAborted(signal)
158
182
  const context = {
159
- provider: "kimi", runId: options.grant.runId,
160
- networkId: options.grant.networkId, taskId: options.grant.taskId,
161
- sessionId: options.grant.sessionId, modelAlias: options.grant.modelAlias
183
+ provider: "kimi", runId: runtime.grant.runId,
184
+ networkId: runtime.grant.networkId, taskId: runtime.grant.taskId,
185
+ sessionId: runtime.grant.sessionId, modelAlias: runtime.grant.modelAlias
162
186
  }
163
187
  const grant = options.grants.authorize(token, context)
164
- const body = await readBody(request, options.signal)
165
- options.grants.authorize(token, context)
166
- validateKimiChatBody(body, grant.model)
167
- const accessToken = await options.oauth.getAccessToken({signal: options.signal})
168
- throwIfAborted(options.signal)
169
- const upstream = await abortable(options.fetchImplementation(UPSTREAM_URL, {
170
- method: "POST",
171
- redirect: "error",
172
- headers: {
173
- authorization: `Bearer ${accessToken}`,
174
- "content-type": "application/json",
175
- accept: "text/event-stream",
176
- "user-agent": "threadwire-kimi-model-broker/1"
177
- },
178
- body,
179
- signal: options.signal
180
- }), options.signal)
181
- throwIfAborted(options.signal)
182
- const contentType = safeContentType(upstream.headers.get("content-type"))
183
- response.writeHead(upstream.status, {"content-type": contentType, "cache-control": "no-store"})
184
- await streamResponse(upstream, response, options.signal)
188
+ // Couple this authenticated in-flight request to the grant abort signal:
189
+ // revocation or expiry destroys this request's own socket, waking a stalled
190
+ // body read so the upload terminates and releases its request/socket
191
+ // resources. Only this request's socket is touched; unrelated grants and
192
+ // broker shutdown semantics are unaffected.
193
+ const wake = () => request.socket.destroy()
194
+ signal.addEventListener("abort", wake, {once: true})
195
+ // Never miss an abort that raced the hook installation.
196
+ if (signal.aborted) {
197
+ signal.removeEventListener("abort", wake)
198
+ wake()
199
+ throwIfAborted(signal)
200
+ }
201
+ // Optional lifecycle seam: fires only after the grant is authorized and the
202
+ // abort hook is installed, i.e. once authenticated body processing begins.
203
+ options.onWorkerRequestAuthorized?.(token)
204
+ try {
205
+ const body = await readBody(request, signal)
206
+ options.grants.authorize(token, context)
207
+ validateKimiChatBody(body, grant.model)
208
+ const accessToken = await options.oauth.getAccessToken({signal})
209
+ throwIfAborted(signal)
210
+ const upstream = await abortable(options.fetchImplementation(UPSTREAM_URL, {
211
+ method: "POST", redirect: "error",
212
+ headers: {
213
+ authorization: `Bearer ${accessToken}`,
214
+ "content-type": "application/json", accept: "text/event-stream",
215
+ "user-agent": "threadwire-kimi-model-broker/1"
216
+ },
217
+ body, signal
218
+ }), signal)
219
+ throwIfAborted(signal)
220
+ const contentType = safeContentType(upstream.headers.get("content-type"))
221
+ response.writeHead(upstream.status, {"content-type": contentType, "cache-control": "no-store"})
222
+ await streamResponse(upstream, response, signal)
223
+ } finally {
224
+ signal.removeEventListener("abort", wake)
225
+ }
185
226
  }
186
227
 
187
228
  /** @param {Response} upstream @param {import("node:http").ServerResponse} response @param {AbortSignal} signal */
@@ -206,24 +247,20 @@ async function streamResponse(upstream, response, signal) {
206
247
  } finally { reader.releaseLock() }
207
248
  }
208
249
 
209
- /** @param {unknown} value @param {Map<string, ApprovedKimiModel>} approvedModels @returns {{brokerAddress: string, grant: KimiGrant}} */
250
+ /** @param {unknown} value @param {Map<string, ApprovedKimiModel>} approvedModels @returns {KimiGrant} */
210
251
  function parseGrant(value, approvedModels) {
211
- if (!record(value) || !exactKeys(value, ["brokerAddress", "modelAlias", "networkId", "provider", "runId", "sessionId", "taskId", "ttlMs"])
252
+ if (!record(value) || !exactKeys(value, ["modelAlias", "networkId", "provider", "runId", "sessionId", "taskId", "ttlMs"])
212
253
  || value.provider !== "kimi" || typeof value.runId !== "string" || typeof value.networkId !== "string"
213
254
  || typeof value.taskId !== "string" || typeof value.sessionId !== "string"
214
- || typeof value.modelAlias !== "string" || typeof value.brokerAddress !== "string" || typeof value.ttlMs !== "number") {
255
+ || typeof value.modelAlias !== "string" || typeof value.ttlMs !== "number") {
215
256
  throw new Error("Invalid Kimi broker grant")
216
257
  }
217
- if (!/^(?:\d{1,3}\.){3}\d{1,3}$/u.test(value.brokerAddress)) throw new Error("Invalid Kimi broker grant")
218
258
  const model = approvedModels.get(value.modelAlias)
219
259
  if (model === undefined) throw new Error("Kimi model is not allowed")
220
260
  return {
221
- brokerAddress: value.brokerAddress,
222
- grant: {
223
- provider: "kimi", runId: value.runId, networkId: value.networkId,
224
- taskId: value.taskId, sessionId: value.sessionId,
225
- modelAlias: model.alias, model: model.model, ttlMs: value.ttlMs
226
- }
261
+ provider: "kimi", runId: value.runId, networkId: value.networkId,
262
+ taskId: value.taskId, sessionId: value.sessionId,
263
+ modelAlias: model.alias, model: model.model, ttlMs: value.ttlMs
227
264
  }
228
265
  }
229
266
 
@@ -273,6 +310,18 @@ function safeHost(value) {
273
310
  if (value !== "0.0.0.0" && value !== "127.0.0.1" && value !== "::") throw new Error("Invalid Kimi model broker host")
274
311
  return value
275
312
  }
313
+ /** @param {string | undefined} value @param {string} name @returns {string} */
314
+ function bindSetting(value, name) {
315
+ const bind = safeSetting(value, name).trim()
316
+ // A host interface only: no port, no whitespace, no wildcard. Loopback is
317
+ // the load-time sentinel; an explicit task-reachable address is the only
318
+ // functional choice.
319
+ if (bind === "0.0.0.0" || bind === "::") throw new Error(`${name} must not be a wildcard bind`)
320
+ if (bind !== "127.0.0.1" && bind !== "::1" && !/^\d{1,3}(?:\.\d{1,3}){3}$/u.test(bind)) {
321
+ throw new Error(`${name} must be an explicit interface address`)
322
+ }
323
+ return bind
324
+ }
276
325
  /** @param {string | undefined} value @param {number} fallback @returns {number} */
277
326
  function portValue(value, fallback) {
278
327
  if (value === undefined) return fallback
@@ -316,10 +365,3 @@ function boundedClose(server) {
316
365
  new Promise((resolve) => { const timer = setTimeout(resolve, 1000); timer.unref() })
317
366
  ])
318
367
  }
319
- /** @param {Map<string, import("node:http").Server>} listeners @param {string} token */
320
- async function closeListener(listeners, token) {
321
- const listener = listeners.get(token)
322
- listeners.delete(token)
323
- if (listener === undefined) return
324
- await boundedClose(listener)
325
- }
@@ -36,10 +36,7 @@ export function createGrantStore(options = {}) {
36
36
  activate(token) {
37
37
  const grant = grants.get(token)
38
38
  if (!grant) throw new Error("Broker grant unknown token")
39
- if (grant.expiresAt < now()) {
40
- grants.delete(token)
41
- throw new Error("Broker grant expired")
42
- }
39
+ if (grant.expiresAt < now()) throw new Error("Broker grant expired")
43
40
  if (grant.active) throw new Error("Broker grant already active")
44
41
  grant.active = true
45
42
  return {...grant}
@@ -50,10 +47,16 @@ export function createGrantStore(options = {}) {
50
47
  if (!grant) throw new Error("Broker grant unknown token")
51
48
  if (grant.provider !== context.provider || grant.networkId !== context.networkId || (context.runId !== undefined && grant.runId !== context.runId)) throw new Error("Broker grant lineage denied")
52
49
  if (!grant.active) throw new Error("Broker grant pending")
53
- if (grant.expiresAt < now()) {
54
- grants.delete(token)
55
- throw new Error("Broker grant expired")
56
- }
50
+ if (grant.expiresAt < now()) throw new Error("Broker grant expired")
51
+ return {...grant}
52
+ },
53
+ /** @param {string} token @param {number} ttlMs */
54
+ renew(token, ttlMs) {
55
+ if (!Number.isSafeInteger(ttlMs) || ttlMs < 1 || ttlMs > 3_600_000) throw new Error("Invalid broker grant")
56
+ const grant = grants.get(token)
57
+ if (!grant) throw new Error("Broker grant unknown token")
58
+ if (grant.expiresAt < now()) throw new Error("Broker grant expired")
59
+ grant.expiresAt = now() + ttlMs
57
60
  return {...grant}
58
61
  },
59
62
  /** @param {string} token */
@@ -93,6 +93,20 @@ export async function startModelBroker(options = {}) {
93
93
  sendJson(response, 201, {token, generation, port: address.port})
94
94
  return
95
95
  }
96
+ if (request.url?.startsWith("/admin/grants/") && request.url.endsWith("/renew") && request.method === "POST") {
97
+ requireAdmin(request.headers.authorization, adminToken)
98
+ const token = decodeURIComponent(request.url.slice("/admin/grants/".length, -"/renew".length))
99
+ const body = await readJson(request, shutdown.signal)
100
+ if (!isRecord(body)) throw new Error("Invalid broker grant")
101
+ const renewed = grants.renew(token, body.ttlMs)
102
+ const runtime = grantRuntimes.get(token)
103
+ if (runtime === undefined) throw new Error("Broker grant unknown token")
104
+ clearTimeout(runtime.timer)
105
+ runtime.timer = setTimeout(() => { void revokeGrant(token) }, Math.max(1, renewed.expiresAt - Date.now()))
106
+ runtime.timer.unref()
107
+ sendJson(response, 200, {ok: true})
108
+ return
109
+ }
96
110
  if (request.url?.startsWith("/admin/grants/") && request.method === "DELETE") {
97
111
  requireAdmin(request.headers.authorization, adminToken)
98
112
  const token = decodeURIComponent(request.url.slice("/admin/grants/".length))
@@ -0,0 +1,8 @@
1
+ // @ts-check
2
+
3
+ import {isAbsolute} from "node:path"
4
+
5
+ /** @param {string} path */
6
+ export function escapesDirectory(path) {
7
+ return path === ".." || path.startsWith("../") || isAbsolute(path)
8
+ }
@@ -105,7 +105,7 @@ function recognizeKimiRecord(record, state) {
105
105
  const parsed = parseToolCall(toolCall)
106
106
  if (parsed === undefined) return emptyRecognition()
107
107
  state?.tools.set(parsed.id, parsed.name)
108
- events.push({type: "tool", phase: "started", name: parsed.name, key: `tool:${parsed.id}`})
108
+ events.push({type: "tool", phase: "started", name: parsed.name, key: `tool:${parsed.id}`, ...(parsed.detail === undefined ? {} : {detail: parsed.detail})})
109
109
  }
110
110
  }
111
111
  if (events.length === 0) return emptyRecognition()
@@ -132,14 +132,26 @@ function recognizeKimiRecord(record, state) {
132
132
  return emptyRecognition()
133
133
  }
134
134
 
135
- /** @param {unknown} value @returns {{id: string, name: string} | undefined} */
135
+ /** @param {unknown} value @returns {{id: string, name: string, detail?: string} | undefined} */
136
136
  function parseToolCall(value) {
137
137
  if (!isRecord(value) || !exactKeys(value, ["type", "id", "function"])
138
138
  || value.type !== "function" || typeof value.id !== "string" || !TOOL_ID_PATTERN.test(value.id)
139
139
  || !isRecord(value.function) || !exactKeys(value.function, ["name", "arguments"])
140
140
  || typeof value.function.name !== "string" || !TOOL_NAME_PATTERN.test(value.function.name)
141
141
  || typeof value.function.arguments !== "string") return undefined
142
- return {id: value.id, name: value.function.name}
142
+ return {id: value.id, name: value.function.name, ...toolDetail(value.function.arguments)}
143
+ }
144
+
145
+ /** @param {string} serialized @returns {{detail?: string}} */
146
+ function toolDetail(serialized) {
147
+ let arguments_
148
+ try { arguments_ = JSON.parse(serialized) } catch { return {} }
149
+ if (!isRecord(arguments_)) return {}
150
+ for (const key of ["command", "file_path", "path", "pattern", "query", "description"]) {
151
+ const value = arguments_[key]
152
+ if (typeof value === "string" && value.length > 0) return {detail: Array.from(value).slice(0, 512).join("")}
153
+ }
154
+ return {}
143
155
  }
144
156
 
145
157
  /** @param {WorkerEvent[]} events @param {KimiRecordState | undefined} state @returns {WorkerEvent[]} */
@@ -27,6 +27,7 @@ import {parseCodeCommand, parseEvidenceCommand} from "./command.js"
27
27
  * Relay?: typeof Relay,
28
28
  * processNumber?: number,
29
29
  * providerEnvironment?: NodeJS.ProcessEnv,
30
+ * kimiBinding?: unknown,
30
31
  * workspaceProfileOperations?: import("../workspace-profile.js").WorkspaceProfileOperations,
31
32
  * activity?: Pick<import("../activity-log.js").ActivityLog, "recordWorkspace" | "recordStarted" | "recordSession" | "close">,
32
33
  * evidenceStore?: import("../evidence-store.js").EvidenceStore,
@@ -113,8 +114,9 @@ export async function dispatchWorker(job, config, dependencies = {}) {
113
114
  const processNumber = dependencies.processNumber ?? process.pid
114
115
  // Explicit only: never fall back to ambient process.env (avoids secret leakage).
115
116
  const providerEnvironment = buildProviderEnvironment(dependencies.providerEnvironment ?? {})
117
+ const kimiBinding = job.provider === "kimi" ? dependencies.kimiBinding : undefined
116
118
  const workspace = await resolveWorkspaceProfile(
117
- {provider: job.provider},
119
+ {provider: job.provider, ...(kimiBinding === undefined ? {} : {binding: kimiBinding})},
118
120
  dependencies.workspaceProfileOperations
119
121
  )
120
122
  const isolatedRuntimeClient = job.provider === "kimi"
@@ -124,8 +126,7 @@ export async function dispatchWorker(job, config, dependencies = {}) {
124
126
  ? await /** @type {NonNullable<typeof isolatedRuntimeClient>} */ (isolatedRuntimeClient).preflight({
125
127
  provider: "kimi",
126
128
  profile: workspace.profile,
127
- repositoryRoot: workspace.repositoryRoot,
128
- cwd: workspace.cwd,
129
+ binding: kimiBinding,
129
130
  providerArguments: []
130
131
  })
131
132
  : undefined
@@ -164,8 +165,9 @@ export async function dispatchWorker(job, config, dependencies = {}) {
164
165
  const preflight = earlyKimiPreflight ?? await isolatedRuntimeClient.preflight({
165
166
  provider: job.provider,
166
167
  profile: workspace.profile,
167
- repositoryRoot: workspace.repositoryRoot,
168
- cwd: workspace.cwd,
168
+ ...(job.provider === "kimi"
169
+ ? {binding: kimiBinding}
170
+ : {repositoryRoot: workspace.repositoryRoot, cwd: workspace.cwd}),
169
171
  providerArguments: []
170
172
  })
171
173
  const exitCode = await isolatedRuntimeClient.run({
@@ -6,6 +6,7 @@ import {EvidenceStore} from "./evidence-store.js"
6
6
  import {buildProviderEnvironment, parseIngressConfig, resolveIngressEnvironment} from "./telegram-ingress/config.js"
7
7
  import {createWebhookHandler} from "./telegram-ingress/http.js"
8
8
  import {isolatedRuntimeClientFromEnvironment, kimiIsolatedRuntimeClientFromEnvironment} from "./isolated-runtime-client.js"
9
+ import {parseTrustedThreadwireBinding} from "./threadwire-binding.js"
9
10
 
10
11
  /**
11
12
  * Start the standalone Threadwire Telegram webhook service.
@@ -24,6 +25,14 @@ import {isolatedRuntimeClientFromEnvironment, kimiIsolatedRuntimeClientFromEnvir
24
25
  */
25
26
  export async function startTelegramWebhook(options = {}) {
26
27
  const environment = await resolveIngressEnvironment(options.env ?? process.env)
28
+ // Capture this trusted topology attestation before anything derives a
29
+ // provider environment. The parsed object is passed only to Kimi preflight.
30
+ // Codex-only Compose renders the variable as an empty string; a blank value
31
+ // means "absent", while a malformed non-blank value still fails closed.
32
+ const rawKimiBinding = environment.THREADWIRE_KIMI_TASK_BINDING
33
+ const kimiBinding = rawKimiBinding === undefined || rawKimiBinding.trim().length === 0
34
+ ? undefined
35
+ : parseTrustedThreadwireBinding(rawKimiBinding)
27
36
  const config = parseIngressConfig(environment)
28
37
  const createServerImpl = options.createServerImpl ?? createServer
29
38
  const onOperationalError = options.onOperationalError ?? defaultOperationalError
@@ -55,6 +64,7 @@ export async function startTelegramWebhook(options = {}) {
55
64
  providerEnvironment,
56
65
  ...(isolatedRuntimeClient === undefined ? {} : {isolatedRuntimeClient}),
57
66
  ...(kimiIsolatedRuntimeClient === undefined ? {} : {kimiIsolatedRuntimeClient}),
67
+ ...(kimiBinding === undefined ? {} : {kimiBinding}),
58
68
  onOperationalError,
59
69
  onWorkerFailure
60
70
  })