Skip to content

Add SubscriptionExhausted hook for auth rotation - #1

Open
lawrencecchen wants to merge 1 commit into
mainfrom
feat/subrouter-auto-rotate
Open

Add SubscriptionExhausted hook for auth rotation#1
lawrencecchen wants to merge 1 commit into
mainfrom
feat/subrouter-auto-rotate

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented May 4, 2026

Copy link
Copy Markdown

Summary:

  • Add a first-class SubscriptionExhausted lifecycle hook.
  • Run the hook once on UsageLimitReached/QuotaExceeded, reload managed ChatGPT auth, and retry when at least one hook succeeds.
  • Clear cached WebSocket and x-codex-turn-state routing state before the retry so sr pick can move the next request to a fresh route.
  • Remove Codex-specific Subrouter env wiring so Subrouter is configured as a normal hook.
  • Regenerate hook, config, app-server protocol, and TS schema artifacts.

Example hook:

[features]
hooks = true

[hooks]
[[hooks.SubscriptionExhausted]]
[[hooks.SubscriptionExhausted.hooks]]
type = "command"
command = "sr pick"

Tests:

  • sr --help
  • sr pick --help
  • cargo test -p codex-core --test all quota_exceeded -- --nocapture
  • cargo test -p codex-hooks schema -- --nocapture
  • just fix -p codex-core -p codex-hooks -p codex-config -p codex-protocol -p codex-app-server-protocol -p codex-analytics -p codex-tui -p codex-app-server

@coderabbitai

coderabbitai Bot commented May 4, 2026

Copy link
Copy Markdown

Warning

Rate limit exceeded

@lawrencecchen has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 42 minutes and 35 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 726e06fd-c92b-4f70-9234-627ebb656b14

📥 Commits

Reviewing files that changed from the base of the PR and between cb6533a211b6a180d914c0b7df0bc1029e54ebca and b860208.

⛔ Files ignored due to path filters (7)
  • codex-rs/hooks/schema/generated/permission-request.command.output.schema.json is excluded by !**/generated/**
  • codex-rs/hooks/schema/generated/post-tool-use.command.output.schema.json is excluded by !**/generated/**
  • codex-rs/hooks/schema/generated/pre-tool-use.command.output.schema.json is excluded by !**/generated/**
  • codex-rs/hooks/schema/generated/session-start.command.output.schema.json is excluded by !**/generated/**
  • codex-rs/hooks/schema/generated/subscription-exhausted.command.input.schema.json is excluded by !**/generated/**
  • codex-rs/hooks/schema/generated/subscription-exhausted.command.output.schema.json is excluded by !**/generated/**
  • codex-rs/hooks/schema/generated/user-prompt-submit.command.output.schema.json is excluded by !**/generated/**
📒 Files selected for processing (38)
  • codex-rs/analytics/src/events.rs
  • codex-rs/app-server-protocol/schema/json/ServerNotification.json
  • codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json
  • codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json
  • codex-rs/app-server-protocol/schema/json/v2/ConfigRequirementsReadResponse.json
  • codex-rs/app-server-protocol/schema/json/v2/HookCompletedNotification.json
  • codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json
  • codex-rs/app-server-protocol/schema/json/v2/HooksListResponse.json
  • codex-rs/app-server-protocol/schema/typescript/v2/HookEventName.ts
  • codex-rs/app-server-protocol/schema/typescript/v2/ManagedHooksRequirements.ts
  • codex-rs/app-server-protocol/src/protocol/v2.rs
  • codex-rs/app-server/src/config_api.rs
  • codex-rs/config/src/hook_config.rs
  • codex-rs/core/config.schema.json
  • codex-rs/core/src/client.rs
  • codex-rs/core/src/hook_runtime.rs
  • codex-rs/core/src/lib.rs
  • codex-rs/core/src/session/turn.rs
  • codex-rs/core/src/subscription_exhaustion.rs
  • codex-rs/core/src/thread_manager.rs
  • codex-rs/core/tests/common/test_codex.rs
  • codex-rs/core/tests/suite/quota_exceeded.rs
  • codex-rs/hooks/src/engine/discovery.rs
  • codex-rs/hooks/src/engine/dispatcher.rs
  • codex-rs/hooks/src/engine/mod.rs
  • codex-rs/hooks/src/engine/output_parser.rs
  • codex-rs/hooks/src/engine/schema_loader.rs
  • codex-rs/hooks/src/events/common.rs
  • codex-rs/hooks/src/events/mod.rs
  • codex-rs/hooks/src/events/subscription_exhausted.rs
  • codex-rs/hooks/src/lib.rs
  • codex-rs/hooks/src/registry.rs
  • codex-rs/hooks/src/schema.rs
  • codex-rs/login/src/auth/manager.rs
  • codex-rs/protocol/src/protocol.rs
  • codex-rs/tui/src/bottom_pane/hooks_browser_view.rs
  • codex-rs/tui/src/chatwidget/tests/helpers.rs
  • codex-rs/tui/src/history_cell/hook_cell.rs
📝 Walkthrough

Walkthrough

A new subrouter rotation feature is introduced that automatically rotates authentication credentials when specific quota or usage limit errors occur. The feature includes error detection, rotation logic via subprocess execution, integration into the turn processing error handler, and comprehensive integration tests with environment variable management.

Changes

Subrouter Rotation Feature

Layer / File(s) Summary
Public API
codex-rs/login/src/auth/manager.rs
AuthManager::codex_home() accessor added to expose the configured codex_home directory.
Module Implementation
codex-rs/core/src/subrouter_rotation.rs
New module implementing should_rotate_for_error() to detect UsageLimitReached and QuotaExceeded errors, and rotate_if_available() to execute a subrouter command with auth state snapshots, timeout handling (30s), and eligibility checks.
Core Integration
codex-rs/core/src/lib.rs
codex-rs/core/src/session/turn.rs
Module declaration added; run_turn() now imports and calls subrouter rotation on matching errors, reset websocket session on successful rotation, and retry the turn loop.
Test Infrastructure
codex-rs/core/src/thread_manager.rs
AuthManager test construction updated to use from_auth_for_testing_with_home() to pass custom codex_home.
Tests / Documentation
codex-rs/core/tests/suite/quota_exceeded.rs
Unix-gated integration test usage_limit_reached_runs_subrouter_and_retries_with_rotated_auth added with EnvVarGuard helper for environment variable management, test auth JSON generation, and assertions on rotated credentials.

Sequence Diagram

sequenceDiagram
    actor Client
    participant Turn as run_turn()
    participant SubRouter as subrouter_rotation
    participant SubProcess as Subrouter CMD
    participant AuthMgr as AuthManager

    Client->>Turn: Request processing
    Turn->>Turn: Sample/get response
    Turn-->>Turn: HTTP 429 (UsageLimitReached)
    Turn->>SubRouter: should_rotate_for_error()
    SubRouter-->>Turn: true (matches UsageLimitReached)
    Turn->>SubRouter: rotate_if_available()
    SubRouter->>AuthMgr: Check auto-rotate enabled<br/>& eligible auth
    AuthMgr-->>SubRouter: Valid cached auth
    SubRouter->>SubRouter: Snapshot current auth state
    SubRouter->>SubProcess: Spawn subrouter (30s timeout)
    SubProcess-->>SubRouter: Updated auth in CODEX_HOME
    SubRouter->>AuthMgr: Reload auth from storage
    AuthMgr-->>SubRouter: New auth state
    SubRouter-->>Turn: Ok(true, auth changed)
    Turn->>Turn: Reset websocket session
    Turn->>Turn: Emit rotation success warning
    Turn->>Turn: continue (retry turn loop)
    Turn->>Client: New request with rotated token
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes


🐰 A subrouter spins, and quota limits flip,
With rotated tokens on each slip,
Auth refreshed, the turn retries,
No more limits blocking the skies! 🌟

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description lacks required contribution guidelines compliance statement and references an outdated hook implementation that contradicts the actual code changes. Add a statement confirming compliance with contribution guidelines from docs/contributing.md, and update the description to accurately reflect the final implementation (subrouter_rotation module functions instead of lifecycle hooks).
Title check ❓ Inconclusive Title is vague and doesn't accurately reflect the main change—it mentions 'SubscriptionExhausted hook' but the actual implementation adds subrouter auto-rotation logic. Clarify the title to better reflect the main objective, e.g., 'Auto-rotate auth with Subrouter on quota exhaustion' or 'Add automatic auth rotation on usage limits.'
✅ Passed checks (3 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/subrouter-auto-rotate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 42 minutes and 35 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@codex-rs/core/src/subrouter_rotation.rs`:
- Around line 56-62: After reload(), do not treat any changed auth snapshot as a
successful rotation without enforcing the AuthManager's
forced_chatgpt_workspace_id restriction: after obtaining auth_after via
auth_manager.auth_cached(), check the manager's forced_chatgpt_workspace_id (or
call an existing AuthManager method that indicates a forced workspace) and
compare it to the workspace/account identifier in auth_after (or compare to the
workspace in the original before snapshot); if the forced value exists and the
workspace changed to a different id, treat this as not a valid rotation and
return Ok(false); otherwise proceed with the existing auth_snapshot(&auth_after)
!= before comparison.
- Around line 65-72: The auto_rotate_enabled() function currently defaults to
true when the CODEX_SUBROUTER_AUTO_ROTATE env var is unset, causing unexpected
side effects; change its behavior so rotation is opt-in by returning false on
Err(_) (unset/missing) and only returning true when the env var explicitly
contains a truthy value (e.g., not "0"/"false"/"no"/"off")—update the match in
auto_rotate_enabled() to treat Err(_) as false so rotation only occurs when the
environment variable is explicitly set to an enabled value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43f2c702-62a8-4bfd-99d0-651445f17af3

📥 Commits

Reviewing files that changed from the base of the PR and between c8c30d9 and cb6533a211b6a180d914c0b7df0bc1029e54ebca.

📒 Files selected for processing (6)
  • codex-rs/core/src/lib.rs
  • codex-rs/core/src/session/turn.rs
  • codex-rs/core/src/subrouter_rotation.rs
  • codex-rs/core/src/thread_manager.rs
  • codex-rs/core/tests/suite/quota_exceeded.rs
  • codex-rs/login/src/auth/manager.rs

Comment thread codex-rs/core/src/subrouter_rotation.rs Outdated
Comment on lines +56 to +62
auth_manager.reload().await;

let Some(auth_after) = auth_manager.auth_cached() else {
return Ok(false);
};

Ok(auth_snapshot(&auth_after) != before)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Preserve forced workspace restrictions after rotation.

This path accepts any changed auth snapshot after reload(), but AuthManager can be configured with forced_chatgpt_workspace_id. That means a quota-triggered Subrouter switch can move the session onto a different workspace/account even though codex-rs/login/src/auth/manager.rs:1842-1851 explicitly rejects that case for other refresh flows. Please make the reload here restriction-aware before treating the rotation as successful and retrying the turn.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/subrouter_rotation.rs` around lines 56 - 62, After
reload(), do not treat any changed auth snapshot as a successful rotation
without enforcing the AuthManager's forced_chatgpt_workspace_id restriction:
after obtaining auth_after via auth_manager.auth_cached(), check the manager's
forced_chatgpt_workspace_id (or call an existing AuthManager method that
indicates a forced workspace) and compare it to the workspace/account identifier
in auth_after (or compare to the workspace in the original before snapshot); if
the forced value exists and the workspace changed to a different id, treat this
as not a valid rotation and return Ok(false); otherwise proceed with the
existing auth_snapshot(&auth_after) != before comparison.

Comment thread codex-rs/core/src/subrouter_rotation.rs Outdated
Comment on lines +65 to +72
fn auto_rotate_enabled() -> bool {
match std::env::var(AUTO_ROTATE_ENV) {
Ok(value) => {
let value = value.trim().to_ascii_lowercase();
!matches!(value.as_str(), "0" | "false" | "no" | "off")
}
Err(_) => true,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Default Subrouter rotation to opt-in.

When CODEX_SUBROUTER_AUTO_ROTATE is unset, this still returns true, so any quota/usage-limit error on ChatGPT auth will try to execute sr if it happens to be on PATH. That is a surprising external side effect on a production error path and makes behavior depend on the host environment rather than explicit config.

Suggested fix
 fn auto_rotate_enabled() -> bool {
     match std::env::var(AUTO_ROTATE_ENV) {
         Ok(value) => {
             let value = value.trim().to_ascii_lowercase();
             !matches!(value.as_str(), "0" | "false" | "no" | "off")
         }
-        Err(_) => true,
+        Err(_) => false,
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn auto_rotate_enabled() -> bool {
match std::env::var(AUTO_ROTATE_ENV) {
Ok(value) => {
let value = value.trim().to_ascii_lowercase();
!matches!(value.as_str(), "0" | "false" | "no" | "off")
}
Err(_) => true,
}
fn auto_rotate_enabled() -> bool {
match std::env::var(AUTO_ROTATE_ENV) {
Ok(value) => {
let value = value.trim().to_ascii_lowercase();
!matches!(value.as_str(), "0" | "false" | "no" | "off")
}
Err(_) => false,
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@codex-rs/core/src/subrouter_rotation.rs` around lines 65 - 72, The
auto_rotate_enabled() function currently defaults to true when the
CODEX_SUBROUTER_AUTO_ROTATE env var is unset, causing unexpected side effects;
change its behavior so rotation is opt-in by returning false on Err(_)
(unset/missing) and only returning true when the env var explicitly contains a
truthy value (e.g., not "0"/"false"/"no"/"off")—update the match in
auto_rotate_enabled() to treat Err(_) as false so rotation only occurs when the
environment variable is explicitly set to an enabled value.

@lawrencecchen
lawrencecchen force-pushed the feat/subrouter-auto-rotate branch from cb6533a to e895b84 Compare May 4, 2026 08:44
@lawrencecchen lawrencecchen changed the title Auto-rotate Codex auth with Subrouter Add SubscriptionExhausted hook for auth rotation May 4, 2026
@lawrencecchen
lawrencecchen force-pushed the feat/subrouter-auto-rotate branch from e895b84 to 1bea7dd Compare May 4, 2026 08:45

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1bea7dd173

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +36 to +41
let Some(auth_before) = auth_manager.auth_cached() else {
return false;
};

if !can_refresh_managed_chatgpt_auth(&auth_before) {
return false;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Gate hook retry on effective auth, not cached auth.json

This branch uses auth_cached() to decide whether a SubscriptionExhausted hook can refresh credentials, but request auth can be overridden by external API-key auth (AuthManager::auth()/get_api_auth_mode() prefer external API key mode). In sessions where a user has both cached ChatGPT auth and an active external API key, quota errors from the API key path will incorrectly run the subscription hook and may emit a misleading “refreshed auth, retrying” retry even though the active auth source cannot be changed by rotating auth.json. Please gate on the effective auth mode (or resolved auth) before attempting the hook/retry path.

Useful? React with 👍 / 👎.

@lawrencecchen
lawrencecchen force-pushed the feat/subrouter-auto-rotate branch from 1bea7dd to b860208 Compare May 4, 2026 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant