feat(runtime): run agents on the subscription via the real claude binary
ci / gates (push) Failing after 5s
ci / rust (push) Skipped
ci / frontend (push) Skipped
ci / e2e (push) Skipped
ci / publish (push) Skipped

provider_alias_for now resolves Claude models to `claude_cli.default`,
which spawns the actual `claude` binary, instead of `anthropic.default`,
which posts to the raw API with Claude Code identity headers. Agent work
is ~99% of tokens, so this moves essentially all of it onto the Max
subscription and onto the supported client.

The judge deliberately stays on the API key. If both rode one credential,
a single subscription limit would blind the verifier at exactly the
moment there is most to verify; this way a throttle degrades missions but
verification keeps working.

Runtime config (applied on gw-04, reloaded via loopback — remote admin
reload is disabled by design):
  - [providers.models.claude_cli.default] with mcp_config pointing at the
    §15 door, so a subscription agent can ACT and not merely reason
  - disallowed_tools denies Claude Code's own Bash/Write/Edit/WebFetch so
    the gated door is the ONLY actuator and nothing bypasses the audit log
  - env CLAUDE_CODE_OAUTH_TOKEN = "$CLAUDE_CODE_OAUTH_TOKEN" — the $NAME
    form reads the daemon env, keeping the token out of config.toml
  - anthropic.judge swapped to the API key (0 oat01 left in config)

Verified before changing anything: the real binary returned SUBSCRIPTION-OK
through the token, then ENV-OK once the daemon carried it in env.

Note for future readers: /api/config/prop reflects what is CONFIGURED, not
what the binary supports — `openai` 404s there too. An earlier note that
the image "has no claude_cli in its schema" was true of the old :sync
image and is not true of the rebuilt one.

400 tests, clippy clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-08-03 17:14:42 -07:00
co-authored by Claude Opus 5
parent ad89ef94cd
commit ac47dcbe94
+32 -22
View File
@@ -20,12 +20,20 @@ pub fn claw_alias(claw_id: Uuid) -> String {
/// Map a claw's chosen model to a configured provider alias.
///
/// v0.8.3 fold: `claude_cli.*` and `kimi_cli.*` families were deleted
/// upstream; every alias now lives under a real provider family
/// (`anthropic`, `groq`, `gemini`, ...). Our compose currently
/// configures `anthropic.default`, `anthropic.door`, `groq.default`,
/// and `gemini.default`, so unknown models resolve to
/// `anthropic.default` — the workspace's high-quality baseline.
/// Claude models resolve to `claude_cli.default`, which spawns the real
/// `claude` binary against the Max subscription rather than posting to the
/// raw API with Claude Code identity headers. The API-key path still exists
/// and the judge uses it deliberately (see below), but agent work — which is
/// ~99% of the tokens — belongs on the subscription and on the supported
/// client.
///
/// The judge stays on `anthropic.judge`/API key on purpose: if the
/// subscription throttles, missions degrade but verification keeps working.
/// Putting both on one credential would mean a single limit blinds the
/// verifier at exactly the moment there is most to verify.
///
/// Non-Claude families are unchanged: `groq.default`, `gemini.default`, and
/// the GLM/Kimi substitution below.
pub fn provider_alias_for(model: &str) -> &'static str {
let m = model.trim().to_ascii_lowercase();
// Prefix families first (covers claude-sonnet-5, claude-opus-4-8,
@@ -33,7 +41,7 @@ pub fn provider_alias_for(model: &str) -> &'static str {
// decides what "its own family" means, so the two can't drift apart.
if is_exact_provider_match(&m) {
if m.starts_with("claude") {
return "anthropic.default";
return "claude_cli.default";
}
if m.starts_with("gemini") {
return "gemini.default";
@@ -54,18 +62,18 @@ pub fn provider_alias_for(model: &str) -> &'static str {
| "kimi" | "kimi-k2" | "kimi-for-coding" => {
eprintln!(
"runtime_provision: model {m:?} has no provider family configured — \
substituting anthropic.default, which spends ANTHROPIC_API_KEY"
substituting claude_cli.default, which spends the Claude subscription"
);
"anthropic.default"
"claude_cli.default"
}
_ => {
if !m.is_empty() {
eprintln!(
"runtime_provision: unrecognised model {m:?} — defaulting to \
anthropic.default"
claude_cli.default"
);
}
"anthropic.default"
"claude_cli.default"
}
}
}
@@ -342,14 +350,15 @@ mod tests {
/// The GLM/Kimi substitution is intentional but must be reported as a
/// substitution, because its consequence is that a user who picked a
/// non-Anthropic model is spending the Anthropic key.
/// non-Anthropic model is spending someone else's budget — now the
/// Claude subscription rather than the Anthropic API key.
#[test]
fn substituted_families_are_not_reported_as_exact_matches() {
for m in ["kimi", "glm-4.7", "glm5", "kimi-k2", "something-unknown"] {
assert_eq!(super::provider_alias_for(m), "anthropic.default");
assert_eq!(super::provider_alias_for(m), "claude_cli.default");
assert!(
!super::is_exact_provider_match(m),
"{m} resolves to anthropic.default by substitution, not by family"
"{m} resolves to claude_cli.default by substitution, not by family"
);
}
for m in [
@@ -395,19 +404,20 @@ mod tests {
fn provider_alias_mapping() {
assert_eq!(provider_alias_for("gemini"), "gemini.default");
assert_eq!(provider_alias_for("gemini-2.0-flash"), "gemini.default");
// v0.8.3: glm/kimi families fall back to anthropic until their
// own provider tables are configured in the runtime template.
assert_eq!(provider_alias_for("GLM-4.7"), "anthropic.default");
assert_eq!(provider_alias_for("kimi"), "anthropic.default");
// glm/kimi families fall back to Claude until their own provider
// tables are configured in the runtime template.
assert_eq!(provider_alias_for("GLM-4.7"), "claude_cli.default");
assert_eq!(provider_alias_for("kimi"), "claude_cli.default");
assert_eq!(provider_alias_for("groq"), "groq.default");
assert_eq!(
provider_alias_for("llama-3.3-70b-versatile"),
"groq.default"
);
assert_eq!(provider_alias_for("claude"), "anthropic.default");
assert_eq!(provider_alias_for("claude-sonnet-5"), "anthropic.default");
assert_eq!(provider_alias_for("claude-opus-4-8"), "anthropic.default");
assert_eq!(provider_alias_for("anything-else"), "anthropic.default");
// Claude models spawn the real CLI against the subscription.
assert_eq!(provider_alias_for("claude"), "claude_cli.default");
assert_eq!(provider_alias_for("claude-sonnet-5"), "claude_cli.default");
assert_eq!(provider_alias_for("claude-opus-4-8"), "claude_cli.default");
assert_eq!(provider_alias_for("anything-else"), "claude_cli.default");
}
#[test]