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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.1.9 - 2026-07-28
|
|
6
|
+
|
|
7
|
+
- Add native Kimi Code subscription support through official OAuth, a distinct
|
|
8
|
+
narrow Kimi model broker, an immutable pinned worker/auth image set, and a
|
|
9
|
+
mandatory provider-dedicated isolated runtime with no direct fallback.
|
|
10
|
+
- Normalize only trusted Kimi assistant/tool/session envelopes, preserve exact
|
|
11
|
+
native continuation and Telegram topic routing, and suppress thinking, raw
|
|
12
|
+
tool payloads, arbitrary metadata, stderr/stdout, grants, and OAuth material.
|
|
13
|
+
- Bind Kimi grants and authenticated session state to provider, task, session,
|
|
14
|
+
run network, approved alias, and wire model; add adversarial fresh/resume,
|
|
15
|
+
failure, cancellation, refresh, redaction, network, and cross-provider tests.
|
|
16
|
+
|
|
5
17
|
## 0.1.8 - 2026-07-26
|
|
6
18
|
|
|
7
19
|
- Add `threadwire run --transcript <path>` to durably capture the exact normalized
|
package/README.md
CHANGED
|
@@ -60,11 +60,11 @@ Use the local launcher. Install or refresh it after updating Threadwire with `np
|
|
|
60
60
|
-- --model example-model
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
The target always has the form `telegram:<chat-id>:<thread-id>` for topics, or `telegram:<chat-id>` for direct messages (DMs) and ordinary chats. The chat ID is always required and must be a nonzero signed integer. The thread ID is optional and valid only as the explicit topic suffix: when present it must be a positive safe integer, and a DM target simply omits it — a missing thread ID is never an error. `--provider` must be exactly one of `codex`, `claude`, or `opencode`. Malformed or missing provider, chat ID, or thread ID arguments are hard failures (exit code 2) reported through Threadwire's normal error path with fixed messages; they fail before the Telegram token is read, before any transport is created, and before any provider is launched, and the messages never echo the bot token or the untrusted argument value. `--process-number` accepts an explicit positive safe integer for automation; when omitted, Threadwire uses its launcher PID. Every Telegram message is labeled consistently, such as `[P42] …`, including chunked continuations.
|
|
63
|
+
The target always has the form `telegram:<chat-id>:<thread-id>` for topics, or `telegram:<chat-id>` for direct messages (DMs) and ordinary chats. The chat ID is always required and must be a nonzero signed integer. The thread ID is optional and valid only as the explicit topic suffix: when present it must be a positive safe integer, and a DM target simply omits it — a missing thread ID is never an error. `--provider` must be exactly one of `codex`, `claude`, `kimi`, or `opencode`. Malformed or missing provider, chat ID, or thread ID arguments are hard failures (exit code 2) reported through Threadwire's normal error path with fixed messages; they fail before the Telegram token is read, before any transport is created, and before any provider is launched, and the messages never echo the bot token or the untrusted argument value. `--process-number` accepts an explicit positive safe integer for automation; when omitted, Threadwire uses its launcher PID. Every Telegram message is labeled consistently, such as `[P42] …`, including chunked continuations.
|
|
64
64
|
|
|
65
|
-
Pass `--workspace-profile <name>` only when invoking Threadwire inside the container-native runtime, for example with `node bin/threadwire.js run` as shown in [Container runtime](docs/container-runtime.md). A profile selects a reviewed entry from the checked-in [threadwire.workspace-profiles.json](threadwire.workspace-profiles.json). It supplies the absolute in-container repository root, the exact in-container working directory, and the allowlisted providers for that workspace. Threadwire validates the selected profile, rejects malformed or unknown names, verifies the configured workspace exists and is still a healthy Git worktree, and records the selected profile plus immutable revision/worktree identity in the local activity log before provider spawn. The checked-in `container-runtime` default targets `/workspace/threadwire` inside the container-native runtime and
|
|
65
|
+
Pass `--workspace-profile <name>` only when invoking Threadwire inside the container-native runtime, for example with `node bin/threadwire.js run` as shown in [Container runtime](docs/container-runtime.md). A profile selects a reviewed entry from the checked-in [threadwire.workspace-profiles.json](threadwire.workspace-profiles.json). It supplies the absolute in-container repository root, the exact in-container working directory, and the allowlisted providers for that workspace. Threadwire validates the selected profile, rejects malformed or unknown names, verifies the configured workspace exists and is still a healthy Git worktree, and records the selected profile plus immutable revision/worktree identity in the local activity log before provider spawn. The checked-in `container-runtime` default targets `/workspace/threadwire` inside the container-native runtime and allowlists `codex` and isolated `kimi`.
|
|
66
66
|
|
|
67
|
-
Provider tool-start and tool-finish events do not produce Telegram messages by default. Pass the boolean `--tool-messages` flag to opt in, as shown above. When enabled, each tool is one concise, silent status line rather than a pair of noisy start/finish notices: when a tool starts, Threadwire sends `🛠 [P42] Tool: <safe command or tool description>` (for example `🛠 [P42] Tool: command — git status --short`), and when that same tool finishes, it edits the original message in place so it begins `✅` instead — no second completion message is sent, and Telegram edits raise no notification. The command/description shows a safe CLI-style preview with credential-bearing values (tokens, passwords, keys, authorization/cookie forms, credentials in URLs) redacted and control characters stripped; only normalized Codex command-completion output may also be appended to that same edited status as a Telegram-native expandable HTML blockquote with the visible label `Output →`; it is credential-redacted, HTML-escaped, and Unicode-safe bounded. Stderr, reasoning, and every other raw provider result remain excluded. Use `--max-output-length <positive-integer>` to truncate only that preview Unicode-safely; omit the option for unlimited tool detail. A prompt may instead come from `--prompt-file` or piped stdin, and every source rejects blank/whitespace content. Arguments following `--` pass to the selected provider, except output-stream options owned by Threadwire.
|
|
67
|
+
Provider tool-start and tool-finish events do not produce Telegram messages by default. Pass the boolean `--tool-messages` flag to opt in, as shown above. When enabled, each tool is one concise, silent status line rather than a pair of noisy start/finish notices: when a tool starts, Threadwire sends `🛠 [P42] Tool: <safe command or tool description>` (for example `🛠 [P42] Tool: command — git status --short`), and when that same tool finishes, it edits the original message in place so it begins `✅` instead — no second completion message is sent, and Telegram edits raise no notification. The command/description shows a safe CLI-style preview with credential-bearing values (tokens, passwords, keys, authorization/cookie forms, credentials in URLs) redacted and control characters stripped; only normalized Codex command-completion output may also be appended to that same edited status as a Telegram-native expandable HTML blockquote with the visible label `Output →`; it is credential-redacted, HTML-escaped, and Unicode-safe bounded. Stderr, reasoning, and every other raw provider result remain excluded. Use `--max-output-length <positive-integer>` to truncate only that preview Unicode-safely; omit the option for unlimited tool detail. A prompt may instead come from `--prompt-file` or piped stdin, and every source rejects blank/whitespace content. Arguments following `--` pass to the selected provider, except output-stream options owned by Threadwire. Kimi is narrower: it always uses the dedicated isolated subscription runtime and accepts only one server-approved model alias through `--model <alias>` (or `-m <alias>`); Threadwire owns its prompt, output, session, permissions, configuration, tools, skills, plugins, MCP, and directories.
|
|
68
68
|
|
|
69
69
|
Threadwire directly spawns the structurally separate terminal adapters `/opt/data/libexec/threadwire/codex`, `/opt/data/libexec/threadwire/claude`, or `/opt/data/libexec/threadwire/opencode-local-fleet` with an argv array and `shell: false`. These libexec adapters hold the real provider-CLI logic and are never on a worker's `PATH`, so relaying can never recurse back into the front-door shims. Each spawn also sets `THREADWIRE_ACTIVE=1` in the provider child environment. The executable can be overridden per provider with `THREADWIRE_CODEX_BIN`, `THREADWIRE_CLAUDE_BIN`, or `THREADWIRE_OPENCODE_BIN` for staged cutover and rollback. It does not impose a worker timeout. `SIGINT` and `SIGTERM` are forwarded to the worker as explicit cancellation.
|
|
70
70
|
|
package/TELEGRAM-INGRESS.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
# Telegram Ingress for Threadwire
|
|
2
2
|
|
|
3
3
|
Standalone Threadwire webhook service that accepts Telegram Bot API updates.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Codex, Claude, and OpenCode retain the existing worker behavior. Kimi always
|
|
5
|
+
uses its dedicated isolated preflight/run authority with no native fallback;
|
|
6
|
+
a missing Kimi runtime fails before provider or transport construction. Hermes
|
|
7
|
+
is not involved.
|
|
7
8
|
|
|
8
9
|
## Important bot ownership
|
|
9
10
|
|
|
@@ -44,7 +45,7 @@ The process binds to **loopback (`127.0.0.1`) by default**. Expose it only throu
|
|
|
44
45
|
- **Residual restart boundary:** this store is process-local memory only. A process restart clears reservations and completed IDs, so Telegram redelivery after restart can launch work again until IDs are re-observed. This is not durable multi-instance replay protection.
|
|
45
46
|
- Bound concurrent spawned workers (default 4, configurable). When at capacity, reject with `503` without launching. The concurrency slot is released when the background worker settles (success or failure after spawn), or immediately on pre-spawn failure.
|
|
46
47
|
- Both chat and sender allowlists are mandatory and exact; missing IDs fail closed.
|
|
47
|
-
- Command grammar is exact: `/code[optional @bot] <codex|claude|opencode> <nonblank prompt>`.
|
|
48
|
+
- Command grammar is exact: `/code[optional @bot] <codex|claude|kimi|opencode> <nonblank prompt>`.
|
|
48
49
|
- Direct target is derived only from authenticated `message.chat.id` and optional positive `message_thread_id`.
|
|
49
50
|
- DM/chat: `telegram:<chat-id>`
|
|
50
51
|
- Topic: `telegram:<chat-id>:<thread-id>`
|
|
@@ -82,6 +83,8 @@ Optional:
|
|
|
82
83
|
| `THREADWIRE_TELEGRAM_REQUEST_TIMEOUT_MS` | Deadline for each outbound Telegram request, including response parsing (default `30000`, positive safe integer no greater than `2147483647`) |
|
|
83
84
|
| `THREADWIRE_UPDATE_ID_CAPACITY` | Max retained completed `update_id` entries (default `10000`) |
|
|
84
85
|
| `THREADWIRE_UPDATE_ID_TTL_MS` | Retention TTL for completed `update_id` entries in ms (default `86400000`) |
|
|
86
|
+
| `THREADWIRE_KIMI_ISOLATED_RUNTIME_URL` | Dedicated Kimi supervisor URL; Compose defaults to `http://kimi-isolated-runtime:8790` |
|
|
87
|
+
| `THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN` | Dedicated Kimi control token. When absent, `/code kimi` remains recognized but fails closed before launch. |
|
|
85
88
|
|
|
86
89
|
## Run
|
|
87
90
|
|
|
@@ -111,6 +114,7 @@ The wrapper starts the service exported by the importable
|
|
|
111
114
|
/code codex write a hello world program
|
|
112
115
|
/code@your_bot claude explain quantum computing
|
|
113
116
|
/code opencode build a todo app
|
|
117
|
+
/code kimi inspect this repository
|
|
114
118
|
```
|
|
115
119
|
|
|
116
120
|
## Modules
|
|
@@ -199,3 +199,25 @@ Back them up using an operator-approved volume backup process before migration.
|
|
|
199
199
|
`docker compose down --volumes` is destructive and removes the container-owned
|
|
200
200
|
repository and provider state; use it only for an explicitly confirmed teardown.
|
|
201
201
|
Never export or archive mounted secret files with workspace/provider backups.
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
## Kimi subscription containers
|
|
205
|
+
|
|
206
|
+
Kimi is intentionally excluded from the same-UID container-native provider
|
|
207
|
+
launch described above. Never mount `threadwire-kimi-oauth`, its access/refresh
|
|
208
|
+
tokens, or the auth home into the Threadwire runtime, supervisor, worker,
|
|
209
|
+
workspace, or session-state volume. Kimi uses four immutable build targets:
|
|
210
|
+
`kimi-auth`, `kimi-model-broker`, `kimi-relay-worker`, and `isolated-runtime`.
|
|
211
|
+
The first two share UID/GID 10003 and only the OAuth volume; the worker uses
|
|
212
|
+
UID/GID 10002 with only the selected worktree and provider-bound state; the
|
|
213
|
+
root supervisor alone receives the Docker socket. The broker/auth service uses
|
|
214
|
+
Node `22.19.0` and exactly `@moonshot-ai/kimi-code@0.29.2` with the checked npm
|
|
215
|
+
integrity and archive SHA-512 in the Dockerfile.
|
|
216
|
+
|
|
217
|
+
Enable the checked-in override with `--profile kimi` only after the OAuth/model
|
|
218
|
+
operator flow in [Isolated provider runtime](isolated-provider-runtime.md).
|
|
219
|
+
The default workspace profile allowlists Kimi because isolation is mandatory;
|
|
220
|
+
without the dedicated Kimi control token/client, both CLI and Telegram Kimi
|
|
221
|
+
jobs fail closed rather than using the native child path. Keep broker egress
|
|
222
|
+
restricted to official Kimi Code subscription/auth endpoints and do not provide
|
|
223
|
+
a Moonshot API key, custom base URL, Docker socket, or ingress secret.
|
|
@@ -135,3 +135,98 @@ Run `npm run all-checks`, `npm run verify:package`, and
|
|
|
135
135
|
production relay-worker, and fake-upstream image variables documented
|
|
136
136
|
by that script. E2E uses host-volume canaries and fake-upstream authorization
|
|
137
137
|
logs; worker self-report is not the sole assertion.
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
## Native Kimi Code subscription boundary
|
|
141
|
+
|
|
142
|
+
Kimi uses the same proven supervisor mechanics but a distinct protocol and
|
|
143
|
+
credential service. `THREADWIRE_ISOLATED_PROVIDER=kimi` makes the supervisor
|
|
144
|
+
accept only Kimi preflights, select only `THREADWIRE_KIMI_RELAY_WORKER_IMAGE`,
|
|
145
|
+
issue grants only to `THREADWIRE_KIMI_MODEL_BROKER_URL`, and persist
|
|
146
|
+
provider-bound state in the separate Kimi namespace. Codex requests, sessions,
|
|
147
|
+
workers, grants, and volumes cannot be adopted by this service, and the Codex
|
|
148
|
+
broker remains unchanged.
|
|
149
|
+
|
|
150
|
+
The Kimi broker is not a generic credential proxy. It accepts only
|
|
151
|
+
`POST /v1/chat/completions`, validates a closed request schema and one approved
|
|
152
|
+
wire model, substitutes OAuth immediately before the fixed official
|
|
153
|
+
`https://api.kimi.com/coding/v1/chat/completions` request, and streams the
|
|
154
|
+
bounded response. A grant is pending until the inspected worker is ready and is
|
|
155
|
+
bound to provider, task hash, exact resume session (or fresh lineage), run,
|
|
156
|
+
Docker network, approved alias, and wire model. Revocation, timeout, shutdown,
|
|
157
|
+
or cleanup closes its private listener and active sockets.
|
|
158
|
+
|
|
159
|
+
The immutable Kimi worker image pins `@moonshot-ai/kimi-code@0.29.2` and verifies
|
|
160
|
+
the npm archive SHA-512 before installation. It receives no OAuth file, Docker
|
|
161
|
+
socket, Telegram credential, supervisor token, broker admin token, ambient
|
|
162
|
+
provider executable override, external network, or unrelated home directory.
|
|
163
|
+
Its fixed entrypoint owns `--model`, optional exact `--session`, `--prompt`,
|
|
164
|
+
`--output-format stream-json`, and an empty skills directory. Static deny rules,
|
|
165
|
+
workspace/config ancestry checks, a sanitized private home, read-only root,
|
|
166
|
+
and the existing mount/inode checks remain mandatory. There is no native unsafe
|
|
167
|
+
fallback and no `THREADWIRE_KIMI_BIN` control.
|
|
168
|
+
|
|
169
|
+
Only exact pinned stream records are normalized: assistant string content,
|
|
170
|
+
validated tool name/key start and finish, and one exact
|
|
171
|
+
`session.resume_hint`. Thinking, tool arguments/results, retry errors, version
|
|
172
|
+
or arbitrary metadata, malformed records, raw stdout/stderr, synthetic grants,
|
|
173
|
+
and OAuth material are discarded. The worker is the only reader of native Kimi
|
|
174
|
+
stdout and Docker receives only sanitized envelopes and fixed lifecycle events;
|
|
175
|
+
the supervisor returns `rawChunks: []` for every Kimi run.
|
|
176
|
+
|
|
177
|
+
### Operator OAuth and model approval
|
|
178
|
+
|
|
179
|
+
Build and publish the `kimi-auth`, `kimi-model-broker`, `kimi-relay-worker`, and
|
|
180
|
+
existing `isolated-runtime` targets as immutable digests. Set distinct random
|
|
181
|
+
Kimi runtime and broker admin tokens of at least 32 characters, the Kimi image digests, and an allowlist
|
|
182
|
+
such as:
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
export THREADWIRE_ALLOWED_KIMI_MODELS='{"default":{"model":"<verified-account-model>","protocol":"kimi"}}'
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
OAuth writes only the named `threadwire-kimi-oauth` volume at the official
|
|
189
|
+
`credentials/kimi-code.json` location. Stop the broker during interactive auth
|
|
190
|
+
so the CLI and broker cannot race a refresh, then run the opt-in, workspace-free
|
|
191
|
+
auth service:
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
docker compose -f compose.yaml -f compose.relay-write.yaml stop kimi-isolated-runtime kimi-model-broker
|
|
195
|
+
docker compose -f compose.yaml -f compose.relay-write.yaml --profile kimi-auth run --rm kimi-auth login
|
|
196
|
+
docker compose -f compose.yaml -f compose.relay-write.yaml --profile kimi-auth run --rm kimi-auth verify '<verified-account-model>'
|
|
197
|
+
docker compose -f compose.yaml -f compose.relay-write.yaml --profile kimi up --detach --wait
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Complete the official device flow directly in the operator terminal. Do not
|
|
201
|
+
copy device codes into tickets, chat, logs, environment files, or Threadwire.
|
|
202
|
+
The auth service has no workspace, ingress network, Docker socket, Threadwire
|
|
203
|
+
control token, or Telegram credential and uses Docker logging driver `none`.
|
|
204
|
+
`verify <account-model>` discards native output and prints only a fixed
|
|
205
|
+
success/failure; use it to confirm each exact account model value before mapping
|
|
206
|
+
that value to a server-approved Threadwire alias.
|
|
207
|
+
API keys, Moonshot platform mode, custom base URLs, discovery by ordinary jobs,
|
|
208
|
+
and arbitrary aliases are unsupported.
|
|
209
|
+
|
|
210
|
+
For logout or account rotation, stop the Kimi runtime and broker, run
|
|
211
|
+
`--profile kimi-auth run --rm kimi-auth logout` or `login`, verify approved
|
|
212
|
+
models, then restart the Kimi profile. The broker validates strict owner/mode,
|
|
213
|
+
regular-file, link-count, schema, and size constraints; refreshes are serialized
|
|
214
|
+
and atomically persisted, and corruption, symlinks, revocation, or unreadable
|
|
215
|
+
state makes health and preflight fail closed with redacted errors.
|
|
216
|
+
|
|
217
|
+
### Kimi validation and rollback
|
|
218
|
+
|
|
219
|
+
Run `npm run test:kimi-isolated-runtime-e2e` from an approved Docker supervisor
|
|
220
|
+
boundary after supplying the documented immutable image variables. A real
|
|
221
|
+
account smoke must then cover fresh execution, capture of exactly one native
|
|
222
|
+
session ID, exact resume with that ID, an approved non-default alias if used,
|
|
223
|
+
Telegram DM/topic routing, cancellation, and confirmation that service logs and
|
|
224
|
+
activity/evidence contain no OAuth token, device code, raw Kimi record, tool
|
|
225
|
+
payload, or synthetic grant.
|
|
226
|
+
|
|
227
|
+
Rollback by stopping `kimi-isolated-runtime` and `kimi-model-broker`, removing
|
|
228
|
+
the `kimi` Compose profile/control variables, and reverting the Threadwire image.
|
|
229
|
+
Kimi commands then fail closed; Codex behavior is unaffected. Preserve the OAuth
|
|
230
|
+
and Kimi state volumes for a recoverable rollback. Remove them only as an
|
|
231
|
+
explicit credential/session teardown after OAuth revocation; never archive them
|
|
232
|
+
with workspace backups.
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "threadwire",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Stream Codex, Claude, and OpenCode worker progress to an explicit Telegram destination",
|
|
3
|
+
"version": "0.1.9",
|
|
4
|
+
"description": "Stream Codex, Claude, Kimi Code, and OpenCode worker progress to an explicit Telegram destination",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
7
7
|
"claude",
|
|
8
8
|
"cli",
|
|
9
9
|
"codex",
|
|
10
|
+
"kimi",
|
|
10
11
|
"opencode",
|
|
11
12
|
"telegram"
|
|
12
13
|
],
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
],
|
|
25
26
|
"bin": {
|
|
26
27
|
"threadwire-isolated-runtime": "bin/isolated-runtime.js",
|
|
28
|
+
"threadwire-kimi-model-broker": "bin/kimi-model-broker.js",
|
|
27
29
|
"threadwire-model-broker": "bin/model-broker.js",
|
|
28
30
|
"threadwire": "bin/threadwire.js",
|
|
29
31
|
"threadwire-telegram-webhook": "bin/telegram-webhook.js"
|
|
@@ -54,7 +56,8 @@
|
|
|
54
56
|
"test:container-e2e": "sh docker/test/production-e2e.sh",
|
|
55
57
|
"test:isolated-runtime-e2e": "sh docker/test/isolated-runtime-e2e.sh",
|
|
56
58
|
"typecheck": "tsc --noEmit",
|
|
57
|
-
"verify:package": "node scripts/verify-package.js"
|
|
59
|
+
"verify:package": "node scripts/verify-package.js",
|
|
60
|
+
"test:kimi-isolated-runtime-e2e": "sh docker/test/kimi-isolated-runtime-e2e.sh"
|
|
58
61
|
},
|
|
59
62
|
"devDependencies": {
|
|
60
63
|
"@eslint/js": "10.0.1",
|
|
@@ -13,6 +13,7 @@ const npmEnvironment = {
|
|
|
13
13
|
NPM_CONFIG_DRY_RUN: "false",
|
|
14
14
|
npm_config_dry_run: "false"
|
|
15
15
|
}
|
|
16
|
+
const packageTmpdir = process.env.THREADWIRE_PACKAGE_TMPDIR ?? tmpdir()
|
|
16
17
|
const EXPECTED_FILES = [
|
|
17
18
|
"CHANGELOG.md",
|
|
18
19
|
"LICENSE",
|
|
@@ -21,6 +22,7 @@ const EXPECTED_FILES = [
|
|
|
21
22
|
"bin/telegram-webhook.js",
|
|
22
23
|
"bin/threadwire.js",
|
|
23
24
|
"bin/isolated-runtime.js",
|
|
25
|
+
"bin/kimi-model-broker.js",
|
|
24
26
|
"bin/model-broker.js",
|
|
25
27
|
"docs/card-10520-plan.md",
|
|
26
28
|
"docs/container-runtime.md",
|
|
@@ -48,6 +50,9 @@ const EXPECTED_FILES = [
|
|
|
48
50
|
"src/isolated-runtime.js",
|
|
49
51
|
"src/isolated-state.js",
|
|
50
52
|
"src/isolated-worker.js",
|
|
53
|
+
"src/kimi-model-broker-policy.js",
|
|
54
|
+
"src/kimi-model-broker.js",
|
|
55
|
+
"src/kimi-oauth-store.js",
|
|
51
56
|
"src/model-broker-policy.js",
|
|
52
57
|
"src/model-broker.js",
|
|
53
58
|
"src/mount-policy.js",
|
|
@@ -58,6 +63,7 @@ const EXPECTED_FILES = [
|
|
|
58
63
|
"src/providers/codex.js",
|
|
59
64
|
"src/providers/executable.js",
|
|
60
65
|
"src/providers/index.js",
|
|
66
|
+
"src/providers/kimi.js",
|
|
61
67
|
"src/providers/opencode.js",
|
|
62
68
|
"src/relay.js",
|
|
63
69
|
"src/relay-write.js",
|
|
@@ -78,7 +84,7 @@ const EXPECTED_FILES = [
|
|
|
78
84
|
|
|
79
85
|
/** @returns {Promise<void>} */
|
|
80
86
|
async function main() {
|
|
81
|
-
const temporaryDirectory = await mkdtemp(join(
|
|
87
|
+
const temporaryDirectory = await mkdtemp(join(packageTmpdir, "threadwire-package-"))
|
|
82
88
|
const cacheDirectory = join(temporaryDirectory, "npm-cache")
|
|
83
89
|
|
|
84
90
|
try {
|
|
@@ -133,6 +139,7 @@ async function main() {
|
|
|
133
139
|
], {
|
|
134
140
|
env: {
|
|
135
141
|
PATH: process.env.PATH,
|
|
142
|
+
HOME: process.env.HOME,
|
|
136
143
|
NPM_CONFIG_DRY_RUN: "false",
|
|
137
144
|
npm_config_dry_run: "false"
|
|
138
145
|
},
|
package/src/activity-log.js
CHANGED
|
@@ -24,13 +24,13 @@ export class ActivityLog {
|
|
|
24
24
|
this.write({type: "workspace-selected", profile, repositoryRoot, revision, sourceIdentity})
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/** @param {"codex" | "claude" | "opencode"} provider @param {number} pid */
|
|
27
|
+
/** @param {"codex" | "claude" | "kimi" | "opencode"} provider @param {number} pid */
|
|
28
28
|
recordStarted(provider, pid) {
|
|
29
29
|
if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error("Provider child PID is unavailable")
|
|
30
30
|
this.write({type: "provider-started", provider, pid})
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** @param {"codex" | "claude" | "opencode"} provider @param {string} sessionId */
|
|
33
|
+
/** @param {"codex" | "claude" | "kimi" | "opencode"} provider @param {string} sessionId */
|
|
34
34
|
recordSession(provider, sessionId) {
|
|
35
35
|
if (!SESSION_ID_PATTERN.test(sessionId)) return
|
|
36
36
|
this.write({type: "session-available", provider, sessionId})
|
|
@@ -42,7 +42,7 @@ export class ActivityLog {
|
|
|
42
42
|
closeSync(this.fileDescriptor)
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/** @param {{type: "workspace-selected", profile: string, repositoryRoot: string, revision: string, sourceIdentity: string} | {type: "provider-started", provider: "codex" | "claude" | "opencode", pid: number} | {type: "session-available", provider: "codex" | "claude" | "opencode", sessionId: string}} fact */
|
|
45
|
+
/** @param {{type: "workspace-selected", profile: string, repositoryRoot: string, revision: string, sourceIdentity: string} | {type: "provider-started", provider: "codex" | "claude" | "kimi" | "opencode", pid: number} | {type: "session-available", provider: "codex" | "claude" | "kimi" | "opencode", sessionId: string}} fact */
|
|
46
46
|
write(fact) {
|
|
47
47
|
if (this.closed) throw new Error("Activity log is closed")
|
|
48
48
|
writeSync(this.fileDescriptor, `${JSON.stringify(fact)}\n`)
|
package/src/cli.js
CHANGED
|
@@ -15,12 +15,13 @@ import {resolveWorkspaceProfile} from "./workspace-profile.js"
|
|
|
15
15
|
import {WorkerControl} from "./worker-control.js"
|
|
16
16
|
import {EvidenceStore} from "./evidence-store.js"
|
|
17
17
|
import {ContextBudgetMetrics} from "./context-budget-metrics.js"
|
|
18
|
-
import {isolatedRuntimeClientFromEnvironment} from "./isolated-runtime-client.js"
|
|
18
|
+
import {isolatedRuntimeClientFromEnvironment, kimiIsolatedRuntimeClientFromEnvironment} from "./isolated-runtime-client.js"
|
|
19
|
+
import {kimiSessionEnvelopeId} from "./providers/kimi.js"
|
|
19
20
|
import {validateRelayWriteProviderArguments} from "./relay-write.js"
|
|
20
21
|
import {abortable} from "./absolute-deadline.js"
|
|
21
22
|
import {NormalizedOutput} from "./normalized-output.js"
|
|
22
23
|
|
|
23
|
-
const HELP = `Usage: threadwire run --provider <codex|claude|opencode> --target telegram:<chat-id> | telegram:<chat-id>:<thread-id>
|
|
24
|
+
const HELP = `Usage: threadwire run --provider <codex|claude|kimi|opencode> --target telegram:<chat-id> | telegram:<chat-id>:<thread-id>
|
|
24
25
|
[--process-number <positive-integer>] [--cwd <directory>]
|
|
25
26
|
[--workspace-profile <name>]
|
|
26
27
|
[--relay-write]
|
|
@@ -45,8 +46,9 @@ const HELP = `Usage: threadwire run --provider <codex|claude|opencode> --target
|
|
|
45
46
|
* evidenceStore?: EvidenceStore,
|
|
46
47
|
* evidenceOwnerScope?: {destinationId: string, runId: string},
|
|
47
48
|
* workerControlOptions?: Pick<ConstructorParameters<typeof WorkerControl>[0], "queueOptions">,
|
|
48
|
-
* workspaceProfileOperations?: import("./workspace-profile.js").WorkspaceProfileOperations
|
|
49
|
-
* isolatedRuntimeClient?: Pick<import("./isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run"
|
|
49
|
+
* workspaceProfileOperations?: import("./workspace-profile.js").WorkspaceProfileOperations,
|
|
50
|
+
* isolatedRuntimeClient?: Pick<import("./isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run">,
|
|
51
|
+
* kimiIsolatedRuntimeClient?: Pick<import("./isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run">
|
|
50
52
|
* }} MainDependencies
|
|
51
53
|
*/
|
|
52
54
|
|
|
@@ -112,6 +114,7 @@ export function parseArguments(arguments_) {
|
|
|
112
114
|
throw new Error("--transcript and --activity-log must resolve to different paths")
|
|
113
115
|
}
|
|
114
116
|
if (parsed.relayWrite && parsed.workspaceProfile === undefined) throw new Error("--relay-write requires --workspace-profile")
|
|
117
|
+
if (parsed.provider === "kimi" && parsed.workspaceProfile === undefined) throw new Error("Kimi requires --workspace-profile")
|
|
115
118
|
if (parsed.relayWrite && parsed.provider !== "codex") throw new Error("--relay-write is only supported for codex")
|
|
116
119
|
return /** @type {ParsedArguments} */ (parsed)
|
|
117
120
|
}
|
|
@@ -199,15 +202,18 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
199
202
|
const resolvedWorkspace = parsed.workspaceProfile === undefined
|
|
200
203
|
? undefined
|
|
201
204
|
: await resolveWorkspaceProfile(
|
|
202
|
-
{provider: /** @type {"codex" | "claude" | "opencode"} */ (parsed.provider), profile: parsed.workspaceProfile},
|
|
205
|
+
{provider: /** @type {"codex" | "claude" | "kimi" | "opencode"} */ (parsed.provider), profile: parsed.workspaceProfile},
|
|
203
206
|
dependencies.workspaceProfileOperations
|
|
204
207
|
)
|
|
205
208
|
if (validateOnly) return 0
|
|
206
|
-
const
|
|
207
|
-
|
|
209
|
+
const usesIsolatedRuntime = parsed.relayWrite || parsed.provider === "kimi"
|
|
210
|
+
const isolatedRuntimeClient = usesIsolatedRuntime
|
|
211
|
+
? parsed.provider === "kimi"
|
|
212
|
+
? dependencies.kimiIsolatedRuntimeClient ?? kimiIsolatedRuntimeClientFromEnvironment(sourceEnvironment)
|
|
213
|
+
: dependencies.isolatedRuntimeClient ?? isolatedRuntimeClientFromEnvironment(sourceEnvironment)
|
|
208
214
|
: undefined
|
|
209
|
-
const isolatedPreflight =
|
|
210
|
-
? await isolatedRuntimeClient
|
|
215
|
+
const isolatedPreflight = usesIsolatedRuntime
|
|
216
|
+
? await /** @type {NonNullable<typeof isolatedRuntimeClient>} */ (isolatedRuntimeClient).preflight({
|
|
211
217
|
provider: parsed.provider,
|
|
212
218
|
profile: /** @type {string} */ (parsed.workspaceProfile),
|
|
213
219
|
repositoryRoot: /** @type {import("./workspace-profile.js").ResolvedWorkspaceProfile} */ (resolvedWorkspace).repositoryRoot,
|
|
@@ -257,7 +263,7 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
257
263
|
parsed.providerArguments,
|
|
258
264
|
prompt,
|
|
259
265
|
parsed.resumeSession,
|
|
260
|
-
|
|
266
|
+
usesIsolatedRuntime ? {} : providerEnvironment
|
|
261
267
|
)
|
|
262
268
|
if (parsed.resumeSession !== undefined) admission.setContinuationHandle(parsed.resumeSession)
|
|
263
269
|
if (parsed.workspaceProfile !== undefined) {
|
|
@@ -303,7 +309,7 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
303
309
|
},
|
|
304
310
|
onRecord: (record) => {
|
|
305
311
|
metrics.recordParsedProviderRecord("provider_stdout")
|
|
306
|
-
const id = provider.sessionId(record)
|
|
312
|
+
const id = parsed.provider === "kimi" ? kimiSessionEnvelopeId(record) : provider.sessionId(record)
|
|
307
313
|
if (id !== undefined) {
|
|
308
314
|
admission.setContinuationHandle(id)
|
|
309
315
|
activity?.recordSession(provider.name, id)
|
|
@@ -322,7 +328,7 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
322
328
|
return evidence.append("provider-stderr", chunk).then(() => { evidencePayloadBytes += chunk.length })
|
|
323
329
|
}
|
|
324
330
|
}
|
|
325
|
-
const exitCode =
|
|
331
|
+
const exitCode = usesIsolatedRuntime
|
|
326
332
|
? await /** @type {NonNullable<typeof isolatedRuntimeClient>} */ (isolatedRuntimeClient).run({
|
|
327
333
|
preflightId: /** @type {{preflightId: string}} */ (isolatedPreflight).preflightId,
|
|
328
334
|
prompt,
|
|
@@ -330,8 +336,8 @@ export async function main(arguments_, dependencies = {}) {
|
|
|
330
336
|
...(parsed.resumeSession === undefined ? {} : {resumeSession: parsed.resumeSession}),
|
|
331
337
|
onEvent: workerOptions.onEvent,
|
|
332
338
|
onRecord: /** @type {NonNullable<typeof workerOptions.onRecord>} */ (workerOptions.onRecord),
|
|
333
|
-
...(workerOptions.onStdoutChunk === undefined ? {} : {onStdoutChunk: workerOptions.onStdoutChunk}),
|
|
334
|
-
...(workerOptions.onStderrChunk === undefined ? {} : {onStderrChunk: workerOptions.onStderrChunk}),
|
|
339
|
+
...(parsed.provider === "kimi" || workerOptions.onStdoutChunk === undefined ? {} : {onStdoutChunk: workerOptions.onStdoutChunk}),
|
|
340
|
+
...(parsed.provider === "kimi" || workerOptions.onStderrChunk === undefined ? {} : {onStderrChunk: workerOptions.onStderrChunk}),
|
|
335
341
|
...(launchDeadline === undefined ? {} : {deadline: launchDeadline})
|
|
336
342
|
})
|
|
337
343
|
: await (dependencies.workerRunner ?? runWorker)(workerOptions)
|
|
@@ -121,6 +121,15 @@ export function isolatedRuntimeClientFromEnvironment(environment) {
|
|
|
121
121
|
})
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/** @param {NodeJS.ProcessEnv} environment */
|
|
125
|
+
export function kimiIsolatedRuntimeClientFromEnvironment(environment) {
|
|
126
|
+
return new IsolatedRuntimeClient({
|
|
127
|
+
url: nonempty(environment.THREADWIRE_KIMI_ISOLATED_RUNTIME_URL, "THREADWIRE_KIMI_ISOLATED_RUNTIME_URL"),
|
|
128
|
+
controlToken: nonempty(environment.THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN, "THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN"),
|
|
129
|
+
timeoutMs: environment.THREADWIRE_ISOLATED_RUNTIME_CLIENT_TIMEOUT_MS
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
124
133
|
function normalizedUrl(value) {
|
|
125
134
|
const url = new URL(value)
|
|
126
135
|
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("THREADWIRE_ISOLATED_RUNTIME_URL must use HTTP or HTTPS")
|