threadwire 0.1.6 → 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.
@@ -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,6 +239,19 @@ 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
243
+ if (
244
+ key.startsWith("THREADWIRE_ISOLATED_RUNTIME_")
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"
251
+ || key === "THREADWIRE_RELAY_WORKER_IMAGE"
252
+ || key === "THREADWIRE_ALLOWED_WORKTREE_ROOTS"
253
+ || key === "THREADWIRE_WORKTREE_VOLUME"
254
+ ) return true
237
255
  return key === "TELEGRAM_BOT_TOKEN" ||
238
256
  key === "THREADWIRE_TELEGRAM_BOT_TOKEN" ||
239
257
  key === "THREADWIRE_WEBHOOK_SECRET" ||
@@ -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,
@@ -29,6 +30,8 @@ import {parseCodeCommand, parseEvidenceCommand} from "./command.js"
29
30
  * workspaceProfileOperations?: import("../workspace-profile.js").WorkspaceProfileOperations,
30
31
  * activity?: Pick<import("../activity-log.js").ActivityLog, "recordWorkspace" | "recordStarted" | "recordSession" | "close">,
31
32
  * evidenceStore?: import("../evidence-store.js").EvidenceStore,
33
+ * isolatedRuntimeClient?: Pick<import("../isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run">,
34
+ * kimiIsolatedRuntimeClient?: Pick<import("../isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run">,
32
35
  * onWorkerFailure?: (message: string) => void,
33
36
  * onWorkerSettled?: () => void
34
37
  * }} IngressDependencies
@@ -87,12 +90,19 @@ export function interpretUpdate(update, config) {
87
90
  * spawn rejects (HTTP 500). Failure after spawn is observed via
88
91
  * onWorkerFailure without empty-swallowing the error.
89
92
  * @param {AcceptedJob} job
90
- * @param {{botToken: string, telegramRequestTimeoutMs?: number, toolMessages?: boolean}} config
93
+ * @param {{botToken: string, telegramRequestTimeoutMs?: number, toolMessages?: boolean, requireCodexIsolation?: boolean}} config
91
94
  * @param {IngressDependencies} [dependencies]
92
95
  * @returns {Promise<void>}
93
96
  */
94
97
  export async function dispatchWorker(job, config, dependencies = {}) {
95
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
+ }
96
106
  const createProviderImpl = dependencies.createProvider ?? createProvider
97
107
  const createFetchTransportImpl = dependencies.createFetchTransport ?? createFetchTransport
98
108
  const createTelegramSenderImpl = dependencies.createTelegramSender ?? createTelegramSender
@@ -107,6 +117,18 @@ export async function dispatchWorker(job, config, dependencies = {}) {
107
117
  {provider: job.provider},
108
118
  dependencies.workspaceProfileOperations
109
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
110
132
 
111
133
  const provider = createProviderImpl(job.provider, [], job.prompt, undefined, providerEnvironment)
112
134
  dependencies.activity?.recordWorkspace(
@@ -136,6 +158,47 @@ export async function dispatchWorker(job, config, dependencies = {}) {
136
158
  try {
137
159
  await evidence?.append("prompt", `prompt\n${job.prompt}\nprovider-stream\n`)
138
160
 
161
+ if (isolatedRuntimeClient !== undefined) {
162
+ evidenceTransferred = true
163
+ try {
164
+ const preflight = earlyKimiPreflight ?? await isolatedRuntimeClient.preflight({
165
+ provider: job.provider,
166
+ profile: workspace.profile,
167
+ repositoryRoot: workspace.repositoryRoot,
168
+ cwd: workspace.cwd,
169
+ providerArguments: []
170
+ })
171
+ const exitCode = await isolatedRuntimeClient.run({
172
+ preflightId: preflight.preflightId,
173
+ prompt: job.prompt,
174
+ providerArguments: [],
175
+ ...(preflight.deadline === undefined ? {} : {deadline: preflight.deadline}),
176
+ onEvent: async (event) => control.accept(event),
177
+ onRecord: async (record) => {
178
+ const id = job.provider === "kimi" ? kimiSessionEnvelopeId(record) : provider.sessionId(record)
179
+ if (id !== undefined) dependencies.activity?.recordSession(provider.name, id)
180
+ },
181
+ ...(job.provider === "kimi" ? {} : {
182
+ onStdoutChunk: (chunk) => evidence?.append("provider-stdout", chunk),
183
+ onStderrChunk: (chunk) => evidence?.append("provider-stderr", chunk)
184
+ })
185
+ })
186
+ if (exitCode !== 0) throw new Error(`Isolated ${job.provider === "kimi" ? "Kimi" : "Codex"} worker exited with status ${exitCode}`)
187
+ await control.close()
188
+ if (evidence !== undefined) {
189
+ await evidence.finalize()
190
+ await sender.send(`Evidence: ${evidence.handle}`)
191
+ }
192
+ return
193
+ } catch (error) {
194
+ await control.close().catch(() => {})
195
+ await evidence?.abort()
196
+ throw error
197
+ } finally {
198
+ dependencies.onWorkerSettled?.()
199
+ }
200
+ }
201
+
139
202
  /** @type {{resolve: () => void, reject: (error: Error) => void, settled: boolean}} */
140
203
  const spawnGate = {
141
204
  settled: false,
@@ -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: () => {
@@ -5,6 +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, kimiIsolatedRuntimeClientFromEnvironment} from "./isolated-runtime-client.js"
8
9
 
9
10
  /**
10
11
  * Start the standalone Threadwire Telegram webhook service.
@@ -34,6 +35,14 @@ export async function startTelegramWebhook(options = {}) {
34
35
  const providerEnvironment = buildProviderEnvironment(
35
36
  options.providerEnvironment ?? environment
36
37
  )
38
+ const isolatedConfigured = typeof environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN === "string"
39
+ && environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN.trim().length > 0
40
+ const isolatedRuntimeClient = options.handlerDependencies?.isolatedRuntimeClient
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)
37
46
  const activity = options.activity ?? options.handlerDependencies?.activity ?? new ActivityLog("/var/lib/threadwire/activity/threadwire.jsonl")
38
47
  const evidenceStore = options.evidenceStore ?? options.handlerDependencies?.evidenceStore ?? await EvidenceStore.open({
39
48
  root: config.evidenceRoot ?? "/var/lib/threadwire/evidence"
@@ -44,6 +53,8 @@ export async function startTelegramWebhook(options = {}) {
44
53
  activity,
45
54
  evidenceStore,
46
55
  providerEnvironment,
56
+ ...(isolatedRuntimeClient === undefined ? {} : {isolatedRuntimeClient}),
57
+ ...(kimiIsolatedRuntimeClient === undefined ? {} : {kimiIsolatedRuntimeClient}),
47
58
  onOperationalError,
48
59
  onWorkerFailure
49
60
  })
@@ -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") {
@@ -5,7 +5,7 @@
5
5
  "container-runtime": {
6
6
  "repositoryRoot": "/workspace/threadwire",
7
7
  "cwd": "/workspace/threadwire",
8
- "providers": ["codex"]
8
+ "providers": ["codex", "kimi"]
9
9
  }
10
10
  }
11
11
  }