diff --git a/docs/api_to_audit.md b/docs/api_to_audit.md index b6bcce73b0..5cc98f19e1 100644 --- a/docs/api_to_audit.md +++ b/docs/api_to_audit.md @@ -17,6 +17,38 @@ content-block vocabulary; decide whether legacy aggregate fields still need to be accepted; and define any image MIME validation, decoding, or payload-size policy at the server boundary before making the helper stable. +## The ACP bridge kit (`@get-bb/plugin-sdk/provider-bridge/acp`) + +**What it does.** Publishes bb's generic Agent Client Protocol bridge so any +plugin can add an ACP agent without bb-side code. `experimental_acpProviderBridge` +is the bridge a plugin re-exports from its `bb.host` artifact; the agent to +launch arrives per command in `providerOptions.acpLaunchSpec`, so one +implementation serves every agent. `experimental_registerAcpDialect` and +`experimental_resolveAcpDialect` are the dialect hooks: version 1 of the +protocol has no sub-agent concept and standardizes nothing about `rawInput`, +so each agent's vendor side channels (grok's `_meta["x.ai/tool"]`, Cursor's +`cursor/task` request, an agent's own health/usage/installation surface) are +read by a small profile-keyed module that a plugin can supply for its own +agent and name in its registration's bridge options (`acpDialect`). +`experimental_handleAcpBridgeLine` is the raw line handler for harnesses. +`experimental_parseAcpAgentModelLines` / `experimental_buildAcpAgentModelCatalog` +/ `experimental_splitAcpPrimaryModels` build a model picker from an agent's +`--list-models` output. `experimental_acpProfileFromLaunchSpec` and +`experimental_ACP_*` expose the launch profile and the protocol vocabularies. + +**Audit before stabilizing.** Decide whether `AcpDialect` is the right shape +for a third-party agent — today it has four optional hooks (`toolIdentity`, +`classifyToolCall`, `handleClientRequest`, `maintenance`) and no versioning, +so adding a fifth is a silent capability change for every dialect. Decide +whether `registerAcpDialect`'s process-global registry is right, or whether a +dialect should be named by value in the provider registration instead of by +id. Confirm the dialect id namespace (ids are unscoped strings today, so two +plugins can collide) and whether a plugin may override a built-in dialect. +Settle whether the bridge itself should be a factory rather than a module +singleton before a host artifact ever needs two configured differently, and +whether the model-catalog helpers belong in this kit at all or in a +CLI-model-discovery kit of their own. + ## Bridge record mode (`experimental_recordProviderChildIo` and `experimental_isProviderBridgeRecording`) **What it does.** `experimental_recordProviderChildIo` tees a provider diff --git a/packages/plugin-build/src/builtin-host-artifacts.test.ts b/packages/plugin-build/src/builtin-host-artifacts.test.ts index bfc6f49118..fc9dfe2293 100644 --- a/packages/plugin-build/src/builtin-host-artifacts.test.ts +++ b/packages/plugin-build/src/builtin-host-artifacts.test.ts @@ -7,6 +7,19 @@ import { resolvePluginBuildToolchain } from "./toolchain.js"; const repositoryRoot = resolve(import.meta.dirname, "../../.."); +interface BuiltProviderBridge { + readonly experimental_apiVersion: 1; + readonly handleLine: (line: string) => void; +} + +function isBuiltProviderBridge(value: unknown): value is BuiltProviderBridge { + if (typeof value !== "object" || value === null) return false; + return ( + Reflect.get(value, "experimental_apiVersion") === 1 && + typeof Reflect.get(value, "handleLine") === "function" + ); +} + interface BuiltHostEntry { readonly experimental_apiVersion: 1; readonly handlers: Readonly< @@ -78,4 +91,38 @@ describe("builtin host artifacts", () => { supported: process.platform === "darwin", }); }, 20_000); -}); + + /** + * The ACP plugin's whole host side is one re-export of the published kit + * (`@get-bb/plugin-sdk/provider-bridge/acp`), which is exactly what a + * third-party ACP plugin writes. This builds that artifact the way the + * daemon does — inlining the SDK's published bundle from the plugin's own + * node_modules — and imports the result, so a kit that only resolves + * through the workspace source condition cannot pass. + */ + it("builds the ACP provider bridge from the published SDK subpath", async () => { + const root = await mkdtemp(join(repositoryRoot, ".builtin-host-test-")); + tempDirs.push(root); + const source = join(repositoryRoot, "plugins", "provider-acp"); + for (const fileName of ["package.json", "server.ts"]) { + await cp(join(source, fileName), join(root, fileName)); + } + await cp(join(source, "src"), join(root, "src"), { recursive: true }); + // The manifest's branding icon must resolve for the build to run. + await cp(join(source, "icons"), join(root, "icons"), { recursive: true }); + await symlink( + join(source, "node_modules"), + join(root, "node_modules"), + "dir", + ); + const toolchain = await resolvePluginBuildToolchain( + join(repositoryRoot, "node_modules", ".unused-toolchain"), + ); + const built = await buildPluginHost(root, "0.9.0-test", toolchain); + const imported: unknown = await import( + `${pathToFileURL(built.jsPath).href}?test=${Date.now()}` + ); + const bridge = Reflect.get(Object(imported), "experimental_providerBridge"); + expect(isBuiltProviderBridge(bridge)).toBe(true); + }, 60_000); +}); \ No newline at end of file diff --git a/packages/plugin-sdk/package.json b/packages/plugin-sdk/package.json index dec2ece2ca..fb3d034087 100644 --- a/packages/plugin-sdk/package.json +++ b/packages/plugin-sdk/package.json @@ -38,6 +38,12 @@ "import": "./dist/provider-bridge-testing.js", "default": "./dist/provider-bridge-testing.js" }, + "./provider-bridge/acp": { + "source": "./src/provider-bridge-acp.ts", + "types": "./bundled-types/bb-plugin-sdk-provider-bridge-acp.d.ts", + "import": "./dist/provider-bridge-acp.js", + "default": "./dist/provider-bridge-acp.js" + }, "./app": { "source": "./src/app.ts", "types": "./bundled-types/bb-plugin-sdk-app.d.ts", @@ -113,6 +119,7 @@ "@bb/domain": "workspace:*", "@bb/host-daemon-contract": "workspace:*", "@bb/process-utils": "workspace:*", + "@bb/provider-bridge-acp": "workspace:*", "@bb/provider-bridge-protocol": "workspace:*", "@bb/sdk": "workspace:*", "@bb/server-contract": "workspace:*", diff --git a/packages/plugin-sdk/scripts/build-bundled-dts.mjs b/packages/plugin-sdk/scripts/build-bundled-dts.mjs index bf78cb3a14..abd94f3d75 100644 --- a/packages/plugin-sdk/scripts/build-bundled-dts.mjs +++ b/packages/plugin-sdk/scripts/build-bundled-dts.mjs @@ -48,6 +48,10 @@ const outputs = { pkgRoot, "src/provider-bridge-testing.ts", ), + "bb-plugin-sdk-provider-bridge-acp.d.ts": path.join( + pkgRoot, + "src/provider-bridge-acp.ts", + ), "bb-plugin-sdk-host.d.ts": path.join(pkgRoot, "src/host.ts"), "bb-plugin-sdk-internal-composer-customization-validation.d.ts": path.join( pkgRoot, diff --git a/packages/plugin-sdk/scripts/build-runtime.mjs b/packages/plugin-sdk/scripts/build-runtime.mjs index e19a390d37..14519af141 100644 --- a/packages/plugin-sdk/scripts/build-runtime.mjs +++ b/packages/plugin-sdk/scripts/build-runtime.mjs @@ -28,6 +28,14 @@ const entries = [ output: "dist/provider-bridge-testing.js", external: ["zod", "zod/*"], }, + // The ACP kit: the generic Agent Client Protocol bridge a provider plugin + // re-exports from its host artifact, plus the dialect hooks. Real code, so + // only zod stays external. + { + source: "src/provider-bridge-acp.ts", + output: "dist/provider-bridge-acp.js", + external: ["zod", "zod/*"], + }, { source: "src/host.ts", output: "dist/host.js", external: [] }, { source: "src/internal/composer-customization-validation.ts", diff --git a/packages/plugin-sdk/src/__tests__/package-exports.test.ts b/packages/plugin-sdk/src/__tests__/package-exports.test.ts index 01d6b67ccf..dfba57a18b 100644 --- a/packages/plugin-sdk/src/__tests__/package-exports.test.ts +++ b/packages/plugin-sdk/src/__tests__/package-exports.test.ts @@ -25,6 +25,7 @@ describe("packed plugin SDK exports", () => { ".", "./provider-bridge", "./provider-bridge/testing", + "./provider-bridge/acp", "./app", "./host", "./internal/composer-customization-validation", diff --git a/packages/plugin-sdk/src/provider-bridge-acp.ts b/packages/plugin-sdk/src/provider-bridge-acp.ts new file mode 100644 index 0000000000..b5ad515fce --- /dev/null +++ b/packages/plugin-sdk/src/provider-bridge-acp.ts @@ -0,0 +1,83 @@ +/** + * `@get-bb/plugin-sdk/provider-bridge/acp` — the published ACP bridge kit. + * + * The Agent Client Protocol (https://agentclientprotocol.com) is one wire + * protocol spoken by many agents, so bb runs all of them through one generic + * bridge: the agent to launch arrives per command in the provider options, + * and nothing in the bridge is bb-first-party. A plugin that wants to add an + * ACP agent re-exports the bridge from its `bb.host` artifact and registers + * its providers as any other plugin does: + * + * ```ts + * // host.ts (the plugin's `bb.host` entry) + * export { experimental_acpProviderBridge as experimental_providerBridge } + * from "@get-bb/plugin-sdk/provider-bridge/acp"; + * + * // server.ts + * bb.providers.register({ + * id: "amp", + * displayName: "Amp", + * experimental_bridgeOptions: { + * acpLaunchSpec: { displayName: "Amp", command: "amp", args: ["acp"], env: {} }, + * acpDialect: "amp", + * }, + * // …the rest of the declaration + * }) + * ``` + * + * **Dialects.** Version 1 of the protocol has no sub-agent concept and + * standardizes nothing about `rawInput`, so what most distinguishes one + * agent from another lives beside the protocol: grok stamps + * `_meta["x.ai/tool"]` on every tool event, Cursor reports sub-agents + * through a vendor `cursor/task` request. A dialect is a small module that + * reads those channels; a plugin registers one for its own agent with + * `experimental_registerAcpDialect` and names its id in the registration's + * bridge options. Everything a dialect does is optional — the shared + * classifier decides everything it declines. + * + * Curated by hand — named exports only, never `export *`. Value exports + * carry the `experimental_` prefix every new plugin API member ships with + * (see docs/api_to_audit.md); types are unprefixed. + */ +export { + acpProviderBridge as experimental_acpProviderBridge, + handleAcpBridgeLine as experimental_handleAcpBridgeLine, +} from "@bb/provider-bridge-acp"; + +export { + CURSOR_ACP_DIALECT as experimental_CURSOR_ACP_DIALECT, + GENERIC_ACP_DIALECT as experimental_GENERIC_ACP_DIALECT, + GROK_ACP_DIALECT as experimental_GROK_ACP_DIALECT, + acpDialectIds as experimental_acpDialectIds, + registerAcpDialect as experimental_registerAcpDialect, + resolveAcpDialect as experimental_resolveAcpDialect, +} from "@bb/provider-bridge-acp"; +export type { + AcpClassifiedToolCall, + AcpClientRequestOutcome, + AcpDelegationReport, + AcpDialect, + AcpToolIdentity, +} from "@bb/provider-bridge-acp"; + +export { acpProfileFromLaunchSpec as experimental_acpProfileFromLaunchSpec } from "@bb/provider-bridge-acp"; +export type { AcpAgentProfile } from "@bb/provider-bridge-acp"; + +export { + ACP_PROTOCOL_VERSION as experimental_ACP_PROTOCOL_VERSION, + ACP_TOOL_CALL_STATUSES as experimental_ACP_TOOL_CALL_STATUSES, + ACP_TOOL_KINDS as experimental_ACP_TOOL_KINDS, +} from "@bb/provider-bridge-acp"; +export type { + AcpToolCallContent, + AcpToolCallStatus, + AcpToolCallUpdateEvent, + AcpToolKind, +} from "@bb/provider-bridge-acp"; + +export { + buildAgentModelCatalog as experimental_buildAcpAgentModelCatalog, + parseAgentModelLines as experimental_parseAcpAgentModelLines, + splitPrimaryModels as experimental_splitAcpPrimaryModels, +} from "@bb/provider-bridge-acp"; +export type { AgentModelCatalog as AcpAgentModelCatalog } from "@bb/provider-bridge-acp"; diff --git a/packages/provider-bridge-acp/package.json b/packages/provider-bridge-acp/package.json new file mode 100644 index 0000000000..f3d85ecea2 --- /dev/null +++ b/packages/provider-bridge-acp/package.json @@ -0,0 +1,33 @@ +{ + "name": "@bb/provider-bridge-acp", + "version": "0.0.1", + "type": "module", + "exports": { + ".": { + "source": "./src/index.ts", + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "types": "./src/index.ts", + "scripts": { + "clean": "rimraf dist tsconfig.tsbuildinfo", + "typecheck": "tsc --noEmit", + "test": "vitest run --config vitest.config.ts" + }, + "dependencies": { + "@bb/domain": "workspace:*", + "@bb/host-daemon-contract": "workspace:*", + "@bb/process-utils": "workspace:*", + "@bb/provider-bridge-protocol": "workspace:*", + "zod": "^4.3.6" + }, + "devDependencies": { + "@bb/tsconfig": "workspace:*", + "@modelcontextprotocol/sdk": "^1.29.0", + "@types/node": "^22.0.0", + "typescript": "npm:@typescript/typescript6@^6.0.2", + "typescript-7": "npm:typescript@^7.0.2", + "vitest": "^4.1.1" + } +} diff --git a/plugins/provider-acp/src/bridge-protocol.ts b/packages/provider-bridge-acp/src/bridge-protocol.ts similarity index 87% rename from plugins/provider-acp/src/bridge-protocol.ts rename to packages/provider-bridge-acp/src/bridge-protocol.ts index 0015de744c..ce3f54b5f5 100644 --- a/plugins/provider-acp/src/bridge-protocol.ts +++ b/packages/provider-bridge-acp/src/bridge-protocol.ts @@ -6,23 +6,8 @@ * why they are schemas rather than ad-hoc objects. */ -import { - acpPermissionCliSchema as acpBridgePermissionCliSchema, - acpNativeReasoningSchema as acpBridgeNativeReasoningSchema, - acpReasoningCliSchema as acpBridgeReasoningCliSchema, - modelListParamsSchema as canonicalModelListParamsSchema, - threadDiscardParamsSchema as canonicalThreadDiscardParamsSchema, - threadForkParamsSchema as canonicalThreadForkParamsSchema, - threadResumeParamsSchema as canonicalThreadResumeParamsSchema, - threadStartParamsSchema as canonicalThreadStartParamsSchema, - threadStopParamsSchema as canonicalThreadStopParamsSchema, - turnStartParamsSchema as canonicalTurnStartParamsSchema, - turnSteerParamsSchema as canonicalTurnSteerParamsSchema, - skillsConfigureParamsSchema, - experimental_providerMaintenanceParamsSchema, - experimental_providerInstallationRunParamsSchema, - experimental_providerInstallationStatusParamsSchema, -} from "@get-bb/plugin-sdk/provider-bridge"; +import { acpNativeReasoningSchema as acpBridgeNativeReasoningSchema, acpPermissionCliSchema as acpBridgePermissionCliSchema, acpReasoningCliSchema as acpBridgeReasoningCliSchema } from "@bb/domain"; +import { experimental_providerInstallationRunParamsSchema, experimental_providerInstallationStatusParamsSchema, experimental_providerMaintenanceParamsSchema, modelListParamsSchema as canonicalModelListParamsSchema, skillsConfigureParamsSchema, threadDiscardParamsSchema as canonicalThreadDiscardParamsSchema, threadForkParamsSchema as canonicalThreadForkParamsSchema, threadResumeParamsSchema as canonicalThreadResumeParamsSchema, threadStartParamsSchema as canonicalThreadStartParamsSchema, threadStopParamsSchema as canonicalThreadStopParamsSchema, turnStartParamsSchema as canonicalTurnStartParamsSchema, turnSteerParamsSchema as canonicalTurnSteerParamsSchema } from "@bb/provider-bridge-protocol"; import { z } from "zod"; import { acpSessionUpdateSchema, acpStopReasonSchema } from "./wire.js"; diff --git a/plugins/provider-acp/src/bridge/agent-connection.test.ts b/packages/provider-bridge-acp/src/bridge/agent-connection.test.ts similarity index 100% rename from plugins/provider-acp/src/bridge/agent-connection.test.ts rename to packages/provider-bridge-acp/src/bridge/agent-connection.test.ts diff --git a/plugins/provider-acp/src/bridge/agent-connection.ts b/packages/provider-bridge-acp/src/bridge/agent-connection.ts similarity index 99% rename from plugins/provider-acp/src/bridge/agent-connection.ts rename to packages/provider-bridge-acp/src/bridge/agent-connection.ts index 05255d430a..6ff85504c2 100644 --- a/plugins/provider-acp/src/bridge/agent-connection.ts +++ b/packages/provider-bridge-acp/src/bridge/agent-connection.ts @@ -8,7 +8,7 @@ import { spawn, type ChildProcess } from "node:child_process"; import { createInterface } from "node:readline"; -import { experimental_recordProviderChildIo } from "@get-bb/plugin-sdk/provider-bridge"; +import { experimental_recordProviderChildIo } from "@bb/provider-bridge-protocol/bridge-kit"; import type { z } from "zod"; const STDERR_TAIL_MAX_CHUNKS = 40; diff --git a/plugins/provider-acp/src/bridge/bridge.conformance-calibration.test.ts b/packages/provider-bridge-acp/src/bridge/bridge.conformance-calibration.test.ts similarity index 90% rename from plugins/provider-acp/src/bridge/bridge.conformance-calibration.test.ts rename to packages/provider-bridge-acp/src/bridge/bridge.conformance-calibration.test.ts index fe24d8fc97..7dace77222 100644 --- a/plugins/provider-acp/src/bridge/bridge.conformance-calibration.test.ts +++ b/packages/provider-bridge-acp/src/bridge/bridge.conformance-calibration.test.ts @@ -4,16 +4,16 @@ import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { afterEach, beforeEach, expect, it } from "vitest"; import { - experimental_captureBridgeJsonRpcOutput as captureBridgeJsonRpcOutput, - experimental_createBridgeDeltaEventCollector as createBridgeDeltaEventCollector, - experimental_formatConformanceReport as formatConformanceReport, - experimental_runBridgeConformance as runBridgeConformance, - experimental_toConformanceMessages as toConformanceMessages, -} from "@get-bb/plugin-sdk/provider-bridge/testing"; -import type { - BridgeConformanceTransport, - CapturedBridgeJsonRpcOutput, -} from "@get-bb/plugin-sdk/provider-bridge/testing"; + formatConformanceReport, + runBridgeConformance, +} from "@bb/provider-bridge-protocol/conformance"; +import type { BridgeConformanceTransport } from "@bb/provider-bridge-protocol/conformance"; +import { + captureBridgeJsonRpcOutput, + createBridgeDeltaEventCollector, + toConformanceMessages, +} from "@bb/provider-bridge-protocol/testing"; +import type { CapturedBridgeJsonRpcOutput } from "@bb/provider-bridge-protocol/testing"; import { handleLine } from "./bridge.js"; diff --git a/plugins/provider-acp/src/bridge/bridge.recorded-conformance.test.ts b/packages/provider-bridge-acp/src/bridge/bridge.recorded-conformance.test.ts similarity index 100% rename from plugins/provider-acp/src/bridge/bridge.recorded-conformance.test.ts rename to packages/provider-bridge-acp/src/bridge/bridge.recorded-conformance.test.ts diff --git a/plugins/provider-acp/src/bridge/bridge.test.ts b/packages/provider-bridge-acp/src/bridge/bridge.test.ts similarity index 99% rename from plugins/provider-acp/src/bridge/bridge.test.ts rename to packages/provider-bridge-acp/src/bridge/bridge.test.ts index c3ad0e461e..7bd09d0755 100644 --- a/plugins/provider-acp/src/bridge/bridge.test.ts +++ b/packages/provider-bridge-acp/src/bridge/bridge.test.ts @@ -13,18 +13,15 @@ import { fileURLToPath } from "node:url"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createStandaloneBuiltinCompactCommandInput } from "@bb/domain"; import type { DynamicTool, ReasoningLevel } from "@bb/domain"; +import { PROVIDER_BRIDGE_PROTOCOL_VERSION, THREAD_DELTA_NOTIFICATION_METHOD } from "@bb/provider-bridge-protocol"; import { - PROVIDER_BRIDGE_PROTOCOL_VERSION, - THREAD_DELTA_NOTIFICATION_METHOD, -} from "@bb/provider-bridge-protocol"; -import { - experimental_assembleCapturedThreadEvents as assembleCapturedThreadEvents, - experimental_captureBridgeJsonRpcOutput as captureBridgeJsonRpcOutput, -} from "@get-bb/plugin-sdk/provider-bridge/testing"; + assembleCapturedThreadEvents, + captureBridgeJsonRpcOutput, +} from "@bb/provider-bridge-protocol/testing"; import type { BridgeJsonRpcOutputMessage, CapturedBridgeJsonRpcOutput, -} from "@get-bb/plugin-sdk/provider-bridge/testing"; +} from "@bb/provider-bridge-protocol/testing"; import { handleLine } from "./bridge.js"; import { ACP_BRIDGE_NO_ACTIVE_TURN_ERROR_CODE } from "../bridge-protocol.js"; diff --git a/plugins/provider-acp/src/bridge/bridge.ts b/packages/provider-bridge-acp/src/bridge/bridge.ts similarity index 97% rename from plugins/provider-acp/src/bridge/bridge.ts rename to packages/provider-bridge-acp/src/bridge/bridge.ts index e7ee3a94c0..57c1f9ad69 100644 --- a/plugins/provider-acp/src/bridge/bridge.ts +++ b/packages/provider-bridge-acp/src/bridge/bridge.ts @@ -1,5 +1,3 @@ -#!/usr/bin/env node - /** * Generic ACP bridge. * @@ -11,33 +9,13 @@ * workspace write policy on client `fs/write_text_file` requests. */ -import { - isStandaloneBuiltinCompactCommand, - pendingInteractionResolutionSchema, - reasoningEffortsForLevels, - type AvailableModel, - type PromptInput, - type ReasoningLevel, - type ThreadDelta, - hostDaemonAcpLaunchSpecSchema, - bridgeRequestEnvelopeSchema, - createBridgeIo, - createBridgeLineHandler, - decodeBridgeJsonRpcResponse, - decodeToolCallResponsePayload, - mimeTypeFromExtension, - runBridgeRequest, - withoutBridgeRuntimeEnv, - type BridgeJsonRpcResponse, - BRIDGE_INBOUND_REQUEST_METHODS, - BRIDGE_JSON_RPC_ERRORS, - BRIDGE_NOTIFICATION_METHODS, - PROVIDER_BRIDGE_PROTOCOL_VERSION, - THREAD_DELTA_GRAMMAR_V3, - THREAD_DELTA_NOTIFICATION_METHOD, - type InitializeResult, - experimental_defineProviderBridge, -} from "@get-bb/plugin-sdk/provider-bridge"; +import { isStandaloneBuiltinCompactCommand, pendingInteractionResolutionSchema, reasoningEffortsForLevels } from "@bb/domain"; +import type { AvailableModel, PromptInput, ReasoningLevel } from "@bb/domain"; +import { hostDaemonAcpLaunchSpecSchema } from "@bb/host-daemon-contract"; +import { BRIDGE_INBOUND_REQUEST_METHODS, BRIDGE_JSON_RPC_ERRORS, BRIDGE_NOTIFICATION_METHODS, PROVIDER_BRIDGE_PROTOCOL_VERSION, THREAD_DELTA_GRAMMAR_V3, THREAD_DELTA_NOTIFICATION_METHOD } from "@bb/provider-bridge-protocol"; +import type { InitializeResult, ThreadDelta } from "@bb/provider-bridge-protocol"; +import { bridgeRequestEnvelopeSchema, createBridgeIo, createBridgeLineHandler, decodeBridgeJsonRpcResponse, decodeToolCallResponsePayload, experimental_defineProviderBridge, mimeTypeFromExtension, runBridgeRequest, withoutBridgeRuntimeEnv } from "@bb/provider-bridge-protocol/bridge-kit"; +import type { BridgeJsonRpcResponse } from "@bb/provider-bridge-protocol/bridge-kit"; import { execFile } from "node:child_process"; import { randomBytes } from "node:crypto"; import { promises as fs, readFileSync } from "node:fs"; @@ -71,6 +49,7 @@ import { type AcpDeltaTranslator, } from "../delta-translation.js"; import { resolveAcpDialect, type AcpDialect } from "../dialect.js"; +import type { AcpMaintenanceDialect } from "./provider-maintenance.js"; import { buildAcpPermissionInteractionPayload, resolveAcpPermissionDecision, @@ -2426,6 +2405,22 @@ function decodeDialectId( return acpProviderOptionsSchema.parse(providerOptions ?? {}).acpDialect; } +/** + * The maintenance surface for a provider-maintenance request: the agent's + * dialect owns it, so the bridge never asks which bb provider is calling. + */ +function maintenanceForRequest( + providerOptions: Record | undefined, +): AcpMaintenanceDialect | undefined { + const profile = decodeLaunchProfile(providerOptions); + return resolveAcpDialect({ + ...(decodeDialectId(providerOptions) === undefined + ? {} + : { dialectId: decodeDialectId(providerOptions) }), + command: profile?.agentCommand.command ?? "", + }).maintenance; +} + async function handleRequest( request: AcpBridgeCommand & { id: string | number }, ): Promise { @@ -2485,7 +2480,7 @@ async function handleRequest( sendResult( request.id, await getAcpProviderHealth({ - providerId: request.params.providerId, + maintenance: maintenanceForRequest(request.params.providerOptions), command: profile?.agentCommand.command ?? null, }), ); @@ -2497,7 +2492,7 @@ async function handleRequest( sendResult( request.id, await getAcpProviderUsage({ - providerId: request.params.providerId, + maintenance: maintenanceForRequest(request.params.providerOptions), command: profile?.agentCommand.command ?? null, }), ); @@ -2509,7 +2504,7 @@ async function handleRequest( sendResult( request.id, await getAcpProviderInstallationStatus({ - providerId: request.params.providerId, + maintenance: maintenanceForRequest(request.params.providerOptions), command: profile?.agentCommand.command ?? null, }), ); @@ -2521,7 +2516,7 @@ async function handleRequest( sendResult( request.id, await getAcpProviderInstallationRun({ - providerId: request.params.providerId, + maintenance: maintenanceForRequest(request.params.providerOptions), command: profile?.agentCommand.command ?? null, action: request.params.action, }), diff --git a/plugins/provider-acp/src/bridge/cursor-mcp-approval.test.ts b/packages/provider-bridge-acp/src/bridge/cursor-mcp-approval.test.ts similarity index 100% rename from plugins/provider-acp/src/bridge/cursor-mcp-approval.test.ts rename to packages/provider-bridge-acp/src/bridge/cursor-mcp-approval.test.ts diff --git a/plugins/provider-acp/src/bridge/cursor-mcp-approval.ts b/packages/provider-bridge-acp/src/bridge/cursor-mcp-approval.ts similarity index 100% rename from plugins/provider-acp/src/bridge/cursor-mcp-approval.ts rename to packages/provider-bridge-acp/src/bridge/cursor-mcp-approval.ts diff --git a/plugins/provider-acp/src/bridge/fake-acp-agent.mjs b/packages/provider-bridge-acp/src/bridge/fake-acp-agent.mjs similarity index 100% rename from plugins/provider-acp/src/bridge/fake-acp-agent.mjs rename to packages/provider-bridge-acp/src/bridge/fake-acp-agent.mjs diff --git a/plugins/provider-acp/src/bridge/mcp-server-entry.test.ts b/packages/provider-bridge-acp/src/bridge/mcp-server-entry.test.ts similarity index 100% rename from plugins/provider-acp/src/bridge/mcp-server-entry.test.ts rename to packages/provider-bridge-acp/src/bridge/mcp-server-entry.test.ts diff --git a/plugins/provider-acp/src/bridge/model-catalog.test.ts b/packages/provider-bridge-acp/src/bridge/model-catalog.test.ts similarity index 100% rename from plugins/provider-acp/src/bridge/model-catalog.test.ts rename to packages/provider-bridge-acp/src/bridge/model-catalog.test.ts diff --git a/plugins/provider-acp/src/bridge/model-catalog.ts b/packages/provider-bridge-acp/src/bridge/model-catalog.ts similarity index 99% rename from plugins/provider-acp/src/bridge/model-catalog.ts rename to packages/provider-bridge-acp/src/bridge/model-catalog.ts index 8f3a11ce65..a3a1de8f1f 100644 --- a/plugins/provider-acp/src/bridge/model-catalog.ts +++ b/packages/provider-bridge-acp/src/bridge/model-catalog.ts @@ -31,12 +31,8 @@ * marker, and Cursor's own `(default)`/`(current)` annotations. */ -import { - reasoningLevelValues, - type AvailableModel, - type ReasoningLevel, - type ServiceTier, -} from "@get-bb/plugin-sdk/provider-bridge"; +import { reasoningLevelValues } from "@bb/domain"; +import type { AvailableModel, ReasoningLevel, ServiceTier } from "@bb/domain"; import type { AcpConfigOption, AcpSessionModels } from "../wire.js"; interface RawAgentModel { diff --git a/plugins/provider-acp/src/bridge/provider-maintenance.test.ts b/packages/provider-bridge-acp/src/bridge/provider-maintenance.test.ts similarity index 66% rename from plugins/provider-acp/src/bridge/provider-maintenance.test.ts rename to packages/provider-bridge-acp/src/bridge/provider-maintenance.test.ts index 5140caf153..c7d9c0dc29 100644 --- a/plugins/provider-acp/src/bridge/provider-maintenance.test.ts +++ b/packages/provider-bridge-acp/src/bridge/provider-maintenance.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from "vitest"; -import { __testing } from "./provider-maintenance.js"; +import { + CURSOR_ACP_MAINTENANCE, + __testing, +} from "./provider-maintenance.js"; function cursorMissingInstallationStatus() { return { @@ -57,11 +60,18 @@ describe("ACP provider maintenance", () => { }); }); - it("offers the Cursor installer only through a fresh matching action", () => { + // The installer belongs to the agent's dialect, not to a bb provider id: + // an agent whose dialect has no maintenance surface offers none, whatever + // it is registered as. + it("offers the installer only through a fresh matching action", () => { expect( __testing.buildProviderInstallationRun( cursorMissingInstallationStatus(), - { providerId: "acp-cursor", action: "install" }, + { + maintenance: CURSOR_ACP_MAINTENANCE, + command: "cursor-agent", + action: "install", + }, ), ).toMatchObject({ available: true, @@ -71,11 +81,22 @@ describe("ACP provider maintenance", () => { expect( __testing.buildProviderInstallationRun( { ...cursorMissingInstallationStatus(), installAction: null }, - { providerId: "acp-opencode", action: "install" }, + { maintenance: undefined, command: "opencode", action: "install" }, ), ).toEqual({ available: false, - message: "acp-opencode install is not available on this host.", + message: "opencode install is not available on this host.", + }); + // A matching action with no maintenance surface still declines. + expect( + __testing.buildProviderInstallationRun(cursorMissingInstallationStatus(), { + maintenance: undefined, + command: "opencode", + action: "install", + }), + ).toEqual({ + available: false, + message: "opencode install is not available on this host.", }); }); }); diff --git a/plugins/provider-acp/src/bridge/provider-maintenance.ts b/packages/provider-bridge-acp/src/bridge/provider-maintenance.ts similarity index 80% rename from plugins/provider-acp/src/bridge/provider-maintenance.ts rename to packages/provider-bridge-acp/src/bridge/provider-maintenance.ts index afcc3e2d20..ef2119a783 100644 --- a/plugins/provider-acp/src/bridge/provider-maintenance.ts +++ b/packages/provider-bridge-acp/src/bridge/provider-maintenance.ts @@ -5,20 +5,12 @@ import os from "node:os"; import path from "node:path"; import { DatabaseSync } from "node:sqlite"; import { promisify } from "node:util"; -import type { - ExperimentalProviderHealthResult, - ExperimentalProviderInstallationRunResult, - ExperimentalProviderInstallationStatus, - ExperimentalProviderUsage, - ExperimentalProviderUsageResult, - ExperimentalProviderUsageWindow, -} from "@get-bb/plugin-sdk/provider-bridge"; +import type { ExperimentalProviderHealthResult, ExperimentalProviderInstallationRunResult, ExperimentalProviderInstallationStatus, ExperimentalProviderUsage, ExperimentalProviderUsageResult, ExperimentalProviderUsageWindow } from "@bb/provider-bridge-protocol"; import { z } from "zod"; const execFileAsync = promisify(execFile); const COMMAND_TIMEOUT_MS = 5_000; const USAGE_FETCH_TIMEOUT_MS = 15_000; -const CURSOR_PROVIDER_ID = "acp-cursor"; const CURSOR_DASHBOARD_URL = "https://api2.cursor.sh/aiserver.v1.DashboardService"; const CURSOR_KEYCHAIN_ACCOUNT = "cursor-user"; @@ -155,14 +147,32 @@ function readAccountEmail(): string | null { } } +/** + * What an agent's own dialect knows about keeping it healthy: how a user + * signs in, whether bb can install it, and where its account and usage live. + * ACP standardizes none of it, so a generic bridge can only report whether + * the executable exists — everything richer belongs to the agent, and is + * therefore the dialect's (see `dialect.ts`), never a bb provider id's. + */ +export interface AcpMaintenanceDialect { + /** The shell command that signs the user in. */ + loginCommand: string; + /** How bb installs or updates the agent, when it can. */ + installer(): { command: string; args: string[]; displayCommand: string }; + /** The signed-in account, or null when the agent is not signed in. */ + readAccount(): Promise<{ email: string | null } | null>; + /** The agent's usage windows, for the usage surfaces. */ + readUsage(): Promise; +} + function healthResult(args: { - providerId: string; + maintenance: AcpMaintenanceDialect | undefined; status: "ready" | "not_installed" | "unauthenticated" | "unknown"; accountEmail?: string | null; installedVersion?: string | null; statusMessage?: string | null; }): ExperimentalProviderHealthResult { - const cursor = args.providerId === CURSOR_PROVIDER_ID; + const maintained = args.maintenance !== undefined; return { supported: true, health: { @@ -172,49 +182,47 @@ function healthResult(args: { planLabel: null, installedVersion: args.installedVersion ?? null, minimumSupportedVersion: null, - canInstall: cursor, - canUpdate: cursor && args.status !== "not_installed", - loginCommand: cursor ? "cursor-agent login" : null, + canInstall: maintained, + canUpdate: maintained && args.status !== "not_installed", + loginCommand: args.maintenance?.loginCommand ?? null, }, }; } export async function getAcpProviderHealth(args: { - providerId: string; + maintenance: AcpMaintenanceDialect | undefined; command: string | null; }): Promise { + const maintenance = args.maintenance; if (args.command === null) { return healthResult({ - providerId: args.providerId, + maintenance, status: "unknown", statusMessage: "The ACP provider has no launch command.", }); } if ((await executablePath(args.command)) === null) { - return healthResult({ - providerId: args.providerId, - status: "not_installed", - }); + return healthResult({ maintenance, status: "not_installed" }); } const version = await installedVersion(args.command); - if (args.providerId !== CURSOR_PROVIDER_ID) { + if (maintenance === undefined) { return healthResult({ - providerId: args.providerId, + maintenance, status: "ready", installedVersion: version, }); } try { - const accessToken = await readAccessToken(); + const account = await maintenance.readAccount(); return healthResult({ - providerId: args.providerId, - status: accessToken === null ? "unauthenticated" : "ready", - accountEmail: accessToken === null ? null : readAccountEmail(), + maintenance, + status: account === null ? "unauthenticated" : "ready", + accountEmail: account?.email ?? null, installedVersion: version, }); } catch (error) { return healthResult({ - providerId: args.providerId, + maintenance, status: "unknown", installedVersion: version, statusMessage: error instanceof Error ? error.message : String(error), @@ -237,10 +245,10 @@ function cursorInstallerCommand(): { } export async function getAcpProviderInstallationStatus(args: { - providerId: string; + maintenance: AcpMaintenanceDialect | undefined; command: string | null; }): Promise { - const executableName = args.command ?? "cursor-agent"; + const executableName = args.command ?? ""; const resolvedExecutable = args.command === null ? null : await executablePath(args.command); const installed = resolvedExecutable !== null; @@ -249,11 +257,11 @@ export async function getAcpProviderInstallationStatus(args: { ? await installedVersion(args.command) : null; const installAction = - args.providerId === CURSOR_PROVIDER_ID && !installed + args.maintenance !== undefined && !installed ? { kind: "install" as const, label: "Install" as const, - command: cursorInstallerCommand().displayCommand, + command: args.maintenance.installer().displayCommand, } : null; return { @@ -273,7 +281,7 @@ export async function getAcpProviderInstallationStatus(args: { } export async function getAcpProviderInstallationRun(args: { - providerId: string; + maintenance: AcpMaintenanceDialect | undefined; command: string | null; action: "install" | "update"; }): Promise { @@ -283,17 +291,21 @@ export async function getAcpProviderInstallationRun(args: { function buildAcpProviderInstallationRun( status: ExperimentalProviderInstallationStatus, - args: { providerId: string; action: "install" | "update" }, + args: { + maintenance: AcpMaintenanceDialect | undefined; + command: string | null; + action: "install" | "update"; + }, ): ExperimentalProviderInstallationRunResult { - if (status.installAction?.kind !== args.action) { + if (status.installAction?.kind !== args.action || args.maintenance === undefined) { return { available: false, - message: `${args.providerId} ${args.action} is not available on this host.`, + message: `${args.command ?? "This ACP agent"} ${args.action} is not available on this host.`, }; } return { available: true, - command: cursorInstallerCommand(), + command: args.maintenance.installer(), verification: { kind: "installed" }, }; } @@ -409,13 +421,28 @@ function fetchDashboard( } export async function getAcpProviderUsage(args: { - providerId: string; + maintenance: AcpMaintenanceDialect | undefined; command: string | null; }): Promise { - if (args.providerId !== CURSOR_PROVIDER_ID) return { supported: false }; + if (args.maintenance === undefined) return { supported: false }; if (args.command === null || (await executablePath(args.command)) === null) { return { supported: true, usage: { status: "not_installed" } }; } + return args.maintenance.readUsage(); +} + +/** Cursor's own maintenance surface; the cursor dialect carries it. */ +export const CURSOR_ACP_MAINTENANCE: AcpMaintenanceDialect = { + loginCommand: "cursor-agent login", + installer: cursorInstallerCommand, + readAccount: async () => { + const accessToken = await readAccessToken(); + return accessToken === null ? null : { email: readAccountEmail() }; + }, + readUsage: readCursorUsage, +}; + +async function readCursorUsage(): Promise { const accessToken = await readAccessToken(); if (!accessToken) { return { supported: true, usage: { status: "unauthenticated" } }; diff --git a/plugins/provider-acp/src/bridge/tool-proxy-mcp.test.ts b/packages/provider-bridge-acp/src/bridge/tool-proxy-mcp.test.ts similarity index 100% rename from plugins/provider-acp/src/bridge/tool-proxy-mcp.test.ts rename to packages/provider-bridge-acp/src/bridge/tool-proxy-mcp.test.ts diff --git a/plugins/provider-acp/src/bridge/tool-proxy-mcp.ts b/packages/provider-bridge-acp/src/bridge/tool-proxy-mcp.ts similarity index 97% rename from plugins/provider-acp/src/bridge/tool-proxy-mcp.ts rename to packages/provider-bridge-acp/src/bridge/tool-proxy-mcp.ts index d95fdcd8a6..dea0615c5a 100644 --- a/plugins/provider-acp/src/bridge/tool-proxy-mcp.ts +++ b/packages/provider-bridge-acp/src/bridge/tool-proxy-mcp.ts @@ -1,8 +1,6 @@ -import { - dynamicToolSchema, - experimental_buildBridgeToolCallContent, - type DynamicTool, -} from "@get-bb/plugin-sdk/provider-bridge"; +import { dynamicToolSchema } from "@bb/domain"; +import type { DynamicTool } from "@bb/domain"; +import { buildBridgeToolCallContent as experimental_buildBridgeToolCallContent } from "@bb/provider-bridge-protocol/bridge-kit"; import { createConnection } from "node:net"; import { createInterface } from "node:readline"; import { z } from "zod"; diff --git a/plugins/provider-acp/src/delta-translation.test.ts b/packages/provider-bridge-acp/src/delta-translation.test.ts similarity index 99% rename from plugins/provider-acp/src/delta-translation.test.ts rename to packages/provider-bridge-acp/src/delta-translation.test.ts index 4052baf951..a49eaf924b 100644 --- a/plugins/provider-acp/src/delta-translation.test.ts +++ b/packages/provider-bridge-acp/src/delta-translation.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it } from "vitest"; import { threadScope, turnScope, type ThreadEvent } from "@bb/domain"; import type { ProviderRuntimeEvent } from "@bb/provider-bridge-protocol/bridge-kit"; -import { experimental_createDeltaAssembler as createDeltaAssembler } from "@get-bb/plugin-sdk/provider-bridge/testing"; -import type { DeltaAssembler } from "@get-bb/plugin-sdk/provider-bridge/testing"; +import { createDeltaAssembler } from "@bb/provider-bridge-protocol/assembler"; +import type { DeltaAssembler } from "@bb/provider-bridge-protocol/assembler"; import { ACP_COMPACTION_COMPLETED_METHOD, ACP_COMPACTION_STARTED_METHOD, diff --git a/plugins/provider-acp/src/delta-translation.ts b/packages/provider-bridge-acp/src/delta-translation.ts similarity index 98% rename from plugins/provider-acp/src/delta-translation.ts rename to packages/provider-bridge-acp/src/delta-translation.ts index b53425d8aa..f11035be54 100644 --- a/plugins/provider-acp/src/delta-translation.ts +++ b/packages/provider-bridge-acp/src/delta-translation.ts @@ -15,22 +15,12 @@ * in the assembler. */ -import { - errorEnvelopeSchema, - jsonRpcEnvelopeSchema, - providerRawEventSchema, - type JsonRpcMessage, - type ProviderRawEvent, - type ProviderRuntimeEvent, -} from "@get-bb/plugin-sdk/provider-bridge"; -import type { - DeltaItemShape, - DeltaNoTurnFallback, - ThreadDelta, - ThreadEventItemStatus, - ThreadEventPlanStep, - ThreadEventTurnStatus, -} from "@get-bb/plugin-sdk/provider-bridge"; +import { providerRawEventSchema } from "@bb/domain"; +import type { ProviderRawEvent } from "@bb/domain"; +import { errorEnvelopeSchema, jsonRpcEnvelopeSchema } from "@bb/provider-bridge-protocol/bridge-kit"; +import type { JsonRpcMessage, ProviderRuntimeEvent } from "@bb/provider-bridge-protocol/bridge-kit"; +import type { ThreadEventItemStatus, ThreadEventPlanStep, ThreadEventTurnStatus } from "@bb/domain"; +import type { DeltaItemShape, DeltaNoTurnFallback, ThreadDelta } from "@bb/provider-bridge-protocol"; import { ACP_COMPACTION_COMPLETED_METHOD, ACP_COMPACTION_STARTED_METHOD, diff --git a/plugins/provider-acp/src/dialect.test.ts b/packages/provider-bridge-acp/src/dialect.test.ts similarity index 100% rename from plugins/provider-acp/src/dialect.test.ts rename to packages/provider-bridge-acp/src/dialect.test.ts diff --git a/plugins/provider-acp/src/dialect.ts b/packages/provider-bridge-acp/src/dialect.ts similarity index 86% rename from plugins/provider-acp/src/dialect.ts rename to packages/provider-bridge-acp/src/dialect.ts index e082da02da..2dff929e83 100644 --- a/plugins/provider-acp/src/dialect.ts +++ b/packages/provider-bridge-acp/src/dialect.ts @@ -18,9 +18,13 @@ * nothing and leaves every decision to the protocol fields. */ -import type { DeltaItemShape } from "@get-bb/plugin-sdk/provider-bridge"; +import type { DeltaItemShape } from "@bb/provider-bridge-protocol"; import { basename } from "node:path"; import { z } from "zod"; +import { + CURSOR_ACP_MAINTENANCE, + type AcpMaintenanceDialect, +} from "./bridge/provider-maintenance.js"; import { delegationPresentation } from "./presentation.js"; import type { AcpClassifiedToolCall } from "./tool-classification.js"; import { @@ -80,6 +84,12 @@ export interface AcpDialect { method: string, params: unknown, ): AcpClientRequestOutcome | undefined; + /** + * How bb keeps this agent healthy: sign-in, installation, account and + * usage. ACP standardizes none of it, so an agent without one reports only + * whether its executable exists. + */ + maintenance?: AcpMaintenanceDialect; } export interface AcpClientRequestOutcome { @@ -266,17 +276,34 @@ export const CURSOR_ACP_DIALECT: AcpDialect = { id: "cursor", classifyToolCall: cursorClassifyToolCall, handleClientRequest: cursorHandleClientRequest, + maintenance: CURSOR_ACP_MAINTENANCE, }; // --------------------------------------------------------------------------- // Selection // --------------------------------------------------------------------------- -/** Every dialect this kit ships, by id. */ -const DIALECTS_BY_ID: Readonly> = { - [CURSOR_ACP_DIALECT.id]: CURSOR_ACP_DIALECT, - [GROK_ACP_DIALECT.id]: GROK_ACP_DIALECT, -}; +/** + * Every dialect the bridge can select, by id: the ones this kit ships plus + * any a plugin registered. A plugin registers its own from the module scope + * of its `bb.host` artifact, which the bridge shares, before any session + * starts. + */ +const DIALECTS_BY_ID = new Map([ + [CURSOR_ACP_DIALECT.id, CURSOR_ACP_DIALECT], + [GROK_ACP_DIALECT.id, GROK_ACP_DIALECT], +]); + +/** + * Teach the bridge one agent's side channels. A plugin that registers an ACP + * agent bb has never seen — Amp, or an in-house agent — ships a dialect for + * it and names the id in its provider registration's bridge options + * (`acpDialect`). Registering an id twice replaces the earlier dialect, so a + * plugin can override a built-in for its own registration. + */ +export function registerAcpDialect(dialect: AcpDialect): void { + DIALECTS_BY_ID.set(dialect.id, dialect); +} /** The executable name each dialect's agent is normally launched as. */ const DIALECT_IDS_BY_COMMAND: Readonly> = { @@ -284,8 +311,10 @@ const DIALECT_IDS_BY_COMMAND: Readonly> = { grok: GROK_ACP_DIALECT.id, }; -/** Every dialect id a profile may name. */ -export const ACP_DIALECT_IDS = Object.keys(DIALECTS_BY_ID); +/** Every dialect id a profile may name, registered ones included. */ +export function acpDialectIds(): string[] { + return [...DIALECTS_BY_ID.keys()]; +} /** * The dialect for an agent launch. The profile names it (the ACP plugin puts @@ -304,10 +333,10 @@ export function resolveAcpDialect(launch: { command: string; }): AcpDialect { if (launch.dialectId !== undefined) { - return DIALECTS_BY_ID[launch.dialectId] ?? GENERIC_ACP_DIALECT; + return DIALECTS_BY_ID.get(launch.dialectId) ?? GENERIC_ACP_DIALECT; } const byCommand = DIALECT_IDS_BY_COMMAND[basename(launch.command)]; return byCommand === undefined ? GENERIC_ACP_DIALECT - : (DIALECTS_BY_ID[byCommand] ?? GENERIC_ACP_DIALECT); + : (DIALECTS_BY_ID.get(byCommand) ?? GENERIC_ACP_DIALECT); } diff --git a/packages/provider-bridge-acp/src/index.ts b/packages/provider-bridge-acp/src/index.ts new file mode 100644 index 0000000000..02251658c4 --- /dev/null +++ b/packages/provider-bridge-acp/src/index.ts @@ -0,0 +1,60 @@ +/** + * The ACP provider-bridge kit. + * + * bb runs Cursor, grok and every other Agent Client Protocol agent through + * one generic bridge: it speaks bb's runtime JSON-RPC on stdio, acts as the + * ACP *client* for the agent it launches, and translates the agent's session + * updates into bb's thread-delta grammar. Nothing in it is bb-first-party — + * the agent to launch arrives per command in the provider options — so the + * same bridge serves a plugin bb has never heard of. + * + * This barrel is what `@get-bb/plugin-sdk/provider-bridge/acp` publishes. + * The plugin that owns an ACP agent needs three things from it: the bridge + * to re-export from its `bb.host` artifact, the dialect hooks to describe + * its agent's vendor side channels, and the profile type its registration + * fills in. + */ + +export { + experimental_providerBridge as acpProviderBridge, + handleLine as handleAcpBridgeLine, +} from "./bridge/bridge.js"; + +export { + CURSOR_ACP_DIALECT, + GENERIC_ACP_DIALECT, + GROK_ACP_DIALECT, + acpDialectIds, + registerAcpDialect, + resolveAcpDialect, +} from "./dialect.js"; +export type { + AcpClientRequestOutcome, + AcpDelegationReport, + AcpDialect, + AcpToolIdentity, +} from "./dialect.js"; + +export type { AcpAgentProfile } from "./profiles.js"; +export { acpProfileFromLaunchSpec } from "./profiles.js"; + +export type { AcpClassifiedToolCall } from "./tool-classification.js"; + +export { + ACP_PROTOCOL_VERSION, + ACP_TOOL_CALL_STATUSES, + ACP_TOOL_KINDS, +} from "./wire.js"; +export type { + AcpToolCallContent, + AcpToolCallStatus, + AcpToolCallUpdateEvent, + AcpToolKind, +} from "./wire.js"; + +export { + buildAgentModelCatalog, + parseAgentModelLines, + splitPrimaryModels, +} from "./bridge/model-catalog.js"; +export type { AgentModelCatalog } from "./bridge/model-catalog.js"; diff --git a/plugins/provider-acp/src/interactions.test.ts b/packages/provider-bridge-acp/src/interactions.test.ts similarity index 100% rename from plugins/provider-acp/src/interactions.test.ts rename to packages/provider-bridge-acp/src/interactions.test.ts diff --git a/plugins/provider-acp/src/interactions.ts b/packages/provider-bridge-acp/src/interactions.ts similarity index 96% rename from plugins/provider-acp/src/interactions.ts rename to packages/provider-bridge-acp/src/interactions.ts index 448e016fe5..db612add8b 100644 --- a/plugins/provider-acp/src/interactions.ts +++ b/packages/provider-bridge-acp/src/interactions.ts @@ -9,14 +9,8 @@ * subject carrying the same presentation its timeline row does. */ -import { - type PendingInteractionApprovalDecision, - type PendingInteractionApprovalSubject, - type PendingInteractionPayload, - type PendingInteractionResolution, - isApprovalPendingInteractionPayload, - isApprovalPendingInteractionResolution, -} from "@get-bb/plugin-sdk/provider-bridge"; +import { isApprovalPendingInteractionPayload, isApprovalPendingInteractionResolution } from "@bb/domain"; +import type { PendingInteractionApprovalDecision, PendingInteractionApprovalSubject, PendingInteractionPayload, PendingInteractionResolution } from "@bb/domain"; import { toolKindPresentation } from "./presentation.js"; import { type AcpToolCallOperation, diff --git a/plugins/provider-acp/src/presentation.ts b/packages/provider-bridge-acp/src/presentation.ts similarity index 99% rename from plugins/provider-acp/src/presentation.ts rename to packages/provider-bridge-acp/src/presentation.ts index 23f85b4d6e..f766118be5 100644 --- a/plugins/provider-acp/src/presentation.ts +++ b/packages/provider-bridge-acp/src/presentation.ts @@ -13,7 +13,7 @@ * Icons are host glyph names from the shared icon registry * (`@bb/shared-ui/icon`); the persisted form is glyph-only by design. */ -import type { DeltaPresentation } from "@get-bb/plugin-sdk/provider-bridge"; +import type { DeltaPresentation } from "@bb/provider-bridge-protocol"; import type { AcpToolKind } from "./wire.js"; /** Row headlines stay one line and short; the item carries the full text. */ diff --git a/plugins/provider-acp/src/profiles.ts b/packages/provider-bridge-acp/src/profiles.ts similarity index 90% rename from plugins/provider-acp/src/profiles.ts rename to packages/provider-bridge-acp/src/profiles.ts index 2b61a1e853..61f5e218fe 100644 --- a/plugins/provider-acp/src/profiles.ts +++ b/packages/provider-bridge-acp/src/profiles.ts @@ -1,7 +1,5 @@ -import { - normalizeHostDaemonAcpLaunchSpec, - type HostDaemonAcpLaunchSpec, -} from "@get-bb/plugin-sdk/provider-bridge"; +import { normalizeHostDaemonAcpLaunchSpec } from "@bb/host-daemon-contract"; +import type { HostDaemonAcpLaunchSpec } from "@bb/host-daemon-contract"; /** * CLI model surface of the agent's launch binary: how to discover models and diff --git a/plugins/provider-acp/src/session-params.test.ts b/packages/provider-bridge-acp/src/session-params.test.ts similarity index 100% rename from plugins/provider-acp/src/session-params.test.ts rename to packages/provider-bridge-acp/src/session-params.test.ts diff --git a/plugins/provider-acp/src/session-params.ts b/packages/provider-bridge-acp/src/session-params.ts similarity index 98% rename from plugins/provider-acp/src/session-params.ts rename to packages/provider-bridge-acp/src/session-params.ts index 59e6eaa319..57669812cf 100644 --- a/plugins/provider-acp/src/session-params.ts +++ b/packages/provider-bridge-acp/src/session-params.ts @@ -4,12 +4,7 @@ * model-list params out. */ -import { - type DynamicTool, - type PermissionMode, - type ReasoningLevel, - type ServiceTier, -} from "@get-bb/plugin-sdk/provider-bridge"; +import type { DynamicTool, PermissionMode, ReasoningLevel, ServiceTier } from "@bb/domain"; import path from "node:path"; import { ACP_DEFAULT_MODEL_ID } from "./bridge-protocol.js"; diff --git a/plugins/provider-acp/src/tool-call-operation.test.ts b/packages/provider-bridge-acp/src/tool-call-operation.test.ts similarity index 100% rename from plugins/provider-acp/src/tool-call-operation.test.ts rename to packages/provider-bridge-acp/src/tool-call-operation.test.ts diff --git a/plugins/provider-acp/src/tool-call-operation.ts b/packages/provider-bridge-acp/src/tool-call-operation.ts similarity index 98% rename from plugins/provider-acp/src/tool-call-operation.ts rename to packages/provider-bridge-acp/src/tool-call-operation.ts index 0457018d45..be83148908 100644 --- a/plugins/provider-acp/src/tool-call-operation.ts +++ b/packages/provider-bridge-acp/src/tool-call-operation.ts @@ -9,7 +9,7 @@ */ import path from "node:path"; -import { toOptionalString } from "@get-bb/plugin-sdk/provider-bridge"; +import { toOptionalString } from "@bb/provider-bridge-protocol/bridge-kit"; import { z } from "zod"; import type { AcpToolCallContent } from "./wire.js"; diff --git a/plugins/provider-acp/src/tool-classification.ts b/packages/provider-bridge-acp/src/tool-classification.ts similarity index 98% rename from plugins/provider-acp/src/tool-classification.ts rename to packages/provider-bridge-acp/src/tool-classification.ts index 7c868ce59e..6be81732cf 100644 --- a/plugins/provider-acp/src/tool-classification.ts +++ b/packages/provider-bridge-acp/src/tool-classification.ts @@ -20,13 +20,8 @@ * disagree (#1803). */ -import { - extractResultText, - toOptionalString, - type DeltaFileChange, - type DeltaItemShape, - type DeltaPresentation, -} from "@get-bb/plugin-sdk/provider-bridge"; +import type { DeltaFileChange, DeltaItemShape, DeltaPresentation } from "@bb/provider-bridge-protocol"; +import { extractResultText, toOptionalString } from "@bb/provider-bridge-protocol/bridge-kit"; import { z } from "zod"; import { bbToolPresentation, diff --git a/plugins/provider-acp/src/visibility.ts b/packages/provider-bridge-acp/src/visibility.ts similarity index 91% rename from plugins/provider-acp/src/visibility.ts rename to packages/provider-bridge-acp/src/visibility.ts index d6903766cc..1fedd5171e 100644 --- a/plugins/provider-acp/src/visibility.ts +++ b/packages/provider-bridge-acp/src/visibility.ts @@ -1,11 +1,5 @@ -import { - createProviderVisibilityMetadata, - getStringProperty, - isRecord, - type JsonRpcMessage, - type ProviderRawEventDescription, - type ProviderVisibilityMetadata, -} from "@get-bb/plugin-sdk/provider-bridge"; +import { createProviderVisibilityMetadata, getStringProperty, isRecord } from "@bb/provider-bridge-protocol/bridge-kit"; +import type { JsonRpcMessage, ProviderRawEventDescription, ProviderVisibilityMetadata } from "@bb/provider-bridge-protocol/bridge-kit"; import { ACP_FS_WRITE_METHOD, ACP_TURN_COMPLETED_METHOD, diff --git a/plugins/provider-acp/src/wire.test.ts b/packages/provider-bridge-acp/src/wire.test.ts similarity index 100% rename from plugins/provider-acp/src/wire.test.ts rename to packages/provider-bridge-acp/src/wire.test.ts diff --git a/plugins/provider-acp/src/wire.ts b/packages/provider-bridge-acp/src/wire.ts similarity index 100% rename from plugins/provider-acp/src/wire.ts rename to packages/provider-bridge-acp/src/wire.ts diff --git a/packages/provider-bridge-acp/tsconfig.json b/packages/provider-bridge-acp/tsconfig.json new file mode 100644 index 0000000000..945db56508 --- /dev/null +++ b/packages/provider-bridge-acp/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": [ + "@bb/tsconfig/base.json", + "@bb/tsconfig/typecheck-overrides.json" + ], + "compilerOptions": { + "rootDir": ".", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/provider-bridge-acp/vitest.config.ts b/packages/provider-bridge-acp/vitest.config.ts new file mode 100644 index 0000000000..eae740f21e --- /dev/null +++ b/packages/provider-bridge-acp/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineWorkspaceTestConfig } from "../../vitest.shared.js"; + +export default defineWorkspaceTestConfig({ + test: { + silent: "passed-only", + name: "@bb/provider-bridge-acp", + include: ["src/**/*.test.ts"], + exclude: ["dist/**", "node_modules/**"], + }, +}); diff --git a/packages/provider-bridge-protocol/src/testing/parity.ts b/packages/provider-bridge-protocol/src/testing/parity.ts index 643677b3ae..254edd2b42 100644 --- a/packages/provider-bridge-protocol/src/testing/parity.ts +++ b/packages/provider-bridge-protocol/src/testing/parity.ts @@ -75,7 +75,7 @@ export const FIRST_PARTY_BRIDGE_MODULES: Readonly< pluginId: "provider-claude-code", }, acp: { - modulePath: "plugins/provider-acp/src/bridge/bridge.ts", + modulePath: "plugins/provider-acp/src/host.ts", pluginId: "provider-acp", }, pi: { diff --git a/packages/scripts/test/provider-literal-ratchet.test.mjs b/packages/scripts/test/provider-literal-ratchet.test.mjs index 5efa908523..19a174c876 100644 --- a/packages/scripts/test/provider-literal-ratchet.test.mjs +++ b/packages/scripts/test/provider-literal-ratchet.test.mjs @@ -93,6 +93,19 @@ describe("scanTree (pure)", () => { expect(scanTree(dir).total).toBe(0); }); + // The published ACP bridge kit is a provider implementation that lives + // under packages/ so the plugin SDK can re-export it; it gets the same + // carve-out a provider plugin does. Its neighbours do not. + it("excludes the published provider bridge kit but not its neighbours", () => { + write( + "packages/provider-bridge-acp/src/dialect.ts", + 'const dialects = { "cursor-agent": "cursor" };\n', + ); + expect(scanTree(dir).total).toBe(0); + write("packages/provider-bridge-protocol/src/a.ts", 'const id = "cursor";\n'); + expect(scanTree(dir).total).toBe(1); + }); + it('providerLiteralRegex does not double-count `providerId === "codex"`', () => { const line = 'if (providerId === "codex") {}'; expect(line.match(providerLiteralRegex())).toHaveLength(1); diff --git a/plugins/provider-acp/README.md b/plugins/provider-acp/README.md index ae1f58c7cf..e7e5d997b0 100644 --- a/plugins/provider-acp/README.md +++ b/plugins/provider-acp/README.md @@ -1,12 +1,26 @@ # ACP providers -First-party plugin for ACP (Agent Client Protocol) agent providers. - -Today this plugin registers only the `acp-cursor` (Cursor) provider -declaration. The rest of BB's ACP surface — the known-agents list (opencode, -Grok, Hermes Agent, OMP, ...) and the `customAcpAgents` server config — stays -composed server-side transitionally. This plugin is destined to own the -Cursor profile, the known-agents list, and the `customAcpAgents` config -(which then finally gets a settings UI); until that migration lands, only the -Cursor declaration lives here and the server keeps composing the other ACP -providers into listings itself. +First-party plugin for ACP (Agent Client Protocol) agent providers: Cursor, +opencode, omp, Grok Build and Hermes Agent. + +The plugin has no bridge of its own. Every agent it registers runs on the +published ACP kit, `@get-bb/plugin-sdk/provider-bridge/acp`, which its +`bb.host` entry re-exports in one line (`src/host.ts`). That is the whole +point of the kit: a third-party plugin adds an ACP agent exactly the way this +one does, with no bb-side code, and `public-sdk-only.test.ts` proves this +plugin takes no shortcut — no file here may import a private `@bb/*` package. + +What lives here: + +- `server.ts` — the provider registrations: ids, display names, icons, + capabilities, and the bridge options each agent launches with + (`acpLaunchSpec`, and `acpDialect` for the agents whose vendor side + channels the kit reads). +- `src/host.ts` — the `bb.host` artifact: one re-export of the kit's bridge. +- `icons/` — the provider logos. + +The kit itself, including the ACP wire schema, the delta translation, the +per-agent dialects and the bridge process, is `packages/provider-bridge-acp`. + +Still composed server-side, transitionally: the `customAcpAgents` server +config for user-configured agents. diff --git a/plugins/provider-acp/package.json b/plugins/provider-acp/package.json index 2966376bf2..e265436a9d 100644 --- a/plugins/provider-acp/package.json +++ b/plugins/provider-acp/package.json @@ -14,7 +14,7 @@ "icon": "./icons/acp.svg" }, "server": "./server.ts", - "host": "./src/bridge/bridge.ts" + "host": "./src/host.ts" }, "keywords": [ "bb-plugin" @@ -24,10 +24,6 @@ "typecheck": "tsc --noEmit" }, "devDependencies": { - "@bb/agent-runtime": "workspace:*", - "@bb/domain": "workspace:*", - "@bb/host-daemon-contract": "workspace:*", - "@bb/provider-bridge-protocol": "workspace:*", "@modelcontextprotocol/sdk": "^1.29.0", "@types/node": "^22.0.0", "typescript": "npm:@typescript/typescript6@^6.0.2", diff --git a/plugins/provider-acp/public-sdk-only.test.ts b/plugins/provider-acp/public-sdk-only.test.ts new file mode 100644 index 0000000000..7e54b6299c --- /dev/null +++ b/plugins/provider-acp/public-sdk-only.test.ts @@ -0,0 +1,111 @@ +/** + * The first-party ACP plugin has no privilege: it reaches every capability + * through the public SDK alone, exactly as a third-party ACP plugin (Amp) + * would. No file in this package may import a private `@bb/*` workspace + * package — not the plugin code, not the tests. Plugin code may import only + * `@get-bb/plugin-sdk` (and its published subpaths), `zod`, node built-ins, + * and its own files; tests may add the published testing kit and the test + * runner. + * + * A `@bb/*` import would still typecheck and run inside this monorepo, which + * is exactly why it needs a test: the workspace hides the privilege. This is + * the same guard the echo-provider canary carries (#2189). + */ +import { readdirSync, readFileSync } from "node:fs"; +import { dirname, join, relative } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const packageRoot = dirname(fileURLToPath(import.meta.url)); + +const SKIPPED_DIRECTORIES = new Set(["node_modules", "dist"]); +const SOURCE_EXTENSIONS = /\.(?:[cm]?[jt]s|tsx)$/u; + +/** Specifiers plugin code (server, host) may import. */ +const PLUGIN_IMPORT_ALLOWLIST = [ + /^@get-bb\/plugin-sdk$/u, + /^@get-bb\/plugin-sdk\/(?:provider-bridge|host|app)$/u, + /^@get-bb\/plugin-sdk\/provider-bridge\/acp$/u, + /^zod$/u, + /^node:/u, + /^\.\.?\//u, +]; + +/** What a test file may import beyond the plugin allowlist. */ +const TEST_IMPORT_ALLOWLIST = [ + /^@get-bb\/plugin-sdk\/provider-bridge\/testing$/u, + /^vitest$/u, +]; + +const IMPORT_SPECIFIER_PATTERN = + /(?:\bfrom\s*|\bimport\s*\(?\s*|\brequire\s*\(\s*)["']([^"']+)["']/gu; + +function listSourceFiles(directory: string): string[] { + const files: string[] = []; + for (const entry of readdirSync(directory, { withFileTypes: true })) { + if (entry.isDirectory()) { + if (!SKIPPED_DIRECTORIES.has(entry.name)) { + files.push(...listSourceFiles(join(directory, entry.name))); + } + continue; + } + if (SOURCE_EXTENSIONS.test(entry.name)) { + files.push(join(directory, entry.name)); + } + } + return files; +} + +function importSpecifiers(source: string): string[] { + return [...source.matchAll(IMPORT_SPECIFIER_PATTERN)].map( + (match) => match[1] ?? "", + ); +} + +function isTestFile(path: string): boolean { + return /\.test\.[cm]?[jt]sx?$/u.test(path); +} + +describe("provider-acp imports only the public SDK", () => { + const files = listSourceFiles(packageRoot); + + it("scans the plugin's source files", () => { + const names = files.map((file) => relative(packageRoot, file)); + expect(names).toContain("server.ts"); + expect(names).toContain(join("src", "host.ts")); + }); + + for (const file of files) { + const name = relative(packageRoot, file); + it(`${name} has no @bb/* import and stays inside the allowlist`, () => { + const source = readFileSync(file, "utf8"); + const specifiers = importSpecifiers(source); + const privateImports = specifiers.filter((specifier) => + specifier.startsWith("@bb/"), + ); + expect(privateImports, `${name} imports private packages`).toEqual([]); + + const allowlist = isTestFile(file) + ? [...PLUGIN_IMPORT_ALLOWLIST, ...TEST_IMPORT_ALLOWLIST] + : PLUGIN_IMPORT_ALLOWLIST; + const disallowed = specifiers.filter( + (specifier) => !allowlist.some((pattern) => pattern.test(specifier)), + ); + expect(disallowed, `${name} imports outside the allowlist`).toEqual([]); + }); + } + + it("declares no @bb/* dependency in package.json", () => { + const manifest = JSON.parse( + readFileSync(join(packageRoot, "package.json"), "utf8"), + ) as { + dependencies?: Record; + devDependencies?: Record; + }; + const declared = [ + ...Object.keys(manifest.dependencies ?? {}), + ...Object.keys(manifest.devDependencies ?? {}), + ]; + expect(declared.filter((name) => name.startsWith("@bb/"))).toEqual([]); + }); +}); diff --git a/plugins/provider-acp/server.ts b/plugins/provider-acp/server.ts index 4eebbc5f50..35c535d862 100644 --- a/plugins/provider-acp/server.ts +++ b/plugins/provider-acp/server.ts @@ -69,7 +69,7 @@ const ACP_PROVIDERS: readonly PluginProviderDeclaration[] = [ experimental_serviceTiers: [...ACP_SERVICE_TIERS], experimental_bridgeOptions: { // Which vendor side channels the bridge reads for this agent - // (plugins/provider-acp/src/dialect.ts). Declared per registration so + // (packages/provider-bridge-acp/src/dialect.ts). Declared per registration so // a third-party plugin that registers a known agent gets the same // reporting fidelity a first-party registration does. acpDialect: "cursor", diff --git a/plugins/provider-acp/src/host.ts b/plugins/provider-acp/src/host.ts new file mode 100644 index 0000000000..5b9573cc38 --- /dev/null +++ b/plugins/provider-acp/src/host.ts @@ -0,0 +1,9 @@ +/** + * The plugin's `bb.host` artifact. + * + * Every ACP agent bb ships runs on the published ACP kit — the same module a + * third-party plugin uses — so this plugin's host side is one re-export. The + * daemon's bridge bootstrap imports the artifact and looks for the named + * `experimental_providerBridge` export. + */ +export { experimental_acpProviderBridge as experimental_providerBridge } from "@get-bb/plugin-sdk/provider-bridge/acp"; diff --git a/plugins/provider-acp/src/bridge/issue-1688-cursor-list-models.txt b/plugins/provider-acp/src/issue-1688-cursor-list-models.txt similarity index 100% rename from plugins/provider-acp/src/bridge/issue-1688-cursor-list-models.txt rename to plugins/provider-acp/src/issue-1688-cursor-list-models.txt diff --git a/plugins/provider-acp/src/bridge/issue-1688-grok-4.6-primary.test.ts b/plugins/provider-acp/src/issue-1688-grok-4.6-primary.test.ts similarity index 86% rename from plugins/provider-acp/src/bridge/issue-1688-grok-4.6-primary.test.ts rename to plugins/provider-acp/src/issue-1688-grok-4.6-primary.test.ts index a945e248b6..0bff528605 100644 --- a/plugins/provider-acp/src/bridge/issue-1688-grok-4.6-primary.test.ts +++ b/plugins/provider-acp/src/issue-1688-grok-4.6-primary.test.ts @@ -11,12 +11,12 @@ */ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; -import { CURSOR_PRIMARY_MODELS } from "../../server.js"; import { - buildAgentModelCatalog, - parseAgentModelLines, - splitPrimaryModels, -} from "./model-catalog.js"; + experimental_buildAcpAgentModelCatalog as buildAgentModelCatalog, + experimental_parseAcpAgentModelLines as parseAgentModelLines, + experimental_splitAcpPrimaryModels as splitPrimaryModels, +} from "@get-bb/plugin-sdk/provider-bridge/acp"; +import { CURSOR_PRIMARY_MODELS } from "../server.js"; // Captured with `cursor-agent --list-models` (2026.08.11-e8db854). const CURSOR_LIST_MODELS = readFileSync( diff --git a/plugins/provider-acp/tsconfig.json b/plugins/provider-acp/tsconfig.json index 6acac8aade..e76b240e49 100644 --- a/plugins/provider-acp/tsconfig.json +++ b/plugins/provider-acp/tsconfig.json @@ -4,7 +4,10 @@ "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", - "lib": ["ES2022", "DOM"], + "lib": [ + "ES2022", + "DOM" + ], "noEmit": true, "skipLibCheck": true, "paths": { @@ -13,9 +16,19 @@ ], "@get-bb/plugin-sdk/app": [ "../../packages/plugin-sdk/bundled-types/bb-plugin-sdk-app.d.ts" + ], + "@get-bb/plugin-sdk/provider-bridge/acp": [ + "../../packages/plugin-sdk/bundled-types/bb-plugin-sdk-provider-bridge-acp.d.ts" ] }, - "types": ["node"] + "types": [ + "node" + ] }, - "include": ["server.ts", "src", "vitest.config.ts"] + "include": [ + "server.ts", + "src", + "public-sdk-only.test.ts", + "vitest.config.ts" + ] } diff --git a/plugins/provider-acp/vitest.config.ts b/plugins/provider-acp/vitest.config.ts index 59659ca205..142f591353 100644 --- a/plugins/provider-acp/vitest.config.ts +++ b/plugins/provider-acp/vitest.config.ts @@ -4,7 +4,7 @@ export default defineWorkspaceTestConfig({ test: { silent: "passed-only", name: "bb-plugin-provider-acp", - include: ["src/**/*.test.ts"], + include: ["*.test.ts", "src/**/*.test.ts"], exclude: ["node_modules/**", "dist/**"], }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01ce3f67e0..6adf7f9aee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2179,6 +2179,9 @@ importers: '@bb/process-utils': specifier: workspace:* version: link:../process-utils + '@bb/provider-bridge-acp': + specifier: workspace:* + version: link:../provider-bridge-acp '@bb/provider-bridge-protocol': specifier: workspace:* version: link:../provider-bridge-protocol @@ -2268,6 +2271,43 @@ importers: specifier: ^4.1.1 version: 4.1.1(@opentelemetry/api@1.9.1)(@types/node@22.19.10)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.14(@types/node@22.19.10)(@typescript/typescript6@6.0.2))(vite@8.0.12(@types/node@22.19.10)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.1)(yaml@2.9.0)) + packages/provider-bridge-acp: + dependencies: + '@bb/domain': + specifier: workspace:* + version: link:../domain + '@bb/host-daemon-contract': + specifier: workspace:* + version: link:../host-daemon-contract + '@bb/process-utils': + specifier: workspace:* + version: link:../process-utils + '@bb/provider-bridge-protocol': + specifier: workspace:* + version: link:../provider-bridge-protocol + zod: + specifier: 4.3.6 + version: 4.3.6 + devDependencies: + '@bb/tsconfig': + specifier: workspace:* + version: link:../tsconfig + '@modelcontextprotocol/sdk': + specifier: ^1.29.0 + version: 1.29.0(zod@4.3.6) + '@types/node': + specifier: ^22.0.0 + version: 22.19.10 + typescript: + specifier: npm:@typescript/typescript6@^6.0.2 + version: '@typescript/typescript6@6.0.2' + typescript-7: + specifier: npm:typescript@^7.0.2 + version: typescript@7.0.2 + vitest: + specifier: ^4.1.1 + version: 4.1.1(@opentelemetry/api@1.9.1)(@types/node@22.19.10)(jsdom@29.0.1(@noble/hashes@2.0.1))(msw@2.12.14(@types/node@22.19.10)(@typescript/typescript6@6.0.2))(vite@8.0.12(@types/node@22.19.10)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.50.0)(tsx@4.23.1)(yaml@2.9.0)) + packages/provider-bridge-protocol: dependencies: '@bb/domain': @@ -3292,18 +3332,6 @@ importers: specifier: 4.3.6 version: 4.3.6 devDependencies: - '@bb/agent-runtime': - specifier: workspace:* - version: link:../../packages/agent-runtime - '@bb/domain': - specifier: workspace:* - version: link:../../packages/domain - '@bb/host-daemon-contract': - specifier: workspace:* - version: link:../../packages/host-daemon-contract - '@bb/provider-bridge-protocol': - specifier: workspace:* - version: link:../../packages/provider-bridge-protocol '@modelcontextprotocol/sdk': specifier: ^1.29.0 version: 1.29.0(zod@4.3.6) @@ -19273,9 +19301,7 @@ snapshots: metro-runtime: 0.84.5 transitivePeerDependencies: - '@babel/core' - - bufferutil - supports-color - - utf-8-validate '@react-native/normalize-colors@0.79.6': {} diff --git a/scripts/check-provider-literal-ratchet.mjs b/scripts/check-provider-literal-ratchet.mjs index a8bf45f207..ce1318ef88 100755 --- a/scripts/check-provider-literal-ratchet.mjs +++ b/scripts/check-provider-literal-ratchet.mjs @@ -54,7 +54,19 @@ const EXCLUDED_SEGMENTS = new Set([ "stories", "dev", // dev-only fixture screens (apps/mobile/src/screens/dev) ]); -const EXCLUDED_PREFIXES = [join("plugins", "provider-"), join("examples", "")]; +/** + * Provider implementations are allowed to name their own provider; the + * ratchet exists to keep provider ids out of CORE. `plugins/provider-*` is + * one such implementation, and so is the published ACP bridge kit — the same + * code, moved into `packages/` so the plugin SDK can re-export it + * (`@get-bb/plugin-sdk/provider-bridge/acp`). It carries no bb provider id + * today: it selects behavior by the agent's dialect, never by a provider id. + */ +const EXCLUDED_PREFIXES = [ + join("plugins", "provider-"), + join("packages", "provider-bridge-acp"), + join("examples", ""), +]; const EXCLUDED_FILE_RE = /\.(test|spec|stories)\.[cm]?[jt]sx?$|\.snap$|\.d\.ts$/; const INCLUDED_FILE_RE = /\.[cm]?[jt]sx?$/; // ts tsx js jsx mjs cjs mts cts