Provider plugins: one API for Codex, Claude Code, pi, and ACP agents - #2325
Merged
Conversation
What was wrong Providers (Codex, Claude Code, pi, and every ACP agent) were wired into core with per-provider carve-outs: the host daemon carried a provider table and provider-specific skill/command scanning, the agent runtime carried provider-specific recovery regexes and session branches, the server and clients branched on provider ids, and third-party agents could only arrive through the ACP tier with a narrower API than first-party providers had. Adding or changing a provider meant touching core in many places, and the "plugin API" did not describe what the built-in providers actually used. What changed Providers are plugins behind one API, with zero first-party privilege: the built-in providers use the same `bb.providers.register` declaration, bridge protocol, and SDK surface a third party uses. - **Bridge protocol v3 + presentation**: items carry a declarative `presentation` (label, glyph, tint, detail); web renders rows from it and plugins may add React renderers for their own kinds; mobile renders the declarative base with no plugin JavaScript. - **Registry**: `bb.providers.register` declares strings, icon, models, reasoning levels, service tiers, permission modes, native skill/command roots, maintenance (health, usage, installation), and AI services; one `ProviderInfo` shape everywhere. - **Interactions**: closed approvals with `toolUse`, open plugin-defined requests (`<pluginId>/<name>` + `request_answer`), one lifecycle event; the server stays the policy owner. - **Runtime and daemon**: typed recovery hints replace regexes; one bridge process per provider artifact with per-thread lanes; the daemon scans declared roots only, and every plugin — pi included — resolves host-only roots through `resolveNativeRoots` (the `absolute` side is gone); a bridge must return `providerThreadId` on `thread/start`, `thread/resume`, and `thread/fork` (the conformance kit pins the error); the ACP tier is deleted and the ACP plugin owns its agents through a published ACP bridge kit. - **pi** moves from a built-in runtime to a plugin over `pi --mode rpc` with an install gate. - **Plugin icons**: a plugin declares `bb.branding.experimental_icons` (name → SVG it ships); rows and provider branding reference `"<pluginId>/<name>"`; ingest rejects undeclared names; an uninstalled plugin falls back to the host glyph; declared-icon SVGs pass a strict validator and are served under `nosniff` + CSP. The pre-existing branding/logo routes now send the same headers; installed plugins' logos keep loading exactly as before, and `bb plugin build` checks logos for script vectors (script/handler/listener elements, `on*` attributes, `javascript:` hrefs) so authors see it when they build. - **Stabilization**: `experimental_` dropped where audited (old keys rejected with messages naming the new ones; one-release aliases for app-runtime members); provider-literal ratchet 209 → 18 references, all allowlisted with reason/owner/dies-at. - **Simplification (S7)**: dead adapter branches, a duplicate maintenance cache, duplicate validators/tests/fixtures removed; cross-plugin duplicates folded into `experimental_` SDK helpers (vendor plugin roots, presentation builders, the maintenance toolkit, bounded line reader, public-SDK-only scan), each with an `api_to_audit.md` entry; eleven already-published `provider-bridge` exports kept and listed under "Scheduled removals (next major)". - **Wire**: `HOST_DAEMON_PROTOCOL_VERSION` 153 → 164 (entries 154–164 in the header). `@get-bb/plugin-sdk` → 0.4.16 (main ships 0.4.15), bumped inside the first layer that changes the published surface so that layer passes the per-PR SDK guard on its own. - **Packaging**: `bb plugin build` now inlines the SDK subpaths a plugin server imports (`@get-bb/plugin-sdk/host`, `/provider-bridge/acp`) and leaves only the bare specifier external; before this, a packaged install loaded none of the provider plugins (the packed server aliases only the bare specifier). `smoke:tarball` is green with pi absent from PATH. - **Kept on purpose**: `experimental_setThreadRowStatus` and its status store stay (main's collapsed-section plugin-status rollup consumes them); the audit doc names the consumer. - **Docs**: `docs/provider-plugin-api.md` (design), `docs/api_to_audit.md` (experimental surface, windows, scheduled removals, deleted members), the plugin-authoring skill, the bb guide, CLI surfaces. Base: the current `main` (the private mirror was brought level with the public tip first). The branch is strictly linear and rebased; every conflict with main's recent changes (shared vitest workers, parallel SDK d.ts bundling, the collapsed-section status rollup, the mobile e2e flow) was resolved three-way and re-verified. The work was built as 151 reviewed layers and squashed into this commit; the layered history is kept on the private repository. How it was verified - Typecheck 76/76 packages; full test sweep green on the rebased tip; CI's build + lint, the package smoke (`smoke:tarball`), the plugin-SDK version guard against the merge-base, the npm version guard, and the version-lockstep check. - Parity harness: 39 recorded bridge cells (codex 16, claude-code 13, acp-cursor 10) replayed through the pre-migration main and the chain with zero diffs outside recordings/parity-allowlist.json (each entry named and reasoned); pi via re-recorded cells and the recorded-conformance kit. - Provider corpus: 340 real threads (307 production codex/claude-code threads + 32 pi threads captured on a dev instance of main, baselines main-minted), 94,365 rows, every changed row in a named, reviewed class, 0 unclassified — re-run at every stack. - Permission matrix: 134 pinned cells identical; native-roots goldens 22/22 (two new pi variants); every bridge's conformance kit green; the real-provider integration suite (live Codex, Claude Code, pi 0.84, ACP agents) 9 files / 64 tests. - Adversarial refutation passes per layer and on every stacked result (independent read-only reviewers probing behaviour, coverage, security, and public-push readiness); every removal in the simplification series was independently refuted before it was stacked. - Live QA in the dev app: Codex, Claude Code, Cursor and grok (ACP), the echo example plugin, and pi each ran a tool turn. Adversarial review of the squashed change: 27 confirmed findings fixed (29 reports with two duplicate pairs folded; one of them was the parity wording corrected above), plus three cheap hardenings the review recommended. Clients: a new client keeps exploration titles for the tool rows a pre-161 server serves; an embedded thread chat scopes plugin renderers to its own provider; a deferred provider plugin's bundle loads for its forms and its panel route; a tint React Native cannot paint falls back instead of painting black, a blank extension detail stays title-only, and images in a presentation detail render as alt text on every surface. Runtime: the retry ladder and the post-unarchive retry act on the typed recovery hint of every attempt (no other kind can become a `rate_limited` error), and a `restartRecommended` restart is scoped to the threads and the process it may touch. Bridges: the ACP kit types `authRequired` only from the agent's own auth error, settles the turn when an interrupt times out, reaps a session released during construction, and defers updates that arrive before the identity announcement; a codex turn whose rebuild hits an archived rollout stays resumable. Server and contracts: the presentation-glyph ingest check runs on every item-bearing event, the delegation and backgroundTask snapshots included; the 0.4.15 provider-bridge exports whose definitions moved are restored as aliases under "Scheduled removals", every removed or renamed 0.4.15 export is named in the audit doc, a pi child's scratch files go with the child, and the presentation-less tool-row window is documented as pi uses it. Tests and oracles: the conformance kit pins the resume and fork identities, the fake plugin host refuses the registrations the server refuses, the public-SDK scan reports relative escapes and dynamic specifiers, the corpus row-class gate judges idle classes per entry, lets a reshaped class name its fields and no longer claims a turn status change under container-bounds, and the native-roots goldens keep skill descriptions. Stacking the fixes surfaced one more, fixed here as well: the web search-reveal timers outlived the timeline rows they targeted, which surfaced as an unhandled error in the web test suite. A second pass took the remaining items: extension kinds are accepted only from their owning plugin's threads, so an extension item or state record whose kind belongs to another plugin is refused at ingest and surfaces as the same visible unhandled row a schema miss does. A childless delegation call in persisted history (an `Agent`, `Task`, or `spawnAgent` call whose subagent never produced a child event) classifies as a delegation row keyed by its call id, the row it had before, instead of a plain tool row; new events keep classifying structurally, the bridge's `delegation` item naming the call id as its `childRef`. A plugin whose host artifact fails to build keeps its provider listed as unavailable, with the build error as the reason, even when the same plugin also registers an AI service (before, such a plugin vanished from the provider list). The server alone authors interaction lifecycle records: the daemon-posted ones are dropped at the internal event-append route instead of being kept for known interaction ids, and the host-daemon protocol moves to 164 to record the narrowed acceptance. Older mobile builds are not supported: the app is on TestFlight and ships with the server, so the timeline wire serves grammar v3 only — no client capability header, no legacy projection, and no `activityIntents` key on tool rows; the readers that existed for those rows are gone with it; the `ProviderInfo` maintenance aliases (`experimental_providerHealth`/`experimental_providerUsage`/`experimental_providerInstallation`) kept for clients built before protocol 161 are gone as well — read `maintenance`. Co-Authored-By: Claude <noreply@anthropic.com>
SawyerHood
force-pushed
the
provider-plugins
branch
from
August 24, 2026 03:14
335aec9 to
537383d
Compare
… yet The tarball smoke polls `bb plugin list` until every expected builtin is running, but threw on the first plugin whose status was "error". The server reports an enabled plugin that `loadAll` has not reached yet with exactly that status and the detail "not loaded" (it has no runtime status at all), and plugins load one at a time after the HTTP server starts listening. On a cold CI runner the poll landed mid-load and the six builtins still queued behind provider-acp failed the smoke. The smoke now treats that explicit no-status-yet detail as pending and fails only on a plugin whose load actually failed. Verified: smoke:tarball on Node 22 and Node 24 with pi and opencode2 absent from PATH. Co-Authored-By: Claude <noreply@anthropic.com>
replayRecording gated each runtime request on the events the recording had before it, but gave up after orderTimeoutMs of bridge silence and sent the request anyway. With a plan from the current lane that shortfall can only be latency, since the same bridge wrote the lane; on a starved CI runner the bridge went quiet with provider lines still unread, the steer then arrived on the bridge's stdin while those lines were pending, and the bridge drained the two streams in an order the recording never had (in codex/steer every row from the fourth command on shifted). An exact plan now waits for its events up to timeoutMs and reports a stall if they never come. The quiet fallback stays for plans from the recorded lane, where a divergent bridge legitimately emits fewer events. Verified: @bb/provider-parity 56/56 twice; codex/steer no slower. Co-Authored-By: Claude <noreply@anthropic.com>
…light An unsolicited restartRecommended hint that arrived while one of the thread's operations was in flight was kept but not scheduled until the thread's next turn. One read can carry the turn/start response, the turn's terminal delta and the hint together: the response settles the turn operation on a microtask that runs only after the whole batch, so the hint found the operation in flight, and a thread that was idle a moment later restarted only at its next turn. The recovery test that expects the sibling-busy deferral to be logged right after the turn failed one run in three under load. The runtime now retries such a hint as soon as the thread's operations drain. A hint carried by a rejected request keeps restarting before the next turn, as before. The scripted echo bridge gains turnStartResponseDelayMs so a test can raise the hint before the turn/start answer; the new test fails without the fix and passes with it. Verified: @bb/agent-runtime 316/316 solo and three times under CPU and suite load; typecheck across packages. Co-Authored-By: Claude <noreply@anthropic.com>
The test that pins the 256-root cap wrote 300 plugins one after another (two filesystem writes each) and then resolved them; on a slow CI runner that ran past the 5 s default test timeout. 260 plugins exceed the cap by the same mechanism and prove the same cut-off and warning, and the fixture writes are now issued together instead of one at a time: the file runs in about 150 ms. Co-Authored-By: Claude <noreply@anthropic.com>
Four pi bridge tests failed on a starved CI runner for three reasons, none of them the bridge misbehaving. The test harness called a request unanswered after 20 s, while the bridge was still inside its own budget: a construction it retries spawns up to eight node children through the transient-auth window, several seconds each on a loaded runner, against a readiness budget of a minute per attempt. The harness now waits 60 s, and the lifecycle and round2 tests that drive such constructions get 90 s each. The test that pins "a child whose extension never reports ready" shortens the readiness budget to 1.5 s, but that budget also capped the child's first get_state, so a slow spawn produced "did not answer get_state in time" instead. The first command now waits on the channel budget like any other and the readiness budget starts once pi has answered, so it bounds the extension's report alone; the default budgets are unchanged. The child-exit check waited 10 s for a close that escalates through a 4 s SIGTERM grace and a 4 s SIGKILL grace before the exit reaches the process log; it now waits 30 s. Verified: bb-plugin-provider-pi 104/104 solo and three times under CPU and suite load (the lifecycle file took up to 48 s there); typecheck. Co-Authored-By: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
Providers (Codex, Claude Code, pi, and every ACP agent) were wired into core with per-provider carve-outs: a provider table and provider-specific skill/command scanning in the daemon, provider-specific recovery regexes and session branches in the runtime, provider-id branches in the server and clients, and a narrower ACP-only path for third-party agents. Adding or changing a provider meant touching core in many places.
What changed
Providers are plugins behind one API with zero first-party privilege: the built-in providers use the same
bb.providers.registerdeclaration, bridge protocol, and SDK a third party uses.presentation(label, glyph, tint, detail); web renders from it, plugins may add React renderers for their own kinds, mobile renders the declarative base.bb.providers.registerdeclares strings, icon, models, reasoning levels, service tiers, permission modes, native skill/command roots, maintenance, and AI services; oneProviderInfoshape everywhere.toolUse, open plugin-defined requests, one lifecycle event; the server stays the policy owner.resolveNativeRoots; the ACP tier is deleted and the ACP plugin owns its agents through a published ACP bridge kit.pi --mode rpcwith an install gate.bb.branding.experimental_icons(name → SVG), namespaced glyphs, strict SVG validation,nosniff+ CSP;bb plugin buildchecks logos for script vectors.experimental_dropped where audited; provider-literal ratchet 209 → 18 allowlisted references; cross-plugin duplicates folded intoexperimental_SDK helpers; eleven publishedprovider-bridgeexports kept under "Scheduled removals".HOST_DAEMON_PROTOCOL_VERSION153 → 164;@get-bb/plugin-sdk0.4.16;bb plugin buildinlines SDK subpaths so packaged installs load provider plugins.activityIntentson tool rows, noProviderInfomaintenance aliases.docs/provider-plugin-api.md,docs/api_to_audit.md, the plugin-authoring skill, the bb guide, CLI surfaces.Built as reviewed layers and squashed into one commit, then adversarially reviewed (51 findings; every confirmed one fixed with a failing-first test and an independent re-check — the commit message lists them).
How you verified
smoke:tarball; SDK version guards.