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.
- package/CHANGELOG.md +10 -0
- package/README.md +10 -1
- package/TELEGRAM-INGRESS.md +2 -0
- package/docs/container-runtime.md +39 -3
- package/docs/isolated-provider-runtime.md +90 -12
- package/package.json +1 -1
- package/scripts/verify-package.js +2 -0
- package/src/absolute-deadline.js +8 -5
- package/src/cli.js +14 -3
- package/src/docker-api.js +100 -14
- package/src/isolated-runtime-client.js +116 -44
- package/src/isolated-runtime.js +673 -64
- package/src/isolated-state.js +62 -11
- package/src/isolated-worker.js +231 -23
- package/src/kimi-model-broker-policy.js +19 -6
- package/src/kimi-model-broker.js +134 -92
- package/src/model-broker-policy.js +11 -8
- package/src/model-broker.js +14 -0
- package/src/path-safety.js +8 -0
- package/src/providers/kimi.js +15 -3
- package/src/telegram-ingress/core.js +7 -5
- package/src/telegram-webhook.js +10 -0
- package/src/threadwire-binding.js +192 -0
- package/src/workspace-profile.js +31 -8
- package/threadwire.workspace-profiles.json +3 -2
package/src/kimi-model-broker.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
54
|
+
const revokeGrant = (token) => {
|
|
47
55
|
grants.revoke(token)
|
|
48
56
|
const runtime = runtimes.get(token)
|
|
49
57
|
runtimes.delete(token)
|
|
50
|
-
if (runtime
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
74
|
-
const
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 {{
|
|
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
|
-
|
|
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:
|
|
160
|
-
networkId:
|
|
161
|
-
sessionId:
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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 {
|
|
250
|
+
/** @param {unknown} value @param {Map<string, ApprovedKimiModel>} approvedModels @returns {KimiGrant} */
|
|
210
251
|
function parseGrant(value, approvedModels) {
|
|
211
|
-
if (!record(value) || !exactKeys(value, ["
|
|
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.
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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 */
|
package/src/model-broker.js
CHANGED
|
@@ -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))
|
package/src/providers/kimi.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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({
|
package/src/telegram-webhook.js
CHANGED
|
@@ -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
|
})
|