Skip to content

Add Account Pool plugin: multi-account Claude hub - #3036

Merged
SawyerHood merged 3 commits into
bb/provider-env-contributionfrom
bb/account-pool-hub
Sep 4, 2026
Merged

Add Account Pool plugin: multi-account Claude hub#3036
SawyerHood merged 3 commits into
bb/provider-env-contributionfrom
bb/account-pool-hub

Conversation

@SawyerHood

Copy link
Copy Markdown
Collaborator

Stack layer 2 of 3. Prerequisite: the provider env contribution hook PR (layer 1) below it in this stack; this layer does not use the hook yet, layer 3 wires the plugin to it.

Human comments

What was wrong

BB had no server-side account pool for Claude traffic, so one Claude Code process could use only its local credential and could not rotate away from an exhausted subscription. BB-88 establishes the first stack layer: a manually configured Anthropic-compatible hub with durable account, secret, and quota state.

What changed

  • Added the disabled-by-default account-pool built-in plugin with provider-keyed account metadata in plugin KV, per-account 0600 token files, a generated 0600 hub bearer key, and SQLite quota/hold/error state.
  • Added authenticated Anthropic-compatible routes for Messages and token counting plus an unauthenticated 200 response for Claude Code's HEAD /api/hello, with request-header rewriting, query forwarding, byte-streaming responses, quota tracking, threshold-aware selection, quota-429 rotation, per-minute pacing, OAuth refresh serialization/writeback, and graceful drain. The hub accepts requests before account setup, reports a 503 while no account is enabled, and uses newly added or enabled accounts without reload.
  • Added bb pool account add|list|remove|enable|disable and bb pool status, mirrored through plugin RPC, plus built-in catalog registration and CLI guide, skill, and configuration documentation.
  • No Plugin SDK or host-daemon wire contract changed. The requested wildcard 404 handler is omitted because the current bb.http.route SDK supports exact paths only and this stack layer explicitly forbids an SDK change; unknown plugin paths retain BB's standard 404.

How you verified

  • Added fake-upstream and Plugin SDK harness tests covering immediate use after first-account CLI add, hub authentication, unauthenticated hello probes, header/query/body forwarding, first-chunk latency and byte-identical SSE, quota bucket parsing, threshold skip, quota-429 resend, per-minute same-account pacing, serialized OAuth refresh with 0600 writeback, refresh/auth failures, CLI/RPC operations, content-type-aware stream failures, and stop/drain behavior. These tests have no implementation to load before this change and pass with the new plugin.
  • pnpm exec turbo run typecheck test --filter=bb-plugin-account-pool --continue — 9 tests passed; typecheck passed.
  • pnpm exec turbo run typecheck test --filter=@bb/server --filter=@bb/plugin-api-map --filter=@bb/templates --continue — 13 tasks passed; server 227 files/2202 tests, template and plugin-map suites passed.
  • pnpm exec oxlint plugins/account-pool/src and targeted Prettier checks passed.
  • In the running dev app, HEAD /api/hello returned 200; the hub streamed fake upstream SSE chunks every 20 seconds through the real mounted plugin route for at least 200 seconds; bb pool status showed the service accepting traffic and recorded 42% 5-hour / 64% 7-day quota utilization.
  • Imported the server host's Claude OAuth credential, restored the upstream to https://api.anthropic.com, and ran ANTHROPIC_BASE_URL=<dev hub> ANTHROPIC_AUTH_TOKEN=<hub key> claude -p "Reply with exactly: POOL OK". It exited 0 with POOL OK; account quota changed from unknown to 18% five-hour and 32% seven-day utilization.

Task: BB-88

AGENT GENERATED

@SawyerHood

Copy link
Copy Markdown
Collaborator Author

CI for this head ran via manual dispatch after the restack (the pull_request trigger did not fire): https://github.com/get-bb/bb/actions/runs/33832576981 — all jobs passed.

AGENT GENERATED

@SawyerHood
SawyerHood marked this pull request as ready for review September 4, 2026 05:14
@SawyerHood
SawyerHood merged commit 9006bf4 into main Sep 4, 2026
29 checks passed
@SawyerHood
SawyerHood deleted the bb/account-pool-hub branch September 4, 2026 05:31
SawyerHood added a commit that referenced this pull request Sep 4, 2026
Stack layer 3 of 3 (top). Prerequisites: #3035 (provider env
contribution hook) and #3036 (Account Pool hub) below it in this stack.

## Human comments

## What was wrong

The Account Pool hub existed, but Claude Code sessions were not routed
to it
automatically: the plugin still exposed one global bearer key, had no
per-thread bypass, and could not replace an unauthenticated machine's
local
Claude health with the readiness supplied by an enabled pool account.
This is
the BB-90 stack layer on `bb/account-pool-hub` and
`bb/provider-env-contribution`.

## What changed

- Registered the Account Pool's Claude Code environment contribution
with the
exact server-relative hub route and a secret bearer token minted lazily
per
machine. The resolver contributes nothing when no account is enabled or
the
  thread is bypassed.
- Replaced the global hub key with per-machine 0600 token files loaded
once
into a single-process cache, constant-time bearer matching across every
current and grace token, serialized mint/use/rotation updates, a
ten-minute
rotation grace window, and removal of the obsolete global key file.
Last-use
persistence is throttled to once per machine per minute, expired grace
tokens
  are pruned with the next write, and status never returns token values.
- Added `bb pool token rotate --machine <id-or-name>` and
`bb pool bypass <thread-id> [--off]`, with matching plugin RPC
operations and
  discoverability updates in the CLI skill, generated guide source, and
  configuration docs.
- Tracked the last 24 hours of routed threads and made unsafe
disablement
visible in `bb pool status`, including proxied hosts that are relying on
the
pool, plus a plugin warning log when a disable leaves those threads on
hosts
without usable local Claude credentials. The dispose-time inspection is
  guarded against failures and bounded to two seconds.
- Added the single experimental SDK member
`bb.providers.experimental_contributeEnvHealth`, with fake-host support,
Plugin Guide documentation, API audit notes, API map inventory, and SDK
version 0.4.45. The server presents an `unauthenticated` or `expired`
Claude
provider as ready/`Proxied` only when the same live plugin also has an
env
  resolver and the Account Pool has an enabled account.
- Reused the lower stack layers' env command payload and hub drain. This
PR
  does not change the server-to-daemon wire, so it does not bump
  `HOST_DAEMON_PROTOCOL_VERSION`.

## Not in this PR

- BB's plugin SDK has no system-notice append surface or fitting backend
thread
action. `bb.sdk.threads.send` would create a real provider turn, so this
PR
does not fake the requested disable-time thread notice or bypass action.
The
safety gap is instead explicit in `bb pool status` and the disable log.
- Disabling a plugin reloads the Claude bridge, which rebuilds the
session but
  discards the previous attachment before it can emit the same cosmetic
  rebuild warning used by an in-place bypass change. In e2e thread
  `thr_stxrb938ec`, the fresh `provider.env-resolved` event without pool
entries and the successful `LOCAL AGAIN` response demonstrate the actual
disable behavior. The bypass path in that thread does retain the
attachment
  and emits the expected rebuild warning.
- A packaged remote daemon cannot be enrolled into an isolated dev app.
The
multi-machine path is verified by the daemon-side runtime test that
expands
`serverPath` against its loopback machine-auth URL, plus a live 200
response
from the dev hub's `HEAD /api/hello`; this does not claim a live remote
  thread.

## How you verified

- Added Account Pool regressions for empty resolver/health output, exact
per-host contributions and secret masking, distinct machine tokens,
bypass
through CLI and RPC, token rotation and grace expiry, the single-process
  cache and last-use write throttle, secret-free status, proxied and
unauthenticated routed-host warnings, bounded/failure-safe disable
logging,
and the existing authenticated streaming/drain paths. These behaviors
had no
  implementation before this change; all 16 tests pass.
- Added server and fake-plugin-host coverage proving health is ignored
without
an adjacent env resolver and marks only credential-blocked provider
health
  ready when a contribution is active.
- `pnpm exec turbo run typecheck --filter=@get-bb/plugin-sdk
--filter=@bb/server --filter=@bb/plugin-api-map
--filter=bb-plugin-account-pool --continue` — 9 tasks passed.
- `pnpm exec turbo run test --filter=@get-bb/plugin-sdk
--filter=@bb/server --filter=@bb/plugin-api-map
--filter=bb-plugin-account-pool --continue --force` — 11 tasks passed;
Account Pool 13 tests, SDK 22 files, API map 11 files, and server 227
files / 2205 tests.
- Review follow-up: `pnpm exec turbo run typecheck test
--filter=bb-plugin-account-pool --filter=@bb/server
--filter=@get-bb/plugin-sdk --continue --force` — 13 tasks passed;
Account Pool 16 tests, SDK 22 files, and server 227 files / 2205 tests.
- `pnpm exec turbo run typecheck test --filter=@bb/agent-runtime
--continue --force` — typecheck passed and 22 files / 319 tests passed,
including loopback `serverPath` expansion and secret event masking.
- `node .github/workflows/check-plugin-sdk-version.mjs` — passed with
the SDK surface change and version bump.
- `git diff --check origin/bb/account-pool-hub...HEAD` — passed.
- In the isolated dev app, Claude Code thread `thr_stxrb938ec` returned
`POOL OK` through the hub with an expanded loopback URL and masked token
in
`provider.env-resolved`; quota utilization moved from unknown to
28%/34%.
Bypass returned `DIRECT OK` with no pool env and the rebuild warning.
Plugin
disable returned `LOCAL AGAIN` using local credentials with no pool env,
and
the plugin was re-enabled. Per-machine token rotation also completed,
and a
live hub hello request returned HTTP 200. BB-90 has the reduced JSON
evidence
  and both rendered UI screenshots attached.

Part of #1552

Task: BB-90

> AGENT GENERATED

---------

Co-authored-by: Sawyer Hood <kirbyhood@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
SawyerHood added a commit that referenced this pull request Sep 4, 2026
Stack layer 4 of 4 (top). Prerequisites: #3035, #3036, and #3042 below
it in this stack.

## Human comments

## What was wrong

Account Pool had no settings UI or interactive Claude login path, so
users had to provision accounts through the CLI. The plugin could also
report `needs-configuration` when empty, but the app frontend loader
discarded every plugin frontend outside `running`; that made the
configuration UI that an empty pool needs impossible to load. Part of
#1552.

## What changed

- Added an Account Pool settings section with the minimal account list,
ready/held/exhausted/error/disabled status, 5-hour and 7-day
percentages, enable/disable, confirmed removal, machine import, and a
password-field API-key flow.
- Added Claude Code-compatible PKCE login with a ten-minute in-memory
session, manual callback paste parsing, state validation, token
exchange, profile enrichment, protected account storage, and
user-readable errors that do not expose upstream payloads.
- Exposed login through typed plugin RPC and two plugin-owned CLI
commands: `bb pool account add --provider claude --login` prints the
authorization URL and session ID, then `bb pool account login-complete
--session <id> --code-stdin` completes it without putting the pasted
code in process arguments.
- Generalized the plugin CLI proxy's protected stdin handling so any
`--<name>-stdin` flag becomes `--<name> <single-line-value>` with the
existing 16 KiB limit; core contains no Account Pool-specific routing.
- Published account mutation notifications and refreshed the settings
list after mutations.
- Allowed frontend bundles for `needs-configuration` and `degraded`
plugins, with regression coverage for all supported settings statuses.
- Updated the plugin guide, built-in bb CLI skill, and configuration
documentation. There is no server/host-daemon wire change, so
`HOST_DAEMON_PROTOCOL_VERSION` is unchanged.

## How you verified

- `pnpm exec turbo run typecheck test --filter=bb-plugin-account-pool
--filter=@bb/cli --filter=@bb/app --filter=@bb/templates --continue
--force` — 14/14 tasks passed: Account Pool 28 tests, CLI 530 tests, app
3,853 tests (4 skipped), templates 43 tests.
- `pnpm --silent bb plugin build plugins/account-pool` — emitted server
and app JS/CSS/meta bundles.
- `pnpm install --frozen-lockfile --offline` — lockfile accepted without
resolution changes.
- Live dev-app proof: captured the empty state, started the
PKCE/manual-paste flow, verified the API-key input is `type=password`,
imported the machine OAuth account, ran Claude Code thread
`thr_qr935hirfw` to an `ok` response through the pool, observed
5-hour/7-day utilization, removed the account in the UI, and confirmed
its mode-600 secret file was deleted. Screenshots and command artifacts
are attached to BB-92.
- The isolated headless QA browser reached Claude's Cloudflare
“Performing security verification” page, so live OAuth completion
stopped at the brief's human-browser boundary. The exact state is
attached; successful exchange/profile/storage and all three paste shapes
are covered against fake HTTP servers.

Part of #1552

> AGENT GENERATED
SawyerHood added a commit that referenced this pull request Sep 4, 2026
…rite (#3053)

Standalone follow-up to the Account Pool stack (#3035, #3036, #3042,
#3049); based on main.

## Human comments

## What was wrong

Account Pool selected accounts using only shared 5-hour and 7-day quota
observations, even though Anthropic applies separate weekly limits to
model families such as Fable. Scoped `7d_<bucket>` headers were recorded
as opaque diagnostics and never governed selection, the OAuth usage
endpoint was not polled, and Claude Code request metadata could retain
an account UUID belonging to a different OAuth account after routing.
Part of #1552.

## What changed

- Replaced opaque bucket exhaustion diagnostics with fixed Fable,
Sonnet, Opus, Haiku, and other weekly quota slots carrying utilization,
reset, status, observation time, and source.
- Added five-minute idle OAuth usage refreshes, immediate refresh after
add or enable, scoped usage-limit parsing, and lazy account UUID
backfill through the OAuth profile endpoint. Refresh failures preserve
the last known state.
- Parsed the request model once and made eligibility and reset
tie-breaking use its governing family bucket. Scoped 429 responses now
rotate accounts without globally exhausting the rejected account.
- Stored account UUIDs during Claude Code import and OAuth sign-in, and
guarded request rewriting to the selected account for recognized
stringified-JSON and `user_*_account_<uuid>_session_*` metadata forms.
Unrecognized, absent, non-JSON, or UUID-less requests remain
byte-identical.
- Added dynamic family columns to `bb pool account list` and updated the
CLI skill, plugin guide, and configuration docs.
- Added focused migration, usage parsing/refresh, family routing,
scoped-header/429, UUID import/sign-in/backfill/rewrite, passthrough,
CLI, and app regression coverage. There is no Plugin SDK or
server/host-daemon wire change, so neither protocol version changed.

## How you verified

- `pnpm exec turbo run typecheck test --filter=bb-plugin-account-pool
--continue --force` — 6 tasks passed; 4 test files and 35 tests passed.
The new routing, scoped-429, refresh, migration, and UUID tests fail
against the prior implementation and pass with this change.
- `pnpm exec turbo run typecheck test --filter=@bb/templates --continue
--force` — 7 tasks passed; 7 test files and 43 tests passed.
- `pnpm exec turbo run typecheck --filter=@bb/server --continue` — 5
tasks passed.
- `pnpm --silent bb plugin build plugins/account-pool` — server and app
bundles built successfully.
- `git diff --check origin/main...HEAD` — passed after rebasing onto
current `origin/main`.
- Isolated dev-app proof with two OAuth accounts and
`switchThreshold=0.70`: Account A had shared 7d at 43% but Fable at 80%;
Account B had Fable at 7%. A real `claude-fable-5-1` turn completed
through Account B, then a real `claude-sonnet-5` turn completed through
Account A, proving family-only exhaustion does not disable the account
globally. A redacted shape probe on a further real turn found
`metadata.user_id` present without a recognized account UUID component,
so the production path correctly preserved it byte-for-byte. The probe
was removed, both imported test accounts were removed from the isolated
store, and the dev app was stopped. Evidence is attached to BB-94.

Part of #1552

> AGENT GENERATED
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