Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions lib/crates/fabro-auth/src/resolve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,22 @@ impl CredentialResolver {
let mut env_vars = HashMap::new();
let provider = Provider::from_id(&credential.provider);
let login_command = match (provider, &credential.details, kind) {
// Opt-in: when FABRO_CLI_TRUST_HOST_AUTH is set, do not inject
// ANTHROPIC_API_KEY into the claude subprocess environment. claude
// CLI then falls back to its own credentials on the host (e.g. a
// Max subscription's OAuth session), so the registered API key is
// not billed for CLI-backend runs. A registered credential is
// still required to satisfy the resolver gate; the key value
// itself is never propagated to the subprocess.
//
// The flag must reach the worker subprocess that runs the
// credential resolver; see WORKER_ENV_ALLOWLIST in
// `lib/crates/fabro-server/src/spawn_env.rs`.
(Some(Provider::Anthropic), AuthDetails::ApiKey { .. }, CliAgentKind::Claude)
if claude_cli_trust_host_auth() =>
{
None
}
(Some(Provider::OpenAi), AuthDetails::ApiKey { key }, CliAgentKind::Codex) => {
env_vars.insert(EnvVars::OPENAI_API_KEY.to_string(), key.clone());
Some(codex_login_command(key))
Expand Down Expand Up @@ -457,6 +473,28 @@ fn primary_api_key_env_var(provider: &ProviderId) -> Option<&'static str> {
})
}

/// Read the opt-in `FABRO_CLI_TRUST_HOST_AUTH` flag.
///
/// When set to a truthy value, Fabro skips injecting provider API-key env vars
/// into the CLI agent subprocess for providers that support a host-side
/// credential fallback (currently: Anthropic + claude). The CLI then resolves
/// auth using whatever it finds on the host (for example, a Max subscription
/// OAuth session under `~/.claude/`).
///
/// Truthy: any non-empty value except `0` / `false` (case-insensitive).
#[expect(
clippy::disallowed_methods,
reason = "Opt-in env flag for trusting host-side CLI credentials instead of injecting API keys into subprocesses."
)]
fn claude_cli_trust_host_auth() -> bool {
std::env::var(EnvVars::FABRO_CLI_TRUST_HOST_AUTH)
.ok()
.is_some_and(|value| {
let trimmed = value.trim();
!trimmed.is_empty() && trimmed != "0" && !trimmed.eq_ignore_ascii_case("false")
})
}

fn codex_login_command(api_key: &str) -> String {
let quoted =
try_quote(api_key).map_or_else(|_| api_key.to_string(), std::borrow::Cow::into_owned);
Expand Down
1 change: 1 addition & 0 deletions lib/crates/fabro-server/src/spawn_env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const WORKER_ENV_ALLOWLIST: &[&str] = &[
EnvVars::FABRO_LOG,
EnvVars::FABRO_HOME,
EnvVars::FABRO_STORAGE_ROOT,
EnvVars::FABRO_CLI_TRUST_HOST_AUTH,
EnvVars::TERM,
EnvVars::NO_COLOR,
EnvVars::CLICOLOR,
Expand Down
2 changes: 2 additions & 0 deletions lib/crates/fabro-static/src/env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ impl EnvVars {
pub const FABRO_BUILD_DATE: &'static str = "FABRO_BUILD_DATE";
pub const FABRO_BUILD_PROFILE: &'static str = "FABRO_BUILD_PROFILE";
pub const FABRO_BUILD_PROFILE_SUFFIX: &'static str = "FABRO_BUILD_PROFILE_SUFFIX";
pub const FABRO_CLI_TRUST_HOST_AUTH: &'static str = "FABRO_CLI_TRUST_HOST_AUTH";
pub const FABRO_CONFIG: &'static str = "FABRO_CONFIG";
pub const FABRO_DEBUG: &'static str = "FABRO_DEBUG";
pub const FABRO_DEV_TOKEN: &'static str = "FABRO_DEV_TOKEN";
Expand Down Expand Up @@ -151,6 +152,7 @@ mod tests {
EnvVars::FABRO_BUILD_DATE,
EnvVars::FABRO_BUILD_PROFILE,
EnvVars::FABRO_BUILD_PROFILE_SUFFIX,
EnvVars::FABRO_CLI_TRUST_HOST_AUTH,
EnvVars::FABRO_CONFIG,
EnvVars::FABRO_DEBUG,
EnvVars::FABRO_DEV_TOKEN,
Expand Down