diff --git a/packages/app/e2e/browser/claude-bypass-permissions.real.spec.ts b/packages/app/e2e/browser/claude-bypass-permissions.real.spec.ts deleted file mode 100644 index 5d65c7e5a8..0000000000 --- a/packages/app/e2e/browser/claude-bypass-permissions.real.spec.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - realpathSync, - writeFileSync, -} from "node:fs"; -import { tmpdir } from "node:os"; -import path from "node:path"; -import { expect, test } from "../support/fixtures"; -import { submitMessage } from "../support/helpers/composer"; -import { cleanupRewindFlow, type AgentHandle, launchAgent } from "../support/helpers/rewind-flow"; - -const FILE_NAME = "BYPASS.md"; -const FILE_CONTENT = "PASEO_BYPASS_PLAYWRIGHT_OK\n"; -const COMPLETION = "BYPASS_WRITE_COMPLETE"; - -function configureClaudeToAskForFileTools(handle: AgentHandle): void { - const settingsDirectory = path.join(handle.cwd, ".claude"); - mkdirSync(settingsDirectory); - writeFileSync( - path.join(settingsDirectory, "settings.local.json"), - `${JSON.stringify({ permissions: { ask: ["Bash", "Write", "Edit"] } }, null, 2)}\n`, - ); -} - -async function switchClaudeToBypassPermissions(handle: AgentHandle): Promise { - const modeControl = handle.page.getByRole("button", { name: /^Select agent mode \(/ }); - await expect(modeControl).toHaveAccessibleName("Select agent mode (Always ask)"); - await modeControl.click(); - - const modePicker = handle.page.getByTestId("combobox-desktop-container").last(); - await expect(modePicker).toBeVisible(); - await modePicker.getByText("Bypass", { exact: true }).click(); - await expect(modeControl).toHaveAccessibleName("Select agent mode (Bypass)"); -} - -async function askClaudeToWriteWithoutApproval(handle: AgentHandle): Promise { - await submitMessage( - handle.page, - `Call the Write tool exactly once to create ${FILE_NAME} with exactly ${JSON.stringify( - FILE_CONTENT, - )}. Do not use Bash or another tool. After Write succeeds, reply exactly ${COMPLETION}.`, - ); -} - -async function expectBypassWriteToComplete(handle: AgentHandle): Promise { - await expect( - handle.page.getByTestId("assistant-message").filter({ hasText: COMPLETION }).last(), - ).toBeVisible({ timeout: 120_000 }); - await expect(handle.page.getByTestId("permission-request-question")).toHaveCount(0); - await expect.poll(() => existsSync(path.join(handle.cwd, FILE_NAME))).toBe(true); - await expect - .poll(() => readFileSync(path.join(handle.cwd, FILE_NAME), "utf8")) - .toBe(FILE_CONTENT); -} - -test.describe("real Claude bypass permissions", () => { - test.setTimeout(240_000); - - test("honors Bypass when project policy asks to approve Write", async ({ page }, testInfo) => { - const cwd = realpathSync(mkdtempSync(path.join(tmpdir(), "paseo-claude-bypass-"))); - let handle: AgentHandle | undefined; - - try { - handle = await launchAgent({ - page, - provider: "claude", - cwd, - mode: "full-access", - providerConfig: { modeId: "default" }, - }); - configureClaudeToAskForFileTools(handle); - await switchClaudeToBypassPermissions(handle); - await askClaudeToWriteWithoutApproval(handle); - await expectBypassWriteToComplete(handle); - await page.screenshot({ path: testInfo.outputPath("claude-bypass-write-complete.png") }); - } finally { - await cleanupRewindFlow({ handle, cwd }); - } - }); -}); diff --git a/packages/server/src/server/agent/providers/claude/agent.ts b/packages/server/src/server/agent/providers/claude/agent.ts index 7b2ed75499..fb9726e127 100644 --- a/packages/server/src/server/agent/providers/claude/agent.ts +++ b/packages/server/src/server/agent/providers/claude/agent.ts @@ -4571,23 +4571,9 @@ class ClaudeAgentSession implements AgentSession { input, options, ): Promise => { + const requestId = `permission-${randomUUID()}`; const kind = resolvePermissionKind(toolName, input); const requestInput = normalizeClaudeAskUserQuestionRequestInput(toolName, input); - - // Supplying canUseTool gives Paseo a host UI for interactive modes, but the SDK can - // still call it for ordinary tools while bypassPermissions is active. Honor the mode - // contract here so a Bypass session never turns Write/Bash into a blocking UI card. - // A human steer keeps precedence: tools from the superseded turn must be denied until - // Claude consumes the steer instead of being allowed to race it. - if ( - kind === "tool" && - this.currentMode === "bypassPermissions" && - this.permissionClearingSteerUuids.size === 0 - ) { - return { behavior: "allow", updatedInput: input }; - } - - const requestId = `permission-${randomUUID()}`; const metadata: AgentMetadata = {}; if (options.toolUseID) { metadata.toolUseId = options.toolUseID;