feat(fleet): Kimi as a microVM backend — the URL settled by measurement

The base URL took three measurements to find, and the first two were wrong in
instructive ways.

`api.moonshot.ai/anthropic/v1/messages` EXISTS and speaks the protocol — it
answers with Moonshot's own structured error rather than a 404. It also rejects
an `sk-kimi-` key, because it belongs to the platform.moonshot.ai account
namespace. Two endpoints that both "work" for different accounts is precisely
the shape that makes a guessed URL look like a broken key, and it is why this
was refused rather than guessed for as long as it was.

The Kimi CODE service is the one an `sk-kimi-` key belongs to:
`POST https://api.kimi.com/coding/v1/messages` returns a real Anthropic Messages
body — `msg_` id, `content` blocks, a `thinking` block with a signature. So
`ANTHROPIC_BASE_URL=https://api.kimi.com/coding`, WITHOUT the `/v1`: Claude Code
appends `/v1/messages` itself, and `/v1/v1/messages` would 404 in a way that
reads as a broken image rather than a bad URL.

Two more measured, each otherwise a silent failure at the first turn:
`Authorization: Bearer` is accepted (so ANTHROPIC_AUTH_TOKEN is the right
injection channel), and a `claude-*` model id is ACCEPTED AND ANSWERED — Kimi
maps it onto `kimi-for-coding` exactly as z.ai does, so no ANTHROPIC_MODEL
override is needed.

Claude Code rather than Moonshot's own `kimi` CLI, deliberately. The mission
harness is Claude-Code-shaped throughout: `--agents` JSON roles, the verifier's
tool allowlist, the `Stop` hook behind the completion gate, the per-subagent
transcripts counted as delegation evidence. `kimi` has none of those flags — its
equivalents are TOML files and markdown agent dirs — so using it would mean a
second executor with its own untested failure modes.

TWO STALE MAPS, caught by the rootfs harness refusing to bless the image: both
`fc-build-rootfs.sh` and the node's `required_cli` expected backend `kimi` to
contain Moonshot's `kimi` binary. That assumption predates the measurement, and
it failed a rootfs that was correct. Both now say `claude` for glm and kimi
alike — the binary is the same in all three images; only the endpoint differs.

Egress for `kimi` is `api.kimi.com` alone: not moonshot.ai (wrong namespace),
not z.ai, not Anthropic. Asserted both ways, like the other two.

The image and rootfs are built on tank and the rootfs passes all four checks
(boots, git, writable /mission, `claude --version`). 534 tests, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-06 17:52:45 -07:00
co-authored by Claude Opus 5
parent d3a53e7bf1
commit 742724e53c
5 changed files with 109 additions and 16 deletions
+10 -5
View File
@@ -214,10 +214,15 @@ pub fn available_backends() -> Vec<String> {
fn required_cli(backend: Option<&str>) -> Option<(&'static str, &'static str)> {
match backend {
Some("claude") => Some(("claude", "claude --version")),
Some("kimi") => Some(("kimi", "kimi --version")),
// GLM is Claude Code pointed at z.ai's anthropic-compatible endpoint;
// the binary in the image is still `claude`.
Some("glm") => Some(("claude", "claude --version")),
// GLM and Kimi are both Claude Code pointed at another provider's
// Anthropic-compatible endpoint (z.ai, and api.kimi.com/coding), so the
// binary in all three images is `claude`.
//
// `kimi` used to expect Moonshot's own `kimi` CLI here. That was written
// before the endpoint was measured, and it failed the selftest of a
// rootfs that was in fact correct — the image runs Claude Code because
// the whole mission harness is Claude-Code-shaped.
Some("glm") | Some("kimi") => Some(("claude", "claude --version")),
_ => None,
}
}
@@ -949,7 +954,7 @@ mod tests {
/// from the wrong image passes the selftest by saying nothing.
#[test]
fn every_per_cli_backend_declares_the_cli_it_must_contain() {
for (backend, want) in [("claude", "claude"), ("kimi", "kimi"), ("glm", "claude")] {
for (backend, want) in [("claude", "claude"), ("kimi", "claude"), ("glm", "claude")] {
let (cli, probe) = required_cli(Some(backend))
.unwrap_or_else(|| panic!("backend {backend} requires no CLI"));
assert_eq!(cli, want, "backend {backend}");