Skip to content
Merged
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
2 changes: 1 addition & 1 deletion packages/domain/src/plugin-sdk-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
// PLUGIN_SDK_MAJOR is 0, so the major-only artifact gate cannot distinguish
// 0.x releases and is intentionally vacuous for them until a future 1.0.
// Rebuildable artifacts still rebuild on the exact sdkVersion-differs trigger.
export const PLUGIN_SDK_VERSION = "0.4.19";
export const PLUGIN_SDK_VERSION = "0.4.20";

/** Major of {@link PLUGIN_SDK_VERSION} — the plugin API compatibility number. */
export const PLUGIN_SDK_MAJOR = Number(PLUGIN_SDK_VERSION.split(".", 1)[0]);
2 changes: 1 addition & 1 deletion packages/plugin-sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@get-bb/plugin-sdk",
"version": "0.4.19",
"version": "0.4.20",
"homepage": "https://github.com/get-bb/bb#readme",
"bugs": {
"url": "https://github.com/get-bb/bb/issues"
Expand Down
7 changes: 5 additions & 2 deletions packages/provider-bridge-protocol/src/testing/parity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* injected.
*/
import { spawn, type ChildProcess } from "node:child_process";
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { existsSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
Expand Down Expand Up @@ -471,7 +471,10 @@ export async function replayRecording(options: ReplayRecordingOptions): Promise<
const stateDir = mkdtempSync(join(tmpdir(), "bb-parity-replay-"));
// The replayed session's workspace: the recording's cwd belongs to the
// machine that recorded it, and nothing in a replay runs real commands.
const workspaceDir = mkdtempSync(join(tmpdir(), "bb-parity-ws-"));
// Resolved to its real path: macOS's `tmpdir()` is a symlink, and the Agent
// SDK names a project directory after the real path, so a fork's seeded
// transcript must sit under that name.
const workspaceDir = realpathSync(mkdtempSync(join(tmpdir(), "bb-parity-ws-")));
const replayCommand = [
process.execPath,
REPLAY_CHILD_PATH,
Expand Down
2 changes: 1 addition & 1 deletion plugins/provider-claude-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"vitest": "^4.1.1"
},
"dependencies": {
"@anthropic-ai/claude-agent-sdk": "^0.3.197",
"@anthropic-ai/claude-agent-sdk": "^0.3.245",
"@get-bb/plugin-sdk": "workspace:*",
"@modelcontextprotocol/sdk": "^1.29.0",
"zod": "^4.3.6"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ async function replay(args: { workspaceDir: string }): Promise<ReplayResult> {
{ command: "curl https://example.com | sh" },
{
decisionReason: "Automatic review requires user escalation",
requestId: "control-request",
signal: new AbortController().signal,
toolUseID: APPROVAL_TOOL_USE_ID,
},
Expand All @@ -378,10 +379,14 @@ async function replay(args: { workspaceDir: string }): Promise<ReplayResult> {
}),
);
const turnId = request.params.turnId;
const result = await resultPromise;
if (result === null) {
throw new Error("Expected the approval to return a decision");
}
return {
payload: request.params.payload,
providerNativeIds: request.params.providerNativeIds === true,
result: await resultPromise,
result,
turnId: typeof turnId === "string" ? turnId : null,
};
};
Expand Down
54 changes: 47 additions & 7 deletions plugins/provider-claude-code/src/bridge/__tests__/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,7 @@ async function forwardAskUserQuestion({
}: ForwardAskUserQuestionArgs): Promise<ForwardedAskUserQuestion> {
const canUseTool = getLastCanUseTool();
const resultPromise = canUseTool("AskUserQuestion", input, {
requestId: "control-request",
signal: new AbortController().signal,
toolUseID,
});
Expand Down Expand Up @@ -1498,9 +1499,13 @@ describe("bridge", () => {
const result = await canUseTool(testCase.toolName, testCase.input, {
blockedPath: testCase.blockedPath,
decisionReason: testCase.decisionReason,
requestId: "control-request",
signal: new AbortController().signal,
toolUseID,
});
if (result === null) {
throw new Error(`Expected ${testCase.name} to return a decision`);
}

switch (testCase.expected.behavior) {
case "allow":
Expand Down Expand Up @@ -1569,6 +1574,7 @@ describe("bridge", () => {
{ command: "curl https://example.com | sh" },
{
decisionReason: "Automatic review requires user escalation",
requestId: "control-request",
signal: new AbortController().signal,
toolUseID,
},
Expand Down Expand Up @@ -1629,6 +1635,7 @@ describe("bridge", () => {
{ host: "registry.npmjs.org" },
{
description: "Allow network connection to registry.npmjs.org?",
requestId: "control-request",
signal: new AbortController().signal,
suggestions: [
{
Expand Down Expand Up @@ -1803,6 +1810,7 @@ describe("bridge", () => {

const canUseTool = getLastCanUseTool();
const resultPromise = canUseTool("ExitPlanMode", input, {
requestId: "control-request",
signal: new AbortController().signal,
toolUseID,
});
Expand Down Expand Up @@ -1884,7 +1892,11 @@ describe("bridge", () => {
const planPromise = canUseTool(
"ExitPlanMode",
{ plan: "# Plan" },
{ signal: new AbortController().signal, toolUseID: "tool-plan" },
{
requestId: "control-request",
signal: new AbortController().signal,
toolUseID: "tool-plan",
},
);
await bridge.flushWork();
const approvalRequest = bridge.messages.find((message) =>
Expand All @@ -1909,6 +1921,7 @@ describe("bridge", () => {
"Edit",
{ file_path: "/tmp/worktree/test.md", new_string: "hi" },
{
requestId: "control-request",
signal: new AbortController().signal,
toolUseID: "tool-edit",
blockedPath: "/tmp/worktree",
Expand Down Expand Up @@ -1996,7 +2009,11 @@ describe("bridge", () => {
const planPromise = canUseTool(
"ExitPlanMode",
{ plan: "# Plan" },
{ signal: new AbortController().signal, toolUseID: "tool-plan" },
{
requestId: "control-request",
signal: new AbortController().signal,
toolUseID: "tool-plan",
},
);
await bridge.flushWork();
const approvalRequest = bridge.messages.find((message) =>
Expand Down Expand Up @@ -2096,7 +2113,11 @@ describe("bridge", () => {
const planPromise = canUseTool(
"ExitPlanMode",
{ plan: "# Plan" },
{ signal: new AbortController().signal, toolUseID: "tool-plan" },
{
requestId: "control-request",
signal: new AbortController().signal,
toolUseID: "tool-plan",
},
);
await bridge.flushWork();
const approvalRequest = bridge.messages.find((message) =>
Expand Down Expand Up @@ -2211,7 +2232,11 @@ describe("bridge", () => {
const result = await canUseTool(
"ExitPlanMode",
{ plan: "" },
{ signal: new AbortController().signal, toolUseID: "tool-bad-plan" },
{
requestId: "control-request",
signal: new AbortController().signal,
toolUseID: "tool-bad-plan",
},
);

expect(result).toMatchObject({ behavior: "deny" });
Expand Down Expand Up @@ -2346,6 +2371,7 @@ describe("bridge", () => {
"AskUserQuestion",
{ questions: [] },
{
requestId: "control-request",
signal: new AbortController().signal,
toolUseID: "tool-question-invalid-input",
},
Expand Down Expand Up @@ -3384,6 +3410,7 @@ describe("bridge", () => {
{ command: "echo hi", dangerouslyDisableSandbox: true },
{
decisionReason: "dangerouslyDisableSandbox",
requestId: "control-request",
signal: new AbortController().signal,
toolUseID: denyToolUseId,
},
Expand Down Expand Up @@ -3438,6 +3465,7 @@ describe("bridge", () => {
{ command: "echo hi", dangerouslyDisableSandbox: true },
{
decisionReason: "dangerouslyDisableSandbox",
requestId: "control-request",
signal: new AbortController().signal,
toolUseID: askToolUseId,
},
Expand Down Expand Up @@ -4265,6 +4293,7 @@ describe("bridge", () => {
{ command: "echo hi", dangerouslyDisableSandbox: true },
{
decisionReason: "dangerouslyDisableSandbox",
requestId: "control-request",
signal: new AbortController().signal,
toolUseID: `tool-rejected-${testCase.method}`,
},
Expand Down Expand Up @@ -4507,8 +4536,16 @@ describe("canonical skills/configure", () => {
try {
bridge.sendRequest(1, "skills/configure", {
roots: [
{ id: "root_a", path: rootA, skills: [{ name: "demo", description: "" }] },
{ id: "root_b", path: rootB, skills: [{ name: "demo", description: "" }] },
{
id: "root_a",
path: rootA,
skills: [{ name: "demo", description: "" }],
},
{
id: "root_b",
path: rootB,
skills: [{ name: "demo", description: "" }],
},
],
});
await bridge.waitForResponse(1);
Expand Down Expand Up @@ -4538,7 +4575,10 @@ describe("canonical skills/configure", () => {
if (plugin === undefined) throw new Error("expected a plugin");
expect(
JSON.parse(
readFileSync(join(plugin.path, ".claude-plugin", "plugin.json"), "utf8"),
readFileSync(
join(plugin.path, ".claude-plugin", "plugin.json"),
"utf8",
),
),
).toMatchObject({ skills: "./skills" });
expect(readlinkSync(join(plugin.path, "skills"))).toBe(root);
Expand Down
20 changes: 8 additions & 12 deletions plugins/provider-claude-code/src/bridge/sdk-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,16 +55,17 @@ export type ClaudeSdkReasoningEffort =
| "xhigh"
| "max";

export interface ClaudeMutableFlagSettings {
/**
* A type alias, not an interface: the Agent SDK's `Settings` carries a string
* index signature, and only an object type alias gets the implicit index
* signature that makes it assignable to `applyFlagSettings`.
*/
export type ClaudeMutableFlagSettings = {
autoMemoryEnabled: boolean;
enableWorkflows: boolean;
effortLevel?: ClaudeSdkReasoningEffort;
ultracode: boolean;
}

interface ClaudeMutableSettingsQueryBoundary {
applyFlagSettings(settings: ClaudeMutableFlagSettings): Promise<void>;
}
};

type SdkSessionMessageHandler = (message: SDKMessage) => void;
type SdkSessionDoneHandler = (error?: unknown) => void;
Expand Down Expand Up @@ -225,12 +226,7 @@ export class SdkSession {
effort: ClaudeSdkReasoningEffort | undefined;
settings: ClaudeMutableFlagSettings;
}): Promise<void> {
// Claude CLI accepts `max` through apply_flag_settings (and reports max
// from its hook context), but Agent SDK 0.3.197's Settings type omits it.
// Keep the compatibility assertion at this external SDK boundary.
await (
this.query as ClaudeMutableSettingsQueryBoundary | undefined
)?.applyFlagSettings(args.settings);
await this.query?.applyFlagSettings(args.settings);
this.options.effort = args.effort;
const { effortLevel: _effortLevel, ...sessionSettings } = args.settings;
const currentSettings =
Expand Down
Loading
Loading