7. Provider bridges — @get-bb/plugin-sdk/bridge

@get-bb/plugin-sdk/bridge is the surface a provider plugin's bridge (host role provider-bridge, a child process) and its server half share. It re-exports @bb/bridge-kit/runtimedefineBridge, BridgeRuntime, runBridge, childEnv, autoDeny, every protocol v3 schema, and the declaration helpers — and adds defineProviderDeclaration and bridgeHostRole. Source: next/packages/plugin-sdk/src/bridge/index.ts. Kernel: next/packages/bridge-kit/src/ (paths below are relative to it unless noted). Spec: 06 §2–§4, §7§9. Where spec and build differ, the build wins and the row says so. Bridges may be written in any language (06 §4, A11); the kit is a convenience over line-delimited JSON-RPC 2.0 on stdio.

7.1 A provider plugin's anatomy

A provider plugin is an ordinary plugin with three halves: server.ts (hooks), app.tsx (renderers, optional), host.ts (the bridge). Only the declaration and the bridge are mandatory (06 §2; 11c §0).

Manifest row Content Spec As built
bb.contributes.providers[] ProviderDeclaration[]; bb plugin build inlines src/declaration.ts so the manifest stays data 06 §2, §3 declaration.ts providerDeclarationSchema
bb.contributes.hostRoles[] {role: "provider-bridge", name: <provider id>, launch: {kind: "module", export} | {kind: "exec", command, args}, limits}; declaration.bridge equals name 06 §2; 05 §6.5 bridgeHostRole() builds the module row
bb.contributes.eventKinds one KindDeclarationInput per extension kind; the declaration repeats the names in extensionKinds 06 §2; 04 §5.2 the assembler compiles these schemas (§7.12)
bb.contributes.interactionKinds {schema, resolutionSchema} per open request kind; names repeated in interactionKinds 06 §2; 04 §3.6 loader-owned; no kit schema (README BK-7)
bb.provides["providers/hooks"] the multi hook contract, factsKey: "providerId" 06 §3.1 contract not in the next tree; providerFacts() is
bb.requires["providers/registry"], bb.uses["skills/catalog"] service edges; @get-bb/plugin-sdk/bridge is a package import, not an edge 11c §0
bb.contributes.settings, slots, icons declarative settings, claimed slot names, <pluginId>/<icon> 06 §2 section 1
bb.host defineHostEntry({ roles: { "provider-bridge": { <name>: defineBridge(...) } } }) 06 §2; 05 §6.2 plugin-sdk/src/host/index.ts
// host.ts
import { defineHostEntry } from "@get-bb/plugin-sdk/host";
import { defineBridge } from "@get-bb/plugin-sdk/bridge";
export default defineHostEntry({
  roles: { "provider-bridge": { codex: defineBridge(codexBridge) } },
  signals: {}, async dispose() {},
});

Server half (06 §3.1): one ctx.provide(providerHooks, { deriveProviderOptions, bridgeEnv, modelCatalog }, providerFacts(declaration)) per provider. deriveProviderOptions runs once per thread.create|send|fork and becomes ExecutionOptions.providerOptions (2 s timeout; 502 provider_options_failed). bridgeEnv becomes RoleSpec.env for the bridge child (BB_* names rejected with bridge_env_reserved; 502 bridge_env_failed). modelCatalog returns ModelDecl[] | null; null means use the bridge probe. A plugin with no server half gets {} options, {} env, and the probe. As built, the providers plugin and the providerHooks contract are spec-only in next/; providerFacts exists.

Member Signature (abridged) What it does Spec As built
defineProviderDeclaration (input: ProviderDeclaration) => ProviderDeclaration parses with providerDeclarationSchema; throws KernelError {code: "invalid_contract", issues} naming the provider id and each failing path 06 §3 plugin-sdk/src/bridge/index.ts
bridgeHostRole (declaration, exportName = declaration.bridge) => {role: "provider-bridge", name: declaration.bridge, launch: {kind: "module", export: exportName}} the contributes.hostRoles[] row for a module bridge 06 §2; 05 §6.5 plugin-sdk/src/bridge/index.ts
providerFacts (decl) => {providerId, nativeUserQuestion, permissionModes, approvals, features} the providers/hooks facts, computed so they are never typed twice 06 §3.1 declaration.ts

7.2 ProviderDeclaration

Every field is required; nothing defaults (06 §3). Validated by the loader without executing code and again by defineProviderDeclaration. As built: declaration.ts.

Field Type Meaning
id /^[a-z0-9][a-z0-9-]{1,63}$/ flat provider id; persisted on threads; a collision across plugins is an install-time error
family "native" | "acp" acp = the bridge speaks ACP to a child named in bridgeOptions.launch; core never branches on it
bridge string (min 1) name of the provider-bridge host role row
icon string (min 1) <pluginId>/<iconName> from contributes.icons
strings {name, short, description, modelBrandPrefix: string | null} display strings
permissionModes PermissionMode[] (min 1) subset of accept-edits < auto < full; a request outside it is 400 permission_mode_unsupported
reasoningLevels {id, label, rank: int, description}[] open list, low to high; rank is one shared integer scale for cross-provider reconciliation (06 §7.3)
serviceTiers {id, label, description}[] open list; [] = no tier toggle
fork "none" | "tip" | "checkpoint" UI ceiling; the handshake may narrow it, never widen it
nativeUserQuestion boolean the provider asks questions natively (a fact)
manualCompaction boolean the compact composer action exists
maintenance {health, usage, installation} booleans gates the sessionless provider/* methods
visibility "always" | "installed" installed: listed only on hosts whose provider/health is not not_installed
features string[] open facts sibling plugins query ("workflows", "goal"); core reads none
composerActions {id, trigger, label, description, sticky}[] ids ride turn/start.actions; sticky stays armed across turns
approvals ApprovalSubjectKind[] closed subjects the bridge may request; feeds the facts (a kit addition, README "Notes")
extensionKinds ExtensionKindName[] names only; each must exist in contributes.eventKinds
interactionKinds ExtensionKindName[] names only; each must exist in contributes.interactionKinds
models {catalog: "bridge" | "fallback-only", fallback: ModelDecl[]} bridge calls model/list; fallback serves offline, on probe failure, or always for fallback-only
skills {scanRoots: SkillScanRoot[], discover: boolean} declarative native-skill roots; discover additionally enables skills/discover
bridgeOptions Record<string, JsonValue> static launch data, opaque to core, sent on every sessionless call and in SessionParams

7.3 defineBridge, runBridge, BridgeRuntime

Member Signature (abridged) What it does Spec As built
defineBridge (bridge: ProviderBridge) => BridgeDefinition returns {apiVersion: 1, kind: "provider-bridge", bridge}; throws when declarations is empty 06 §2 runtime/bridge.ts
BridgeRuntime new (def, io: {write(line)}); handleLine(line), end(), closed: Promise<void>, context: BridgeContext JSON-RPC peer over a line writer: negotiates, parses params, gates methods, dispatches, parses results 06 §4.1–§4.3 runtime/bridge.ts
runBridge (def, io = process, maxLineBytes?) => Promise<void> pumps stdin lines through a LineDecoder into handleLine, calls end() at EOF, resolves after closed 06 §4.1 runtime/bridge.ts
childEnv (sessionEnv, processEnv = process.env) => Record<string, string> the bridge's env minus every BB_* name, overlaid with SessionParams.env; use it for every agent shell the bridge spawns 06 §4.3, §6.1 runtime/env.ts
autoDeny (policy: PermissionPolicy, request: InteractionRequest) => InteractionResolution | null {kind: "approval", decision: "deny", granted: null} when the request is an approval and policy.escalation === "deny"; null otherwise 06 §8 runtime/env.ts

ProviderBridge is { declarations: ProviderDeclaration[]; initialize(params: InitializeParams, ctx): BridgeCapabilities; methods: BridgeMethods }. Required handlers: thread/start, turn/start, thread/stop, thread/discard, shutdown. Every other method is optional; an advertised method with no handler answers METHOD_NOT_FOUND.

Dispatch (runtime/bridge.ts invoke): parse params (INVALID_PARAMS with data.issues) → pick the declaration by params.providerId (the first declaration when the method carries none; an unknown id is INVALID_PARAMS) → isMethodAdvertised (CAPABILITY_NOT_ADVERTISED) → thread/fork with a non-null sourceCheckpointId needs capabilities.fork === "checkpoint" (FORK_CHECKPOINT_UNSUPPORTED) → the handler → the result is parsed with coreToBridgeSpecs[method].result. Any method before initialize is BRIDGE_ERROR. shutdown is answered {} first; then it, or EOF (which runs the shutdown handler with graceMs: 0), answers every outstanding request with INTERACTION_INTERRUPTED. Core sends the bridge no notifications.

BridgeContext, the ctx every handler receives:

Member Signature What it does
capabilities BridgeCapabilities the negotiated facts; throws before initialize
sendThreadDeltas (threadId, deltas: Delta[]) => void the only timeline lane; validated with threadDeltaParamsSchema; an invalid delta throws (a bridge bug)
emitForSession (threadId, {method, params}) => void thread/identity, session/replaced, thread/openWork, provider/recovery with threadId added
recovery (threadId: string | null, {hint, reason, retryable}) => void provider/recovery; null is process-wide
log (level, message, threadId = null) => void bridge/log
raw (threadId | null, coverage: "noise" | "unknown", payload) => void provider/raw; droppable, never persisted
toolCall (params) => Promise<{success, content, ui}> blocking tool/call
requestInteraction (params) => Promise<InteractionResolution> blocking interaction/request
childEnv, autoDeny as above bound helpers

Core → bridge methods (06 §4.3; protocol/requests.ts coreToBridgeRequests). S = {providerId, cwd, bridgeOptions}; Ref = {threadId, providerThreadId}; SP = SessionParams (§7.9).

Method Params Result Gate
initialize InitializeParams InitializeResult — (runtime-owned; calls bridge.initialize)
model/list S {models: ModelDecl[]} models.catalog === "bridge"
provider/health S {supported: false} | {supported: true, health: ProviderHealth} maintenance.health
provider/usage S {supported: false} | {supported: true, usage: ProviderUsage} maintenance.usage
provider/installation/status S & {requirement: "thread_rewind" | null} InstallationStatus maintenance.installation
provider/installation/run S & {action: "install" | "update"} {available: false, message} | {available: true, command: InstallCommand, verification: InstallVerification} maintenance.installation
skills/discover S & {scopes: ("user" | "project")[]} {skills: DiscoveredSkill[]} skills.discover
skills/configure {threadId, roots: SkillRoot[]} {}
thread/start SP & {input: PromptInput[] | null} SessionIdentity — (required)
thread/resume SP & {providerThreadId} SessionIdentity sessionRestore
thread/fork SP & {sourceProviderThreadId, sourceCheckpointId: string | null} SessionIdentity fork !== "none"; checkpoint needs "checkpoint"
thread/stop Ref & {intent: "interrupt" | "release", activeTurnId: string | null} {} — (required)
thread/discard Ref {} — (required)
thread/name/set Ref & {title} {} threadRename
thread/archive, thread/unarchive Ref {} threadArchive
thread/goal/clear Ref {} threadGoal
turn/start TurnStartParams (TurnParams without turnId) {} — (required)
turn/steer TurnSteerParams (turnId = the provider's key for the running turn) {}
shutdown {graceMs: int ≥ 0} {} — (required)

thread/stop {intent: "interrupt"} expects settlement deltas within the stop grace; release expects none. Compaction is not a method: it is turn/start with actions: ["compact"] and empty input; a refused compaction settles with provider.warning {category: "compaction-skipped"} (06 §4.3).

Bridge → core requests (06 §4.4; bridgeToCoreRequests). Both block until core answers. Keys are provider-native; the host driver maps them through the assembler. requestKey forms the dedupe key <providerId>:<providerThreadId>:<requestKey>.

Method Params Result
tool/call {threadId, providerThreadId, turnKey: string | null, callId, tool, arguments: JsonValue} {success, content: ToolContent[], ui: {renderer, payload} | null}
interaction/request {threadId, providerThreadId, turnKey: string | null, requestKey (min 1), request: InteractionRequest} InteractionResolution

Notifications, bridge → core (06 §4.5; protocol/notifications.ts bridgeNotifications). thread/delta is strict; the rest are additive.

Method Params Notes
thread/delta {threadId, deltas: Delta[]} the only timeline lane; consumed by the assembler on the host
thread/identity {threadId, providerThreadId, sessionRestorable} precedes the session's first delta; becomes thread.identity
session/replaced {threadId, providerThreadId | null, reason, contextLost} mandatory on every rebuild, before new traffic; becomes provider.warning {category: "session-replaced"}
thread/openWork {threadId, open} level-triggered; read by the reaper
provider/recovery {threadId | null, hint: RecoveryHint, reason, retryable} typed hints; the providers plugin acts
provider/raw {threadId | null, coverage: "noise" | "unknown", payload} record mode only
bridge/log {level: debug|info|warn|error, threadId | null, message} routed to the plugin log
export const codexBridge = defineBridge({
  declarations: [codexDeclaration],
  initialize: () => ({ sessionRestore: true, threadArchive: false, threadRename: false, threadGoal: true,
    fork: "checkpoint", approvalEnforcedBy: "runtime", steer: "native" }),
  methods: {
    "thread/start": async (p, ctx) => {
      ctx.emitForSession(p.threadId, { method: "thread/identity", params: { providerThreadId: id, sessionRestorable: true } });
      ctx.sendThreadDeltas(p.threadId, [{ kind: "session.reset" }]);
      return { providerThreadId: id, sessionRestorable: true };
    },
    "turn/start": async (p, ctx) => { /* input.accepted, turn.open, items…, turn.boundary */ return {}; },
    "thread/stop": async () => ({}), "thread/discard": async () => ({}), shutdown: async () => ({}),
  },
});

7.4 Handshake and capabilities

initialize carries {protocol: {min, max}, client: {name: "bb", version}, plugin: {id, version}, providerIds} and answers {protocol, capabilities} (06 §4.2; protocol/handshake.ts). The runtime picks the highest version in both ranges; no intersection is PROTOCOL_UNSUPPORTED with {core, bridge} in error.data. It then parses the bridge's capabilities and checks every declaration with capabilityWidens. A second initialize is BRIDGE_ERROR. Versioning rule: the delta grammar is strict and enters persistence, so any grammar change bumps max; requests and results are looseObject (// additive-wire) and may gain fields without a bump.

Capability Type Gates
sessionRestore boolean thread/resume
threadArchive boolean thread/archive, thread/unarchive
threadRename boolean thread/name/set
threadGoal boolean thread/goal/clear
fork "none" | "tip" | "checkpoint" thread/fork; checkpoint forks; may only narrow declaration.fork
approvalEnforcedBy "runtime" | "provider" who auto-denies approvals (§7.8)
steer "native" | "queue" queue: the bridge holds a steer until the turn ends
Member Signature (abridged) What it does Spec As built
negotiateProtocol (a: ProtocolRange, b: ProtocolRange) => number | null min(a.max, b.max) when it is ≥ max(a.min, b.min), else null 06 §4.2 protocol/handshake.ts
capabilityWidens (caps, decl) => string | null FORK_RANK[caps.fork] > FORK_RANK[decl.fork]"fork: … widens declared …"; the runtime answers BRIDGE_ERROR with data.rule: "handshake/narrows-only" (06 §4.9 assigns no code) 06 §4.2 declaration.ts
isMethodAdvertised (method, caps, decl) => boolean METHOD_GATES[method]?.(caps, decl) ?? true; ungated methods are always sent 06 §4.3 gate column declaration.ts

7.5 The delta grammar

thread/delta {threadId, deltas} is strict (protocol/deltas.ts deltaSchema; 06 §4.6). Scoping is explicit: the eight kinds in SCOPED_DELTA_KINDS carry scope: "turn", turnKey or scope: "thread" (scopeSchema), with no default. turnKey is z.string().min(1).nullable(); null means the bridge's current open turn, and a non-null key must be one this bridge opened. The "04 event" column is what the host assembler emits; the bridge never sees it.

Kind Fields Scope 04 event
input.accepted clientRequestId (min 1), turnKey turn turn.accepted; held until its turn opens
turn.open turnKey, parentRef: string | null turn turn.started under the core-minted id bound FIFO (R4)
turn.boundary status: completed|failed|interrupted, error: {message} | null, checkpointId | null, claimIfIdle, turnKey turn turn.completed
turn.diff files: DiffFile[], turnKey turn turn.diff, keep-latest per turn
item.open key, item, presentation + Scope explicit {kind, phase: "open", payload + providerItemId, presentation}; itemId minted here
item.progress key, progress: JsonValue, presentation | null + Scope explicit set {"$.progress"}; one event each, never throttled
item.close key, status, item, presentation, approval: "denied" | null + Scope explicit phase: "close"; the close shape wins
text.delta key, channel: message|reasoning|reasoningSummary|plan, text, part (int for reasoningSummary, null otherwise), turnKey turn append $.text or $.summary[part]; an unknown key synthesizes an open
output.delta key, text, turnKey turn append $.output (command); unknown key buffered ≤ 256 KiB
output.snapshot key, text, turnKey turn set $.output verbatim; nothing diffs
todo items: TodoItem[], explanation | null + Scope explicit todo, keep-latest per turn
usage last: TokenBreakdown, total | null, contextWindow: {used | null, size | null, estimated} | null + Scope explicit usage; total: null is accumulated
context.cleared thread thread.context_cleared
thread.name name thread thread.renamed
provider.rateLimits rateLimits: RateLimitState thread provider.rate_limits, keep-latest
provider.error message, detail | null, category, providerCode | null, httpStatus | null, willRetry, settlesTurn + Scope explicit provider.error; settlesTurn closes the turn and its items failed
provider.warning message, detail | null, category + Scope explicit provider.warning
provider.modelFallback from, to, reason: refusal|provider, message thread provider.model_fallback, one per (from, to) per turn
unhandled raw: JsonValue, rawType + Scope explicit provider.unhandled
session.ended no event; settles the open turn and items interrupted
session.reset no event; flushes buffers, re-draws id entropy; first delta of every session

Rules a bridge must hold (06 §4.6, §5.2): a turn-scoped delta with no open turn is never dropped — it becomes a thread-scoped provider.unhandled (turnless/unhandled); an unrequested turn.open (no handed-in turn id) takes the same path (turn/bound-to-requested); scope: "thread" is for a task that finishes after its turn or a compaction between turns; an item's scope is fixed at open. The driver enforces GRAMMAR_RULE_IDS live (grammar/rules.ts): events/schema-valid, scope/explicit, item/presentation-present, item/extension-kind-declared, turn/vouched-keys-only, item/opens-before-delta, item/settles-once; The driver drops a delta that breaks a rule and logs bridge/log warn naming the rule.

7.6 Items

An item key (itemKeySchema, 06 §4.6/§4.10) is {providerItemId, channel, parentRef}, each string | null, at least one non-null, no part containing U+001F (ITEM_KEY_SEPARATOR). The key string is the parts joined by U+001F; parentRef names a parent by its key string or its providerItemId and becomes parentItemId. providerItemId is copied into payload.providerItemId by the assembler for correlation and is never the stored id (R4). itemStatusSchema is pending | completed | failed | interrupted; denied is approval, not a status. Payloads are strict (protocol/items.ts coreItemSchema), snake_case (R27; D11 built wins).

Kind Payload
message {role: "assistant", text: string | null, status}; text: null on close keeps the streamed text
reasoning {summary: string[], text, status}; persisted with suppress: true (R28)
plan {text | null, status} plan-mode prose
command {command, cwd, intent: RenderIntent, output | null, exitCode | null, durationMs | null, approval: ApprovalState, status}
file_change {changes: FileChange[], approval, status}; FileChange = {path, kind: add|update|delete, movePath | null, diff | null, oldText | null, newText | null}
tool {name, server | null, args, result, error | null, intent, durationMs | null, progress | null, ui: {renderer, payload} | null, approval, status}
web_search {queries: string[], results | null, status}
web_fetch {url, prompt | null, summary | null, status}
image_view {path, status}
task TaskPayload: {role: delegation|background|workflow, familyId, name, description, prompt | null, childThreadId | null, taskStatus: pending|running|paused|completed|failed|killed|stopped, skipTranscript, progress | null, usage: {totalTokens, toolUses, durationMs} | null, summary | null, error | null, outputFile | null, status}; delegations, background shells, workflows
compaction {summary | null, status}
<pluginId>/<kind> extensionItemSchema: JsonObject that does not set providerItemId; validated on the host against the contributes.eventKinds schema; item: false kinds are refused on the item plane

detachesMessage(kind) is true for command, tool, file_change, task, and every extension kind: their item.open closes the open message stream in the same scope (message/auto-detach). extensionKindNameSchema enforces EXTENSION_KIND_PATTERN. Delegation is task {role: "delegation"} with child items nested through parentRef; todo is a fact, not an item (06 §4.7).

7.7 Presentation and render intents

presentationSchema (06 §4.7, I16; protocol/common.ts): {label (min 1), icon (min 1), title?, detail?, suppress?, tint?: neutral|info|success|warning|danger}. The optionals are the only optionals in the grammar; omission means "no such line". Presentation is required on item.open and item.close, nullable on item.progress, persisted per event; the row shows the latest. label is a verb phrase for the current status ("Running tests" → "Ran tests"); the pending/completed pair is expressed by sending a new presentation on close. renderIntentSchema is generic | terminal | diff | search | read | list | web | image; it lives on command.intent, tool.intent, and DynamicTool.presentation.intent, and replaces every tool-name classifier in the UI. approvalStateSchema is "waiting" | "denied" | null.

Member Signature What it does Spec As built
reasoningPresentation (p: Omit<Presentation, "suppress">) => Presentation returns {...p, suppress: true}; stamp it on every reasoning open and close. The assembler forces it on close anyway (R28) 06 §4.7; R28 protocol/common.ts; assembler/assembler.ts hidePresentation

7.8 Approvals, questions, interactions

interactionRequestSchema (06 §4.4; protocol/interactions.ts) is one of: {kind: "approval", subject: ApprovalSubject, reason \| null, decisions: ApprovalDecision[] (min 1)}, {kind: "question", questions: Question[] (1–4, unique ids)}, or {kind: "<pluginId>/<kind>", title (≤ 200), data: JsonValue} for a kind declared in contributes.interactionKinds. Subjects are closed and snake_case (R27):

Subject (approvalSubjectSchema) Fields beyond itemKey
command command, cwd | null, actions: CommandAction[], sessionGrant: GrantProfile | null
file_change writeScope | null, sessionGrant | null
permission_grant toolName | null, permissions: GrantProfile
plan plan, planFilePath | null

approvalDecisionSchema is allow_once | allow_for_session | deny. grantProfileSchema is {network: {enabled: boolean | null} | null, fileSystem: {read: string[], write: string[]} | null}. commandActionSchema is read {command, name, path} | listFiles {command, path | null} | search {command, query | null, path | null} | unknown {command}. questionSchema is {id, prompt, shortLabel | null, multiSelect, options: {value, label, description | null}[] (≤ 4, unique), allowFreeText} with options empty only when allowFreeText; answerSchema is {selected: string[] (≤ 4), freeText: string | null (≤ 4096, non-blank)}. interactionResolutionSchema is {kind: "approval", decision: allow_once|allow_for_session, granted: GrantProfile | null}, {kind: "approval", decision: "deny", granted: null}, {kind: "answer", answers: Record<questionId, Answer>}, {kind: "submitted", data}, or {kind: "interrupted"}. A tool_use subject is not core; a provider declares <providerId>/tool_use as an open kind (R27).

Member Signature What it does Spec As built
subjectGrant (subject: ApprovalSubject) => GrantProfile | null permissions for permission_grant, null for plan, sessionGrant otherwise: the grant an allow must carry 06 §4.4 granted rule protocol/interactions.ts
resolutionMatchesRequest (request, resolution) => string | null null when the resolution fits the request; else why: wrong kind, a decision not offered, granted present/absent against subjectGrant, missing answers, open kind without submitted. interrupted always fits 06 §8 step 4 protocol/interactions.ts

Flow (06 §8): core validates and dedupes, then auto-denies an approval when policy.escalation === "deny" and approvalEnforcedBy === "runtime" (actor system:auto-deny); a provider-enforcing bridge applies autoDeny() itself. Questions and open kinds are never auto-denied. Otherwise a pending_interactions row opens and the JSON-RPC id waits for interaction.resolve. Interrupt, bridge exit, or host disconnect answers outstanding ids with INTERACTION_INTERRUPTED and marks rows interrupted. allow_for_session grants persist on the thread and reach the bridge again only through providerOptions if the plugin's deriveProviderOptions opts in.

7.9 Execution options and prompt input

executionOptionsSchema (06 §7.1; protocol/common.ts): {model, reasoningLevel: string | null, serviceTier: string | null, permission: PermissionPolicy, instructions, providerOptions: Record<string, JsonValue>}. It rides every thread/* and turn/* command; the bridge reconciles and announces a rebuild with session/replaced. permissionPolicySchema is the three product presets: {mode: "accept-edits", scope: "workspace", reviewer: "user", escalation: ask|deny}, {mode: "auto", scope: "workspace", reviewer: "automatic", escalation: ask|deny}, {mode: "full", scope: "full", reviewer: null, escalation: null}; permissionModeSchema is the mode enum. Resolution order, clamps, and ResolvedExecutionOptions provenance are server-side (06 §7.2) and never cross the wire (README BK-8).

promptInputSchema (06 §4.3) is text {text, mentions: PromptMention[], visibility}, image {url, visibility}, localImage {path, visibility}, or localFile {path, name | null, sizeBytes | null, mimeType | null, visibility}. promptVisibilitySchema is "user" | "agent-only"; agent-only reaches the provider and is omitted from the user-message row; the bridge only forwards it. promptMentionSchema is {start, end, resource: {kind, label, …}} (loose; the composer owns resources).

sessionParamsSchema: {threadId, providerId, cwd, workspace: {root, writeRoots}, env, options: ExecutionOptions, tools: DynamicTool[], disallowedTools: string[], instructionMode: "append" | "replace", actions: string[], bridgeOptions}. workspace and env are filled by the host driver, never by core; env is the ambient BB_* set (BB_CLI, BB_SERVER_URL, BB_THREAD_ID, BB_PROJECT_ID, BB_ENVIRONMENT_ID, BB_THREAD_STORAGE, BB_HOST_ID, plus threads/agent-config names); pass it to agent shells through childEnv. turnParamsSchema is {threadId, providerThreadId, turnId, input: PromptInput[], clientRequestId, options, actions} as core sends the driver; turnStartParamsSchema omits turnId (no provider key exists yet) and turnSteerParamsSchema keeps it holding the provider's own key (README "Notes"; 06 §4.3). There is no expectedTurnId. threadRefSchema is {threadId, providerThreadId}; sessionIdentitySchema is {providerThreadId, sessionRestorable}. dynamicToolSchema is {name, description, inputSchema: JsonObject, presentation: {label: {pending, completed}, icon, intent, suppress}}; the bridge exposes these to the agent and answers calls through ctx.toolCall.

7.10 Recovery, errors, warnings

recoveryHintSchema (06 §4.5): restart-process (account restart), resume-fresh (session file gone), reauthenticate, reinstall, none. Send it with ctx.recovery; the providers plugin acts and records provider.warning {category: "recovery"}. errorCategorySchema (persisted, snake_case): context_window_exceeded, billing, budget_exceeded, internal, max_output_tokens, max_turns, overloaded, policy, rate_limit, sandbox, stream_disconnected, structured_output_retries, thread_rollback_failed, too_many_failed_attempts, unauthorized, unknown. A quota failure is provider.error {category: "rate_limit", settlesTurn: true} plus provider.rateLimits with resetsAt set; provider-retry keys on exactly those two. warningCategorySchema: deprecation, config, general, compaction-skipped, turn-watchdog, session-replaced, recovery. rateLimitStateSchema: {status: allowed|warning|blocked|unknown, kind: subscription-window|credits|spend-control|unknown, windows: {providerKey | null, label | null, status, resetsAtMs | null}[], resetsAt: number | null, reachedReason | null, overageStatus: allowed|warning|rejected|unavailable | null, overageReason | null}.

Member Signature What it does Spec As built
JsonRpcError class extends Error { code: number; data: JsonValue | null; toJSON() } the wire error; throw it from a handler to answer the request with an error 06 §4.9 protocol/errors.ts
bridgeError (name: keyof BRIDGE_ERRORS, message, data = null) => JsonRpcError builds a JsonRpcError from a named code 06 §4.9 protocol/errors.ts
BRIDGE_ERRORS name Code Raised when
INVALID_PARAMS -32602 schema-invalid params (data.issues); unknown providerId
METHOD_NOT_FOUND -32601 unknown method; advertised method with no handler
BRIDGE_ERROR -32000 method before initialize; second initialize; handshake/narrows-only (data.rule)
NO_ACTIVE_TURN -32001 reserved for bridges: a turn-bound request with no turn
SESSION_NOT_RESTORABLE -32002 reserved for bridges: thread/resume of a lost session
FORK_CHECKPOINT_UNSUPPORTED -32003 thread/fork with sourceCheckpointId under fork !== "checkpoint"
PROTOCOL_UNSUPPORTED -32004 no common protocol version (data: {core, bridge})
INTERACTION_INTERRUPTED -32005 every outstanding request at shutdown or EOF
CAPABILITY_NOT_ADVERTISED -32006 a gated method the bridge did not advertise

7.11 Usage, maintenance, skills, models

Usage rides the usage delta: tokenBreakdownSchema is {totalTokens, inputTokens, cachedInputTokens, outputTokens, reasoningOutputTokens}; send total: null to let the assembler accumulate, or the provider's exact totals (06 §4.8 "one delta"). Maintenance methods are sessionless and run on a maintenance bridge instance in the host's scratch environment (06 §14.1). providerHealthSchema: {status: ready|not_installed|unauthenticated|expired|unsupported_version|unknown, statusMessage | null, accountEmail | null, planLabel | null, installedVersion | null, minimumSupportedVersion | null, canInstall, canUpdate, loginCommand | null}. providerUsageSchema: {status: "ok", accountEmail | null, planLabel | null, windows: UsageWindow[]} or {status: not_installed|unauthenticated|expired} or {status: "error", message, planLabel | null, accountEmail | null}; usageWindowSchema is {label, usedPercent, resetsAt: string | null, cost: {usedUsdCents, limitUsdCents} | null}. installationStatusSchema: {executableName, executablePath | null, installed, installSource: notInstalled|npmGlobal|external, currentVersion | null, latestVersion | null, minimumSupportedVersion | null, npmPackageName | null, npmGlobalPackageVersion | null, installAction: {kind: install|update, label: Install|Update, command} | null, needsUpdate, versionUnsupported}. installCommandSchema is {command, args: string[] (≤ 64), displayCommand}; installVerificationSchema is {kind: "installed"} | {kind: "version_changed", previousVersion} | {kind: "version_at_least", version}; core runs the command and verifies.

Skills (06 §9): injection is skills/configure {threadId, roots} with skillRootSchema {id, path, label} — staged SKILL.md directories the bridge converts to its native form. Discovery is declarative: skillScanRootSchema {scope: user|project, path, format: skill-md|claude-plugin|codex-plugin-cache, walkParents, recursive, label | null} expanded on the host (~, $CODEX_HOME, $OPENCODE_CONFIG_DIR); layouts that need code set skills.discover: true and answer skills/discover with discoveredSkillSchema {name, description, path, scope: user|project, source}. Models (06 §14.1): modelDeclSchema is {id, label, description, reasoningLevels: string[], defaultReasoningLevel | null, serviceTiers: string[], isDefault}; model/list runs only for models.catalog: "bridge", cached per (providerId, hostId, cwd) for 10 min; the provider default is the isDefault model and its defaultReasoningLevel.

7.12 Helpers and constants

Constant Value Spec As built
PROTOCOL_VERSION 3 06 §4.2 protocol/handshake.ts
SUPPORTED_PROTOCOL_RANGE {min: 3, max: 3} 06 §4.2 protocol/handshake.ts
FORK_RANK {none: 0, tip: 1, checkpoint: 2} 06 §4.2 narrows-only protocol/handshake.ts
GATED_METHODS model/list, provider/health, provider/usage, provider/installation/status, provider/installation/run, skills/discover, thread/resume, thread/fork, thread/name/set, thread/archive, thread/unarchive, thread/goal/clear 06 §4.3 gate column declaration.ts (keys of METHOD_GATES)
SCOPED_DELTA_KINDS item.open, item.progress, item.close, todo, usage, provider.error, provider.warning, unhandled (derived from the scoped() members of deltaSchema) 06 §4.6 protocol/deltas.ts
ITEM_KEY_SEPARATOR "\u001f" (U+001F) 06 §4.6 protocol/common.ts
EXTENSION_KIND_PATTERN /^[a-z0-9][a-z0-9-]*\/[a-z0-9][a-zA-Z0-9_-]*$/ (<pluginId>/<kind>) 06 §4.4, §4.7 protocol/interactions.ts
APPROVAL_SUBJECT_KINDS ["command", "file_change", "permission_grant", "plan"] 06 §4.4; R27 protocol/interactions.ts
BRIDGE_ERRORS the nine codes of §7.10 06 §4.9 protocol/errors.ts
Member Signature What it does Spec As built
coreToBridgeRequests Record<method, {params, result}> the zod table of §7.3; CoreToBridgeMethod is its key type 06 §4.3 protocol/requests.ts
coreToBridgeSpecs CoreToBridgeSpecs (the same table typed per method) lets code generic over M read one method's schemas without a cast; the runtime parses params and results through it 06 §4.3 protocol/requests.ts
bridgeToCoreRequests {"tool/call": {params, result}, "interaction/request": {params, result}} the bridge → core table; ctx.toolCall/ctx.requestInteraction parse results through it 06 §4.4 protocol/requests.ts
bridgeNotifications Record<method, schema> the seven notification schemas of §7.3 06 §4.5 protocol/notifications.ts
jsonValueSchema, jsonObjectSchema kernel-core's JSON schemas re-exported beside the wire schemas (M1) protocol/index.ts

The assembler (assembler/assembler.ts; 06 §5) runs on the host, one instance per (bridge process, thread), and the bridge never sees it: the driver feeds it thread/delta and it emits section 04 events synchronously, one delta → one event, with no timers. It binds turn keys FIFO to core-minted turnIds, mints itemIds as <entropy>-<serial>, copies key.providerItemId into payload.providerItemId, validates extension kinds against their JSON schemas (createExtensionKinds), settles zero-work turns, routes turnless deltas to provider.unhandled, and buffers unknown-key output up to 256 KiB. Its rule ids (ASSEMBLER_RULE_IDS, 06 §5.2): ids/minted-centrally, turn/bound-to-requested, turn/opens-only-explicitly, turn/accepted-input-queues, turn/zero-work-settles, item/opens-before-delta, item/close-shape-wins, item/settles-once, item/scope-fixed-at-open, message/auto-detach, stateless/one-delta-one-event, usage/accumulate, turnless/unhandled, settle/error, session/reset-flushes, fallback/dedup. The conformance kit (06 §10; 33 rule ids) and record mode (06 §11) live in @bb/bridge-kit/conformance; the spec's @get-bb/plugin-sdk/bridge/conformance subpath is not in the SDK's package.json#exports as built.

7.13 Schema index

Schema Validates
Handshake (protocol/handshake.ts)
protocolRangeSchema {min, max} positive ints, min ≤ max
initializeParamsSchema {protocol, client: {name: "bb", version}, plugin: {id, version}, providerIds} (loose)
bridgeCapabilitiesSchema the seven capability facts of §7.4 (loose)
initializeResultSchema {protocol: int, capabilities} (loose)
forkCapabilitySchema "none" | "tip" | "checkpoint"
Declaration (declaration.ts)
providerDeclarationSchema the full ProviderDeclaration of §7.2
reasoningLevelDeclSchema {id, label, rank: int, description}
serviceTierDeclSchema {id, label, description}
composerActionDeclSchema {id, trigger, label, description, sticky}
skillScanRootSchema {scope, path, format, walkParents, recursive, label | null}
Common (protocol/common.ts)
itemKeySchema {providerItemId, channel, parentRef}, ≥ 1 non-null, no U+001F (strict)
itemStatusSchema pending | completed | failed | interrupted
renderIntentSchema generic | terminal | diff | search | read | list | web | image
approvalStateSchema "waiting" | "denied" | null
presentationSchema {label, icon, title?, detail?, suppress?, tint?} (strict)
permissionModeSchema accept-edits | auto | full
permissionPolicySchema the three presets keyed on mode
executionOptionsSchema {model, reasoningLevel, serviceTier, permission, instructions, providerOptions}
promptMentionSchema {start, end, resource: {kind, label}} (loose resource)
promptVisibilitySchema "user" | "agent-only"
promptInputSchema text | image | localImage | localFile parts, each with visibility
dynamicToolSchema {name, description, inputSchema, presentation: {label: {pending, completed}, icon, intent, suppress}}
modelDeclSchema {id, label, description, reasoningLevels, defaultReasoningLevel, serviceTiers, isDefault}
Deltas (protocol/deltas.ts)
scopeSchema {scope: "turn", turnKey} | {scope: "thread"} (strict)
deltaSchema the 21-kind union of §7.5 (strict; text.delta.part refined against channel)
threadDeltaParamsSchema {threadId (min 1), deltas: Delta[]} (strict; protocol/notifications.ts)
todoItemSchema {id, text, status: pending | active | completed | failed}
tokenBreakdownSchema the five token counters
rateLimitStateSchema RateLimitState of §7.10
errorCategorySchema the 16 snake_case categories
warningCategorySchema the 7 warning categories
diffFileSchema {path, kind: add | update | delete, added: int, removed: int, diff | null}
textChannelSchema message | reasoning | reasoningSummary | plan
Items (protocol/items.ts)
fileChangeSchema {path, kind, movePath, diff, oldText, newText} (strict)
taskPayloadSchema TaskPayload of §7.6 (strict)
coreItemSchema the 11 core {kind, payload} shapes (strict)
extensionItemSchema {kind: <pluginId>/<kind>, payload: JsonObject} with no providerItemId
itemSchema coreItemSchema | extensionItemSchema
Interactions (protocol/interactions.ts)
grantProfileSchema {network: {enabled} | null, fileSystem: {read, write} | null}
commandActionSchema read | listFiles | search | unknown classification
approvalSubjectKindSchema enum over APPROVAL_SUBJECT_KINDS
approvalSubjectSchema the four subjects of §7.8
approvalDecisionSchema allow_once | allow_for_session | deny
questionSchema one question; ≤ 4 unique options; empty only with allowFreeText
answerSchema {selected (≤ 4), freeText | null (≤ 4096, non-blank)}
extensionKindNameSchema `${string}/${string}` matching EXTENSION_KIND_PATTERN
interactionRequestSchema approval | question | <pluginId>/<kind> requests
interactionResolutionSchema approval (allow | deny) | answer | submitted | interrupted
Requests (protocol/requests.ts)
threadRefSchema {threadId, providerThreadId} (loose)
sessionParamsSchema SessionParams of §7.9 (loose)
turnParamsSchema TurnParams with the core turnId (driver-facing)
turnStartParamsSchema TurnParams without turnId (what turn/start receives)
turnSteerParamsSchema TurnParams with turnId = the provider's turn key (what turn/steer receives)
sessionIdentitySchema {providerThreadId, sessionRestorable}
toolContentSchema {type: "text", text} | {type: "image", imageUrl}
Maintenance and skills (protocol/maintenance.ts)
providerHealthSchema ProviderHealth of §7.11
usageWindowSchema {label, usedPercent, resetsAt | null, cost | null}
providerUsageSchema ok | not_installed | unauthenticated | expired | error usage
installationStatusSchema InstallationStatus of §7.11
installCommandSchema {command, args (≤ 64), displayCommand}
installVerificationSchema installed | version_changed | version_at_least
skillRootSchema {id, path, label}
discoveredSkillSchema {name, description, path, scope, source}
recoveryHintSchema the five recovery hints
JSON (protocol/index.ts, from kernel-core)
jsonValueSchema any JSON value
jsonObjectSchema a JSON object

7.14 Differences from the old tree's docs/provider-plugin-api.md

Topic Old target doc Built (next/)
Registration bb.providers.register({...}) in server code, with deriveProviderOptions inline and env.passthrough manifest contributes.providers[] (inlined from src/declaration.ts) plus ctx.provide(providerHooks, …, providerFacts(decl)); env.passthrough deleted, bridgeEnv hook and childEnv() instead
Bridge entry defineProviderBridge({handleLine, start, onClose}) defineBridge({declarations, initialize, methods}) under defineHostEntry role provider-bridge; runBridge drives stdio
Handshake grammarVersions: [min, max] reported per session; steerMode: "inject" | "queue" initialize {protocol: {min, max}} negotiated once per process; capabilities add threadGoal, fork; steer: "native" | "queue"; narrows-only check
Item vocabulary 13 camelCase kinds (fileChange, fileRead, search, delegation, planSteps, …) 12 snake_case core kinds; task {role} replaces delegation; todo fact replaces planSteps; fileRead/search are tool with intent
Presentation label: {pending, completed}, icon: {glyph} | {asset}, tint: {light, dark} label: string, icon: string, tint enum; a new presentation on close; suppress: true forced on reasoning
Recovery {kind: sessionArchived | authRequired | restartRecommended | staleTurn | rateLimited, message, retryable} provider/recovery {threadId | null, hint: restart-process | resume-fresh | reauthenticate | reinstall | none, reason, retryable}
Interactions approvals command · fileChange · toolUse · permissionGrant; requests userQuestion, planReview closed command, file_change, permission_grant, plan; question; open <pluginId>/<kind>; tool_use only as a declared open kind; requestKey dedupe
Execution options {model, serviceTier?, reasoningLevel, promptMode?, instructions, providerOptions} & PermissionPolicy {model, reasoningLevel | null, serviceTier | null, permission, instructions, providerOptions}; no promptMode; composer actions ride turn/start.actions
Request and state lanes item/tool/call; thread state usage, contextWindow, rateLimits, modelFallback as snapshots; skills/scanRoots {cwd} tool/call; usage {last, total, contextWindow}, provider.rateLimits, provider.modelFallback deltas with explicit scope; declarative skills.scanRoots plus gated skills/discover
Testing kit @get-bb/plugin-sdk/provider-bridge/testing, /acp conformance kit and fixture bridge in @bb/bridge-kit/conformance; no ACP kit subpath