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.
- package/CHANGELOG.md +52 -0
- package/README.md +16 -5
- package/TELEGRAM-INGRESS.md +13 -3
- package/bin/isolated-runtime.js +5 -0
- package/bin/kimi-model-broker.js +5 -0
- package/bin/model-broker.js +5 -0
- package/docs/container-runtime.md +22 -0
- package/docs/isolated-provider-runtime.md +232 -0
- package/package.json +10 -3
- package/scripts/provider-shims/front-door.sh.template +18 -0
- package/scripts/verify-package.js +23 -1
- package/src/absolute-deadline.js +94 -0
- package/src/activity-log.js +3 -3
- package/src/cli.js +130 -25
- package/src/delegated-result-admission.js +1 -1
- package/src/docker-api.js +131 -0
- package/src/isolated-runtime-client.js +158 -0
- package/src/isolated-runtime.js +1117 -0
- package/src/isolated-state.js +470 -0
- package/src/isolated-worker.js +144 -0
- 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/model-broker-policy.js +139 -0
- package/src/model-broker.js +313 -0
- package/src/mount-policy.js +28 -0
- package/src/normalized-output.js +68 -0
- package/src/providers/index.js +8 -3
- package/src/providers/kimi.js +165 -0
- package/src/relay-write.js +44 -0
- package/src/telegram-ingress/command.js +4 -4
- package/src/telegram-ingress/config.js +18 -0
- package/src/telegram-ingress/core.js +65 -2
- package/src/telegram-ingress/http.js +2 -1
- package/src/telegram-webhook.js +11 -0
- package/src/workspace-profile.js +3 -3
- package/threadwire.workspace-profiles.json +1 -1
|
@@ -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: () => {
|
package/src/telegram-webhook.js
CHANGED
|
@@ -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
|
})
|
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") {
|