feat(kimi): Kimi K3 provider on the official kimi-code CLI (#713)

* feat(kimi): Kimi K3 provider on the official kimi-code CLI (Wave 1)

ModelProvider.KIMI routes through KimiCliProvider driving Moonshot's kimi
CLI on a Kimi subscription (OAuth device-code, no metered key). One-shot
delivery roles only (V1), interactive ban wired in both guard lists.

Auth: one shared RW auth mount; containers symlink credentials/ and
oauth/ (the CLI's cross-process refresh-lock dir) into a container-local
KIMI_CODE_HOME so every container and the host redeem the SAME rotating
refresh chain - live-verified that per-copy chains cross-invalidate after
the reuse-grace window. No orchestrator refresh daemon; an expires_at
preflight exits 78.

Config renderer mirrors the login-managed provider/model blocks
field-for-field (live-captured; the model value is the CLI-side name,
never the raw API id), plus per-role deny rules and the bash-guard as a
PreToolUse hook via a wrapper script (an env key on a hooks entry makes
the CLI silently drop ALL hooks - live-verified). Usage capture sums
wire.jsonl usage.record 4-bucket events; sniff classifies rate-limit/auth
from structured error text only, mapped to the shared 75/78 park
contract. Image installs the CLI latest-at-build (no version pin, by
policy) with the resolved version stamped as provenance, binary split to
/usr/local away from mutable state.

Migrations 090 (enum) + 091 (provider seed); catalog, pricing, routing
mode, and orchestrator park/usage wiring mirror the codex integration.

* feat(kimi): surface sweep + fleet-wide pin drop (Wave 2)

Compose x3 gain the agent-kimi-image service and the orchestrator's
read-write ~/.kimi-code mount + kimi-usage dir; .env.example documents
the Kimi block. Panel mirrors ModelProvider.KIMI and adds the kimi
routing mode (catalog filter, mode button, mix-picker group, badge) with
tests; provider routes gain the kimi remediation entry. CLAUDE.md and
docs/map document the runtime. Per the no-pins policy, agent-grok/
gemini/codex Dockerfiles drop their version pins for latest-at-build
with resolved-version provenance stamps (grok resolves 0.2.112 vs the
old 0.2.56 pin - verified by real builds of all four images).

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-29 01:48:55 +02:00
committed by GitHub
co-authored by Renn F
parent eb470dfb33
commit 6374bbbed0
43 changed files with 3907 additions and 110 deletions
+31 -2
View File
@@ -1,5 +1,5 @@
## Purpose
This slice is the agent-runtime + LLM-provider seam plus the in-container agent SDK. The provider layer (roboco/llm/providers/) abstracts how agents are spawned/stopped/health-checked/removed across LLM backends (Claude Code default, Grok CLI, Codex CLI, Gemini CLI) behind an AgentProvider ABC + ProviderRegistry, with a Grok auth-token refresh loop keeping the SuperGrok credential live and an orchestrator-side Codex refresh loop keeping the ChatGPT-subscription credential live (Gemini needs neither — its OAuth refresh token is reusable, so each container refreshes its own local copy in-process). The agent SDK (roboco/agent_sdk/) is the FastAPI sidecar running inside every agent container handling A2A messaging, tool-budget/loop/verb-circuit breakers, token-usage capture, and the interactive intake/secretary chat drivers (Claude SDK + Grok CLI — Codex and Gemini are one-shot delivery roles only, no interactive intake/secretary support). The runtime helpers (spawn_manifest, streaming, transcript_retention) build the per-role tool manifest, wire reasoning-stream callbacks, and select old agent transcripts to prune.
This slice is the agent-runtime + LLM-provider seam plus the in-container agent SDK. The provider layer (roboco/llm/providers/) abstracts how agents are spawned/stopped/health-checked/removed across LLM backends (Claude Code default, Grok CLI, Codex CLI, Gemini CLI, Kimi CLI) behind an AgentProvider ABC + ProviderRegistry, with a Grok auth-token refresh loop keeping the SuperGrok credential live and an orchestrator-side Codex refresh loop keeping the ChatGPT-subscription credential live (Gemini needs neither — its OAuth refresh token is reusable, so each container refreshes its own local copy in-process; Kimi also needs no orchestrator daemon, but for the opposite reason — its refresh is rotation-with-short-reuse-grace, so every container shares ONE host-mounted rotating chain instead of refreshing a private copy). The agent SDK (roboco/agent_sdk/) is the FastAPI sidecar running inside every agent container handling A2A messaging, tool-budget/loop/verb-circuit breakers, token-usage capture, and the interactive intake/secretary chat drivers (Claude SDK + Grok CLI — Codex, Gemini, and Kimi are one-shot delivery roles only, no interactive intake/secretary support). The runtime helpers (spawn_manifest, streaming, transcript_retention) build the per-role tool manifest, wire reasoning-stream callbacks, and select old agent transcripts to prune.
## Files
@@ -31,6 +31,10 @@ This slice is the agent-runtime + LLM-provider seam plus the in-container agent
| roboco/llm/providers/gemini.py | GeminiCliProvider: spawns roboco-agent-gemini container, copies host ~/.gemini OAuth creds into a container-local writable copy + usage dir + gemini env | 264 |
| roboco/llm/providers/gemini_cli_config.py | Entrypoint renderer: mcp-config -> ~/.gemini/settings.json + per-role TOML Policy Engine deny rules (no native tool-removal flag), GEMINI.md blueprint | 293 |
| roboco/llm/providers/gemini_cli_usage.py | Capture token usage from gemini --output-format stream-json terminal result event -> usage.json (per-GA-model pricing); remaps quota/rate-limit errors to exit 75 | 290 |
| roboco/llm/providers/kimi.py | KimiCliProvider: spawns roboco-agent-kimi container, mounts host ~/.kimi-code dir (RW, shared credential chain) at a fixed staging path + usage dir + kimi env | 266 |
| roboco/llm/providers/kimi_cli_config.py | Entrypoint renderer: renders login-managed config.toml provider/model/service blocks as constants + per-role [[permission.rules]] deny set + bash-guard [[hooks]] wiring, mcp.json (near-passthrough of Claude's schema), AGENTS.md; also the `--check` auth preflight CLI (no separate kimi_auth.py — no orchestrator refresh daemon exists) | 441 |
| roboco/llm/providers/kimi_cli_usage.py | Capture token usage from wire.jsonl usage.record events -> usage.json (real inputOther/output/inputCacheRead/inputCacheCreation 4-bucket split, priced per-bucket); session id from stdout resume_hint, falling back to newest on-disk session dir | 280 |
| roboco/llm/providers/kimi_cli_sniff.py | Classify a kimi run's terminal state (rate_limit/auth/none) from ONLY a structured error field off any JSONL event + stderr, never the model's own echoed assistant/tool content | 150 |
| roboco/agent_sdk/__init__.py | Package docstring only | 10 |
| roboco/agent_sdk/models.py | Pydantic models: A2A messages, budget/terminal/verb-circuit/token-usage request+status | 258 |
| roboco/agent_sdk/prompt_guard.py | Prompt-injection detector (5 patterns) + CLI for grok entrypoint turn scan | 93 |
@@ -95,6 +99,21 @@ This slice is the agent-runtime + LLM-provider seam plus the in-container agent
| extract_model_stats / usage_and_cost (gemini) | functions | roboco/llm/providers/gemini_cli_usage.py:130 / 146 | Reads the run's own `--output-format stream-json` terminal `result` event for per-model FLAT token stats — no session-file scraping — and prices each of the three GA models (`gemini-2.5-pro`/`-flash`/`-flash-lite`) at its own rate |
| classify_exit_code (gemini) | function | roboco/llm/providers/gemini_cli_usage.py:232 | Remaps a quota/rate-limit error (no dedicated CLI exit code — parsed from the run's JSON `error.type`) to exit 75, mirroring codex/grok's park signal; exit 41 (the CLI's own auth-failure code) passes straight through |
| gemini_cli_usage.main | function | roboco/llm/providers/gemini_cli_usage.py:268 | Entrypoint: write usage.json for the run |
| KimiCliProvider | class | roboco/llm/providers/kimi.py:135 | Spawns roboco-agent-kimi container: reuse shared mount/auth/git assembly, blank provider routing fields, add the kimi RW auth mount + usage mount + env |
| KimiCliProvider._append_kimi_auth_mount | staticmethod | roboco/llm/providers/kimi.py:197 | Mount host ~/.kimi-code DIRECTORY RW to a fixed staging path (/home/agent/.kimi-code-auth) — RW because Moonshot's refresh token is rotation-with-short-reuse-grace, not truly reusable like gemini's; warn (never fail) if credentials/kimi-code.json is missing |
| KimiCliProvider._append_usage_mount | staticmethod | roboco/llm/providers/kimi.py:224 | Mount per-agent data dir so the entrypoint writes usage.json the orchestrator reads at finalize |
| permission_rules_for_role | function | roboco/llm/providers/kimi_cli_config.py:226 | Per-role [[permission.rules]] deny set (fleet-wide subagent/web/cron/skill deny + Bash for non-bash roles + Write/Edit for non-author roles + the git-mutation/destructive/raw-package-manager prefix set for bash roles) — kimi has no CLI-flag tool-removal equivalent to grok's --disallowed-tools |
| kimi_hooks_config | function | roboco/llm/providers/kimi_cli_config.py:251 | Build the [[hooks]] TOML entry pointing at kimi-bash-guard-wrapper.sh (not the hook script directly) — a kimi hooks entry silently drops the WHOLE section on an extra field like env, so ROBOCO_GUARD_SKIP_GIT=1 rides the wrapper's own export instead |
| render_config_toml (kimi) | function | roboco/llm/providers/kimi_cli_config.py:278 | Renders the login-managed [providers."managed:kimi-code"]/[models."kimi-code/<alias>"]/[services.moonshot_*] blocks as constants (not read off any mount) + telemetry/upgrade knobs + permission rules + hooks |
| render_mcp_json | function | roboco/llm/providers/kimi_cli_config.py:322 | Near-passthrough translation of the mounted Claude Code mcp-config.json into kimi's mcp.json — Claude-identical mcpServers schema, unlike grok's TOML or codex's config.toml translation |
| write_agents_md (kimi) | function | roboco/llm/providers/kimi_cli_config.py:344 | Install the composed role blueprint as ~/.kimi-code/AGENTS.md (grok's proven additive-instruction-file mechanism; SYSTEM.md would fully replace kimi's own built-in prompt and is deliberately not used) |
| is_valid / seconds_until_expiry (kimi) | functions | roboco/llm/providers/kimi_cli_config.py:399 / 383 | The `--check` auth preflight backstop (entrypoint refuses to start on a missing/expired credentials/kimi-code.json) — folded into this renderer rather than a separate kimi_auth.py, since no orchestrator-side refresh loop exists for Kimi |
| kimi_cli_config.main | function | roboco/llm/providers/kimi_cli_config.py:416 | Entrypoint renderer: config.toml + mcp.json + AGENTS.md, or `--check` auth preflight |
| extract_error_text / is_rate_limited / is_auth_failure / classify (kimi) | functions | roboco/llm/providers/kimi_cli_sniff.py:59-120 | Classifies a kimi run's terminal state (rate_limit/auth/none) from ONLY a structured error field off any JSONL event + stderr — the model's own echoed assistant/tool content can never reach the classifier, mirroring codex_cli_sniff's false-positive-safe design |
| session_id_from_run_log / resolve_session_dir (kimi) | functions | roboco/llm/providers/kimi_cli_usage.py:76 / 128 | Session id from the run's own terminal stdout resume_hint event (no file scraping for the id, unlike grok); falls back to the newest session dir under the workdir-keyed sessions/wd_<cwd-basename>_*/ glob |
| aggregate_usage_from_wire | function | roboco/llm/providers/kimi_cli_usage.py:164 | Sums wire.jsonl usage.record events' real inputOther/output/inputCacheRead/inputCacheCreation 4-bucket split — already-disjoint buckets, unlike codex's cached_input_tokens subset |
| capture_run_usage (kimi) | function | roboco/llm/providers/kimi_cli_usage.py:195 | Entrypoint: write the grok-shaped usage.json for the run |
| kimi_cli_usage.main | function | roboco/llm/providers/kimi_cli_usage.py:249 | Entrypoint: write usage.json for the run |
| refresh_if_stale | function | roboco/llm/providers/grok_auth.py:307 | Mint fresh access token from refresh_token grant if expiry within skew; acquires _refresh_lock then delegates to _recheck_or_refresh to prevent concurrent double-rotation; returns fresh/refreshed/missing/no_refresh_token/failed (best-effort, never raises) |
| _recheck_or_refresh | function | roboco/llm/providers/grok_auth.py:283 | Locked body of refresh_if_stale: re-load bundle + re-check staleness inside _refresh_lock, then call _do_refresh if still stale (prevents concurrent double-rotation of the single-use refresh grant, #94) |
| _atomic_write | function | roboco/llm/providers/grok_auth.py:138 | Rewrite auth.json atomically (tmp+replace) with direct-write fallback so a rotated single-use refresh_token is never lost (F006) |
@@ -173,7 +192,9 @@ The streaming.py callback is set once at bootstrap (websocket_bridge) and invoke
**Gemini spawn/refresh.** `ProviderRegistry` also registers `ModelProvider.GEMINI``GeminiCliProvider`. Spawn stages the host `~/.gemini` (from a one-time interactive `gemini` login, `ROBOCO_HOST_GEMINI_DIR`) read-only, then the entrypoint COPIES it into a container-local writable `~/.gemini` so the CLI's own in-process token refresh (google-auth-library) can write back locally without ever touching the host copy — Google's refresh token is REUSABLE (unlike grok's single-use one), so each container refreshing its own copy independently is safe with NO orchestrator refresh daemon for Gemini at all (a deliberate contrast the module docstring spells out against grok/codex). Tool scoping is expressed entirely through a rendered TOML Policy Engine (`~/.gemini/policies/roboco.toml`, deny-only rules keyed by `toolName`/`commandPrefix`) plus `settings.json` (`experimental.enableAgents=false` fleet-wide subagent ban); `--approval-mode yolo` is universal headless auto-approval. `gemini_cli_usage.py` reads the run's own `--output-format stream-json` terminal `result` event for per-model FLAT token stats (no session-file scraping), prices each of the three GA models at its own rate, and remaps a quota/rate-limit error (parsed from the run's JSON `error.type`, no dedicated CLI exit code) to exit 75 — exit 41 (the CLI's own auth-failure code) passes straight through. Both Codex and Gemini are one-shot delivery-role runtimes only in this release — no interactive Intake/Secretary support (only Claude and Grok drive those chats).
**Interactive-role exemption (post-finale sweep, #661).** GLOBAL/ROLE routing rows on `OPENAI`/`GEMINI` are not-applicable to `intake-1`/`secretary-1` — those two agents stay on Anthropic regardless of a fleet-wide mode switch to Codex or Gemini, since neither provider supports the interactive chat driver; an explicit `AGENT_SLUG` pin attempting to route either of them onto `OPENAI`/`GEMINI` is refused loudly by the spawn guard rather than silently spawning a broken interactive session.
**Kimi spawn/refresh.** `ProviderRegistry` also registers `ModelProvider.KIMI``KimiCliProvider`. Unlike codex (RO directory) and gemini (RO staged + local copy), the host `~/.kimi-code` (`ROBOCO_HOST_KIMI_DIR`, from a one-time `kimi login`) is mounted **read-write** and SHARED across every container plus the orchestrator — Moonshot's refresh token is rotation-with-short-reuse-grace, not truly reusable (live-verified: two isolated per-container copies of one credential snapshot eventually cross-invalidated each other and the CLI wiped the stored tokens outright). The entrypoint keeps a container-local writable `~/.kimi-code` for config.toml/mcp.json/AGENTS.md (rendered fresh each spawn) but symlinks only `credentials/` and `oauth/` (the lock dir) in from the shared RW mount, so every container redeems the SAME chain and the CLI's own cross-process lock (`oauth/kimi-code.lock`) serializes refreshes — still **no orchestrator refresh daemon** (the CLI refreshes itself, same "no daemon" outcome as gemini but for the opposite structural reason). `kimi login`'s managed config.toml blocks (`[providers."managed:kimi-code"]`/`[models."kimi-code/<alias>"]`/`[services.moonshot_*]`) are account-fixed and rendered as constants by `kimi_cli_config.py` rather than read off any mount (the symlink step deliberately does not carry the host's own config.toml forward). Tool scoping is the rendered `[[permission.rules]]` deny-first array (no CLI-flag tool-removal equivalent, like gemini's TOML policy engine); the same `bash-guard-hook.sh` is wired as a `[[hooks]]` entry via a wrapper script, since a kimi hooks entry silently drops the whole section on an extra field like `env`. Kimi has no exit-code taxonomy for `-p` either (a claimed 75/1 split is unverified noise) — `kimi_cli_sniff.py` classifies from ONLY a structured `error` field off any JSONL event + stderr, the same false-positive-safe design as `codex_cli_sniff.py`. `kimi_cli_usage.py` sums `wire.jsonl`'s real 4-bucket usage split (a genuine disjoint split like codex's, unlike grok's output-only fallback).
**Interactive-role exemption (post-finale sweep, #661; extended to Kimi).** GLOBAL/ROLE routing rows on `OPENAI`/`GEMINI`/`KIMI` are not-applicable to `intake-1`/`secretary-1` — those two agents stay on Anthropic regardless of a fleet-wide mode switch to Codex, Gemini, or Kimi, since none of the three providers supports the interactive chat driver; an explicit `AGENT_SLUG` pin attempting to route either of them onto `OPENAI`/`GEMINI`/`KIMI` is refused loudly by the spawn guard rather than silently spawning a broken interactive session.
## Mermaid
```mermaid
@@ -330,6 +351,9 @@ runtime-providers
- CODEX_HOME (codex_auth.default_auth_path)
- ROBOCO_HOST_GEMINI_DIR (gemini.py host `~/.gemini` staging mount source, from a one-time interactive `gemini` login)
- ROBOCO_GEMINI_CLI_MODEL (pins one of the three GA ids: `gemini-2.5-pro`/`-flash`/`-flash-lite`)
- ROBOCO_HOST_KIMI_DIR (kimi.py host `~/.kimi-code` RW shared-mount source, from a one-time `kimi login`)
- ROBOCO_KIMI_CLI_MODEL (login-managed alias, default `kimi-code/k3`; `kimi-code/kimi-for-coding` is the cost lever)
- ROBOCO_KIMI_RATE_LIMIT_RETRY_AFTER_SECONDS / ROBOCO_KIMI_AUTH_RETRY_AFTER_SECONDS (park-and-retry delays, gemini's tunable-Settings-field pattern rather than codex's hardcoded constants)
## Gotchas
@@ -354,6 +378,8 @@ runtime-providers
- gemini's refresh token is REUSABLE (unlike grok/codex) — this is the one structural asymmetry in the whole provider family: no orchestrator-side refresh daemon exists for Gemini at all, by design, because each container can safely refresh its own local copy independently without a single writer serializing rotation.
- codex has NO exit-code taxonomy — every failure exits 1, so `codex_cli_sniff.py` must classify from structured JSONL `error.message` fields only; it deliberately never inspects the model's own transcript text, since this repo's own prompts legitimately use phrases like "quota-limited" that would false-positive a transcript-text classifier.
- gemini's quota/rate-limit signal has no dedicated CLI exit code either — `gemini_cli_usage.classify_exit_code` parses the run's own JSON `error.type` and remaps to exit 75 (the same park signal grok's 429 and codex's rate-limit sniff produce), so the orchestrator's park-and-probe loop treats all three providers identically at that seam despite three different underlying signals.
- kimi's refresh token is rotation-with-short-reuse-grace, NOT truly reusable like gemini's — a first probe (two isolated copies redeeming the same token ~90s apart) looked reusable (a grace window), but the ORIGINAL credential home redeeming that same token ~40min later was refused as reuse-after-grace and the CLI wiped the stored credentials outright (empty-string tokens). The corrected design (a shared RW mount + symlinked-in `credentials/`+`oauth/`, one chain, the CLI's own cross-process lock serializing redemptions) avoids the cross-invalidation the naive per-container-copy design (gemini's shape) would have hit in production.
- kimi's `[[hooks]]` TOML entry silently drops the WHOLE hooks section on an unrecognized field (an `env` key produced a bare "Ignored invalid config" warning, run continues with NO hooks installed at all) — a live-verified, undocumented CLI behavior; `kimi_cli_config.py` routes anything a hook needs (`ROBOCO_GUARD_SKIP_GIT=1`) through a wrapper script's own `export` instead of a hooks `env` block to avoid tripping this.
## Drift from CLAUDE.md
@@ -377,6 +403,8 @@ runtime-providers
>
> **Post-finale completeness sweep (#661, `d4b7e1e7`).** Closes gaps found after the Codex/Gemini rollout: one-click `apply_mode` entries + panel mode cards for both new providers (`docs/map/support-services.md`); the interactive-role exemption (GLOBAL/ROLE rows on OPENAI/GEMINI are not-applicable to `intake-1`/`secretary-1`, an explicit AGENT_SLUG pin onto either is refused loudly by the spawn guard rather than silently breaking the interactive chat driver); compose env passthrough for `ROBOCO_GUARD_TRUSTED_CHAIN_PEERS` and `ROBOCO_TASK_BUDGETS_ENABLED` (the provider host-mount dirs were already wired by the provider PRs themselves); and a `gt=0` validation tightening on the task/project budget fields (`docs/map/gateway-support.md`, `docs/map/orchestrator.md`) so a `0` budget — which would silently block everything — is rejected outright.
>
> **Kimi CLI provider.** New `kimi.py`/`kimi_cli_config.py`/`kimi_cli_usage.py`/`kimi_cli_sniff.py` add `ModelProvider.KIMI` running Moonshot AI's official `kimi` (kimi-code) CLI on a Kimi subscription — `ProviderRegistry` now registers KIMI alongside GROK/OPENAI/GEMINI. No `kimi_auth.py`: the D2 spike found Moonshot's refresh token is rotation-with-short-reuse-grace rather than truly reusable, so the design landed on ONE shared RW host mount + symlinked-in `credentials/`+`oauth/` (the CLI's own cross-process lock serializes redemptions) instead of either grok/codex's orchestrator-refreshed single-use pattern or gemini's per-container-reusable-copy pattern — no orchestrator daemon either way, but for a different structural reason than gemini's. Fleet-wide alongside this: the existing grok/gemini/codex Dockerfiles drop their version pins (latest-at-build, always adapt — a CEO decision record) and gain the same resolved-version provenance stamp (`/etc/<cli>-version` + a `<cli>.cli.pinned="false"` label) kimi's own Dockerfile establishes.
>
> **v0.18.0** (2026-07-04): Fable mode's grok side — `fable_honesty_nudge_hook_config`/`write_grok_fable_hooks` (grok_cli_config.py:308-345), gated by `fable_mode_enabled` (default off). Deliberately narrower than the Claude path's 5 hooks: only the never-denying PostToolUse honesty-nudge is ported, because a grok `PreToolUse`/`Stop` hook deny cancels the entire run (verified live) — the same asymmetry this file's Gotchas section already documents for the bash-guard's git-deny-vs-exfil-cancel split.
## Regression Risks
@@ -391,6 +419,7 @@ runtime-providers
| Missing auth.json now logs warning but still spawns doomed container | roboco/llm/providers/grok.py:185 | When ~/.grok/auth.json is absent, _append_grok_auth_mount now only logs a warning (was: silently skipped the mount). The spawn still proceeds and the container exits 78 at the entrypoint --check. Behavior of the spawn path is unchanged, but an operator who does not tail logs will still diagnose a later exit-78; the warning is only useful if someone reads it. | low |
| codex_cli_sniff classifies from error.message text only | roboco/llm/providers/codex_cli_sniff.py:94 | Codex has no exit-code taxonomy, so every quota/rate-limit/auth distinction rests entirely on the shape of a structured JSONL `error.message` field. If a future codex CLI version changes that field's wording or moves the signal elsewhere, the sniff silently falls through to "none" and the orchestrator crash-retries straight back into the same rate limit instead of parking the provider — the same failure mode the overload-break feature exists to prevent for the other providers. | medium |
| Gemini has no orchestrator-side refresh daemon at all | roboco/llm/providers/gemini.py:135 | The reusable-refresh-token design is safe under the CURRENT assumption (each container's local copy refreshes independently, no shared writer to serialize). If Google ever changes the refresh grant to single-use (matching grok/codex), every container refreshing concurrently would race to burn the same grant with no lock protecting it, unlike grok_auth/codex_auth's process-wide lock — this provider has no equivalent safety net because none was ever needed under the current contract. | low |
| Kimi's shared RW auth mount widens exposure vs a per-agent copy | roboco/llm/providers/kimi.py:207 | Every Kimi agent container mounts the SAME host `~/.kimi-code` directory read-write (not a per-agent copy or an RO staging mount like codex/gemini) so every container can redeem the one rotating refresh chain. A container that could escape its own sandbox could read or corrupt the shared credential state for every OTHER Kimi agent and the host — the tradeoff the D2 spike accepted after the naive per-container-copy design was found to cross-invalidate tokens in production. Mitigated only by container sandboxing, not by the mount itself. | medium |
## Health
This slice is coherent and well-factored: the provider ABC + registry cleanly isolates the Grok backend while the Anthropic/Ollama/LOCAL paths stay on the built-in spawn (additive seam, no destabilization), and the agent_sdk sidecar centralizes budget/loop/verb-circuit/token state that hooks share. The single baseline-to-HEAD commit (15effce0) landed three genuine hardening fixes — the F006 refresh_token-loss guard with direct-write fallback, the JWT-exp decode so a refreshed token isn't forever rejected, and the /usage/sync path-traversal guard — plus the grok directory-mount fix that resolves the inode-pinning hang. The main integrity concerns are operational rather than structural: the grok directory mount widens RO exposure to host grok state, the 6h expires_at default can burn the single-use refresh_token on the rare double-miss, the in-process SDK state is lost on every container restart (by design, but means verb-circuit/budget counters reset), and ClaudeCodeProvider is dead reference code whose 'default' label in CLAUDE.md is misleading. Interactive intake/secretary parity between Claude and Grok is real (shared IntakeDriver, only the SessionFactory differs). No obviously broken logic was introduced; the regression risks are edge-case behavior shifts, not holes. Recommend re-running the grok auth refresh test against a token that omits expires_in to confirm the JWT-exp path, and a /usage/sync test with a symlinked transcript to confirm the new guard fails loud where appropriate.