Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ message agents, or inspect projects, providers, and environments.
- If provider or model choice matters, inspect options with `bb provider list`
and `bb provider models <provider-id>`.
- Known ACP agents can appear automatically when their CLI is installed on the
host; for example `opencode` on PATH appears as provider `acp-opencode`.
host; `opencode`, `copilot`, and `qwen` on PATH appear as `acp-opencode`,
`acp-github-copilot`, and `acp-qwen-code`.
- Custom ACP agents can be registered in the app data-dir `config.json` under
`customAcpAgents`. The user supplies a slug `id`; bb exposes it as provider
id `acp-<id>`. Custom config wins if it uses the same provider id as a known
Expand Down
16 changes: 16 additions & 0 deletions apps/server/src/services/system/execution-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,14 @@ function buildCustomAcpProviderInfo(agent: CustomAcpAgent): ProviderInfo {
});
}

function buildProviderUnavailableError(providerId: string): ApiError {
return new ApiError(
400,
"invalid_request",
`Provider "${providerId}" is not available.`,
);
}

function listConfiguredSystemProviderInfos(
customAcpAgents: CustomAcpAgent[],
installedKnownAcpAgents: readonly KnownAcpAgent[],
Expand Down Expand Up @@ -322,6 +330,14 @@ export async function resolveSystemExecutionOptions(
const requestedProvider = query.providerId
? providers.find((provider) => provider.id === query.providerId)
: undefined;
if (
query.providerId !== undefined &&
requestedProvider === undefined &&
configuredRequestedProvider === undefined
) {
await earlyModelResultPromise?.catch(() => undefined);
throw buildProviderUnavailableError(query.providerId);
}
const modelsProvider =
earlyModelResultPromise !== null
? configuredRequestedProvider
Expand Down
16 changes: 16 additions & 0 deletions apps/server/src/services/system/known-acp-agents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ export const KNOWN_ACP_AGENTS: readonly KnownAcpAgent[] = [
env: {},
executableName: "opencode",
},
{
id: "acp-github-copilot",
displayName: "GitHub Copilot",
command: "copilot",
args: ["--acp", "--stdio"],
env: {},
executableName: "copilot",
},
{
id: "acp-qwen-code",
displayName: "Qwen Code",
command: "qwen",
args: ["--acp"],
env: {},
executableName: "qwen",
},
];

export function listKnownAcpAgentExecutableQueries(): KnownAcpAgentExecutableQuery[] {
Expand Down
29 changes: 28 additions & 1 deletion apps/server/src/services/threads/project-execution-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {
getProjectExecutionDefaults,
upsertProjectExecutionDefaults,
} from "@bb/db";
import { isAcpProviderId, isAgentProviderId } from "@bb/agent-providers";
import { formatCustomAcpAgentProviderId } from "@bb/config/bb-app-managed-config";
import type {
ProjectExecutionDefaults,
ResolvedThreadExecutionOptions,
Expand All @@ -13,6 +15,7 @@ import type {
ThreadCreateServiceRequestInput,
} from "./thread-create-request.js";
import { resolveCreateThreadExecutionDefaults } from "./thread-default-policy.js";
import { findKnownAcpAgentForProviderId } from "../system/known-acp-agents.js";

export interface RememberProjectExecutionDefaultsForCreateArgs {
execution: ResolvedThreadExecutionOptions;
Expand Down Expand Up @@ -72,8 +75,24 @@ function resolveRequestedCreateExecutionValue<TValue>({
return sources[field] === undefined ? undefined : value;
}

function isSupportedCreateProviderId(
deps: Pick<AppDeps, "config">,
providerId: string,
): boolean {
if (!isAcpProviderId(providerId)) {
return true;
}
return (
isAgentProviderId(providerId) ||
findKnownAcpAgentForProviderId(providerId) !== undefined ||
deps.config.customAcpAgents.some(
(agent) => formatCustomAcpAgentProviderId(agent.id) === providerId,
)
);
}

export function resolveProjectExecutionDefaultsForCreate(
deps: Pick<AppDeps, "db">,
deps: Pick<AppDeps, "config" | "db">,
args: ResolveProjectExecutionDefaultsForCreateArgs,
): ResolvedProjectExecutionDefaultsForCreate {
const storedDefaults = getProjectExecutionDefaults(deps.db, {
Expand All @@ -95,6 +114,14 @@ export function resolveProjectExecutionDefaultsForCreate(
});
const { executionDefaults, providerId } = resolution;

if (!isSupportedCreateProviderId(deps, providerId)) {
throw new ApiError(
400,
"invalid_request",
`Provider "${providerId}" is not supported.`,
);
}

if (!requestedModel && !executionDefaults) {
throw new ApiError(
400,
Expand Down
6 changes: 5 additions & 1 deletion apps/server/test/public/public-thread-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2555,7 +2555,11 @@ describe("public thread data routes", () => {
).toEqual([
{
type: "known_acp_agents.status",
agents: [{ id: "acp-opencode", executableName: "opencode" }],
agents: [
{ id: "acp-opencode", executableName: "opencode" },
{ id: "acp-github-copilot", executableName: "copilot" },
{ id: "acp-qwen-code", executableName: "qwen" },
],
},
{ type: "provider.list_models", providerId: "codex" },
]);
Expand Down
42 changes: 42 additions & 0 deletions apps/server/test/services/threads/thread-execution-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,46 @@ describe("thread execution plan input sources", () => {
});
});
});

it("allows non-ACP provider ids for runtime-registered providers", async () => {
await withTestHarness(async (harness) => {
const { host } = seedHostSession(harness.deps, {
id: "host-source-aware-fake-provider",
});
const { project } = seedProjectWithSource(harness.deps, {
hostId: host.id,
});

const resolution = resolveProjectExecutionDefaultsForCreate(
harness.deps,
{
model: "fake-model",
projectId: project.id,
providerId: "fake",
},
);

expect(resolution.providerId).toBe("fake");
expect(resolution.executionDefaults).toBeNull();
});
});

it("rejects ACP providers that are not built in, custom, or known", async () => {
await withTestHarness(async (harness) => {
const { host } = seedHostSession(harness.deps, {
id: "host-source-aware-unsupported-provider",
});
const { project } = seedProjectWithSource(harness.deps, {
hostId: host.id,
});

expect(() =>
resolveProjectExecutionDefaultsForCreate(harness.deps, {
model: "acp-default",
projectId: project.id,
providerId: "acp-gemini",
}),
).toThrow('Provider "acp-gemini" is not supported.');
});
});
});
Loading
Loading