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 ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog
2
+
3
+ ## Unreleased
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
+
17
+ ## 0.1.8 - 2026-07-26
18
+
19
+ - Add `threadwire run --transcript <path>` to durably capture the exact normalized
20
+ stdout JSONL protocol in a private, exclusive file while keeping activity logs
21
+ and scoped raw evidence artifacts separate.
22
+ - Add an opt-in, fail-closed isolated Codex write runtime. A root-owned
23
+ supervisor launches one worktree-only worker per task while a separate narrow
24
+ model broker retains upstream credentials. Fresh/resume, secret rotation,
25
+ filesystem/network/socket escapes, configuration overrides and cleanup are
26
+ covered by adversarial Docker E2E.
27
+ - Bind broker grants to dedicated trusted run-network listeners and an exact
28
+ allowlisted model/request schema; add stable worktree ancestry checks,
29
+ task-private persistent resume state, contained nested cwd and structured
30
+ Codex arguments. The Compose deployment is now an explicit opt-in override.
31
+ - Allocate authenticated, uniquely labeled state per fresh Codex lineage;
32
+ reject volume adoption and nested worktree mounts; use Docker's structured
33
+ host-bind API; and require an explicit supervisor-side model allowlist.
34
+ - Bound and eagerly expire preflight capabilities, serialize writable lineage
35
+ resumes, and automatically reconcile and garbage-collect authenticated state
36
+ volumes with restart-safe cleanup retry.
37
+ - Bound active runs and Docker/broker operations through cleanup, reserve state
38
+ before awaited revalidation, autonomously close expired grant listeners, and
39
+ reconcile exactly owned workers and networks across every container state.
40
+ - Abort accepted broker work and caller-disconnected runs at one absolute
41
+ deadline; make crash cleanup authority an fsync'd authenticated full run
42
+ identity rather than labels; and durably fsync registry rename evidence.
43
+ - Carry one launch deadline from preflight through prompt/secret/evidence/
44
+ Telegram setup, worker execution and cleanup; make supervisor and broker
45
+ shutdown abort control uploads and active work; move Docker GC outside the
46
+ registry transaction; and stream-cap broker/runtime responses.
47
+ - Gate worker start on one-shot activation of a verified pending broker grant
48
+ and address its listener by the inspected per-run IP, removing dynamic
49
+ network-alias readiness races.
50
+ - Route relay-profile Telegram Codex jobs exclusively through the isolated
51
+ runtime, keep validate-only launches local, preserve bounded stdout/stderr
52
+ evidence channels, and protect live lineages from concurrent reconciliation.
package/README.md CHANGED
@@ -7,6 +7,13 @@ non-root Codex provider, Docker secrets, loopback TLS proxy, verification, and
7
7
  rollback path, see [Container runtime](docs/container-runtime.md). The legacy
8
8
  host launcher below remains supported and is separate from that deployment.
9
9
 
10
+ Relay-originated Codex writes are explicit and default off. `--relay-write`
11
+ requires a reviewed `--workspace-profile` plus the root-owned isolated runtime
12
+ and credential broker. The worker gets one writable task worktree, no upstream
13
+ or Git credential, and no route except its run-scoped broker. Missing
14
+ prerequisites fail before prompt or credential reads with no native fallback.
15
+ See [Isolated provider runtime](docs/isolated-provider-runtime.md).
16
+
10
17
  ## Run with npx
11
18
 
12
19
  Threadwire requires Node.js 22 or newer. Run the published CLI without a global
@@ -53,11 +60,11 @@ Use the local launcher. Install or refresh it after updating Threadwire with `np
53
60
  -- --model example-model
54
61
  ```
55
62
 
56
- 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.
57
64
 
58
- 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 currently allowlists only `codex`.
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`.
59
66
 
60
- 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.
61
68
 
62
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.
63
70
 
@@ -67,13 +74,17 @@ Threadwire directly spawns the structurally separate terminal adapters `/opt/dat
67
74
 
68
75
  The relay contract: the caller supplies `THREADWIRE_TARGET` (`telegram:<chat-id>` or `telegram:<chat-id>:<thread-id>`) and, optionally, `THREADWIRE_PROCESS_NUMBER`. The prompt arrives on a piped stdin; all native provider flags pass through after `--`. A positional prompt is never parsed. A missing `THREADWIRE_TARGET` fails closed (exit code 2) and a malformed one is rejected by the launcher's token-free preflight — the shim never invents a default destination and never silently downgrades a requested relay to a direct run. A shim relays only for a worker mode (`codex exec …`, `opencode-local-fleet run …`, or any non-interactive `claude …`); it drops the worker subcommand before relaying and Threadwire re-adds it around the prompt. Version and help queries, non-worker subcommands, interactive TTY sessions, and any invocation already inside a relayed run (`THREADWIRE_ACTIVE=1`, so nested provider workers stay local/native) run the real CLI directly through the libexec adapter.
69
76
 
70
- Pass `--resume-session <exact-provider-session-id>` to continue a stored Codex, Claude, or OpenCode session. Threadwire preserves the provider's native resume ordering and streaming options and never uses implicit latest-session behavior. To request a local JSONL activity log, pass `--activity-log <path>`. It writes only safe local facts: the selected workspace profile plus immutable workspace provenance, provider-started facts (`provider`, child `pid`), and session-available facts (`provider`, native `sessionId`). Raw events, reasoning, tool data, stderr, credentials, prompts, and Telegram token data are excluded. This activity log is local-only and is never enqueued for Telegram delivery.
77
+ Pass `--resume-session <exact-provider-session-id>` to continue a stored Codex, Claude, or OpenCode session. Threadwire preserves the provider's native resume ordering and streaming options and never uses implicit latest-session behavior.
78
+
79
+ Pass `--transcript <path>` to capture the exact normalized, caller-facing JSONL protocol written to run stdout. The file is created privately (mode `0600`), must not already exist, and contains the same records in the same byte order as stdout. It never contains provider JSONL, reasoning, tool payloads, prompts, stderr, or credentials.
80
+
81
+ `--activity-log <path>` is different: it retains its append contract and writes only minimal safe lifecycle facts—the selected workspace profile and immutable provenance, provider start (`provider`, child `pid`), and available native session ID. It is local-only and never enters Telegram or normalized stdout.
71
82
 
72
83
  Provider stdout is decoded as JSONL with a finite 1 MiB UTF-8 byte limit for each pending unterminated record. Exceeding it rejects the run, removes listeners and signal handlers, and terminates the provider without parsing or rendering the oversized content.
73
84
 
74
85
  For normal `threadwire run` execution, Threadwire stdout is the parent-model admission plane. It emits exactly one versioned terminal `delegated_result` JSON object and no progress records. The envelope contains a bounded, credential-redacted, control-normalized final conclusion when one was produced, structured terminal state and exit code, and the exact bounded provider session handle when available. Its closed schema can also represent concise blocker/decision requests, opaque artifact handles, commit/URL references, and a concise validation summary; current provider adapters do not infer these optional fields from prose. Unknown, nested, wrongly typed, cardinality-invalid, or oversized admission candidates fail closed. See [Delegated-result protocol](docs/delegated-result-protocol.md) for the complete schema, limits, and caller obligations.
75
86
 
76
- Set the trusted operator setting `THREADWIRE_EVIDENCE_ROOT` for standalone runs to persist the delegated prompt and raw provider stdout/stderr as a scoped file-backed artifact; Telegram ingress uses its dedicated evidence volume automatically. The parent or owning Telegram topic receives only an opaque bearer handle. Retrieve later with `threadwire evidence read` or the authenticated `/evidence` Telegram command and an explicit bounded selector. See [Evidence artifacts](docs/evidence-artifacts.md) for permissions, quotas, retention, recovery, authorization, redaction, privacy, and cleanup behavior.
87
+ Scoped raw evidence artifacts are separate from both files above. Set the trusted operator setting `THREADWIRE_EVIDENCE_ROOT` for standalone runs to persist the delegated prompt and raw provider stdout/stderr as a scoped file-backed artifact; Telegram ingress uses its dedicated evidence volume automatically. The parent or owning Telegram topic receives only an opaque bearer handle. Retrieve later with `threadwire evidence read` or the authenticated `/evidence` Telegram command and an explicit bounded selector. See [Evidence artifacts](docs/evidence-artifacts.md) for permissions, quotas, retention, recovery, authorization, redaction, privacy, and cleanup behavior.
77
88
 
78
89
  Telegram is the separate user-visible progress plane. Lifecycle notices, diagnostics, tool activity, and assistant streaming continue there independently; `--tool-messages` affects only Telegram. Execution-environment details, provider records and streams, lifecycle progress, diagnostics, tool names/arguments/details/output, stderr, prompts, narration/reasoning, and full transcripts never enter stdout. Threadwire's bounded error diagnostic remains on stderr. Help output is the sole non-run, human-readable stdout mode. This default is an intentional pre-1.0 protocol break from the former mixed Hermes JSONL stream.
79
90
 
@@ -1,6 +1,10 @@
1
1
  # Telegram Ingress for Threadwire
2
2
 
3
- Standalone Threadwire webhook service that accepts Telegram Bot API updates and launches Threadwire's existing worker path (`createProvider` → Telegram transport/sender → `NoticeQueue` → `Relay` → `runWorker`). Hermes is not involved.
3
+ Standalone Threadwire webhook service that accepts Telegram Bot API updates.
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.
4
8
 
5
9
  ## Important bot ownership
6
10
 
@@ -41,7 +45,7 @@ The process binds to **loopback (`127.0.0.1`) by default**. Expose it only throu
41
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.
42
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.
43
47
  - Both chat and sender allowlists are mandatory and exact; missing IDs fail closed.
44
- - 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>`.
45
49
  - Direct target is derived only from authenticated `message.chat.id` and optional positive `message_thread_id`.
46
50
  - DM/chat: `telegram:<chat-id>`
47
51
  - Topic: `telegram:<chat-id>:<thread-id>`
@@ -49,7 +53,10 @@ The process binds to **loopback (`127.0.0.1`) by default**. Expose it only throu
49
53
  - Provider child env strips Telegram tokens, webhook secret, allowlists, bind/port, concurrency, tool-message, and update-guard service config. `runWorker` also strips Telegram tokens.
50
54
  - No logging of headers, bodies, prompts, or secrets.
51
55
  - Webhook secret must be at least 32 characters after trim.
52
- - Accepted work returns `202` only after the worker child has actually spawned (`onSpawn`); process completion alone is never treated as dispatch success.
56
+ - Native work returns `202` only after the worker child has actually spawned
57
+ (`onSpawn`). Isolated Codex work returns `202` only after the authenticated
58
+ isolated run completes successfully; preflight alone is never treated as
59
+ dispatch success.
53
60
  - Failure before spawn rejects the request with `500` and is reported once via the operational error reporter.
54
61
  - Failure after spawn is reported once via the worker-failure reporter (not double-reported through both channels in the real entrypoint wiring).
55
62
  - Authenticated irrelevant updates return `200`.
@@ -76,6 +83,8 @@ Optional:
76
83
  | `THREADWIRE_TELEGRAM_REQUEST_TIMEOUT_MS` | Deadline for each outbound Telegram request, including response parsing (default `30000`, positive safe integer no greater than `2147483647`) |
77
84
  | `THREADWIRE_UPDATE_ID_CAPACITY` | Max retained completed `update_id` entries (default `10000`) |
78
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. |
79
88
 
80
89
  ## Run
81
90
 
@@ -105,6 +114,7 @@ The wrapper starts the service exported by the importable
105
114
  /code codex write a hello world program
106
115
  /code@your_bot claude explain quantum computing
107
116
  /code opencode build a todo app
117
+ /code kimi inspect this repository
108
118
  ```
109
119
 
110
120
  ## Modules
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {startIsolatedRuntime} from "../src/isolated-runtime.js"
4
+
5
+ await startIsolatedRuntime()
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {startKimiModelBroker} from "../src/kimi-model-broker.js"
4
+
5
+ await startKimiModelBroker()
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {startModelBroker} from "../src/model-broker.js"
4
+
5
+ await startModelBroker()
@@ -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.
@@ -0,0 +1,232 @@
1
+ # Isolated provider runtime
2
+
3
+ Relay-originated Codex writes are disabled unless the caller supplies both
4
+ `--relay-write` and a reviewed `--workspace-profile`. The write path never
5
+ falls back to the native provider launcher.
6
+ When the Telegram service is configured with isolated-runtime authority, Codex
7
+ webhook jobs use the same preflight/run boundary exclusively; an unavailable
8
+ runtime fails the job rather than invoking the native runner. Raw provider
9
+ stdout and stderr are returned as separately identified, bounded chunks for
10
+ the existing evidence callbacks.
11
+
12
+ ## Security boundary
13
+
14
+ Telegram ingress has neither provider secrets nor container-runtime authority.
15
+ The root-owned isolated-runtime supervisor has Docker authority and a read-only
16
+ view of allowlisted worktree roots, but no provider credential. The model
17
+ broker alone mounts the upstream credential. Each grant creates a listener
18
+ bound to the broker's trusted per-run network interface. That listener accepts
19
+ one token, one run/network lineage, one allowlisted model, and a closed
20
+ `/v1/responses` schema; it is not a generic HTTP proxy.
21
+ The listener remains non-authorizing while the supervisor creates and verifies
22
+ the worker. The supervisor then activates it exactly once immediately before
23
+ worker start. Workers receive the inspected per-run broker IP directly, so
24
+ launch does not depend on asynchronous container-DNS alias propagation.
25
+
26
+ For each fresh or explicit-resume run the supervisor creates a unique Docker
27
+ internal network, attaches the broker, and launches an immutable worker. The
28
+ worker has a read-only root, all capabilities dropped, no-new-privileges,
29
+ container-local tmp/run, resource limits, and exactly one writable
30
+ worktree mount. It has no host home, common Git directory, sibling worktree,
31
+ host temp, shared provider state, secret, Docker/SSH socket, or external route. Its
32
+ short-lived broker grant is revoked during cleanup.
33
+ The grant owns one absolute deadline and every accepted socket. Expiry,
34
+ explicit revoke, credential reload, or broker shutdown aborts an incomplete
35
+ upload or upstream request, destroys accepted sockets, and closes the listener;
36
+ authorization is checked again after the bounded request body is acquired.
37
+
38
+ Codex state lives in a unique, random, immutable-label Docker volume for each
39
+ fresh lineage, never a host or common Codex home. An HMAC-authenticated,
40
+ root-private supervisor registry maps returned session IDs to that exact
41
+ volume. Resume revalidates the volume's owner, namespace, task, and lineage
42
+ labels before every mount. A collision, unlabeled or differently labeled
43
+ preexisting volume, sibling,
44
+ missing, expired, or lost state fails before worker creation.
45
+ Only one worker may mount a lineage at a time; the supervisor holds an atomic
46
+ reservation from preflight consumption through worker cleanup. Expired aliases
47
+ are removed by authenticated periodic GC. The registry retains lineage
48
+ ownership until Docker confirms deletion, retries cleanup failures, and
49
+ reconciles exactly labelled orphan volumes on startup while preserving volumes
50
+ referenced by in-process runs. On restart, exactly owned created, running, or
51
+ exited orphan workers and their networks are removed before volume GC retries.
52
+ An fsync'd, HMAC-authenticated run record binds the generated names, pinned
53
+ image digest, entrypoint policy, private network, state volume, and exact
54
+ worktree mount source/options. Reconciliation inspects that full identity and
55
+ network endpoint membership; forgeable labels or any mismatched name, image,
56
+ mount, option, command, or endpoint are never deletion authority.
57
+
58
+ Preflight verifies Docker, the immutable image, broker readiness, the
59
+ allowlisted non-symlink worktree with no nested mounts, stable device/inode ancestry, and mount policy before prompt, Telegram
60
+ token, dotenv, evidence, resume state, executable override, or provider
61
+ credential reads. The ancestry is revalidated immediately before container
62
+ creation and start, and the immutable entrypoint verifies the mounted worktree
63
+ and reviewed nested cwd before Codex. Only `--model` and `--color` are accepted;
64
+ they are transported as a JSON array and passed exactly once without shell
65
+ splitting. All other provider arguments are rejected before launch.
66
+ Authenticated preflight admission has strict endpoint/field size limits,
67
+ global and per-task caps, and an eager timer sweep, so abandoned capabilities
68
+ expire without requiring a matching `/run`.
69
+ Consumed capabilities enter a separate global/per-task active-run admission
70
+ before validation can yield. The lineage reservation is acquired synchronously
71
+ at that boundary, preventing periodic GC from deleting resume state during
72
+ revalidation. Docker and broker operations have bounded deadlines, and each
73
+ worker has a bounded operator-configured lifetime; timeout cleanup revokes the
74
+ grant and removes the exactly labelled worker and network before capacity is
75
+ released.
76
+ The relay creates one absolute launch deadline before preflight. The same
77
+ deadline and cancellation signal span prompt/stdin and file-secret reads,
78
+ evidence and Telegram setup, Docker setup, broker readiness, execution,
79
+ response/log reads, upstream work and exact cleanup. A disconnected caller or
80
+ supervisor shutdown aborts its run. Independent per-step timeouts cannot extend
81
+ the overall budget, and response bodies are stream-limited to 2 MiB.
82
+
83
+ Codex bypasses its nested sandbox only inside this mandatory outer container.
84
+ Its task-private home contains only that lineage's session state; persisted
85
+ config, rules, hooks, plugins, MCP files and instruction files are rejected
86
+ before Codex starts. MCP is forced empty and project instruction loading is disabled.
87
+ Failure to create the outer boundary is fatal.
88
+
89
+ ## Deployment and rotation
90
+
91
+ The default `compose.yaml` contains no provider secret, broker, supervisor, or
92
+ Docker socket. Enable writes only with both files and the explicit profile:
93
+
94
+ ```sh
95
+ docker compose -f compose.yaml -f compose.relay-write.yaml --profile relay-write up
96
+ ```
97
+
98
+ The override fails interpolation unless every required image, control token,
99
+ worktree root, allowlist and credential-file setting is present. Set immutable supervisor, broker, and worker images; a host
100
+ `THREADWIRE_WORKSPACE_ROOT`; JSON `THREADWIRE_ALLOWED_WORKTREE_ROOTS`; separate
101
+ runtime and broker control tokens; `THREADWIRE_ALLOWED_CODEX_MODELS` as a JSON
102
+ server-side model allowlist; a unique `THREADWIRE_STATE_NAMESPACE`; a random
103
+ `THREADWIRE_STATE_AUTH_KEY` of at least 32 characters; and a deployment secret file for the Codex
104
+ credential. The supervisor alone receives `/var/run/docker.sock`. Restrict
105
+ broker egress at the host/firewall to provider endpoints.
106
+
107
+ The supervisor defaults to 16 active runs globally, two per reviewed task, a
108
+ 30-second maximum preflight slice, and a one-hour end-to-end launch deadline. Operators may
109
+ lower these finite limits with `THREADWIRE_PREFLIGHT_TIMEOUT_MS`,
110
+ `THREADWIRE_ACTIVE_RUN_CAPACITY`, `THREADWIRE_ACTIVE_TASK_CAPACITY`, and
111
+ `THREADWIRE_WORKER_TIMEOUT_MS`; invalid, zero, or excessive values fail startup.
112
+
113
+ The authenticated registry persists through file fsync, atomic rename, and
114
+ parent-directory fsync. Startup removes only strictly named abandoned temp
115
+ files whose complete contents authenticate under the deployment key; malformed
116
+ or foreign files are left untouched for operator inspection.
117
+
118
+ `THREADWIRE_WORKTREE_VOLUME` is only for nested-Docker/test deployments. Docker
119
+ volume `Subpath` still mounts only the selected task directory. Production uses
120
+ the exact host bind through Docker's structured Mount API with private
121
+ propagation. The supervisor verifies the resulting two-mount inventory before
122
+ start, and the entrypoint independently rejects nested worktree mounts.
123
+
124
+ Replace the broker secret atomically and restart it, or call authenticated
125
+ `/admin/reload`. Reload increments the generation and revokes existing grants.
126
+ Missing, writable, symlinked, empty, or oversized secret files keep it unready.
127
+
128
+ If Docker, broker, pinned image, worktree, security option, or private network
129
+ is unavailable, relay write fails closed. There is no native fallback.
130
+
131
+ ## Verification
132
+
133
+ Run `npm run all-checks`, `npm run verify:package`, and
134
+ `npm run test:isolated-runtime-e2e` with broker, runtime, hostile-probe,
135
+ production relay-worker, and fake-upstream image variables documented
136
+ by that script. E2E uses host-volume canaries and fake-upstream authorization
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.6",
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
  ],
@@ -14,6 +15,7 @@
14
15
  "private": false,
15
16
  "type": "module",
16
17
  "files": [
18
+ "CHANGELOG.md",
17
19
  "bin/",
18
20
  "docs/",
19
21
  "scripts/",
@@ -22,6 +24,9 @@
22
24
  "threadwire.workspace-profiles.json"
23
25
  ],
24
26
  "bin": {
27
+ "threadwire-isolated-runtime": "bin/isolated-runtime.js",
28
+ "threadwire-kimi-model-broker": "bin/kimi-model-broker.js",
29
+ "threadwire-model-broker": "bin/model-broker.js",
25
30
  "threadwire": "bin/threadwire.js",
26
31
  "threadwire-telegram-webhook": "bin/telegram-webhook.js"
27
32
  },
@@ -49,8 +54,10 @@
49
54
  "prepublishOnly": "npm run all-checks && npm run verify:package",
50
55
  "test": "node --test \"test/**/*.test.js\"",
51
56
  "test:container-e2e": "sh docker/test/production-e2e.sh",
57
+ "test:isolated-runtime-e2e": "sh docker/test/isolated-runtime-e2e.sh",
52
58
  "typecheck": "tsc --noEmit",
53
- "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"
54
61
  },
55
62
  "devDependencies": {
56
63
  "@eslint/js": "10.0.1",
@@ -55,6 +55,24 @@ fi
55
55
  @@WORKER_SUBCOMMAND_SHIFT@@
56
56
 
57
57
  threadwire_relay_arguments=(run --provider "$THREADWIRE_PROVIDER" --target "$THREADWIRE_TARGET")
58
+ case "${THREADWIRE_RELAY_WRITE:-0}" in
59
+ 0) ;;
60
+ 1)
61
+ if [[ ! "${THREADWIRE_WORKSPACE_PROFILE:-}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]]; then
62
+ printf '%s\n' 'threadwire: relay write capability requires a valid THREADWIRE_WORKSPACE_PROFILE' >&2
63
+ exit 2
64
+ fi
65
+ if [[ "$THREADWIRE_PROVIDER" != codex ]]; then
66
+ printf '%s\n' 'threadwire: relay write capability is only supported for codex' >&2
67
+ exit 2
68
+ fi
69
+ threadwire_relay_arguments+=(--workspace-profile "$THREADWIRE_WORKSPACE_PROFILE" --relay-write)
70
+ ;;
71
+ *)
72
+ printf '%s\n' 'threadwire: relay write capability must be enabled with THREADWIRE_RELAY_WRITE=1' >&2
73
+ exit 2
74
+ ;;
75
+ esac
58
76
  if [[ -n "${THREADWIRE_PROCESS_NUMBER:-}" ]]; then
59
77
  threadwire_relay_arguments+=(--process-number "$THREADWIRE_PROCESS_NUMBER")
60
78
  fi
@@ -13,16 +13,22 @@ 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 = [
18
+ "CHANGELOG.md",
17
19
  "LICENSE",
18
20
  "README.md",
19
21
  "TELEGRAM-INGRESS.md",
20
22
  "bin/telegram-webhook.js",
21
23
  "bin/threadwire.js",
24
+ "bin/isolated-runtime.js",
25
+ "bin/kimi-model-broker.js",
26
+ "bin/model-broker.js",
22
27
  "docs/card-10520-plan.md",
23
28
  "docs/container-runtime.md",
24
29
  "docs/delegated-result-protocol.md",
25
30
  "docs/evidence-artifacts.md",
31
+ "docs/isolated-provider-runtime.md",
26
32
  "package.json",
27
33
  "scripts/atomic-install.js",
28
34
  "scripts/install-local-launcher.js",
@@ -32,11 +38,24 @@ const EXPECTED_FILES = [
32
38
  "scripts/provider-shims/front-door.sh.template",
33
39
  "scripts/provider-shims/opencode-local-fleet.adapter.sh",
34
40
  "scripts/verify-package.js",
41
+ "src/absolute-deadline.js",
35
42
  "src/activity-log.js",
43
+ "src/normalized-output.js",
36
44
  "src/cli.js",
37
45
  "src/context-budget-metrics.js",
38
46
  "src/delegated-result-admission.js",
47
+ "src/docker-api.js",
39
48
  "src/evidence-store.js",
49
+ "src/isolated-runtime-client.js",
50
+ "src/isolated-runtime.js",
51
+ "src/isolated-state.js",
52
+ "src/isolated-worker.js",
53
+ "src/kimi-model-broker-policy.js",
54
+ "src/kimi-model-broker.js",
55
+ "src/kimi-oauth-store.js",
56
+ "src/model-broker-policy.js",
57
+ "src/model-broker.js",
58
+ "src/mount-policy.js",
40
59
  "src/notice-queue.js",
41
60
  "src/notifiers/fetch-transport.js",
42
61
  "src/notifiers/telegram.js",
@@ -44,8 +63,10 @@ const EXPECTED_FILES = [
44
63
  "src/providers/codex.js",
45
64
  "src/providers/executable.js",
46
65
  "src/providers/index.js",
66
+ "src/providers/kimi.js",
47
67
  "src/providers/opencode.js",
48
68
  "src/relay.js",
69
+ "src/relay-write.js",
49
70
  "src/run-worker.js",
50
71
  "src/sentence-buffer.js",
51
72
  "src/telegram-ingress/command.js",
@@ -63,7 +84,7 @@ const EXPECTED_FILES = [
63
84
 
64
85
  /** @returns {Promise<void>} */
65
86
  async function main() {
66
- const temporaryDirectory = await mkdtemp(join(tmpdir(), "threadwire-package-"))
87
+ const temporaryDirectory = await mkdtemp(join(packageTmpdir, "threadwire-package-"))
67
88
  const cacheDirectory = join(temporaryDirectory, "npm-cache")
68
89
 
69
90
  try {
@@ -118,6 +139,7 @@ async function main() {
118
139
  ], {
119
140
  env: {
120
141
  PATH: process.env.PATH,
142
+ HOME: process.env.HOME,
121
143
  NPM_CONFIG_DRY_RUN: "false",
122
144
  npm_config_dry_run: "false"
123
145
  },