Handle inactive PAT owner as invalid OAuth token

This commit is contained in:
elky
2026-06-21 11:39:20 +08:00
parent 279735ae7f
commit 61bdd304b7
4 changed files with 90 additions and 0 deletions
@@ -149,6 +149,8 @@ fn status_proves_access_token_invalid(status_code: u16, response_text: Option<&s
"expired access token", "expired access token",
"authentication token has been invalidated", "authentication token has been invalidated",
"token has been invalidated", "token has been invalidated",
"personal access token owner is inactive",
"biscuit_baker_service_auth_credential_error_status",
"security token included in the request is expired", "security token included in the request is expired",
] ]
.iter() .iter()
@@ -196,6 +198,12 @@ mod tests {
403, 403,
Some("The security token included in the request is expired") Some("The security token included in the request is expired")
)); ));
assert!(status_proves_access_token_invalid(
403,
Some(
r#"{"error":{"code":"biscuit_baker_service_auth_credential_error_status","message":"Personal access token owner is inactive."}}"#
)
));
assert!(!status_proves_access_token_invalid(403, None)); assert!(!status_proves_access_token_invalid(403, None));
assert!(!status_proves_access_token_invalid( assert!(!status_proves_access_token_invalid(
403, 403,
@@ -2103,6 +2103,49 @@ mod tests {
assert_eq!(stored_key.oauth_invalid_reason, None); assert_eq!(stored_key.oauth_invalid_reason, None);
} }
#[tokio::test]
async fn oauth_invalidation_marks_codex_inactive_pat_owner_403_as_token_invalid() {
let state = codex_state();
let plan = sample_codex_plan();
apply_local_execution_effect(
&state,
LocalExecutionEffectContext {
plan: &plan,
report_context: None,
},
LocalExecutionEffect::OauthInvalidation(LocalOAuthInvalidationEffect {
status_code: 403,
response_text: Some(
r#"{"error":{"code":"biscuit_baker_service_auth_credential_error_status","message":"Personal access token owner is inactive."},"status":403}"#,
),
}),
)
.await;
let stored_key = state
.read_provider_catalog_keys_by_ids(std::slice::from_ref(&plan.key_id))
.await
.expect("provider catalog keys should load")
.into_iter()
.next()
.expect("stored key should exist");
assert!(stored_key.oauth_invalid_at_unix_secs.is_some());
assert_eq!(
stored_key.oauth_invalid_reason.as_deref(),
Some("[OAUTH_EXPIRED] Personal access token owner is inactive.")
);
assert_eq!(
stored_key
.status_snapshot
.as_ref()
.and_then(|value| value.get("oauth"))
.and_then(|value| value.get("code"))
.and_then(Value::as_str),
Some("invalid")
);
}
#[tokio::test] #[tokio::test]
async fn health_failure_projection_updates_key_health_for_format() { async fn health_failure_projection_updates_key_health_for_format() {
let state = health_state(); let state = health_state();
+22
View File
@@ -952,6 +952,9 @@ pub fn codex_looks_like_token_invalidated(message: Option<&str>) -> bool {
|| lowered.contains("authentication token has been invalidated") || lowered.contains("authentication token has been invalidated")
|| lowered.contains("token has been invalidated") || lowered.contains("token has been invalidated")
|| lowered.contains("token invalidated") || lowered.contains("token invalidated")
|| lowered.contains("personal access token owner is inactive")
|| lowered.contains("biscuit_baker_service_auth_credential_error_status")
|| lowered.contains("auth_credential")
|| lowered.contains("invalidated") || lowered.contains("invalidated")
|| lowered.contains("revoked") || lowered.contains("revoked")
|| lowered.contains("已撤销") || lowered.contains("已撤销")
@@ -1777,6 +1780,25 @@ mod tests {
); );
} }
#[test]
fn codex_runtime_invalid_reason_marks_inactive_pat_owner_403_as_token_invalid() {
assert_eq!(
codex_runtime_invalid_reason(403, Some("Personal access token owner is inactive.")),
Some(format!(
"{OAUTH_EXPIRED_PREFIX}Personal access token owner is inactive."
))
);
assert_eq!(
codex_runtime_invalid_reason(
403,
Some("biscuit_baker_service_auth_credential_error_status")
),
Some(format!(
"{OAUTH_EXPIRED_PREFIX}biscuit_baker_service_auth_credential_error_status"
))
);
}
#[test] #[test]
fn codex_runtime_invalid_reason_marks_402_as_account_blocked() { fn codex_runtime_invalid_reason_marks_402_as_account_blocked() {
assert_eq!( assert_eq!(
@@ -158,6 +158,9 @@ pub fn oauth_token_reason_is_hard_invalid(reason: &str) -> bool {
"authentication token has been invalidated", "authentication token has been invalidated",
"token has been invalidated", "token has been invalidated",
"token invalidated", "token invalidated",
"personal access token owner is inactive",
"biscuit_baker_service_auth_credential_error_status",
"auth_credential",
"invalidated", "invalidated",
"revoked", "revoked",
"已撤销", "已撤销",
@@ -722,6 +725,20 @@ mod tests {
assert!(!snapshot.recoverable); assert!(!snapshot.recoverable);
} }
#[test]
fn account_snapshot_marks_inactive_pat_owner_as_token_invalid() {
let snapshot = resolve_account_status_snapshot(
Some("codex"),
None,
Some("[OAUTH_EXPIRED] Personal access token owner is inactive."),
);
assert_eq!(snapshot.code, "oauth_token_invalid");
assert_eq!(snapshot.label.as_deref(), Some("Token 失效"));
assert!(snapshot.blocked);
assert!(!snapshot.recoverable);
}
#[test] #[test]
fn account_snapshot_marks_oauth_expired_as_token_expired() { fn account_snapshot_marks_oauth_expired_as_token_expired() {
let snapshot = resolve_account_status_snapshot( let snapshot = resolve_account_status_snapshot(