threadwire 0.1.8 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +3 -3
- package/TELEGRAM-INGRESS.md +8 -4
- package/bin/kimi-model-broker.js +5 -0
- package/docs/container-runtime.md +22 -0
- package/docs/isolated-provider-runtime.md +95 -0
- package/package.json +6 -3
- package/scripts/verify-package.js +8 -1
- package/src/activity-log.js +3 -3
- package/src/cli.js +20 -14
- package/src/isolated-runtime-client.js +9 -0
- package/src/isolated-runtime.js +199 -64
- package/src/isolated-state.js +105 -44
- package/src/isolated-worker.js +28 -7
- package/src/kimi-model-broker-policy.js +209 -0
- package/src/kimi-model-broker.js +325 -0
- package/src/kimi-oauth-store.js +267 -0
- package/src/providers/index.js +8 -3
- package/src/providers/kimi.js +165 -0
- package/src/telegram-ingress/command.js +4 -4
- package/src/telegram-ingress/config.js +11 -0
- package/src/telegram-ingress/core.js +33 -10
- package/src/telegram-ingress/http.js +2 -1
- package/src/telegram-webhook.js +8 -3
- package/src/workspace-profile.js +3 -3
- package/threadwire.workspace-profiles.json +1 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
const EXECUTABLE = "/usr/local/bin/kimi"
|
|
4
|
+
const ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/u
|
|
5
|
+
const SESSION_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,511}$/u
|
|
6
|
+
const TOOL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/u
|
|
7
|
+
const TOOL_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.:-]{0,127}$/u
|
|
8
|
+
|
|
9
|
+
/** @typedef {import("../types.js").WorkerEvent} WorkerEvent */
|
|
10
|
+
/** @typedef {{started: boolean, sessionEmitted: boolean, tools: Map<string, string>}} KimiRecordState */
|
|
11
|
+
/** @typedef {{trusted: boolean, sessionId: string | undefined, events: WorkerEvent[]}} KimiRecognition */
|
|
12
|
+
|
|
13
|
+
/** @param {string[]} providerArguments */
|
|
14
|
+
export function validateKimiProviderArguments(providerArguments) {
|
|
15
|
+
if (providerArguments.length === 0) return {modelAlias: "default", arguments: []}
|
|
16
|
+
if (providerArguments.length === 2 && (providerArguments[0] === "--model" || providerArguments[0] === "-m")
|
|
17
|
+
&& ALIAS_PATTERN.test(providerArguments[1] ?? "")) {
|
|
18
|
+
return {modelAlias: /** @type {string} */ (providerArguments[1]), arguments: ["--model", /** @type {string} */ (providerArguments[1])]}
|
|
19
|
+
}
|
|
20
|
+
throw new Error("Threadwire owns Kimi prompt, output, session, model configuration, permissions, and extensions")
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** @param {string[]} providerArguments @param {string} prompt @param {string | undefined} [resumeSession] */
|
|
24
|
+
export function buildKimiCommand(providerArguments, prompt, resumeSession) {
|
|
25
|
+
const selection = validateKimiProviderArguments(providerArguments)
|
|
26
|
+
if (resumeSession !== undefined && !SESSION_PATTERN.test(resumeSession)) throw new Error("Kimi session ID is invalid")
|
|
27
|
+
return {
|
|
28
|
+
executable: EXECUTABLE,
|
|
29
|
+
arguments: [
|
|
30
|
+
"--model", selection.modelAlias,
|
|
31
|
+
...(resumeSession === undefined ? [] : ["--session", resumeSession]),
|
|
32
|
+
"--prompt", prompt,
|
|
33
|
+
"--output-format", "stream-json"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @returns {(record: unknown) => string | undefined} */
|
|
39
|
+
export function createKimiSessionId() {
|
|
40
|
+
let emitted = false
|
|
41
|
+
return (record) => {
|
|
42
|
+
if (emitted) return undefined
|
|
43
|
+
const recognized = recognizeKimiRecord(record, undefined)
|
|
44
|
+
if (recognized.sessionId === undefined) return undefined
|
|
45
|
+
emitted = true
|
|
46
|
+
return recognized.sessionId
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** @returns {(record: unknown) => import("../types.js").WorkerEvent[]} */
|
|
51
|
+
export function createKimiParser() {
|
|
52
|
+
const state = createKimiRecordState()
|
|
53
|
+
return (record) => recognizeKimiRecord(record, state).events
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @param {unknown} record @returns {import("../types.js").WorkerEvent[]} */
|
|
57
|
+
export function parseKimiEvent(record) {
|
|
58
|
+
return recognizeKimiRecord(record, createKimiRecordState()).events
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** @returns {KimiRecordState} */
|
|
62
|
+
export function createKimiRecordState() {
|
|
63
|
+
return {started: false, sessionEmitted: false, tools: new Map()}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @param {unknown} record */
|
|
67
|
+
export function kimiSessionEnvelopeId(record) {
|
|
68
|
+
return isRecord(record) && exactKeys(record, ["type", "sessionId"])
|
|
69
|
+
&& record.type === "session" && typeof record.sessionId === "string" && SESSION_PATTERN.test(record.sessionId)
|
|
70
|
+
? record.sessionId
|
|
71
|
+
: undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Convert one pinned Kimi stream-json record into the only envelopes the
|
|
76
|
+
* isolated worker may expose. Raw arguments, results, metadata, and stderr are
|
|
77
|
+
* never represented in the return value.
|
|
78
|
+
* @param {unknown} record
|
|
79
|
+
* @param {ReturnType<typeof createKimiRecordState>} state
|
|
80
|
+
*/
|
|
81
|
+
export function sanitizeKimiRecord(record, state) {
|
|
82
|
+
const recognized = recognizeKimiRecord(record, state)
|
|
83
|
+
/** @type {({type: "worker-event", event: WorkerEvent} | {type: "session", sessionId: string})[]} */
|
|
84
|
+
const output = recognized.events.map((event) => ({type: /** @type {const} */ ("worker-event"), event}))
|
|
85
|
+
if (recognized.sessionId !== undefined && !state.sessionEmitted) {
|
|
86
|
+
state.sessionEmitted = true
|
|
87
|
+
output.push({type: "session", sessionId: recognized.sessionId})
|
|
88
|
+
}
|
|
89
|
+
return output
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** @param {unknown} record @param {KimiRecordState | undefined} state @returns {KimiRecognition} */
|
|
93
|
+
function recognizeKimiRecord(record, state) {
|
|
94
|
+
if (!isRecord(record)) return emptyRecognition()
|
|
95
|
+
if (record.role === "assistant" && exactOptionalKeys(record, ["role"], ["content", "tool_calls"])) {
|
|
96
|
+
/** @type {WorkerEvent[]} */
|
|
97
|
+
const events = []
|
|
98
|
+
if (record.content !== undefined) {
|
|
99
|
+
if (typeof record.content !== "string") return emptyRecognition()
|
|
100
|
+
events.push({type: "text-delta", text: record.content, streamId: "kimi:assistant"})
|
|
101
|
+
}
|
|
102
|
+
if (record.tool_calls !== undefined) {
|
|
103
|
+
if (!Array.isArray(record.tool_calls)) return emptyRecognition()
|
|
104
|
+
for (const toolCall of record.tool_calls) {
|
|
105
|
+
const parsed = parseToolCall(toolCall)
|
|
106
|
+
if (parsed === undefined) return emptyRecognition()
|
|
107
|
+
state?.tools.set(parsed.id, parsed.name)
|
|
108
|
+
events.push({type: "tool", phase: "started", name: parsed.name, key: `tool:${parsed.id}`})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (events.length === 0) return emptyRecognition()
|
|
112
|
+
return {trusted: true, sessionId: undefined, events: withStarted(events, state)}
|
|
113
|
+
}
|
|
114
|
+
if (record.role === "tool" && exactKeys(record, ["role", "tool_call_id", "content"])
|
|
115
|
+
&& typeof record.tool_call_id === "string" && TOOL_ID_PATTERN.test(record.tool_call_id)
|
|
116
|
+
&& typeof record.content === "string") {
|
|
117
|
+
const name = state?.tools.get(record.tool_call_id)
|
|
118
|
+
if (name === undefined || state === undefined) return emptyRecognition()
|
|
119
|
+
state.tools.delete(record.tool_call_id)
|
|
120
|
+
return {
|
|
121
|
+
trusted: true,
|
|
122
|
+
sessionId: undefined,
|
|
123
|
+
events: withStarted([{type: "tool", phase: "finished", name, key: `tool:${record.tool_call_id}`}], state)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (record.role === "meta" && record.type === "session.resume_hint"
|
|
127
|
+
&& exactKeys(record, ["role", "type", "session_id", "command", "content"])
|
|
128
|
+
&& typeof record.session_id === "string" && SESSION_PATTERN.test(record.session_id)
|
|
129
|
+
&& typeof record.command === "string" && typeof record.content === "string") {
|
|
130
|
+
return {trusted: true, sessionId: record.session_id, events: withStarted([], state)}
|
|
131
|
+
}
|
|
132
|
+
return emptyRecognition()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** @param {unknown} value @returns {{id: string, name: string} | undefined} */
|
|
136
|
+
function parseToolCall(value) {
|
|
137
|
+
if (!isRecord(value) || !exactKeys(value, ["type", "id", "function"])
|
|
138
|
+
|| value.type !== "function" || typeof value.id !== "string" || !TOOL_ID_PATTERN.test(value.id)
|
|
139
|
+
|| !isRecord(value.function) || !exactKeys(value.function, ["name", "arguments"])
|
|
140
|
+
|| typeof value.function.name !== "string" || !TOOL_NAME_PATTERN.test(value.function.name)
|
|
141
|
+
|| typeof value.function.arguments !== "string") return undefined
|
|
142
|
+
return {id: value.id, name: value.function.name}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** @param {WorkerEvent[]} events @param {KimiRecordState | undefined} state @returns {WorkerEvent[]} */
|
|
146
|
+
function withStarted(events, state) {
|
|
147
|
+
if (state === undefined || state.started) return events
|
|
148
|
+
state.started = true
|
|
149
|
+
return [{type: "lifecycle", phase: "started", summary: "Kimi worker started"}, ...events]
|
|
150
|
+
}
|
|
151
|
+
/** @returns {KimiRecognition} */
|
|
152
|
+
function emptyRecognition() { return {trusted: false, sessionId: undefined, events: []} }
|
|
153
|
+
/** @param {unknown} value @returns {value is Record<string, unknown>} */
|
|
154
|
+
function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value) }
|
|
155
|
+
/** @param {Record<string, unknown>} value @param {readonly string[]} expected @returns {boolean} */
|
|
156
|
+
function exactKeys(value, expected) {
|
|
157
|
+
const actual = Object.keys(value).sort()
|
|
158
|
+
const wanted = [...expected].sort()
|
|
159
|
+
return actual.length === wanted.length && actual.every((key, index) => key === wanted[index])
|
|
160
|
+
}
|
|
161
|
+
/** @param {Record<string, unknown>} value @param {readonly string[]} required @param {readonly string[]} optional @returns {boolean} */
|
|
162
|
+
function exactOptionalKeys(value, required, optional) {
|
|
163
|
+
const actual = Object.keys(value)
|
|
164
|
+
return required.every((key) => actual.includes(key)) && actual.every((key) => required.includes(key) || optional.includes(key))
|
|
165
|
+
}
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import {PROVIDERS} from "../providers/index.js"
|
|
4
4
|
|
|
5
|
-
const COMMAND_PATTERN = /^\/code(?:@[A-Za-z0-9_]+)?\s+(codex|claude|opencode)\s+(\S[\s\S]*)$/u
|
|
5
|
+
const COMMAND_PATTERN = /^\/code(?:@[A-Za-z0-9_]+)?\s+(codex|claude|kimi|opencode)\s+(\S[\s\S]*)$/u
|
|
6
6
|
const EVIDENCE_PATTERN = /^\/evidence(?:@[A-Za-z0-9_]+)?\s+(evidence_[A-Za-z0-9_-]{43})\s+(bytes|lines)\s+(\d+):(\d+)$/u
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* @typedef {{provider: "codex" | "claude" | "opencode", prompt: string}} CodeCommand
|
|
9
|
+
* @typedef {{provider: "codex" | "claude" | "kimi" | "opencode", prompt: string}} CodeCommand
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Parse the exact ingress command grammar:
|
|
14
|
-
* `/code[optional @bot] <codex|claude|opencode> <nonblank prompt>`
|
|
14
|
+
* `/code[optional @bot] <codex|claude|kimi|opencode> <nonblank prompt>`
|
|
15
15
|
* @param {string} text
|
|
16
16
|
* @returns {CodeCommand | null}
|
|
17
17
|
*/
|
|
@@ -24,7 +24,7 @@ export function parseCodeCommand(text) {
|
|
|
24
24
|
if (prompt.length === 0) return null
|
|
25
25
|
if (!PROVIDERS.includes(/** @type {(typeof PROVIDERS)[number]} */ (providerName))) return null
|
|
26
26
|
return {
|
|
27
|
-
provider: /** @type {"codex" | "claude" | "opencode"} */ (providerName),
|
|
27
|
+
provider: /** @type {"codex" | "claude" | "kimi" | "opencode"} */ (providerName),
|
|
28
28
|
prompt
|
|
29
29
|
}
|
|
30
30
|
}
|
|
@@ -139,6 +139,7 @@ export async function resolveIngressEnvironment(source, operations = {}) {
|
|
|
139
139
|
* host: string,
|
|
140
140
|
* maxConcurrentWorkers: number,
|
|
141
141
|
* toolMessages: boolean,
|
|
142
|
+
* requireCodexIsolation: boolean,
|
|
142
143
|
* telegramRequestTimeoutMs: number,
|
|
143
144
|
* updateIdCapacity: number,
|
|
144
145
|
* updateIdTtlMs: number
|
|
@@ -166,6 +167,9 @@ export function parseIngressConfig(environment) {
|
|
|
166
167
|
DEFAULT_MAX_CONCURRENT_WORKERS
|
|
167
168
|
)
|
|
168
169
|
const toolMessages = parseStrictBoolean(environment.THREADWIRE_TOOL_MESSAGES, "THREADWIRE_TOOL_MESSAGES", false)
|
|
170
|
+
const requireCodexIsolation = parseStrictBoolean(
|
|
171
|
+
environment.THREADWIRE_REQUIRE_CODEX_ISOLATION, "THREADWIRE_REQUIRE_CODEX_ISOLATION", false
|
|
172
|
+
)
|
|
169
173
|
const telegramRequestTimeoutMs = parseTelegramRequestTimeoutMs(environment)
|
|
170
174
|
const updateIdCapacity = parsePositiveSafeInteger(
|
|
171
175
|
environment.THREADWIRE_UPDATE_ID_CAPACITY,
|
|
@@ -187,6 +191,7 @@ export function parseIngressConfig(environment) {
|
|
|
187
191
|
host,
|
|
188
192
|
maxConcurrentWorkers,
|
|
189
193
|
toolMessages,
|
|
194
|
+
requireCodexIsolation,
|
|
190
195
|
telegramRequestTimeoutMs,
|
|
191
196
|
updateIdCapacity,
|
|
192
197
|
updateIdTtlMs,
|
|
@@ -234,9 +239,15 @@ export function buildProviderEnvironment(source) {
|
|
|
234
239
|
/** @param {string} key */
|
|
235
240
|
function isIngressSecretKey(key) {
|
|
236
241
|
if (FILE_BACKED_SETTINGS.some((name) => key === `${name}_FILE`)) return true
|
|
242
|
+
if (key === "THREADWIRE_REQUIRE_CODEX_ISOLATION") return true
|
|
237
243
|
if (
|
|
238
244
|
key.startsWith("THREADWIRE_ISOLATED_RUNTIME_")
|
|
239
245
|
|| key.startsWith("THREADWIRE_MODEL_BROKER_")
|
|
246
|
+
|| key.startsWith("THREADWIRE_KIMI_")
|
|
247
|
+
|| key === "KIMI_MODEL_API_KEY"
|
|
248
|
+
|| key === "KIMI_API_KEY"
|
|
249
|
+
|| key === "MOONSHOT_API_KEY"
|
|
250
|
+
|| key === "KIMI_CODE_HOME"
|
|
240
251
|
|| key === "THREADWIRE_RELAY_WORKER_IMAGE"
|
|
241
252
|
|| key === "THREADWIRE_ALLOWED_WORKTREE_ROOTS"
|
|
242
253
|
|| key === "THREADWIRE_WORKTREE_VOLUME"
|
|
@@ -6,6 +6,7 @@ import {NoticeQueue} from "../notice-queue.js"
|
|
|
6
6
|
import {createFetchTransport} from "../notifiers/fetch-transport.js"
|
|
7
7
|
import {createTelegramSender} from "../notifiers/telegram.js"
|
|
8
8
|
import {createProvider} from "../providers/index.js"
|
|
9
|
+
import {kimiSessionEnvelopeId} from "../providers/kimi.js"
|
|
9
10
|
import {Relay} from "../relay.js"
|
|
10
11
|
import {runWorker} from "../run-worker.js"
|
|
11
12
|
import {WorkerControl} from "../worker-control.js"
|
|
@@ -15,7 +16,7 @@ import {parseCodeCommand, parseEvidenceCommand} from "./command.js"
|
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* @typedef {{platform: "telegram", chatId: string, threadId: number | null}} TelegramTarget
|
|
18
|
-
* @typedef {{provider: "codex" | "claude" | "opencode", prompt: string, target: TelegramTarget, senderId: string} | {type: "evidence", request: unknown, target: TelegramTarget, senderId: string}} AcceptedJob
|
|
19
|
+
* @typedef {{provider: "codex" | "claude" | "kimi" | "opencode", prompt: string, target: TelegramTarget, senderId: string} | {type: "evidence", request: unknown, target: TelegramTarget, senderId: string}} AcceptedJob
|
|
19
20
|
* @typedef {{
|
|
20
21
|
* createProvider?: typeof createProvider,
|
|
21
22
|
* createFetchTransport?: typeof createFetchTransport,
|
|
@@ -30,6 +31,7 @@ import {parseCodeCommand, parseEvidenceCommand} from "./command.js"
|
|
|
30
31
|
* activity?: Pick<import("../activity-log.js").ActivityLog, "recordWorkspace" | "recordStarted" | "recordSession" | "close">,
|
|
31
32
|
* evidenceStore?: import("../evidence-store.js").EvidenceStore,
|
|
32
33
|
* isolatedRuntimeClient?: Pick<import("../isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run">,
|
|
34
|
+
* kimiIsolatedRuntimeClient?: Pick<import("../isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run">,
|
|
33
35
|
* onWorkerFailure?: (message: string) => void,
|
|
34
36
|
* onWorkerSettled?: () => void
|
|
35
37
|
* }} IngressDependencies
|
|
@@ -88,12 +90,19 @@ export function interpretUpdate(update, config) {
|
|
|
88
90
|
* spawn rejects (HTTP 500). Failure after spawn is observed via
|
|
89
91
|
* onWorkerFailure without empty-swallowing the error.
|
|
90
92
|
* @param {AcceptedJob} job
|
|
91
|
-
* @param {{botToken: string, telegramRequestTimeoutMs?: number, toolMessages?: boolean}} config
|
|
93
|
+
* @param {{botToken: string, telegramRequestTimeoutMs?: number, toolMessages?: boolean, requireCodexIsolation?: boolean}} config
|
|
92
94
|
* @param {IngressDependencies} [dependencies]
|
|
93
95
|
* @returns {Promise<void>}
|
|
94
96
|
*/
|
|
95
97
|
export async function dispatchWorker(job, config, dependencies = {}) {
|
|
96
98
|
if ("type" in job) throw new Error("Worker dispatch requires a worker job")
|
|
99
|
+
if (job.provider === "kimi" && dependencies.kimiIsolatedRuntimeClient === undefined) {
|
|
100
|
+
throw new Error("Kimi isolated runtime is required")
|
|
101
|
+
}
|
|
102
|
+
if (job.provider === "codex" && config.requireCodexIsolation === true
|
|
103
|
+
&& dependencies.isolatedRuntimeClient === undefined) {
|
|
104
|
+
throw new Error("Codex isolated runtime is required")
|
|
105
|
+
}
|
|
97
106
|
const createProviderImpl = dependencies.createProvider ?? createProvider
|
|
98
107
|
const createFetchTransportImpl = dependencies.createFetchTransport ?? createFetchTransport
|
|
99
108
|
const createTelegramSenderImpl = dependencies.createTelegramSender ?? createTelegramSender
|
|
@@ -108,6 +117,18 @@ export async function dispatchWorker(job, config, dependencies = {}) {
|
|
|
108
117
|
{provider: job.provider},
|
|
109
118
|
dependencies.workspaceProfileOperations
|
|
110
119
|
)
|
|
120
|
+
const isolatedRuntimeClient = job.provider === "kimi"
|
|
121
|
+
? dependencies.kimiIsolatedRuntimeClient
|
|
122
|
+
: job.provider === "codex" ? dependencies.isolatedRuntimeClient : undefined
|
|
123
|
+
const earlyKimiPreflight = job.provider === "kimi"
|
|
124
|
+
? await /** @type {NonNullable<typeof isolatedRuntimeClient>} */ (isolatedRuntimeClient).preflight({
|
|
125
|
+
provider: "kimi",
|
|
126
|
+
profile: workspace.profile,
|
|
127
|
+
repositoryRoot: workspace.repositoryRoot,
|
|
128
|
+
cwd: workspace.cwd,
|
|
129
|
+
providerArguments: []
|
|
130
|
+
})
|
|
131
|
+
: undefined
|
|
111
132
|
|
|
112
133
|
const provider = createProviderImpl(job.provider, [], job.prompt, undefined, providerEnvironment)
|
|
113
134
|
dependencies.activity?.recordWorkspace(
|
|
@@ -137,30 +158,32 @@ export async function dispatchWorker(job, config, dependencies = {}) {
|
|
|
137
158
|
try {
|
|
138
159
|
await evidence?.append("prompt", `prompt\n${job.prompt}\nprovider-stream\n`)
|
|
139
160
|
|
|
140
|
-
if (
|
|
161
|
+
if (isolatedRuntimeClient !== undefined) {
|
|
141
162
|
evidenceTransferred = true
|
|
142
163
|
try {
|
|
143
|
-
const preflight = await
|
|
144
|
-
provider:
|
|
164
|
+
const preflight = earlyKimiPreflight ?? await isolatedRuntimeClient.preflight({
|
|
165
|
+
provider: job.provider,
|
|
145
166
|
profile: workspace.profile,
|
|
146
167
|
repositoryRoot: workspace.repositoryRoot,
|
|
147
168
|
cwd: workspace.cwd,
|
|
148
169
|
providerArguments: []
|
|
149
170
|
})
|
|
150
|
-
const exitCode = await
|
|
171
|
+
const exitCode = await isolatedRuntimeClient.run({
|
|
151
172
|
preflightId: preflight.preflightId,
|
|
152
173
|
prompt: job.prompt,
|
|
153
174
|
providerArguments: [],
|
|
154
175
|
...(preflight.deadline === undefined ? {} : {deadline: preflight.deadline}),
|
|
155
176
|
onEvent: async (event) => control.accept(event),
|
|
156
177
|
onRecord: async (record) => {
|
|
157
|
-
const id = provider.sessionId(record)
|
|
178
|
+
const id = job.provider === "kimi" ? kimiSessionEnvelopeId(record) : provider.sessionId(record)
|
|
158
179
|
if (id !== undefined) dependencies.activity?.recordSession(provider.name, id)
|
|
159
180
|
},
|
|
160
|
-
|
|
161
|
-
|
|
181
|
+
...(job.provider === "kimi" ? {} : {
|
|
182
|
+
onStdoutChunk: (chunk) => evidence?.append("provider-stdout", chunk),
|
|
183
|
+
onStderrChunk: (chunk) => evidence?.append("provider-stderr", chunk)
|
|
184
|
+
})
|
|
162
185
|
})
|
|
163
|
-
if (exitCode !== 0) throw new Error(`Isolated Codex worker exited with status ${exitCode}`)
|
|
186
|
+
if (exitCode !== 0) throw new Error(`Isolated ${job.provider === "kimi" ? "Kimi" : "Codex"} worker exited with status ${exitCode}`)
|
|
164
187
|
await control.close()
|
|
165
188
|
if (evidence !== undefined) {
|
|
166
189
|
await evidence.finalize()
|
|
@@ -145,7 +145,8 @@ async function handleRequest(request, response, config, dependencies) {
|
|
|
145
145
|
await dispatchWorker(job, {
|
|
146
146
|
botToken: config.botToken,
|
|
147
147
|
telegramRequestTimeoutMs: config.telegramRequestTimeoutMs,
|
|
148
|
-
toolMessages: config.toolMessages
|
|
148
|
+
toolMessages: config.toolMessages,
|
|
149
|
+
requireCodexIsolation: config.requireCodexIsolation
|
|
149
150
|
}, {
|
|
150
151
|
...dependencies,
|
|
151
152
|
onWorkerSettled: () => {
|
package/src/telegram-webhook.js
CHANGED
|
@@ -5,7 +5,7 @@ import {ActivityLog} from "./activity-log.js"
|
|
|
5
5
|
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
|
-
import {isolatedRuntimeClientFromEnvironment} from "./isolated-runtime-client.js"
|
|
8
|
+
import {isolatedRuntimeClientFromEnvironment, kimiIsolatedRuntimeClientFromEnvironment} from "./isolated-runtime-client.js"
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Start the standalone Threadwire Telegram webhook service.
|
|
@@ -35,10 +35,14 @@ export async function startTelegramWebhook(options = {}) {
|
|
|
35
35
|
const providerEnvironment = buildProviderEnvironment(
|
|
36
36
|
options.providerEnvironment ?? environment
|
|
37
37
|
)
|
|
38
|
-
const isolatedConfigured = environment.
|
|
39
|
-
|
|
38
|
+
const isolatedConfigured = typeof environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN === "string"
|
|
39
|
+
&& environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN.trim().length > 0
|
|
40
40
|
const isolatedRuntimeClient = options.handlerDependencies?.isolatedRuntimeClient
|
|
41
41
|
?? (isolatedConfigured ? isolatedRuntimeClientFromEnvironment(environment) : undefined)
|
|
42
|
+
const kimiIsolatedConfigured = typeof environment.THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN === "string"
|
|
43
|
+
&& environment.THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN.trim().length > 0
|
|
44
|
+
const kimiIsolatedRuntimeClient = options.handlerDependencies?.kimiIsolatedRuntimeClient
|
|
45
|
+
?? (kimiIsolatedConfigured ? kimiIsolatedRuntimeClientFromEnvironment(environment) : undefined)
|
|
42
46
|
const activity = options.activity ?? options.handlerDependencies?.activity ?? new ActivityLog("/var/lib/threadwire/activity/threadwire.jsonl")
|
|
43
47
|
const evidenceStore = options.evidenceStore ?? options.handlerDependencies?.evidenceStore ?? await EvidenceStore.open({
|
|
44
48
|
root: config.evidenceRoot ?? "/var/lib/threadwire/evidence"
|
|
@@ -50,6 +54,7 @@ export async function startTelegramWebhook(options = {}) {
|
|
|
50
54
|
evidenceStore,
|
|
51
55
|
providerEnvironment,
|
|
52
56
|
...(isolatedRuntimeClient === undefined ? {} : {isolatedRuntimeClient}),
|
|
57
|
+
...(kimiIsolatedRuntimeClient === undefined ? {} : {kimiIsolatedRuntimeClient}),
|
|
53
58
|
onOperationalError,
|
|
54
59
|
onWorkerFailure
|
|
55
60
|
})
|
package/src/workspace-profile.js
CHANGED
|
@@ -14,7 +14,7 @@ const SOURCE_IDENTITY_PATTERN = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/u
|
|
|
14
14
|
const PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* @typedef {{repositoryRoot: string, cwd: string, providers: ("codex" | "claude" | "opencode")[]}} WorkspaceProfile
|
|
17
|
+
* @typedef {{repositoryRoot: string, cwd: string, providers: ("codex" | "claude" | "kimi" | "opencode")[]}} WorkspaceProfile
|
|
18
18
|
* @typedef {{version: 1, defaultProfile: string, profiles: Record<string, WorkspaceProfile>}} WorkspaceProfilesConfig
|
|
19
19
|
* @typedef {{profile: string, repositoryRoot: string, cwd: string, revision: string, sourceIdentity: string}} ResolvedWorkspaceProfile
|
|
20
20
|
* @typedef {{repositoryRoot: string, revision: string, sourceIdentity: string}} GitProvenance
|
|
@@ -52,7 +52,7 @@ export function parseWorkspaceProfiles(value) {
|
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* Resolve a reviewed workspace profile to a validated in-container workspace.
|
|
55
|
-
* @param {{provider: "codex" | "claude" | "opencode", profile?: string}} selection
|
|
55
|
+
* @param {{provider: "codex" | "claude" | "kimi" | "opencode", profile?: string}} selection
|
|
56
56
|
* @param {WorkspaceProfileOperations} [operations]
|
|
57
57
|
* @returns {Promise<ResolvedWorkspaceProfile>}
|
|
58
58
|
*/
|
|
@@ -137,7 +137,7 @@ async function readSourceIdentity(repositoryRoot, cwd) {
|
|
|
137
137
|
/** @param {unknown} value */
|
|
138
138
|
function parseProviders(value) {
|
|
139
139
|
if (!Array.isArray(value) || value.length === 0) throw new Error("Workspace profile configuration is invalid")
|
|
140
|
-
/** @type {("codex" | "claude" | "opencode")[]} */
|
|
140
|
+
/** @type {("codex" | "claude" | "kimi" | "opencode")[]} */
|
|
141
141
|
const providers = []
|
|
142
142
|
for (const entry of value) {
|
|
143
143
|
if (typeof entry !== "string") {
|