fix(inference): stop Codex CLI import user-state errors flooding Sentry (#3403) (#3404)

This commit is contained in:
oxoxDev
2026-06-05 10:11:55 -04:00
committed by GitHub
parent 77cfea2b61
commit d6dfcfbe27
3 changed files with 172 additions and 1 deletions
+100
View File
@@ -249,10 +249,39 @@ pub enum ExpectedErrorKind {
/// contention (handled by the store's busy-retry loop) and unrelated DB
/// failures in other domains still reach Sentry.
SubconsciousSchemaUnavailable,
/// The user invoked "Import Codex CLI login" (Settings → AI → Codex auth)
/// but the Codex CLI auth at `~/.codex/auth.json` is absent or unusable:
/// the file doesn't exist (the user never ran `codex login`), can't be
/// parsed, or carries no tokens / no access token. The import RPC already
/// returns an actionable error string ("Run `codex login` first, then try
/// Codex auth again.") that the frontend surfaces inline
/// (`AIPanel.tsx` → `setCodexAuthError`), and Sentry has no remediation
/// path — we can't run `codex login` for the user. The error strings also
/// embed the absolute `~/.codex/auth.json` path (home dir / username), so
/// demoting these out of the event stream is a privacy win on top of the
/// noise reduction (mirror `FilesystemUserPathInvalid` / `DiskFull`).
///
/// Drops Sentry TAURI-RUST-83A (~430 events / 40 users on
/// `openhuman@0.57.13`). Anchored to the `codex cli auth` /
/// `.codex/auth.json` envelope produced by
/// [`crate::openhuman::inference::openai_oauth::store::import_codex_cli_auth_from_path`]
/// — a genuine keyring/persist failure in `upsert_profile` carries neither
/// anchor, so a real defect in the import code still reaches Sentry.
CodexCliAuthUnavailable,
}
pub fn expected_error_kind(message: &str) -> Option<ExpectedErrorKind> {
let lower = message.to_ascii_lowercase();
// Check the Codex-CLI import envelope first: it is highly specific
// (literal `codex cli auth` / `.codex/auth.json`) and carries no overlap
// with the generic matchers below, so ordering is for clarity, not
// precedence. See `ExpectedErrorKind::CodexCliAuthUnavailable`. The
// keyring/persist failure path (`upsert_profile`) stringifies to generic
// keychain / "auth profile" / SQLite text that contains neither anchor,
// so a real defect in the import still falls through to capture.
if lower.contains("codex cli auth") || lower.contains(".codex/auth.json") {
return Some(ExpectedErrorKind::CodexCliAuthUnavailable);
}
if lower.contains("local ai is disabled") {
return Some(ExpectedErrorKind::LocalAiDisabled);
}
@@ -1577,6 +1606,21 @@ fn report_expected_message(kind: ExpectedErrorKind, message: &str, domain: &str,
"[observability] {domain}.{operation} skipped expected subconscious schema DB-unavailable error"
);
}
ExpectedErrorKind::CodexCliAuthUnavailable => {
// User-state condition: the Codex CLI login at `~/.codex/auth.json`
// is missing / unparseable / has no tokens. The import RPC already
// returned the actionable "Run `codex login` first" string and the
// UI surfaces it inline — Sentry has nothing to act on. Demote at
// `info!` (mirrors `LocalAiBinaryMissing`). Do NOT include the raw
// `message`: it embeds the absolute `~/.codex/auth.json` path
// (home dir / username). Log only domain/operation/kind — no PII.
tracing::info!(
domain = domain,
operation = operation,
kind = "codex_cli_auth_unavailable",
"[observability] {domain}.{operation} skipped expected codex-cli auth-unavailable error"
);
}
}
}
@@ -2041,6 +2085,62 @@ mod tests {
);
}
/// Sentry TAURI-RUST-83A: the Codex-CLI import user-state errors must
/// classify as `CodexCliAuthUnavailable` so the ~430-event flood stays out
/// of Sentry. Verbatim envelope shapes from
/// `openai_oauth::store::import_codex_cli_auth_from_path` (with a fake path
/// in place of the real `~/.codex/auth.json`).
#[test]
fn classifies_codex_cli_import_user_state_as_expected() {
for msg in [
"Could not read Codex CLI auth at /home/u/.codex/auth.json: No such file \
or directory. Run `codex login` first, then try Codex auth again.",
"Could not parse Codex CLI auth at /home/u/.codex/auth.json: expected value. \
Run `codex login` again, then try Codex auth again.",
"Codex CLI auth at /home/u/.codex/auth.json has no tokens. Run `codex login` first.",
"Codex CLI auth at /home/u/.codex/auth.json has no access token. Run `codex login` first.",
"home directory is not set; cannot find ~/.codex/auth.json",
] {
assert_eq!(
expected_error_kind(msg),
Some(ExpectedErrorKind::CodexCliAuthUnavailable),
"must classify as CodexCliAuthUnavailable: {msg}"
);
}
}
/// Exercise the full demotion path (classifier -> report arm) for a codex
/// auth-unavailable message: it must take the expected branch and not panic.
#[test]
fn report_error_or_expected_demotes_codex_auth_unavailable() {
report_error_or_expected(
"Could not read Codex CLI auth at /home/u/.codex/auth.json: No such file \
or directory. Run `codex login` first, then try Codex auth again.",
"inference",
"openai_oauth_import_codex_cli",
&[],
);
}
/// Guard against over-suppression on the import path: a genuine
/// keyring/persist failure (`upsert_profile`) or an unrelated error carries
/// neither the `codex cli auth` nor the `.codex/auth.json` anchor and MUST
/// still reach Sentry (stay `None`) so a real defect isn't blinded.
#[test]
fn does_not_classify_codex_persist_failure_as_codex_auth_unavailable() {
for msg in [
"failed to write auth profile store: keyring error: access denied",
"Auth profile not found: provider:openai/oauth",
"unable to open database file",
] {
assert_ne!(
expected_error_kind(msg),
Some(ExpectedErrorKind::CodexCliAuthUnavailable),
"real-defect/unrelated error must NOT be demoted as codex auth-unavailable: {msg}"
);
}
}
/// Sentry TAURI-RUST-R4: the composio direct-mode factory bail must
/// classify as `ApiKeyMissing` so any residual emit (explicit
/// execute/authorize call with no key) stays out of Sentry. Uses the
@@ -466,6 +466,67 @@ fn import_codex_cli_auth_file_reports_missing_file_with_login_hint() {
assert!(err.contains("codex login"));
}
/// Drift-proof coupling: every user-state error the real Codex-CLI import
/// producer emits MUST classify as `CodexCliAuthUnavailable`, so the Sentry
/// demotion at `ops.rs` (TAURI-RUST-83A) keeps working even if the wording
/// changes. If a future edit to `store.rs` drops the `codex cli auth` /
/// `.codex/auth.json` anchor from a message, this test fails in CI.
#[test]
fn codex_import_user_state_errors_classify_as_expected() {
use crate::core::observability::{expected_error_kind, ExpectedErrorKind};
let tmp = tempdir().unwrap();
let config = test_config(&tmp);
// Missing file (no `codex login`).
let missing = import_codex_cli_auth_from_path(&config, &tmp.path().join("missing-auth.json"))
.unwrap_err();
// Unparseable file.
let garbage_path = tmp.path().join("garbage-auth.json");
std::fs::write(&garbage_path, b"not json").unwrap();
let garbage = import_codex_cli_auth_from_path(&config, &garbage_path).unwrap_err();
// Parses but carries no tokens.
let no_tokens_path = tmp.path().join("no-tokens-auth.json");
std::fs::write(&no_tokens_path, b"{}").unwrap();
let no_tokens = import_codex_cli_auth_from_path(&config, &no_tokens_path).unwrap_err();
// Parses with a tokens object but no access token.
let no_access_path = tmp.path().join("no-access-auth.json");
std::fs::write(&no_access_path, br#"{"tokens":{"refresh_token":"r"}}"#).unwrap();
let no_access = import_codex_cli_auth_from_path(&config, &no_access_path).unwrap_err();
for err in [&missing, &garbage, &no_tokens, &no_access] {
assert_eq!(
expected_error_kind(err),
Some(ExpectedErrorKind::CodexCliAuthUnavailable),
"codex import user-state error must classify as CodexCliAuthUnavailable: {err}"
);
}
}
/// Exercise the ops entry point (`inference_openai_oauth_import_codex_cli`) on
/// the failure path so the `report_error_or_expected` call at the match arm is
/// covered: point `CODEX_HOME` at an empty dir (no `auth.json`) and assert the
/// RPC surfaces the actionable error.
#[tokio::test]
async fn inference_import_codex_cli_surfaces_error_when_auth_missing() {
let _env_lock = crate::openhuman::config::TEST_ENV_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let tmp = tempdir().unwrap();
let config = test_config(&tmp);
let _env_guard = EnvVarGuard::set("CODEX_HOME", tmp.path());
let err = crate::openhuman::inference::ops::inference_openai_oauth_import_codex_cli(&config)
.await
.unwrap_err();
assert!(err.contains("Could not read Codex CLI auth"));
assert!(err.contains("codex login"));
}
#[test]
fn openai_oauth_status_reports_token_profile_as_disconnected() {
let tmp = tempdir().unwrap();
+11 -1
View File
@@ -446,7 +446,17 @@ pub async fn inference_openai_oauth_import_codex_cli(
.map(|payload| RpcOutcome::single_log(payload, "openai oauth imported from codex cli"));
match &result {
Ok(_) => debug!("{LOG_PREFIX} openai_oauth_import_codex_cli:ok"),
Err(err) => error!(error = %err, "{LOG_PREFIX} openai_oauth_import_codex_cli:error"),
// Most failures here are expected user-state (no `~/.codex/auth.json`,
// user never ran `codex login`, stale/empty file) — the UI already
// surfaces the actionable error, so route through the observability
// classifier to keep that flood out of Sentry (TAURI-RUST-83A) while a
// genuine keyring/persist defect still falls through to a real event.
Err(err) => crate::core::observability::report_error_or_expected(
err,
"inference",
"openai_oauth_import_codex_cli",
&[],
),
}
result
}