Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/agent-runtime/src/acp/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1318,6 +1318,7 @@ export function createAcpProviderAdapter(
id: providerInfo.id,
displayName: providerInfo.displayName,
capabilities: providerInfo.capabilities,
appliesPermissionEscalationPerTurn: false,
process: {
command: opts.bridgeNodeExecutablePath ?? "node",
args: resolveBridgeProcessArgs({
Expand Down
3 changes: 3 additions & 0 deletions packages/agent-runtime/src/claude-code/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,7 @@ export function createClaudeCodeProviderAdapter(
id: providerInfo.id,
displayName: providerInfo.displayName,
capabilities,
appliesPermissionEscalationPerTurn: true,
process: {
command: opts?.bridgeNodeExecutablePath ?? "node",
args: resolveBridgeProcessArgs({
Expand Down Expand Up @@ -1214,6 +1215,7 @@ export function createClaudeCodeProviderAdapter(
...(command.options?.model
? { model: command.options.model }
: {}),
permissionEscalation: command.options.permissionEscalation,
},
};
case "turn/steer":
Expand All @@ -1235,6 +1237,7 @@ export function createClaudeCodeProviderAdapter(
),
}
: {}),
permissionEscalation: command.options.permissionEscalation,
},
};
case "thread/fork": {
Expand Down
113 changes: 90 additions & 23 deletions packages/agent-runtime/src/claude-code/bridge/__tests__/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ interface StartBridgeThreadArgs {

interface ResumeBridgeThreadArgs {
bridge: BridgeJsonRpcTestHarness;
permissionEscalation?: "ask" | "deny";
providerThreadId: string | null;
requestId: number;
threadId: string;
Expand Down Expand Up @@ -460,7 +461,7 @@ function sendResumeThread(args: ResumeBridgeThreadArgs): void {
baseInstructions: "test",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "ask",
permissionEscalation: args.permissionEscalation ?? "ask",
permissionMode: "default",
permissionScope: "workspace",
providerThreadId: args.providerThreadId,
Expand Down Expand Up @@ -555,7 +556,7 @@ describe("bridge", () => {
cwd: "/tmp/worktree",
disallowedTools: ["ExitPlanMode", "NotebookEdit", "Task"],
instructionMode: "replace",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
},
Expand All @@ -580,7 +581,7 @@ describe("bridge", () => {
instructionMode: "append",
reasoningLevel: "ultracode",
workflowsEnabled: true,
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
},
Expand All @@ -603,7 +604,7 @@ describe("bridge", () => {
instructionMode: "append",
reasoningLevel: "high",
workflowsEnabled: true,
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
},
Expand All @@ -625,7 +626,7 @@ describe("bridge", () => {
cwd: "/tmp/worktree",
instructionMode: "append",
reasoningLevel: "xhigh",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
},
Expand All @@ -642,7 +643,7 @@ describe("bridge", () => {
memoryEnabled: false,
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
},
Expand All @@ -660,7 +661,7 @@ describe("bridge", () => {
cwd: "/tmp/worktree",
instructionMode: "append",
reasoningLevel: "xhigh",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
},
Expand Down Expand Up @@ -688,7 +689,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
plugins: [{ type: "local", path: "/tmp/bb-skills" }],
Expand All @@ -709,7 +710,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "deny",
getPermissionEscalation: () => "deny",
permissionMode: "dontAsk",
permissionScope: "workspace",
},
Expand All @@ -727,7 +728,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
},
Expand All @@ -752,7 +753,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
},
Expand All @@ -770,7 +771,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
},
Expand All @@ -791,7 +792,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
},
Expand All @@ -816,7 +817,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
},
Expand All @@ -835,7 +836,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "acceptEdits",
permissionScope: "workspace",
},
Expand All @@ -847,7 +848,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "deny",
getPermissionEscalation: () => "deny",
permissionMode: "auto",
permissionScope: "workspace",
},
Expand All @@ -867,7 +868,7 @@ describe("bridge", () => {
enabled: true,
failIfUnavailable: false,
autoAllowBashIfSandboxed: true,
allowUnsandboxedCommands: false,
allowUnsandboxedCommands: true,
network: { allowLocalBinding: true },
});
});
Expand All @@ -880,7 +881,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "plan",
permissionScope: "workspace",
},
Expand All @@ -903,7 +904,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "deny",
getPermissionEscalation: () => "deny",
permissionMode: "auto",
permissionScope: "workspace",
},
Expand All @@ -918,7 +919,7 @@ describe("bridge", () => {
enabled: true,
failIfUnavailable: false,
autoAllowBashIfSandboxed: true,
allowUnsandboxedCommands: false,
allowUnsandboxedCommands: true,
network: { allowLocalBinding: true },
filesystem: {
allowWrite: ["/repo/.git/worktrees/bb13", "/repo/.git/objects"],
Expand All @@ -933,7 +934,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "ask",
getPermissionEscalation: () => "ask",
permissionMode: "default",
permissionScope: "workspace",
},
Expand All @@ -945,7 +946,7 @@ describe("bridge", () => {
baseInstructions: "You are a coder.",
cwd: "/tmp/worktree",
instructionMode: "append",
permissionEscalation: "deny",
getPermissionEscalation: () => "deny",
permissionMode: "dontAsk",
permissionScope: "workspace",
},
Expand Down Expand Up @@ -1248,6 +1249,23 @@ describe("bridge", () => {
messageIncludes: "bb's workspace sandbox allows work inside",
},
},
{
id: "escalation-deny-unsandboxed-bash",
name: "escalation deny blocks unsandboxed Bash retry",
permissionMode: "auto",
permissionEscalation: "deny",
toolName: "Bash",
decisionReason: "dangerouslyDisableSandbox",
input: {
command: "echo hi",
dangerouslyDisableSandbox: true,
description: "Permission boundary test",
},
expected: {
behavior: "deny",
messageIncludes: "bb's workspace sandbox allows work inside",
},
},
{
id: "full-bypass-allow",
name: "full bypass does not rewrite via readonly Bash auto-allow",
Expand Down Expand Up @@ -1579,6 +1597,7 @@ describe("bridge", () => {
});

bridge.sendRequest(collidingId, "turn/start", {
permissionEscalation: "ask",
threadId,
providerThreadId: null,
input: [{ type: "text", text: "colliding turn", mentions: [] }],
Expand Down Expand Up @@ -1614,6 +1633,7 @@ describe("bridge", () => {
// Any params mismatch used to be dropped without a reply, so the caller
// learned nothing until its 30s timeout. The reply now names the field.
bridge.sendRequest(11, "turn/start", {
permissionEscalation: "ask",
threadId: "thread-invalid-params",
providerThreadId: null,
input: [{ type: "text", text: "hi", mentions: [] }],
Expand Down Expand Up @@ -2309,7 +2329,7 @@ describe("bridge", () => {
sandbox: {
enabled: true,
autoAllowBashIfSandboxed: true,
allowUnsandboxedCommands: false,
allowUnsandboxedCommands: true,
},
});

Expand Down Expand Up @@ -2346,6 +2366,45 @@ describe("bridge", () => {
}
});

it("keeps a live Claude session across an escalation-only resume change", async () => {
const bridge = createBridgeJsonRpcTestHarness(handleLine);
const queries: ControlledClaudeQuery[] = [];
queryMock.mockImplementation(() => {
const query = createControlledClaudeQuery();
queries.push(query);
return query;
});

try {
const threadId = "thread-resume-escalation-only";
const providerThreadId = "provider-thread-escalation-only";
sendResumeThread({ bridge, providerThreadId, requestId: 1, threadId });
await bridge.waitForResponse(1);

expect(queries).toHaveLength(1);

sendResumeThread({
bridge,
permissionEscalation: "deny",
providerThreadId,
requestId: 2,
threadId,
});
await bridge.waitForResponse(2);

expect(queries).toHaveLength(1);
expect(queries[0]?.close).not.toHaveBeenCalled();

bridge.sendRequest(3, "thread/stop", { threadId });
await bridge.flushWork();
queries[0]?.finish();
await bridge.waitForResponse(3);
} finally {
queries.forEach((query) => query.finish());
bridge.restore();
}
});

it("replaces a live thread/resume session when the provider thread differs", async () => {
const bridge = createBridgeJsonRpcTestHarness(handleLine);
const queries: ControlledClaudeQuery[] = [];
Expand Down Expand Up @@ -2581,6 +2640,7 @@ describe("bridge", () => {
).toBe(true);

bridge.sendRequest(2, "turn/start", {
permissionEscalation: "ask",
input: [{ type: "text", text: inputText }],
providerThreadId,
threadId,
Expand Down Expand Up @@ -2635,6 +2695,7 @@ describe("bridge", () => {
await bridge.flushWork();

bridge.sendRequest(2, "turn/start", {
permissionEscalation: "ask",
input: [{ type: "text", text: "" }],
providerThreadId,
threadId,
Expand Down Expand Up @@ -2681,6 +2742,7 @@ describe("bridge", () => {
const providerThreadId = getProviderThreadIdFromResult(startResponse);

bridge.sendRequest(2, "turn/start", {
permissionEscalation: "ask",
input: [
{ type: "text", text: "First grouped input" },
{ type: "text", text: "\n\n" },
Expand Down Expand Up @@ -2731,6 +2793,7 @@ describe("bridge", () => {
await startBridgeThread({ bridge, threadId });

bridge.sendRequest(2, "turn/start", {
permissionEscalation: "ask",
input: [
{ type: "text", text: "First grouped input" },
{ type: "text", text: "\n\n" },
Expand Down Expand Up @@ -2798,6 +2861,7 @@ describe("bridge", () => {
});

bridge.sendRequest(2, "turn/start", {
permissionEscalation: "ask",
input: [{ type: "text", text: inputText }],
providerThreadId: staleProviderThreadId,
threadId,
Expand Down Expand Up @@ -2953,6 +3017,7 @@ describe("bridge", () => {
await startBridgeThread({ bridge, threadId });

bridge.sendRequest(2, "turn/steer", {
permissionEscalation: "ask",
expectedTurnId: "turn-1",
input: [{ type: "text", text: "Please account for the restart" }],
providerThreadId: null,
Expand Down Expand Up @@ -2989,6 +3054,7 @@ describe("bridge", () => {
await startBridgeThread({ bridge, threadId });

bridge.sendRequest(2, "turn/steer", {
permissionEscalation: "ask",
expectedTurnId: "turn-1",
input: [
{ type: "text", text: "First grouped steer" },
Expand Down Expand Up @@ -3031,6 +3097,7 @@ describe("bridge", () => {
): Promise<string> {
await startBridgeThread({ bridge, threadId });
bridge.sendRequest(2, "turn/start", {
permissionEscalation: "ask",
input,
providerThreadId: null,
threadId,
Expand Down
Loading
Loading