feat(credentials): make provider substitution and runtime auth mode visible
Three guardrails around which credential pays for what. 1. Boot announces the mission-runtime auth mode, and warns when subscription auth is configured on a deployment with more than one user. A consumer subscription credential may only run the account holder's own work, and that condition is otherwise invisible -- it holds today and quietly stops holding the first time someone else signs up. Adds users::count_all (dynamic query, so the offline cache needs no regeneration). 2. Reject an ANTHROPIC_API_KEY shaped like a subscription OAuth token (sk-ant-oat...) at boot rather than failing on the first model call far from the mistake. Both credentials start sk-ant-, so the confusion is easy to make and hard to spot. 3. provider_alias_for's GLM/Kimi -> anthropic.default fallback was documented as deliberate but was silent in effect: a user picking "kimi" in the UI got an agent spending the Anthropic key, with nothing saying so. It now logs the substitution, and is_exact_provider_match() lets callers tell a real family match from a substitution so a UI can say which model will actually run. Behaviour is unchanged -- only the silence is. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5
parent
af44c92dd6
commit
ca45597c79
@@ -29,6 +29,16 @@ fn build_provider(config: &AppConfig) -> Result<Arc<dyn LlmProvider>, String> {
|
||||
LlmProviderKind::Anthropic => {
|
||||
let key = std::env::var("ANTHROPIC_API_KEY")
|
||||
.map_err(|_| "llm.provider = \"anthropic\" requires ANTHROPIC_API_KEY")?;
|
||||
// A subscription OAuth token pasted where an API key belongs
|
||||
// authenticates nothing here and fails on the first model call,
|
||||
// far from the mistake. Both start `sk-ant-`, so the confusion is
|
||||
// easy to make and hard to spot.
|
||||
if key.starts_with("sk-ant-oat") {
|
||||
return Err("ANTHROPIC_API_KEY looks like a subscription OAuth token \
|
||||
(sk-ant-oat…), not a Console API key (sk-ant-api…). The \
|
||||
OAuth token belongs to the `claude` CLI, not the server."
|
||||
.to_string());
|
||||
}
|
||||
Ok(Arc::new(AnthropicProvider::new(key)))
|
||||
}
|
||||
LlmProviderKind::OpenAiCompat => {
|
||||
@@ -295,6 +305,29 @@ async fn run() -> Result<(), String> {
|
||||
let recipes = cm_api::workflow_registry::load();
|
||||
eprintln!("workflow_registry: {} recipe(s) available", recipes.len());
|
||||
}
|
||||
// Announce how mission runtimes authenticate. Subscription mode is only
|
||||
// legitimate for a single-operator deployment — a consumer subscription
|
||||
// credential must never serve another person's work — and the mode is
|
||||
// otherwise invisible until it shows up on a bill, so state it at boot.
|
||||
{
|
||||
let mode = cm_api::mission_runtime::runtime_auth_mode();
|
||||
eprintln!(
|
||||
"mission_runtime: auth mode = {} (CLAWMATES_RUNTIME_AUTH)",
|
||||
mode.as_str()
|
||||
);
|
||||
if mode == cm_api::mission_runtime::RuntimeAuth::Subscription {
|
||||
match cm_db::repo::users::count_all(&pool).await {
|
||||
Ok(n) if n > 1 => eprintln!(
|
||||
"mission_runtime: WARNING — subscription auth with {n} users in this \
|
||||
deployment. A consumer subscription credential may only run the \
|
||||
account holder's own work; move the runtime back to \
|
||||
CLAWMATES_RUNTIME_AUTH=api_key before other people use it."
|
||||
),
|
||||
Ok(_) => {}
|
||||
Err(e) => eprintln!("mission_runtime: user count check skipped: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
cm_api::phase_runner::spawn(pool.clone(), runtime.clone());
|
||||
// Per-mission runtime container sweeper (C3): tears down mission
|
||||
// runtime containers 30 min after the mission reaches a terminal
|
||||
|
||||
Reference in New Issue
Block a user