diff --git a/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md b/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md index b7e1bdabe7..7adc2c11e4 100644 --- a/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md +++ b/apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md @@ -53,13 +53,14 @@ BB_HOST_DAEMON_PORT only for an intentional non-default target. - Treat plugin commands as normal top-level commands after installation. The builtin Account Pool plugin is disabled by default. Enable it, add Claude -credentials, and inspect its proxy route and account quota with: +or Codex credentials, and inspect its proxy routes and account quota with: ```sh bb plugin enable account-pool bb pool account add --provider claude --login printf '%s\n' "$CLAUDE_AUTH_CODE" | bb pool account login-complete --session --code-stdin bb pool account add --provider claude --import +bb pool account add --provider codex --import printf '%s\n' "$ANTHROPIC_API_KEY" | bb pool account add --provider claude --api-key-stdin [--label ] [--priority ] bb pool account add --provider claude --api-key [--label ] [--priority ] bb pool account list [--json] @@ -76,8 +77,11 @@ exits. Pipe the manual Claude callback code to `account login-complete` with that session ID within ten minutes. The code stays out of process arguments, and the browser and bb server may be on different machines. Newly added or enabled accounts are available without a plugin reload. With an -enabled account whose secret file remains readable and valid, Claude Code -sessions receive the pool route and a distinct secret token for their machine. +enabled account whose secret file remains readable and valid, matching Claude +Code or Codex sessions receive the pool route and a distinct secret token for +their machine. +Codex receives `CODEX_OPENAI_BASE_URL` and the secret +`CODEX_POOL_AUTH_TOKEN`; bb applies them as in-memory app-server config. Tokens are never printed. `status` prunes tokens for unenrolled machines and shows token timestamps plus recently routed threads whose machines need a local Claude login before the pool can be disabled safely. Rotation keeps the @@ -85,7 +89,8 @@ prior token valid for ten minutes. Agents should pipe API keys to `--api-key-stdin`; `--api-key ` is an unsafe compatibility form that exposes the key in process arguments, shell history, and agent transcripts. Prefer `--import` for -an existing Claude Code login. OAuth quota refreshes on add or enable and every +an existing Claude Code login. Codex import reads `~/.codex/auth.json` on the +bb server host. OAuth quota refreshes on add or enable and every five minutes while an account is idle. Account tables add columns for observed model-family buckets; JSON status exposes their utilization, reset, status, observation time, and source under `familyWeekly`. Selection skips an account diff --git a/docs/configuration.md b/docs/configuration.md index 62986cbbb0..316fdbb93b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -614,9 +614,10 @@ persistently restores undimmed splits. ## Account Pool The builtin Account Pool plugin is disabled on fresh installations. It stores -non-secret Claude account metadata in plugin KV, quota observations in the -plugin SQLite database, and each account token plus per-machine hub tokens in -0600 files under `/plugins/account-pool/secrets/accounts/`. +non-secret Claude and Codex account metadata in plugin KV, quota observations +in the plugin SQLite database, and each account token plus per-machine hub +tokens in 0600 files under +`/plugins/account-pool/secrets/accounts/`. Enable it and add at least one account: ```sh @@ -624,6 +625,7 @@ bb plugin enable account-pool bb pool account add --provider claude --login printf '%s\n' "$CLAUDE_AUTH_CODE" | bb pool account login-complete --session --code-stdin bb pool account add --provider claude --import +bb pool account add --provider codex --import printf '%s\n' "$ANTHROPIC_API_KEY" | bb pool account add --provider claude --api-key-stdin [--label ] [--priority ] ``` @@ -633,10 +635,11 @@ pipe the code shown on Anthropic's manual callback page to `account login-complete` with that session ID. The browser can be on a different machine from the bb server, and the code stays out of process arguments. The Account Pool plugin settings page exposes the same flow with **Sign in to -Claude**, plus the account list, import, API-key, enable/disable, and removal -controls. +Claude**, plus provider-specific import, API-key, enable/disable, and removal +controls. Codex has no browser sign-in flow; **Import Codex from this machine** +reads the bb server host's `~/.codex/auth.json`. -The import path reads the Claude Code login on the bb server host. +The import paths read the Claude Code or Codex login on the bb server host. `--api-key-stdin` reads exactly one non-empty key from piped standard input and is the default API-key path for agents. The compatibility form `--api-key ` remains available, but exposes the secret in process arguments, shell @@ -644,8 +647,12 @@ history, and agent transcripts. The hub starts immediately, so a newly added or enabled account is available without a plugin reload. When the plugin has an enabled account whose secret file is readable and -valid, it automatically contributes the hub route, a machine-specific secret -token, and `ENABLE_TOOL_SEARCH=true` to Claude Code sessions on every host. +valid, it automatically contributes the provider's hub route and a +machine-specific secret token to Claude Code or Codex sessions on every host. +Claude Code also receives `ENABLE_TOOL_SEARCH=true`. +Codex receives `CODEX_OPENAI_BASE_URL` and the secret +`CODEX_POOL_AUTH_TOKEN`; bb applies both when launching `codex app-server` +without writing to `~/.codex/config.toml`. Claude Code disables tool search behind a custom base URL by default; the hub forwards `tool_reference` blocks unchanged, so the override keeps it on. Tokens are never printed @@ -667,15 +674,21 @@ without disabling that account for other families. Imported and newly signed-in accounts retain their Anthropic account UUID, and the hub aligns a present `metadata.user_id` account component with the selected account. -Two settings control routing. `switchThreshold` is the shared or requested +Three settings control routing. `switchThreshold` is the shared or requested model-family quota fraction at which an account stops receiving matching traffic and defaults to `0.98`. -`upstreamBaseUrl` defaults to `https://api.anthropic.com` and exists only for -tests and QA with a controlled fake upstream: +`upstreamBaseUrl` defaults to `https://api.anthropic.com` and +`codexUpstreamBaseUrl` defaults to +`https://chatgpt.com/backend-api/codex`. Codex uses the hub's HTTP Responses +and models routes and prefers its WebSocket Responses route; the hub keeps the +downstream WebSocket session semantics while forwarding upstream over HTTPS +SSE. Both URL settings exist only for tests and QA with a controlled fake +upstream: ```sh bb plugin config account-pool set switchThreshold 0.98 bb plugin config account-pool set upstreamBaseUrl http://127.0.0.1:9000 +bb plugin config account-pool set codexUpstreamBaseUrl http://127.0.0.1:9001 ``` ## bb connect diff --git a/packages/templates/src/templates/bb-guide-plugins.md b/packages/templates/src/templates/bb-guide-plugins.md index 09a1f48c0c..1972c7aca9 100644 --- a/packages/templates/src/templates/bb-guide-plugins.md +++ b/packages/templates/src/templates/bb-guide-plugins.md @@ -24,14 +24,15 @@ The builtin Custom instructions plugin adds a multiline editor under Settings agent task instructions; blank text contributes nothing. The builtin Account Pool plugin is disabled on fresh installations. It stores -Claude account tokens in per-account 0600 secret files and proxies Anthropic -Messages API requests through the bb server. Enable it and add an account: +Claude and Codex account tokens in per-account 0600 secret files and proxies +provider API requests through the bb server. Enable it and add an account: ``` bb plugin enable account-pool bb pool account add --provider claude --login printf '%s\n' "$CLAUDE_AUTH_CODE" | bb pool account login-complete --session --code-stdin bb pool account add --provider claude --import +bb pool account add --provider codex --import printf '%s\n' "$ANTHROPIC_API_KEY" | bb pool account add --provider claude --api-key-stdin [--label ] [--priority ] bb pool account add --provider claude --api-key [--label ] [--priority ] bb pool account list [--json] @@ -48,14 +49,18 @@ sign-in URL and session ID, then exits. After sign-in, pipe the manual callback code to `account login-complete` with that session ID. The browser does not need to run on the bb server machine, and neither the code nor account tokens enter process arguments. The same flow is available in the plugin settings page -through the **Sign in to Claude** button. +through the **Sign in to Claude** button. Codex import reads the bb server +host's `~/.codex/auth.json` and has no browser sign-in flow. The hub starts immediately, even before an account is configured, so newly added or enabled accounts are available without a plugin reload. With an enabled account whose secret file remains readable and valid, the plugin -contributes its server route, a distinct secret token, and -`ENABLE_TOOL_SEARCH=true` (tool search stays on through the hub) to Claude -Code sessions on every host. Tokens are never printed. `status` prunes tokens for +contributes its provider-specific server route and a distinct secret token to +Claude Code or Codex sessions on every host. Claude Code also receives +`ENABLE_TOOL_SEARCH=true` so tool search stays on through the hub. Codex +receives `CODEX_OPENAI_BASE_URL` and the secret `CODEX_POOL_AUTH_TOKEN`; its +app server uses those values without editing `~/.codex/config.toml`. Tokens are +never printed. `status` prunes tokens for unenrolled machines and shows token timestamps plus recently routed threads whose machines need a local Claude login before the pool can be disabled safely. Rotation keeps the prior token valid for ten minutes. Agents should use diff --git a/plugins/account-pool/app.test.tsx b/plugins/account-pool/app.test.tsx index 71b9297339..f8cb581c47 100644 --- a/plugins/account-pool/app.test.tsx +++ b/plugins/account-pool/app.test.tsx @@ -70,7 +70,7 @@ describe("Account Pool settings", () => { }, ); - expect(await slot.findByText("No Claude accounts yet")).toBeTruthy(); + expect(await slot.findByText("No provider accounts yet")).toBeTruthy(); fireEvent.click(slot.getByRole("button", { name: "Sign in to Claude" })); expect(await slot.findByText("Finish signing in to Claude")).toBeTruthy(); expect(opened).toEqual(["https://claude.ai/oauth/authorize?state=state"]); @@ -83,6 +83,7 @@ describe("Account Pool settings", () => { expect(slot.getByText("person@example.com")).toBeTruthy(); expect(slot.getByText("5h 21%")).toBeTruthy(); expect(slot.getByText("7d 43%")).toBeTruthy(); + expect(slot.getByText("Claude")).toBeTruthy(); expect(slot.queryByText("Finish signing in to Claude")).toBeNull(); expect(slot.rpcCalls).toContainEqual({ method: "login.complete", @@ -93,6 +94,46 @@ describe("Account Pool settings", () => { }); }); + it("imports Codex credentials from the server machine", async () => { + const added = { + ...account(), + provider: "codex" as const, + label: "Codex Pro", + }; + const accounts: AccountSummary[] = []; + const slot = renderSlot( + app.settingsSections[0]!, + {}, + { + rpc: { + "account.list": () => [...accounts], + "account.add": () => { + accounts.push(added); + return added; + }, + }, + }, + ); + fireEvent.click( + await slot.findByRole("button", { + name: "Import Codex from this machine", + }), + ); + await waitFor(() => + expect(slot.rpcCalls).toContainEqual({ + method: "account.add", + input: { + provider: "codex", + source: { kind: "import" }, + label: null, + priority: 100, + }, + }), + ); + expect(await slot.findByText("Codex Pro")).toBeTruthy(); + expect(slot.getByText("Codex")).toBeTruthy(); + }); + it("keeps the login step open and shows a completion error inline", async () => { const slot = renderSlot( app.settingsSections[0]!, diff --git a/plugins/account-pool/app.tsx b/plugins/account-pool/app.tsx index d104695e9b..7a2f017d27 100644 --- a/plugins/account-pool/app.tsx +++ b/plugins/account-pool/app.tsx @@ -42,6 +42,7 @@ function AccountPoolSettings() { const navigate = useBbNavigate(); const [accounts, setAccounts] = useState(null); const [loading, setLoading] = useState(false); + const [codexLoading, setCodexLoading] = useState(false); const [error, setError] = useState(null); const [loginStep, setLoginStep] = useState(null); const [pastedCode, setPastedCode] = useState(""); @@ -142,6 +143,20 @@ function AccountPoolSettings() { setLoading(false); } + async function importCodexAccount(): Promise { + if (codexLoading) return; + setCodexLoading(true); + await mutate(async () => { + await rpc.call("account.add", { + provider: "codex", + source: { kind: "import" }, + label: null, + priority: 100, + }); + }); + setCodexLoading(false); + } + async function addApiKey(): Promise { if (apiKey.trim().length === 0 || apiKeyPending) return; setApiKeyPending(true); @@ -191,10 +206,12 @@ function AccountPoolSettings() { return (
-

Claude accounts

+

+ Provider accounts +

- Account Pool routes Claude Code threads through an available account - and moves away from accounts that reach their limits. + Account Pool routes Claude and Codex threads through available + accounts and moves away from accounts that reach their limits.

@@ -205,11 +222,11 @@ function AccountPoolSettings() { ) : accounts.length === 0 ? (

- No Claude accounts yet + No provider accounts yet

- Add an account and the plugin will route Claude Code threads through - the pool automatically. + Add an account and the plugin will route supported provider threads + through the pool automatically.

) : ( @@ -225,6 +242,9 @@ function AccountPoolSettings() { {account.kind === "oauth" ? "OAuth" : "API key"} + + {account.provider === "claude" ? "Claude" : "Codex"} + {statusLabel(account.status)} @@ -276,7 +296,15 @@ function AccountPoolSettings() { disabled={loading} onClick={() => void importAccount()} > - {loading ? "Importing…" : "Import from this machine"} + {loading ? "Importing…" : "Import Claude from this machine"} + +