threadwire 0.1.12 → 0.1.14

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 CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ - Incrementally spool oversized provider JSONL records to private per-record temporary files after the bounded in-memory threshold, preserving byte order, UTF-8 chunk boundaries, malformed/incomplete-record diagnostics, and the exact absolute record cap while cleaning every per-record spill directory on success, failure, and cancellation. Protocol completion stops same-chunk scanning before any trailing record can exceed the cap or emit output. Ordinary valid provider records are no longer constrained to Telegram display sizes. Container-backed cancellation and failure teardown now retain exact Docker ownership through inspect, bounded TERM stop, terminal-state confirmation, and exact-container KILL escalation; CLI and Telegram deadline cancellation close the request side, await bounded terminal cleanup confirmation, and never treat an HTTP abort as cleanup success. Unconfirmed cleanup remains sealed for reconciliation and can never report success. Direct local-process behavior is unchanged.
6
+ - Extend the checked-in development image with build-time `oc-codex-multi-auth`, DeepSeek OpenAI-compatible, and xAI provider runtimes plus a root-owned secret-free OpenCode catalog for GPT-5.6 Sol/Terra/Luna, DeepSeek V4 Pro/Flash, Grok 4.5/Build/Code Fast, and Kimi K3 through OpenCode's built-in `moonshotai` provider. Kimi K3 keeps its 1,048,576-token context, 131,072-token output limit, default `max` reasoning effort, and `low`/`high`/`max` variants. Compose now requires portable source paths for native Codex auth, OpenCode auth, plugin OAuth, and the plugin's global account pool; refreshable files mount narrowly read-write, GitHub/context mounts stay read-only, per-project Codex account copies are disabled, and OpenCode database/session/cache state remains in the dedicated development home. The UID/GID 1000 initializer creates every nested mount target without receiving credentials. Threadwire remains the worker boundary; direct OpenCode is for authentication, discovery, and bounded smoke checks only.
7
+
8
+ ## 0.1.14 - 2026-08-03
9
+
10
+ - Fix native Kimi runs hanging after a successful response: terminal completion is now explicit and provider-owned instead of derived only from process exit. Kimi's parser confirms completion from its stream-json protocol — a final assistant response (assistant content without further tool calls) plus the `session.resume_hint` handoff, in either order — and once confirmed, Threadwire preserves the session continuation handle, terminates the attempt-owned provider process group (`SIGTERM`), and emits exactly one `delegated_result`. Success settles only once the entire owned group is gone: a member that survives `SIGTERM` — including an attempt-owned grandchild after the direct provider exits — keeps the grace-period escalation running and receives `SIGKILL` against the exact group (liveness is probed with signal 0 against the group, never name matching or broad scans) before settlement, so no descendant leaks. Normal exit semantics are unchanged: a provider that exits nonzero before protocol completion remains a failure, and long-running tool activity stays unbounded with no generic idle timeout. Cancellation, consumer/relay failure, and launch failure clean up the complete attempt-owned process tree through the detached process group: parent `SIGINT`/`SIGTERM` cancellation now owns grace-period escalation (surviving members are `SIGKILL`ed against the exact group) and settles with the forwarded signal's exit status (never protocol success), and a consumer failure that lands after the direct child closed probes the exact group and terminates/escalates surviving descendants before rejecting the original error. `runWorker` also accepts an explicit `AbortSignal` (wired from the caller launch deadline where one exists): an abort terminates the exact owned group with the same grace/escalation and settles with the abort failure, never success. Settlement after any `SIGKILL` now waits for bounded cleanup confirmation — the exact process group is re-probed (or, where no process group exists such as win32, the direct child's own close is required) before success, failure, or cancellation settles; if owned processes cannot be confirmed gone within the bounded window (`cleanupConfirmationMs`, default 5s), the run settles as a cleanup failure, never success, preserving the original failure or cancellation context. Once the provider protocol confirms completion, the current stdout chunk stops draining immediately so trailing records can never produce duplicate output or consumer failures.
11
+ - Add a checked-in development container: the `app` service in `compose.dev.yaml` builds `docker/Dockerfile.dev` (Ubuntu 26.04 pinned by registry-verified digest, stable non-root `dev` user `1000:1000`, Node 22 LTS from the official SHA-256-verified tarball, distro GitHub CLI, and the npm-latest Codex/Claude/Kimi/OpenCode CLI baseline with Threadwire resolved through unversioned `npx threadwire`). The complete `DEV_HOME_PATH` home is bind-mounted at `/home/dev` with the checkout at `/home/dev/threadwire`, the host GitHub CLI config mounts read-only at `/home/dev/.config/gh` from the required `GH_CONFIG_SOURCE_PATH`, and an immutable agent-context snapshot mounts read-only at `/opt/agent-context` from the required `AGENT_CONTEXT_PATH`; the entrypoint proves the auth mount is read-only, initializes the HTTPS Git credential helper in the writable home, and links the context's AGENTS/CLAUDE guidance and trusted skills into every installed worker's discovery home (`.claude`, `.codex`, `.config/opencode`, `.kimi-code`). A one-shot `home-init` service precreates the provider parent directories as `1000:1000` on blank homes so Docker never creates the nested auth mount's parent root-owned. Project guidance and validation commands are documented in the new `AGENTS.md`. Production runtime files are unchanged.
12
+ - Add the read-only `threadwire capacity` command: it probes live account capacity for authenticated Codex (a bounded `codex app-server` JSON-RPC `initialize` + `account/rateLimits/read` exchange resolved like the Codex worker provider) and Kimi (`GET https://api.kimi.com/coding/v1/usages` with the OAuth bearer read through the hardened credential store's non-mutating `getCurrentAccessToken` — never refreshed, persisted, or tombstoned; a credential inside the 30-second validity margin classifies as `unavailable`), normalizes both into one closed short/long-window schema (`usedPercent`/`remainingPercent`/`resetAt`/duration, floored remaining percent, fail-closed exact-keys validation), and prints exactly one JSON report with per-provider `ok`/`unavailable`/`auth`/`protocol` statuses and a single deterministic admission selection. Selection rejects candidates below configurable short/long reserves (defaults 20%/10%), scores survivors by their most constrained window (absent long window non-constraining), and breaks ties by long-window remaining then provider name, so identical snapshots always reproduce the identical selection; no survivor yields `selection: null` and exit code 2. Tokens, raw credentials, account and plan data, and server error detail never enter output, errors, fixtures, or logs; the command writes nothing to disk, takes no session arguments, and leaves `run`, `evidence`, ingress, brokers, and the isolated runtime byte-identical. Provider/model/session pinning for tasks remains with the external worker ledger; see `docs/capacity-admission.md`.
13
+
14
+ ## 0.1.13 - 2026-08-01
15
+
5
16
  - Remove the default one-hour full-run deadline from the isolated runtime: when `THREADWIRE_ISOLATED_RUNTIME_CLIENT_TIMEOUT_MS` (client) and `THREADWIRE_WORKER_TIMEOUT_MS` (supervisor) are unset or blank, a healthy running worker continues until natural completion, caller cancellation/disconnect, supervisor shutdown, or concrete failure. Setting either to a positive millisecond value enables the existing single absolute preflight+run deadline with unchanged validation and caps. Compose no longer inserts numeric timeout defaults. Preflight admission, per-operation Docker/network calls, state collection (30 seconds), and emergency cleanup stay independently bounded.
6
17
  - Add authenticated broker grant-lease renewal (Kimi and Codex): grants are issued as a bounded lease (default 60 seconds, `THREADWIRE_GRANT_LEASE_MS`, capped at one hour) that the supervisor renews via `POST /admin/grants/{token}/renew` while the run is active. Cleanup still revokes explicitly; if the supervisor dies or loses contact, renewals stop and the broker expires and aborts the grant within one lease. Malformed renewals (unknown/expired token, invalid ttlMs, missing admin auth) fail closed without extending the grant, and no renewal route exists on worker endpoints.
7
18
  - Add Kimi-only Threadwire binding v1 named-volume admission: task-labelled source/context/lease validation, immutable context manifest validation, offline validator, dynamic task UID/GID workers with context RO/state RW mounts and an exact binding-declared source mount mode, and v5 authenticated binding state. Kimi path/worktree preflights and supervisor host workspace/socket mounts are no longer accepted.
@@ -12,6 +23,19 @@
12
23
  - Reconcile exact relay/worker/network/state/grant identity on restart: genuine resources are adopted and cleaned up by their exact sealed spec, while image, mount, network, or environment tampering fails closed and retains authenticated cleanup evidence. The sealed Kimi worker/validator/relay image is the resolved image content Id (the worker/validator pin the binding's `contextImageId`; the relay pins its inspected image Id), matching how Docker normalizes a created container's `Config.Image`, so a genuine crash survivor is recognized rather than retained as a cleanup failure. The validator and relay environments accept legitimate non-sensitive keys inherited from the image while every owned key must match its sealed value exactly once and any injected sensitive or controlled key fails closed.
13
24
  - Qualify genuine separate-daemon fresh/resume/denial/read-only/concurrency operation with zero residue: adversarial split-daemon E2E proves exact resume, read-only review mounts, binding/argument denial, concurrent task isolation, and complete relay/network/state cleanup on the task DinD.
14
25
 
26
+ - Remove workspace profiles from normal provider execution. `--cwd` now passes
27
+ any caller-accessible directory directly to Codex, Claude, Kimi, and
28
+ OpenCode; no Git, source identity, task binding, or root-allowlist lookup is
29
+ performed. `--workspace-profile` is rejected—migrate invocations to
30
+ `--cwd <directory>`. Native Kimi is the default path, supports
31
+ `THREADWIRE_KIMI_BIN`, translates exact `--resume-session` values to Kimi's
32
+ native `--session` option, and passes compatible provider arguments while
33
+ Threadwire retains prompt and stream-output ownership. The optional isolated
34
+ Kimi path remains explicit and fail-closed without native fallback.
35
+ - Preserve only the explicit caller-owned native Kimi configuration contract,
36
+ report exactly one completed or failed terminal lifecycle outcome, and reject
37
+ provider option terminators that could neutralize Threadwire-owned arguments.
38
+
15
39
  ## 0.1.10 - 2026-07-28
16
40
 
17
41
  - Accept official slash-qualified Kimi model aliases through workspace-free
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Threadwire
2
2
 
3
- Threadwire is a Node.js CLI that launches a Codex, Claude, or OpenCode worker and streams a concise, human-readable activity feed to one explicitly selected Telegram destination: a direct message, normal chat, or forum topic. The default feed includes the assistant's response prose, lifecycle outcomes, and diagnostics. Provider tool-status messages are disabled by default.
3
+ Threadwire is a Node.js CLI that launches a Codex, Claude, Kimi, or OpenCode worker and streams a concise, human-readable activity feed to one explicitly selected Telegram destination: a direct message, normal chat, or forum topic. The default feed includes the assistant's response prose, lifecycle outcomes, and diagnostics. Provider tool-status messages are disabled by default.
4
4
 
5
5
  For the container-native webhook deployment, including its named-volume worktree,
6
6
  non-root Codex provider, Docker secrets, loopback TLS proxy, verification, and
@@ -8,8 +8,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
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
11
+ selects the optional root-owned isolated runtime and credential broker. The worker gets one writable task worktree, no upstream
13
12
  or Git credential, and no route except its run-scoped broker. Missing
14
13
  prerequisites fail before prompt or credential reads with no native fallback.
15
14
  The isolated runtime imposes no default full-run deadline: a healthy running
@@ -71,11 +70,11 @@ Use the local launcher. Install or refresh it after updating Threadwire with `np
71
70
 
72
71
  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.
73
72
 
74
- 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). Non-Kimi providers resolve the reviewed in-container repository path and Git provenance. Kimi instead requires the v2 `threadwire-v1` profile marker and a trusted `THREADWIRE_KIMI_TASK_BINDING`; it never accepts a repository path preflight. The binding names the task-owned source/context volumes, immutable context digests, revision, runtime identity, and live lease. Only safe provenance is recorded locally.
73
+ `--cwd` is the complete workspace contract for every provider. It may name any caller-accessible directory, including a non-Git directory. Threadwire passes it unchanged to the native provider and performs no profile, repository, source-identity, task-binding, or root-allowlist lookup. `--workspace-profile` has been removed and is rejected; migrate invocations by replacing it with `--cwd <directory>`.
75
74
 
76
- 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.
75
+ 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 and session options owned by Threadwire. Native Kimi uses the caller's compatible configuration. Threadwire maps its public `--resume-session <exact-id>` option to Kimi's native `--session <exact-id>` option and owns `--prompt` plus `--output-format stream-json`. The caller environment or image must provide the Kimi executable at `/usr/local/bin/kimi` or set `THREADWIRE_KIMI_BIN`; a missing executable fails normally.
77
76
 
78
- 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.
77
+ Threadwire directly spawns the structurally separate terminal adapters `/opt/data/libexec/threadwire/codex`, `/opt/data/libexec/threadwire/claude`, `/usr/local/bin/kimi`, 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`, `THREADWIRE_KIMI_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. Terminal completion is explicit and provider-owned rather than derived only from process exit: native Kimi confirms a completed run with a final assistant response (assistant content without further tool calls) plus its session handoff record in the stream. Once both records are confirmed, Threadwire preserves the session continuation handle, terminates the attempt-owned provider process group (`SIGTERM`), and emits exactly one `delegated_result`. Success settles only once the entire owned group is gone: members that survive `SIGTERM` — including attempt-owned grandchildren — keep the grace-period escalation running and receive `SIGKILL` against the exact group before settlement, so no descendant is leaked even when the direct provider exits first. A provider that exits nonzero before its protocol confirms completion keeps its failure status, and long-running tool activity remains unbounded: there is no generic idle timeout, only caller cancellation or an explicit deadline. Parent `SIGINT`/`SIGTERM` is owned cancellation, not just forwarding: the signal reaches the exact attempt-owned group, surviving members are escalated to `SIGKILL` after the grace period, and settlement keeps the forwarded signal's exit status (143/130 on Linux) only after the owned tree is gone. Relay/consumer failure cleans up the same owned tree — even when the failure lands after the direct child closed and only descendants survive — and the worker runner also accepts an explicit `AbortSignal`: an abort terminates the exact attempt-owned group with the same grace/escalation and settles with the abort failure, never success. After any `SIGKILL`, settlement waits for bounded cleanup confirmation — the exact group is re-probed, or on platforms without process groups the direct child's close is required — and cleanup that cannot be confirmed within the bounded window settles as failure, never success; this confirmation bound is not an idle or tool-activity timeout.
79
78
 
80
79
  ## Provider front-door shims
81
80
 
@@ -83,11 +82,11 @@ Threadwire directly spawns the structurally separate terminal adapters `/opt/dat
83
82
 
84
83
  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.
85
84
 
86
- 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.
85
+ Pass `--resume-session <exact-provider-session-id>` to continue a stored Codex, Claude, Kimi, or OpenCode session. Threadwire preserves the provider's native resume ordering and streaming options and never uses implicit latest-session behavior.
87
86
 
88
87
  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.
89
88
 
90
- `--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.
89
+ `--activity-log <path>` is different: it retains its append contract and writes only minimal safe lifecycle facts—provider start (`provider`, child `pid`) and available native session ID. It is local-only and never enters Telegram or normalized stdout.
91
90
 
92
91
  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.
93
92
 
@@ -95,6 +94,8 @@ For normal `threadwire run` execution, Threadwire stdout is the parent-model adm
95
94
 
96
95
  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.
97
96
 
97
+ `threadwire capacity` is a read-only reporting command that probes live account capacity for authenticated Codex (`codex app-server` JSON-RPC `account/rateLimits/read`) and Kimi (`GET https://api.kimi.com/coding/v1/usages` with the installed OAuth credential), normalizes both into one closed short/long-window schema, and prints one deterministic reserve-gated admission selection as a single JSON document. It persists nothing, never prints tokens or account data, takes no session arguments, and does not change `run` provider selection; an external worker ledger consumes the report and owns pinning provider/model/session. See [Capacity admission](docs/capacity-admission.md) for the schema, tie-break, error taxonomy, and security boundary.
98
+
98
99
  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.
99
100
 
100
101
  The Telegram bot token is used only by Threadwire's notifier and both `TELEGRAM_BOT_TOKEN` and `THREADWIRE_TELEGRAM_BOT_TOKEN` are removed from every provider child environment. The three libexec adapters repeat that scrub as defense in depth. Other environment variables, including the providers' own authentication and normal process configuration, are preserved. Outbound Telegram fetch and response-body parsing share a 30-second deadline by default; set `THREADWIRE_TELEGRAM_REQUEST_TIMEOUT_MS` to a positive safe integer no greater than `2147483647` to override it.
@@ -107,6 +108,11 @@ The delivery queue has finite defaults of 100 pending notices and 256 KiB of pen
107
108
 
108
109
  ## Development
109
110
 
111
+ The checked-in development container is the `app` service in `compose.dev.yaml`
112
+ (Ubuntu 26.04 pinned by digest, non-root `1000:1000`, Node 22 LTS, npm-latest
113
+ coding CLI baseline). Setup, the read-only GitHub CLI and agent-context mount
114
+ contracts, and container commands are documented in [AGENTS.md](AGENTS.md).
115
+
110
116
  Tests use synthetic provider records, a local fixture child, and injected transports. They never invoke Codex, Claude, or Telegram.
111
117
 
112
118
  ```sh
@@ -1,10 +1,10 @@
1
1
  # Telegram Ingress for Threadwire
2
2
 
3
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
+ Codex, Claude, Kimi, and OpenCode use their native provider adapter by default.
5
+ An operator may explicitly configure the Kimi isolated client; that selected
6
+ path remains fail-closed and never falls back to native execution. Hermes is
7
+ not involved.
8
8
 
9
9
  ## Important bot ownership
10
10
 
@@ -83,9 +83,9 @@ Optional:
83
83
  | `THREADWIRE_TELEGRAM_REQUEST_TIMEOUT_MS` | Deadline for each outbound Telegram request, including response parsing (default `30000`, positive safe integer no greater than `2147483647`) |
84
84
  | `THREADWIRE_UPDATE_ID_CAPACITY` | Max retained completed `update_id` entries (default `10000`) |
85
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. |
88
- | `THREADWIRE_KIMI_TASK_BINDING` | Trusted closed Threadwire binding v1 JSON. It is captured before provider filtering, sent only to Kimi preflight, and is never forwarded to a provider or worker. |
86
+ | `THREADWIRE_KIMI_ISOLATED_RUNTIME_URL` | Optional dedicated Kimi supervisor URL, used only with an explicit control token. |
87
+ | `THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN` | Selects the optional dedicated Kimi isolation client. Without it, `/code kimi` runs natively. |
88
+ | `THREADWIRE_KIMI_TASK_BINDING` | Optional trusted binding used only by the explicitly selected Kimi isolation client; native Kimi never reads it. |
89
89
  | `THREADWIRE_ISOLATED_RUNTIME_CLIENT_TIMEOUT_MS` | Optional absolute preflight+run deadline in ms for isolated (Codex relay-write and Kimi) runs. Empty/unset means no full-run deadline: a healthy running worker is never stopped merely because time elapsed. The supervisor-side equivalent is `THREADWIRE_WORKER_TIMEOUT_MS`. Broker grants always use a bounded lease renewed by the supervisor while the run is active, expiring within one lease if the supervisor dies; unrelated safety timeouts are unchanged. |
90
90
 
91
91
  ## Run
@@ -0,0 +1,147 @@
1
+ # Capacity admission
2
+
3
+ `threadwire capacity` probes live account capacity for the authenticated Codex
4
+ and Kimi providers, normalizes both into one closed schema, and makes a single
5
+ deterministic admission selection. It is a read-only reporting command: it
6
+ writes nothing to disk, changes no provider or runtime state, and never
7
+ launches, resumes, or selects a session. An external worker ledger consumes
8
+ the report and owns pinning the selected provider, model, and native session
9
+ across planning, implementation, debugging, and corrections; Threadwire
10
+ deliberately has no task-ledger ownership, no `--provider auto`, and no
11
+ capacity-based selection inside `threadwire run`.
12
+
13
+ ## Command
14
+
15
+ ```
16
+ threadwire capacity [--provider <codex|kimi>]...
17
+ [--short-reserve-percent <0-100>] # default 20
18
+ [--long-reserve-percent <0-100>] # default 10
19
+ [--timeout-ms <positive-integer>] # default 15000
20
+ ```
21
+
22
+ Stdout carries exactly one JSON document:
23
+
24
+ ```json
25
+ {
26
+ "version": 1,
27
+ "generatedAt": "2026-08-02T00:00:00.000Z",
28
+ "reserves": {"shortPercent": 20, "longPercent": 10},
29
+ "candidates": [
30
+ {
31
+ "provider": "codex",
32
+ "status": "ok",
33
+ "windows": {
34
+ "short": {"usedPercent": 40, "remainingPercent": 60, "resetAt": "2026-08-02T00:00:00.000Z", "windowDurationSeconds": 18000},
35
+ "long": {"usedPercent": 10, "remainingPercent": 90, "resetAt": "2026-08-09T00:00:00.000Z", "windowDurationSeconds": 604800}
36
+ }
37
+ }
38
+ ],
39
+ "rejected": [{"provider": "kimi", "reason": "auth"}],
40
+ "selection": {"provider": "codex", "score": 60}
41
+ }
42
+ ```
43
+
44
+ Exit code is `0` when `selection` is non-null and `2` for invalid arguments or
45
+ when no candidate survives; the report is still printed in the latter case.
46
+ `generatedAt` is the only wall-clock field: the same `candidates` and
47
+ `reserves` always reproduce the same `selection`, so the caller can re-verify a
48
+ pin before relying on it.
49
+
50
+ ## Probes and normalization
51
+
52
+ - **Codex** spawns `codex app-server` and runs a bounded two-request JSON-RPC
53
+ exchange over stdio: `initialize`, then `account/rateLimits/read`. The
54
+ executable resolves via `THREADWIRE_CODEX_BIN` or the default libexec
55
+ adapter, exactly like the Codex worker provider; the anti-recursion default
56
+ is unchanged, so native container callers point the existing safe override
57
+ at the real CLI (for example `THREADWIRE_CODEX_BIN=/usr/local/bin/codex`).
58
+ Only the quota projection of the result is parsed: `rateLimits.primary` maps
59
+ to `short` and `rateLimits.secondary` to `long`; a `null` secondary yields a
60
+ short-only snapshot, and a secondary shorter than the primary fails closed.
61
+ Each window requires exactly `usedPercent`, `windowDurationMins`, and
62
+ `resetsAt` (epoch seconds, normalized to the canonical `resetAt` output
63
+ field); missing, obsolete, or extra keys fail closed, and unrelated result
64
+ or `rateLimits` metadata is neither read nor propagated.
65
+ - **Kimi** calls `GET https://api.kimi.com/coding/v1/usages` with the OAuth
66
+ bearer from the installed Kimi credential store, loaded through the same
67
+ hardened store as the model broker. The credential file resolves in order: a
68
+ non-empty absolute `THREADWIRE_KIMI_OAUTH_FILE` override, then the installed
69
+ CLI's canonical `${KIMI_CODE_HOME}/credentials/kimi-code.json`, then the
70
+ vendor default `${HOME}/.kimi-code/credentials/kimi-code.json`; missing,
71
+ relative, or NUL-containing candidates never qualify and resolution fails
72
+ closed as `auth` without logging any path. Only the quota projection of the
73
+ response is parsed: top-level `usage` is the long account quota (documented
74
+ weekly duration of 604,800 seconds) and the shortest `limits[]` window maps
75
+ to `short`; every returned window is validated (`window.duration` in
76
+ `TIME_UNIT_MINUTE`s, `detail` quota) but intermediate windows are ignored,
77
+ and normalization fails closed if the long duration would not exceed the
78
+ short. Quota fields are strict non-negative decimal strings validated with
79
+ `used + remaining === limit` and a positive limit, reset times must be
80
+ bounded ISO-8601, and remaining percent is floored so normalization never
81
+ overstates capacity. Unrelated top-level response fields are neither read
82
+ nor propagated into output or errors.
83
+
84
+ Both probes are bounded (capped lines/bytes, single absolute timeout) and fail
85
+ closed on any shape drift: exact keys within the parsed projection, forced
86
+ types, `0–100` percents, positive bounded durations, and reset times
87
+ normalized to ISO-8601 UTC.
88
+
89
+ ## Selection semantics
90
+
91
+ `selectProvider` is pure (no I/O, no clock):
92
+
93
+ 1. Candidates whose `status` is not `ok` are rejected with their status as the
94
+ reason (`unavailable`, `auth`, or `protocol`).
95
+ 2. A candidate is rejected with `short-reserve` when its short-window
96
+ `remainingPercent` is below `shortPercent`, and with `long-reserve` when a
97
+ present long window is below `longPercent`. An absent long window is
98
+ non-constraining. A remaining percent equal to the reserve passes.
99
+ 3. Survivors are scored by their most constrained window:
100
+ `min(shortRemaining, longRemaining)` with an absent long window counting as
101
+ 100.
102
+ 4. Ordering is total and deterministic: score descending, then long-window
103
+ remaining descending, then provider name ascending (`codex` before `kimi`).
104
+ 5. No survivor yields `selection: null` and exit code `2`.
105
+
106
+ ## Error taxonomy
107
+
108
+ Per provider, independently of the other:
109
+
110
+ - `ok` — a normalized snapshot was produced.
111
+ - `unavailable` — transport failure, timeout, spawn failure, early child exit,
112
+ 5xx, or OAuth refresh unavailability.
113
+ - `auth` — 401/403, logged-out (tombstone) Kimi credentials, missing Kimi
114
+ credential configuration, or a Codex server-side authentication rejection.
115
+ - `protocol` — malformed JSON, wrong content type, JSON-RPC error, out-of-order
116
+ exchange, or any response shape drift.
117
+
118
+ ## Security boundary
119
+
120
+ - Tokens and raw credentials are used in-process only and are never printed,
121
+ persisted, or embedded in errors. The Kimi bearer is read through
122
+ `openKimiOAuthStore`'s non-mutating `getCurrentAccessToken` (returned only
123
+ while valid beyond a fixed 30-second safety margin): the probe never
124
+ refreshes, persists, renames, or tombstones credentials, and a credential
125
+ inside the margin classifies as `unavailable` with a fixed message. Probe
126
+ failures use the store's fixed-message discipline with no causes. Codex
127
+ server error detail (which can carry account material) is inspected for
128
+ classification only and never propagated.
129
+ - The report schema is closed and carries only provider names, statuses, fixed
130
+ messages, and window numbers — no account IDs, plan names, emails, tokens, or
131
+ raw response bodies. Committed test fixtures contain sanitized structure with
132
+ representative numeric values only.
133
+ - The Codex child is always killed (success, error, and timeout), its stderr is
134
+ consumed but never forwarded, and the exchange is bounded. Kimi requests use
135
+ `redirect: "error"`, strict JSON content-type, and a capped body read.
136
+ - The command introduces no new environment variables and no persistence, so
137
+ the existing evidence redaction sweep is unaffected.
138
+
139
+ ## Ledger contract
140
+
141
+ The external worker ledger calls `threadwire capacity`, verifies the report,
142
+ and pins the selected provider, the model (from its own allowlists — capacity
143
+ is account-level and Threadwire performs no model selection), and the native
144
+ session for the task. Resumed sessions and correction runs reuse that pin and
145
+ must not re-run admission; `threadwire capacity` takes no session arguments and
146
+ `threadwire run` explicit-provider and `--resume-session` behavior is
147
+ unchanged.
@@ -84,16 +84,13 @@ docker compose exec -T threadwire sh -c 'test "$(id -u):$(id -g)" = 10001:10001
84
84
 
85
85
  Telegram commands derive their explicit destination from the authenticated
86
86
  update. A credentialed direct smoke test must also name a target explicitly and
87
- keeps both credentials mounted as secret files. The checked-in
88
- `container-runtime` workspace profile points to the bootstrapped
89
- `/workspace/threadwire` worktree inside the current runtime, so `threadwire run`
90
- selects that reviewed in-container workspace directly rather than deriving a
91
- path from ambient host state or Telegram input:
87
+ keeps both credentials mounted as secret files. Direct execution supplies the
88
+ caller-accessible working directory explicitly:
92
89
 
93
90
  ```sh
94
91
  node bin/threadwire.js run \
95
92
  --provider codex --target telegram:-1001234567890:42 \
96
- --workspace-profile container-runtime \
93
+ --cwd /workspace/threadwire \
97
94
  --prompt 'Inspect the container worktree.'
98
95
  ```
99
96
 
@@ -114,27 +111,11 @@ permissions that deny the provider, or move it to a separate container or
114
111
  sandbox. The same-UID, same-container, capability-free design cannot truthfully
115
112
  provide that stronger boundary.
116
113
 
117
- With `--workspace-profile`, `threadwire run` emits one caller-facing
118
- `execution_environment` JSONL record before provider startup. It includes only
119
- the resolved profile name, in-container repository path, exact Git revision,
120
- and immutable worktree identity; it never includes Telegram or provider
121
- credentials.
122
-
123
- The same reviewed profile file also governs authenticated webhook launches. The
124
- default profile is selected server-side; Telegram message text never supplies a
125
- raw path, container identifier, executable path, Git URL, or environment
126
- assignment. Before every spawn, Threadwire verifies that the configured
127
- workspace still exists, is still a Git worktree at the configured root, and
128
- still has a stable revision/worktree identity. If the worktree is removed or
129
- unhealthy, spawn fails before the provider starts so Telegram can retry after
130
- remediation.
131
-
132
- Provision profile changes and revision updates entirely inside the container
133
- worktree. Edit `threadwire.workspace-profiles.json` in `/workspace/threadwire`,
134
- commit the reviewed change in that worktree, and let the existing bootstrap and
135
- rollback flow move the whole repository plus its `.threadwire-source-identity`
136
- forward or backward together. Do not introduce host-only paths, Docker CLI
137
- arguments, or Compose-generated container names into the profile file.
114
+ `--workspace-profile` is removed and rejected. Threadwire does not inspect Git,
115
+ source identity, task binding, or workspace allowlists for normal provider
116
+ launches. Telegram ingress uses its own process working directory unless its
117
+ operator supplies another caller-accessible directory through the embedding
118
+ configuration; Telegram message text never supplies a path.
138
119
 
139
120
  ## Verification and leakage audit
140
121
 
@@ -205,10 +186,13 @@ Never export or archive mounted secret files with workspace/provider backups.
205
186
 
206
187
  Kimi source is not a host worktree. The task controller creates one labelled source named volume (including its `.git` directory), one labelled immutable context named volume, and a running labelled lease container. The trusted Kimi supervisor reaches only the owning task-DinD API through `THREADWIRE_KIMI_TASK_DOCKER_HOST`; it has no workspace bind or outer Docker socket. It verifies the closed binding, exact labels/volume identities, lease image/user/workdir/mounts, references, Git revision, and manifest before issuing a broker grant. The worker has exactly the binding-declared source mount at `/workspace` (read-write for implementation sources, read-only for review sources), context RO at `/context`, and lineage state RW at `/state`. The worker entrypoint independently revalidates the mounted context manifest task/image identity and the exact source mount mode before Kimi starts; a mismatched manifest task identity or mount mode fails the run closed.
207
188
 
208
- Kimi is intentionally excluded from the same-UID container-native provider
209
- launch described above. Never mount `threadwire-kimi-oauth`, its access/refresh
210
- tokens, or the auth home into the Threadwire runtime, supervisor, worker,
211
- workspace, or session-state volume. Kimi uses five immutable build targets:
189
+ The checked-in standard `runtime` image installs Codex only: it does not install
190
+ Kimi or provide Kimi credentials. Native Kimi therefore requires a caller image
191
+ or environment that provides `/usr/local/bin/kimi` (or sets
192
+ `THREADWIRE_KIMI_BIN`); a missing executable fails normally. Never mount
193
+ `threadwire-kimi-oauth`, its access/refresh tokens, or the auth home into the
194
+ Threadwire runtime, supervisor, worker, workspace, or session-state volume.
195
+ The optional isolated Kimi flow uses five immutable build targets:
212
196
  `kimi-auth`, `kimi-model-broker`, `kimi-model-relay`, `kimi-relay-worker`, and
213
197
  `isolated-runtime`.
214
198
  The first two share UID/GID 10003 and only the OAuth volume; the credential-free
@@ -239,9 +223,9 @@ listener until an explicit nonempty bind is configured, so the sentinel is
239
223
  never an operational default.
240
224
  Only the worker port is published, on that bind — the admin endpoint stays on
241
225
  the internal control network.
242
- The default workspace profile allowlists Kimi because isolation is mandatory;
243
- without the dedicated Kimi control token/client, both CLI and Telegram Kimi
244
- jobs fail closed rather than using the native child path. Keep broker egress
226
+ The optional Kimi supervisor is selected only by its dedicated control
227
+ token/client. Without that explicit selection, CLI and Telegram Kimi jobs use
228
+ the native child path and caller/provider configuration. Keep broker egress
245
229
  restricted to official Kimi Code subscription/auth endpoints and do not provide
246
230
  a Moonshot API key, custom base URL, Docker socket, or ingress secret.
247
231
 
@@ -0,0 +1,77 @@
1
+ # Development container
2
+
3
+ `compose.dev.yaml` provides the persistent `app` service for Threadwire
4
+ development. It is separate from the production runtime definitions. The image
5
+ installs Codex CLI, Claude Code CLI, the official Kimi Code CLI, OpenCode,
6
+ `oc-codex-multi-auth`, `@ai-sdk/openai-compatible`, and `@ai-sdk/xai` during the
7
+ Docker build. Runtime setup never downloads a plugin through `npx`.
8
+
9
+ ## Source paths
10
+
11
+ All source paths in `.env` are required and have no checked-in host defaults:
12
+
13
+ - `DEV_HOME_PATH` is the complete dedicated development home. It remains the
14
+ owner of OpenCode database, session, log, and cache state.
15
+ - `GH_CONFIG_SOURCE_PATH` is the GitHub CLI config directory and mounts
16
+ read-only.
17
+ - `CODEX_AUTH_SOURCE_PATH` is native Codex's `auth.json` and mounts read-write
18
+ so OAuth refreshes persist to its canonical owner.
19
+ - `OPENCODE_AUTH_SOURCE_PATH` is OpenCode's native `auth.json`. DeepSeek, xAI,
20
+ and Moonshot AI keys are entered with OpenCode's auth flow and persist
21
+ through this read-write file mount.
22
+ - `OPENCODE_CODEX_STATE_SOURCE_PATH` is the plugin's dedicated state directory,
23
+ containing the OpenAI OAuth file and global account pool. It mounts
24
+ read-write as a directory because pool updates use atomic sibling-file
25
+ replacement.
26
+ - `AGENT_CONTEXT_PATH` is the immutable agent-context snapshot and mounts
27
+ read-only.
28
+
29
+ Hermes owns the canonical credential files. Do not copy them into the checkout,
30
+ the image, or project-keyed task-home files. The app sets
31
+ `CODEX_AUTH_PER_PROJECT_ACCOUNTS=0`, so `oc-codex-multi-auth` uses its one
32
+ global mounted account pool rather than creating per-project credential copies.
33
+
34
+ ## OpenCode catalog
35
+
36
+ The image-owned `/usr/local/share/threadwire-dev/opencode.json` is root-owned,
37
+ read-only, secret-free, and selected with OpenCode's supported
38
+ `OPENCODE_CONFIG` mechanism. It exposes:
39
+
40
+ - OpenAI OAuth through `oc-codex-multi-auth`: `gpt-5.6-sol`,
41
+ `gpt-5.6-terra`, and `gpt-5.6-luna`.
42
+ - DeepSeek through `@ai-sdk/openai-compatible` at
43
+ `https://api.deepseek.com/v1`: `deepseek-v4-pro` and
44
+ `deepseek-v4-flash`.
45
+ - xAI through `@ai-sdk/xai`: `grok-4.5`, `grok-build-0.1`, and
46
+ `grok-code-fast-1`.
47
+ - Kimi K3 through OpenCode's built-in `moonshotai` provider: `kimi-k3` with
48
+ its 1,048,576-token context, 131,072-token output limit, default `max`
49
+ reasoning effort, and explicit `low`, `high`, and `max` variants.
50
+
51
+ Provider keys never belong in Compose environment values or the image config.
52
+ Use `opencode auth login` to update the mounted OpenCode auth store, including
53
+ the canonical Moonshot credential under provider ID `moonshotai`. Direct
54
+ OpenCode invocation is limited to auth, `opencode models <provider>` discovery,
55
+ and bounded smoke checks. Threadwire remains the worker execution and streaming
56
+ boundary for development tasks.
57
+
58
+ ## Startup
59
+
60
+ Run the profile-free initializer before Docker creates the app container:
61
+
62
+ ```sh
63
+ docker compose -f compose.dev.yaml up --build home-init
64
+ docker compose -f compose.dev.yaml --profile app up --detach
65
+ ```
66
+
67
+ `home-init` runs as `1000:1000`, mounts only `DEV_HOME_PATH`, and precreates
68
+ every nested directory and file mount target. It receives no auth or context
69
+ mounts.
70
+
71
+ Run project commands in the persistent app service:
72
+
73
+ ```sh
74
+ docker compose -f compose.dev.yaml exec app npm ci
75
+ docker compose -f compose.dev.yaml exec app node --test test/container-dev.test.js
76
+ docker compose -f compose.dev.yaml exec app npm run lint
77
+ ```
@@ -1,7 +1,7 @@
1
1
  # Isolated provider runtime
2
2
 
3
3
  Relay-originated Codex writes are disabled unless the caller supplies both
4
- `--relay-write` and a reviewed `--workspace-profile`. The write path never
4
+ `--relay-write` and an explicitly selected supervisor. The write path never
5
5
  falls back to the native provider launcher.
6
6
  When the Telegram service is configured with isolated-runtime authority, Codex
7
7
  webhook jobs use the same preflight/run boundary exclusively; an unavailable
@@ -155,7 +155,15 @@ by that script. E2E uses host-volume canaries and fake-upstream authorization
155
155
  logs; worker self-report is not the sole assertion.
156
156
 
157
157
 
158
- ## Native Kimi Code subscription boundary
158
+ ## Optional isolated Kimi Code subscription boundary
159
+
160
+ Normal `threadwire run --provider kimi` is native execution: it directly runs
161
+ the caller-provided Kimi executable at `/usr/local/bin/kimi`, or the path in
162
+ `THREADWIRE_KIMI_BIN`. It does not select this isolated supervisor, broker, or
163
+ model allowlist. The checked-in standard `runtime` image installs Codex only,
164
+ so native Kimi needs a caller-supplied executable and fails normally when it is
165
+ missing. The remaining section documents the separately configured, optional
166
+ isolated Kimi supervisor.
159
167
 
160
168
  Kimi binding v1 is separate from the Codex worktree contract. A Kimi preflight contains `binding`, never `repositoryRoot` or `cwd`. The supervisor queries the task-DinD daemon for exactly one labelled source volume, context volume, and running lease, rejects ambiguity/RW context references, validates the lease and worker image, then uses an offline two-volume validator to check the real Git revision and complete immutable context manifest. Only after that does it create the per-run data plane on the task daemon: a private internal worker network, a task-owned non-internal egress network, one credential-free relay attached to both, and a three-volume Kimi worker attached only to the internal network. OAuth remains broker-only; workers receive a synthetic per-run grant and no raw binding, volume, lease, control, or OAuth value. Kimi v6 state seals include the binding digest, source/context identities, source mount mode, dynamic UID/GID/workdir, validator, worker, relay, relay image, both networks, the central endpoint identity, state, and image. Old path-based and pre-relay Kimi state is intentionally not adopted.
161
169
 
@@ -226,7 +234,8 @@ Its fixed entrypoint owns `--model`, optional exact `--session`, `--prompt`,
226
234
  `--output-format stream-json`, and an empty skills directory. Static deny rules,
227
235
  workspace/config ancestry checks, a sanitized private home, read-only root,
228
236
  and the existing mount/inode checks remain mandatory. There is no native unsafe
229
- fallback and no `THREADWIRE_KIMI_BIN` control.
237
+ fallback within the explicitly selected isolated path. Normal Kimi execution
238
+ uses the native adapter and may use `THREADWIRE_KIMI_BIN`.
230
239
 
231
240
  Only exact pinned stream records are normalized: assistant string content,
232
241
  validated tool name/key start and finish, and one exact
@@ -263,7 +272,8 @@ such as:
263
272
  export THREADWIRE_ALLOWED_KIMI_MODELS='{"default":{"model":"kimi-for-coding","protocol":"kimi"},"kimi-code/kimi-for-coding":{"model":"kimi-for-coding","protocol":"kimi"}}'
264
273
  ```
265
274
 
266
- `default` is the server-owned selection for no-argument and Telegram jobs.
275
+ `default` is the server-owned selection for isolated supervisor jobs without a
276
+ model argument.
267
277
  `kimi-code/kimi-for-coding` is the exact official provider-qualified alias for
268
278
  explicit callers. Each key is an opaque allowlisted value, not a path, and
269
279
  Threadwire never normalizes or strips its prefix. The `model` field is the
@@ -272,8 +282,8 @@ alias.
272
282
 
273
283
  OAuth writes only the named `threadwire-kimi-oauth` volume at the official
274
284
  `credentials/kimi-code.json` location. Stop the broker during interactive auth
275
- so the CLI and broker cannot race a refresh, then run the opt-in, workspace-free
276
- auth service:
285
+ so the auth CLI and broker cannot race a refresh, then run the opt-in,
286
+ workspace-free auth service:
277
287
 
278
288
  ```sh
279
289
  docker compose -f compose.yaml -f compose.relay-write.yaml stop kimi-isolated-runtime kimi-model-broker
@@ -288,20 +298,12 @@ The auth service has no workspace, ingress network, Docker socket, Threadwire
288
298
  control token, or Telegram credential and uses Docker logging driver `none`.
289
299
  `verify <provider-qualified-alias>` discards native output and prints only a
290
300
  fixed success/failure; use it to confirm each exact official alias before
291
- adding that alias and its independently verified wire model to the server
292
- allowlist. An explicit CLI job selects the exact key as one argv value:
293
-
294
- ```sh
295
- node bin/threadwire.js run \
296
- --provider kimi \
297
- --target telegram:-1001234567890:42 \
298
- --workspace-profile container-runtime \
299
- --prompt 'Inspect the project.' \
300
- -- --model 'kimi-code/kimi-for-coding'
301
- ```
301
+ adding that alias and its independently verified wire model to the isolated
302
+ supervisor allowlist. This operator configuration does not configure or select
303
+ the native CLI Kimi model.
302
304
 
303
- API keys, Moonshot platform mode, custom base URLs, discovery by ordinary jobs,
304
- and arbitrary aliases are unsupported.
305
+ For the isolated supervisor, API keys, Moonshot platform mode, custom base
306
+ URLs, discovery by ordinary jobs, and arbitrary aliases are unsupported.
305
307
 
306
308
  For logout or account rotation, stop the Kimi runtime and broker, run
307
309
  `--profile kimi-auth run --rm kimi-auth logout` or `login`, verify approved
@@ -322,7 +324,8 @@ payload, or synthetic grant.
322
324
 
323
325
  Rollback by stopping `kimi-isolated-runtime` and `kimi-model-broker`, removing
324
326
  the `kimi` Compose profile/control variables, and reverting the Threadwire image.
325
- Kimi commands then fail closed; Codex behavior is unaffected. Preserve the OAuth
327
+ Isolated Kimi requests then fail closed; native Kimi remains independent of
328
+ that optional deployment. Codex behavior is unaffected. Preserve the OAuth
326
329
  and Kimi state volumes for a recoverable rollback. Remove them only as an
327
330
  explicit credential/session teardown after OAuth revocation; never archive them
328
331
  with workspace backups.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadwire",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Stream Codex, Claude, Kimi Code, and OpenCode worker progress to an explicit Telegram destination",
5
5
  "keywords": [
6
6
  "ai-agent",
@@ -20,8 +20,7 @@
20
20
  "docs/",
21
21
  "scripts/",
22
22
  "src/",
23
- "TELEGRAM-INGRESS.md",
24
- "threadwire.workspace-profiles.json"
23
+ "TELEGRAM-INGRESS.md"
25
24
  ],
26
25
  "bin": {
27
26
  "threadwire-isolated-runtime": "bin/isolated-runtime.js",
@@ -58,15 +58,11 @@ threadwire_relay_arguments=(run --provider "$THREADWIRE_PROVIDER" --target "$THR
58
58
  case "${THREADWIRE_RELAY_WRITE:-0}" in
59
59
  0) ;;
60
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
61
  if [[ "$THREADWIRE_PROVIDER" != codex ]]; then
66
62
  printf '%s\n' 'threadwire: relay write capability is only supported for codex' >&2
67
63
  exit 2
68
64
  fi
69
- threadwire_relay_arguments+=(--workspace-profile "$THREADWIRE_WORKSPACE_PROFILE" --relay-write)
65
+ threadwire_relay_arguments+=(--relay-write)
70
66
  ;;
71
67
  *)
72
68
  printf '%s\n' 'threadwire: relay write capability must be enabled with THREADWIRE_RELAY_WRITE=1' >&2