Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
13 changes: 9 additions & 4 deletions apps/server/src/services/skills/builtin-skills/bb-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id> --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 <text>] [--priority <n>]
bb pool account add --provider claude --api-key <key> [--label <text>] [--priority <n>]
bb pool account list [--json]
Expand All @@ -76,16 +77,20 @@ 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
prior token valid for ten minutes. Agents should pipe API keys to
`--api-key-stdin`;
`--api-key <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
Expand Down
35 changes: 24 additions & 11 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -614,16 +614,18 @@ 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 `<data-dir>/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
`<data-dir>/plugins/account-pool/secrets/accounts/`.
Enable it and add at least one account:

```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 <id> --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 <text>] [--priority <n>]
```

Expand All @@ -633,19 +635,24 @@ 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
<key>` remains available, but exposes the secret in process arguments, shell
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
Expand All @@ -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
Expand Down
17 changes: 11 additions & 6 deletions packages/templates/src/templates/bb-guide-plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id> --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 <text>] [--priority <n>]
bb pool account add --provider claude --api-key <key> [--label <text>] [--priority <n>]
bb pool account list [--json]
Expand All @@ -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
Expand Down
43 changes: 42 additions & 1 deletion plugins/account-pool/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
Expand All @@ -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",
Expand All @@ -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]!,
Expand Down
42 changes: 35 additions & 7 deletions plugins/account-pool/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ function AccountPoolSettings() {
const navigate = useBbNavigate();
const [accounts, setAccounts] = useState<AccountSummary[] | null>(null);
const [loading, setLoading] = useState(false);
const [codexLoading, setCodexLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [loginStep, setLoginStep] = useState<LoginStep | null>(null);
const [pastedCode, setPastedCode] = useState("");
Expand Down Expand Up @@ -142,6 +143,20 @@ function AccountPoolSettings() {
setLoading(false);
}

async function importCodexAccount(): Promise<void> {
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<void> {
if (apiKey.trim().length === 0 || apiKeyPending) return;
setApiKeyPending(true);
Expand Down Expand Up @@ -191,10 +206,12 @@ function AccountPoolSettings() {
return (
<div className="w-full space-y-5">
<div>
<h3 className="text-sm font-medium text-foreground">Claude accounts</h3>
<h3 className="text-sm font-medium text-foreground">
Provider accounts
</h3>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
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.
</p>
</div>

Expand All @@ -205,11 +222,11 @@ function AccountPoolSettings() {
) : accounts.length === 0 ? (
<div className="rounded-md border border-border/60 bg-surface-recessed px-4 py-4">
<p className="text-sm font-medium text-foreground">
No Claude accounts yet
No provider accounts yet
</p>
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
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.
</p>
</div>
) : (
Expand All @@ -225,6 +242,9 @@ function AccountPoolSettings() {
<span className="text-xs text-muted-foreground">
{account.kind === "oauth" ? "OAuth" : "API key"}
</span>
<span className="rounded-full border border-border/60 px-2 py-0.5 text-xs text-muted-foreground">
{account.provider === "claude" ? "Claude" : "Codex"}
</span>
<span className="text-xs text-muted-foreground">
{statusLabel(account.status)}
</span>
Expand Down Expand Up @@ -276,7 +296,15 @@ function AccountPoolSettings() {
disabled={loading}
onClick={() => void importAccount()}
>
{loading ? "Importing…" : "Import from this machine"}
{loading ? "Importing…" : "Import Claude from this machine"}
</Button>
<Button
type="button"
variant="outline"
disabled={codexLoading}
onClick={() => void importCodexAccount()}
>
{codexLoading ? "Importing…" : "Import Codex from this machine"}
</Button>
<Button
type="button"
Expand Down
4 changes: 2 additions & 2 deletions plugins/account-pool/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Routes Anthropic Messages API traffic across a pool of Claude accounts.",
"description": "Routes Claude and Codex API traffic across provider account pools.",
"engines": {
"bb": ">=0.0"
},
"bb": {
"name": "Account Pool",
"description": "Routes Anthropic Messages API traffic across a pool of Claude accounts.",
"description": "Routes Claude and Codex API traffic across provider account pools.",
"branding": {
"icon": "Layers"
},
Expand Down
Loading
Loading