diff --git a/docs/shell-permissions.md b/docs/shell-permissions.md index 36f2e9f647..50929a797a 100644 --- a/docs/shell-permissions.md +++ b/docs/shell-permissions.md @@ -64,6 +64,25 @@ The in-memory grant disappears on restart. The decision record does not: an answ `decision` spine event at `scope: external-read`, including the paths and whether the grant was remembered. Each later allowed command records a verdict sourced to `read-outside-grant`. +## Native commit signing + +Copse's native `git_commit` tool honours the repository's Git signing configuration while keeping +the commit subprocess inside the project sandbox. On macOS, Settings › Permissions offers an +off-by-default grant that lets only that commit subprocess connect to the single Unix socket named +by `SSH_AUTH_SOCK`. The path must be absolute, normalised, and a socket at the time of use. Internet +access remains denied. + +The grant is explicit because ssh-agent has no commit-only operation: Git hooks inherit the commit +sandbox and can ask the agent to use any loaded key. Recommend `ssh-add -c` when enabling it. Linux +does not receive the grant because seccomp cannot restrict Unix sockets by path; Windows has no +project sandbox. + +When `user.signingKey` names a private-key path, Copse reads only its non-symlink `.pub` sibling +in the trusted main process and passes the public identity to Git as an inline `key::` value. The +sandbox never gains read access to the private key or the `.ssh` directory. A small pinned patch to +`@anthropic-ai/sandbox-runtime` makes its documented per-spawn `allowUnixSockets` option reach the +macOS seatbelt profile; remove that patch once upstream ships the equivalent fix. + ## What an approval prompt says Classifier reasons are **identifiers, not copy**. The regex pass and the token pass share them diff --git a/patches/@anthropic-ai__sandbox-runtime@0.0.74.patch b/patches/@anthropic-ai__sandbox-runtime@0.0.74.patch index 2845a40e34..5398399a7f 100644 --- a/patches/@anthropic-ai__sandbox-runtime@0.0.74.patch +++ b/patches/@anthropic-ai__sandbox-runtime@0.0.74.patch @@ -1,3 +1,16 @@ +diff --git a/dist/sandbox/sandbox-manager.js b/dist/sandbox/sandbox-manager.js +index 3a2d9c8abd01fefff36edea0cd76e9c31a3ed411..7e422b7a159da0ae00902cefcd5d38ac501c223c 100644 +--- a/dist/sandbox/sandbox-manager.js ++++ b/dist/sandbox/sandbox-manager.js +@@ -1293,7 +1293,7 @@ async function wrapWithSandbox(command, binShell, customConfig, abortSignal, opt + unsetEnvVars: credentialRestrictions.unsetEnvVars, + setEnvVars: credentialRestrictions.setEnvVars, + maskedFileBinds: credentialRestrictions.maskedFileBinds, +- allowUnixSockets: getAllowUnixSockets(), ++ allowUnixSockets: customConfig?.network?.allowUnixSockets ?? getAllowUnixSockets(), + allowAllUnixSockets: getAllowAllUnixSockets(), + allowLocalBinding: getAllowLocalBinding(), + allowMachLookup: getAllowMachLookup(), diff --git a/dist/sandbox/sandbox-utils.js b/dist/sandbox/sandbox-utils.js index 32831bf4b8fb14d9ea6fc5bcfe0dc621702c91f4..f53b047ebb0cd376a0f12a9deb3de08d185145b4 100644 --- a/dist/sandbox/sandbox-utils.js diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5036020257..5798efc284 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,7 +25,7 @@ overrides: patchedDependencies: '@anthropic-ai/sandbox-runtime@0.0.74': - hash: b0a49de8e8d51c3bb484932aff9e8c9515a291b126b08e606878eb3139b2896c + hash: 65062e3ebee83cd5cdd07fb3da05641e5854927d4ce8c016be5ca8be19cbb02e path: patches/@anthropic-ai__sandbox-runtime@0.0.74.patch importers: @@ -34,7 +34,7 @@ importers: dependencies: '@anthropic-ai/sandbox-runtime': specifier: ^0.0.74 - version: 0.0.74(patch_hash=b0a49de8e8d51c3bb484932aff9e8c9515a291b126b08e606878eb3139b2896c) + version: 0.0.74(patch_hash=65062e3ebee83cd5cdd07fb3da05641e5854927d4ce8c016be5ca8be19cbb02e) '@mozilla/readability': specifier: ^0.6.0 version: 0.6.0 @@ -4942,7 +4942,7 @@ snapshots: package-manager-detector: 1.8.0 tinyexec: 1.3.0 - '@anthropic-ai/sandbox-runtime@0.0.74(patch_hash=b0a49de8e8d51c3bb484932aff9e8c9515a291b126b08e606878eb3139b2896c)': + '@anthropic-ai/sandbox-runtime@0.0.74(patch_hash=65062e3ebee83cd5cdd07fb3da05641e5854927d4ce8c016be5ca8be19cbb02e)': dependencies: '@pondwader/socks5-server': 1.0.10 commander: 12.1.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0f36a4c8f7..34b5274712 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -71,9 +71,9 @@ minimumReleaseAgeExclude: - ip-address - undici -# Two performance patches to @anthropic-ai/sandbox-runtime, both in one patch -# file. Drop each once upstream does the same; the named test fails loudly if a -# version bump silently drops the patch. +# Three patches to @anthropic-ai/sandbox-runtime, all in one patch file. Drop +# each once upstream does the same; the named test fails loudly if a version +# bump silently drops the patch. # # 1. `whichSync` forks `/usr/bin/which` via spawnSync on every sandboxed command # to locate the shell binary. That is free under Bun (`Bun.which`) but a @@ -90,5 +90,11 @@ minimumReleaseAgeExclude: # duration of one synchronous profile build and clears on the next microtask, # so no resolution is ever reused by a later command. Guarded by # `sandbox-normalize-path-memo.test.ts`. +# +# 3. `wrapWithSandbox` read `allowUnixSockets` only from the global getter, so a +# caller could not widen it for one command. Native SSH commit signing needs +# the ssh-agent socket and nothing else, so the patch lets `customConfig` +# supply the list and falls back to the getter. Guarded by +# `git-commit-signing.test.ts`. patchedDependencies: '@anthropic-ai/sandbox-runtime@0.0.74': patches/@anthropic-ai__sandbox-runtime@0.0.74.patch diff --git a/src/main/project-sandbox/git-commit-signing.test.ts b/src/main/project-sandbox/git-commit-signing.test.ts new file mode 100644 index 0000000000..99f54223cb --- /dev/null +++ b/src/main/project-sandbox/git-commit-signing.test.ts @@ -0,0 +1,195 @@ +import { describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { mkdtemp, readFile, rm, symlink, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join } from 'node:path' +import { createServer } from 'node:net' +import { createRequire } from 'node:module' +import { SandboxManager } from '@anthropic-ai/sandbox-runtime' +import { + gitCommitSigningSandboxOverlay, + parseSshPublicKey, + resolveInlineSshPublicSigningKey, + resolveSshAgentSocketAllowList, + sshAgentSocketAllowList, +} from './git-commit-signing.ts' + +const LAUNCHD_SOCKET = '/private/tmp/com.apple.launchd.example/Listeners' +const require = createRequire(import.meta.url) + +describe('sandbox-runtime unix-socket patch', () => { + it('honours the per-spawn socket list instead of the process-global list', async () => { + const entry = require.resolve('@anthropic-ai/sandbox-runtime') + const manager = join(dirname(entry), 'sandbox', 'sandbox-manager.js') + const source = await readFile(manager, 'utf8') + assert.match( + source, + /allowUnixSockets: customConfig\?\.network\?\.allowUnixSockets \?\? getAllowUnixSockets\(\)/, + ) + }) + + it( + 'emits the per-spawn socket in the generated macOS seatbelt profile', + { skip: process.platform !== 'darwin' }, + async () => { + await SandboxManager.initialize( + { + network: { allowedDomains: [], deniedDomains: [] }, + filesystem: { denyRead: [], allowWrite: [], denyWrite: [], allowGitConfig: true }, + }, + () => Promise.resolve(false), + false, + ) + try { + const wrapped = await SandboxManager.wrapWithSandboxArgv('true', '/bin/zsh', { + network: { + allowedDomains: [], + deniedDomains: [], + allowUnixSockets: [LAUNCHD_SOCKET], + }, + }) + assert.ok( + wrapped.argv.some((arg) => + arg.includes(`network-outbound (remote unix-socket (subpath "${LAUNCHD_SOCKET}"))`), + ), + ) + } finally { + await SandboxManager.reset() + } + }, + ) +}) + +describe('sshAgentSocketAllowList', () => { + it('admits exactly the named socket on macOS after opt-in', () => { + assert.deepEqual( + sshAgentSocketAllowList({ + enabled: true, + authSock: LAUNCHD_SOCKET, + platform: 'darwin', + isSocket: true, + }), + [LAUNCHD_SOCKET], + ) + }) + + it('fails closed for opt-out, unsupported platforms, and unsafe paths', () => { + const base = { enabled: true, platform: 'darwin' as const, isSocket: true } + assert.deepEqual( + sshAgentSocketAllowList({ ...base, enabled: false, authSock: LAUNCHD_SOCKET }), + [], + ) + assert.deepEqual( + sshAgentSocketAllowList({ ...base, platform: 'linux', authSock: LAUNCHD_SOCKET }), + [], + ) + assert.deepEqual(sshAgentSocketAllowList({ ...base, authSock: 'relative.sock' }), []) + assert.deepEqual( + sshAgentSocketAllowList({ ...base, authSock: '/private/tmp/../tmp/agent.sock' }), + [], + ) + assert.deepEqual(sshAgentSocketAllowList({ ...base, authSock: '/', isSocket: false }), []) + }) + + it('checks the path is a real unix socket', async () => { + const dir = await mkdtemp(join(tmpdir(), 'copse-signing-socket-')) + const socketPath = join(dir, 'agent.sock') + const server = createServer() + await new Promise((resolve) => server.listen(socketPath, resolve)) + try { + assert.deepEqual( + await resolveSshAgentSocketAllowList({ + enabled: true, + authSock: socketPath, + platform: 'darwin', + }), + [socketPath], + ) + assert.deepEqual( + await resolveSshAgentSocketAllowList({ + enabled: true, + authSock: dir, + platform: 'darwin', + }), + [], + ) + } finally { + await new Promise((resolve) => { + server.close(() => { + resolve() + }) + }) + await rm(dir, { recursive: true, force: true }) + } + }) + + it('adds only the socket capability to the normal commit sandbox', () => { + const overlay = gitCommitSigningSandboxOverlay('/tmp/project', [LAUNCHD_SOCKET]) + assert.deepEqual(overlay.network, { + allowedDomains: [], + deniedDomains: [], + allowLocalBinding: false, + allowUnixSockets: [LAUNCHD_SOCKET], + }) + assert.ok(overlay.filesystem) + }) +}) + +describe('SSH public signing identity', () => { + const publicLine = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKey comment@example.test' + const privateKeyMarker = ['-----BEGIN OPENSSH', 'PRIVATE KEY-----'].join(' ') + + it('normalizes one public key and drops its comment', () => { + assert.equal( + parseSshPublicKey(`${publicLine}\n`), + 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKey', + ) + assert.equal(parseSshPublicKey(privateKeyMarker), null) + assert.equal(parseSshPublicKey(`${publicLine}\n${publicLine}`), null) + }) + + it('uses a public sibling instead of exposing the configured private-key path', async () => { + const dir = await mkdtemp(join(tmpdir(), 'copse-signing-key-')) + const privatePath = join(dir, 'id_ed25519') + await writeFile(`${privatePath}.pub`, `${publicLine}\n`) + try { + assert.equal( + await resolveInlineSshPublicSigningKey(privatePath), + 'key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKey', + ) + } finally { + await rm(dir, { recursive: true, force: true }) + } + }) + + it('accepts an explicitly configured public-key path', async () => { + const dir = await mkdtemp(join(tmpdir(), 'copse-signing-public-key-')) + const publicPath = join(dir, 'signing.pub') + await writeFile(publicPath, publicLine) + try { + assert.equal( + await resolveInlineSshPublicSigningKey(publicPath), + 'key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKey', + ) + } finally { + await rm(dir, { recursive: true, force: true }) + } + }) + + it('refuses symlinks and files that are not public keys', async () => { + const dir = await mkdtemp(join(tmpdir(), 'copse-signing-refusal-')) + const target = join(dir, 'target.pub') + const link = join(dir, 'link.pub') + const invalid = join(dir, 'invalid.pub') + await writeFile(target, publicLine) + await symlink(target, link) + await writeFile(invalid, privateKeyMarker) + try { + assert.equal(await resolveInlineSshPublicSigningKey(link), null) + assert.equal(await resolveInlineSshPublicSigningKey(invalid), null) + assert.equal(await resolveInlineSshPublicSigningKey(join(dir, 'missing')), null) + } finally { + await rm(dir, { recursive: true, force: true }) + } + }) +}) diff --git a/src/main/project-sandbox/git-commit-signing.ts b/src/main/project-sandbox/git-commit-signing.ts new file mode 100644 index 0000000000..430643fdcd --- /dev/null +++ b/src/main/project-sandbox/git-commit-signing.ts @@ -0,0 +1,111 @@ +import { constants } from 'node:fs' +import { open, stat } from 'node:fs/promises' +import { isAbsolute, normalize } from 'node:path' +import type { SandboxRuntimeConfig } from '@anthropic-ai/sandbox-runtime' +import { workspaceSandboxOverlay } from './config.ts' + +const MAX_PUBLIC_KEY_BYTES = 16 * 1024 + +/** + * Return the one macOS unix socket that a native git commit may use for SSH + * signing. Every rejected input fails closed to the unchanged sandbox profile. + */ +export function sshAgentSocketAllowList(input: { + readonly enabled: boolean + readonly authSock: string | undefined + readonly platform: NodeJS.Platform + readonly isSocket: boolean +}): string[] { + if (!input.enabled || input.platform !== 'darwin') return [] + const socketPath = input.authSock?.trim() + if (!socketPath || !isAbsolute(socketPath) || normalize(socketPath) !== socketPath) return [] + return input.isSocket ? [socketPath] : [] +} + +/** Resolve and validate the socket named by the environment given to git. */ +export async function resolveSshAgentSocketAllowList(input: { + readonly enabled: boolean + readonly authSock: string | undefined + readonly platform: NodeJS.Platform +}): Promise { + const socketPath = input.authSock?.trim() + let isSocket = false + if (socketPath) { + try { + isSocket = (await stat(socketPath)).isSocket() + } catch { + isSocket = false + } + } + return sshAgentSocketAllowList({ ...input, isSocket }) +} + +/** + * Add the socket to this one git subprocess without widening its existing + * filesystem or internet policy. + */ +export function gitCommitSigningSandboxOverlay( + workspaceRoot: string, + socketPaths: readonly string[], +): Partial { + const base = workspaceSandboxOverlay(workspaceRoot) + if (socketPaths.length === 0) return base + const network = base.network + if (!network) throw new Error('workspaceSandboxOverlay must define a network config') + return { + ...base, + network: { + ...network, + allowUnixSockets: [...new Set(socketPaths)], + }, + } +} + +/** Parse one OpenSSH public-key line and discard its optional comment. */ +export function parseSshPublicKey(text: string): string | null { + const line = text.trim() + if (!line || /[\r\n]/.test(line)) return null + const match = /^(ssh-|ecdsa-|sk-)([^\s]+)\s+([A-Za-z0-9+/]+={0,2})(?:\s+.*)?$/.exec(line) + if (!match) return null + const prefix = match[1] + const suffix = match[2] + const blob = match[3] + if (!prefix || !suffix || !blob) return null + const algorithm = `${prefix}${suffix}` + try { + if (Buffer.from(blob, 'base64').length === 0) return null + } catch { + return null + } + return `${algorithm} ${blob}` +} + +/** + * Convert a configured SSH signing-key path into Git's inline public-key form. + * + * Git commonly stores the private-key path even when ssh-agent performs the + * private operation. The project sandbox must not read that private key, so use + * its sibling .pub file instead. Reading happens in Copse's trusted main process; + * the sandboxed git process receives only the public identity. + */ +export async function resolveInlineSshPublicSigningKey( + configuredPath: string, +): Promise { + const trimmed = configuredPath.trim() + if (!trimmed || trimmed.startsWith('key::')) return null + const publicPath = trimmed.endsWith('.pub') ? trimmed : `${trimmed}.pub` + if (!isAbsolute(publicPath) || normalize(publicPath) !== publicPath) return null + + let handle + try { + handle = await open(publicPath, constants.O_RDONLY | constants.O_NOFOLLOW) + const info = await handle.stat() + if (!info.isFile() || info.size <= 0 || info.size > MAX_PUBLIC_KEY_BYTES) return null + const publicKey = parseSshPublicKey(await handle.readFile({ encoding: 'utf8' })) + return publicKey ? `key::${publicKey}` : null + } catch { + return null + } finally { + await handle?.close().catch(() => {}) + } +} diff --git a/src/main/services/github/git-commit-signing-invocation.test.ts b/src/main/services/github/git-commit-signing-invocation.test.ts new file mode 100644 index 0000000000..2599a94eb9 --- /dev/null +++ b/src/main/services/github/git-commit-signing-invocation.test.ts @@ -0,0 +1,81 @@ +import { afterEach, describe, it } from 'node:test' +import assert from 'node:assert/strict' +import { mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { createServer, type Server } from 'node:net' +import { spawnSync } from 'node:child_process' +import { resolveGitCommitSigningInvocation } from './git-service.ts' +import { setSetting } from '../storage/settings.ts' + +describe('resolveGitCommitSigningInvocation', () => { + let server: Server | undefined + let directory: string | undefined + const originalAuthSock = process.env['SSH_AUTH_SOCK'] + + afterEach(async () => { + await setSetting('gitCommitSshAgentSocketAccess', false) + if (server) { + const activeServer = server + await new Promise((resolve) => { + activeServer.close(() => { + resolve() + }) + }) + } + if (directory) await rm(directory, { recursive: true, force: true }) + server = undefined + directory = undefined + if (originalAuthSock === undefined) delete process.env['SSH_AUTH_SOCK'] + else process.env['SSH_AUTH_SOCK'] = originalAuthSock + }) + + it( + 'builds a commit-only socket overlay and replaces a private path with its public identity', + { skip: process.platform !== 'darwin' }, + async () => { + directory = await mkdtemp(join(tmpdir(), 'copse-git-signing-invocation-')) + const socketPath = join(directory, 'agent.sock') + server = createServer() + const activeServer = server + await new Promise((resolve) => { + activeServer.listen(socketPath, resolve) + }) + + const git = (...args: string[]): void => { + const result = spawnSync('git', args, { cwd: directory, encoding: 'utf8' }) + assert.equal(result.status, 0, result.stderr) + } + git('init', '-q') + git('config', 'commit.gpgSign', 'true') + git('config', 'gpg.format', 'ssh') + const privatePath = join(directory, 'id_ed25519') + git('config', 'user.signingKey', privatePath) + await writeFile( + `${privatePath}.pub`, + 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKey test@example.test\n', + ) + + process.env['SSH_AUTH_SOCK'] = socketPath + await setSetting('gitCommitSshAgentSocketAccess', true) + + const invocation = await resolveGitCommitSigningInvocation(directory) + assert.ok(invocation) + assert.deepEqual(invocation.gitConfigArgs, [ + '-c', + 'user.signingKey=key::ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKey', + ]) + assert.deepEqual(invocation.runOptions.sandboxConfig?.network, { + allowedDomains: [], + deniedDomains: [], + allowLocalBinding: false, + allowUnixSockets: [socketPath], + }) + }, + ) + + it('does nothing without explicit consent', async () => { + directory = await mkdtemp(join(tmpdir(), 'copse-git-signing-disabled-')) + assert.equal(await resolveGitCommitSigningInvocation(directory), null) + }) +}) diff --git a/src/main/services/github/git-service.ts b/src/main/services/github/git-service.ts index d541c84f74..4bc91e44e7 100644 --- a/src/main/services/github/git-service.ts +++ b/src/main/services/github/git-service.ts @@ -4,11 +4,16 @@ import { tmpdir } from 'node:os' import { errorMessage } from '@shared/errors.ts' import { resolvePathWithinRoot, toRelativePathWithinRoot } from '../workspace.ts' import { getActiveWorkspaceFs } from '../workspace-fs/get-workspace-fs.ts' -import { runCommand, type CommandResult } from '../exec/command-runner.ts' +import { runCommand, type CommandResult, type RunCommandOptions } from '../exec/command-runner.ts' import { envForRendererChildProcess } from '../exec/child-process-env.ts' import { afterSandboxedCommand, spawnInProjectSandbox } from '../../project-sandbox/spawn.ts' import { isSpawnableWorkingDirectory } from '../../project-sandbox/spawn-cwd.ts' import { readOnlyWorkspaceSandboxOverlay } from '../../project-sandbox/config.ts' +import { + gitCommitSigningSandboxOverlay, + resolveInlineSshPublicSigningKey, + resolveSshAgentSocketAllowList, +} from '../../project-sandbox/git-commit-signing.ts' import { leaseGitSshEnv, withGitInvocationArgs } from '../ssh-workspace/git-ssh-env.ts' import { isActiveSshWorkspace } from '../ssh-workspace/execution-target.ts' import { isGitAvailableForTarget } from '../tool-availability.ts' @@ -19,6 +24,7 @@ import { describeBranchCheckoutFailure } from '@shared/git/branch-held.ts' import { imageMimeType } from '@shared/fs/image-path.ts' import { computeLineDiffStats } from '@shared/diff/line-stats.ts' import { getAgentExecutionRoot } from '../execution-root.ts' +import { getSetting } from '../storage/settings.ts' import { DEFAULT_GIT_BRANCH, type GitBranchInfo, @@ -54,14 +60,17 @@ function unreachableRootResult(root: string): CommandResult { } } +type RunGitOptions = Pick + async function runGit( args: string[], root: string | null = getAgentExecutionRoot(), + options: RunGitOptions = {}, ): Promise<{ stdout: string; stderr: string; code: number }> { const cwd = root if (!cwd) return { stdout: '', stderr: 'No workspace open.', code: 1 } if (!(await isRootReachable(cwd))) return unreachableRootResult(cwd) - return runCommand('git', args, { cwd }) + return runCommand('git', args, { cwd, ...options }) } async function runGitRead( @@ -1442,6 +1451,57 @@ export async function getGitDiffText( return combined || '(no output)' } +export interface GitCommitSigningInvocation { + gitConfigArgs: string[] + runOptions: RunGitOptions +} + +/** + * Prepare the one native commit spawn that is allowed to reach ssh-agent. + * + * The opt-in alone is insufficient: the repository must actually request SSH + * commit signing and the environment must name a real socket. A configured + * private-key path is replaced with the sibling public identity, passed inline + * to Git, so neither Git nor its hooks gain read access to the private key. + */ +export async function resolveGitCommitSigningInvocation( + root: string, +): Promise { + if (isActiveSshWorkspace()) return null + if (!getSetting('gitCommitSshAgentSocketAccess', false)) return null + + const signingEnabled = await runGitRead(['config', '--bool', '--get', 'commit.gpgSign'], root) + if (signingEnabled.code !== 0 || signingEnabled.stdout.trim() !== 'true') return null + + const format = await runGitRead(['config', '--get', 'gpg.format'], root) + if (format.code !== 0 || format.stdout.trim().toLowerCase() !== 'ssh') return null + + const childEnv = envForRendererChildProcess() + const socketPaths = await resolveSshAgentSocketAllowList({ + enabled: true, + authSock: childEnv['SSH_AUTH_SOCK'], + platform: process.platform, + }) + if (socketPaths.length === 0) return null + + const gitConfigArgs: string[] = [] + const rawKey = await runGitRead(['config', '--get', 'user.signingKey'], root) + if (rawKey.code === 0 && !rawKey.stdout.trim().startsWith('key::')) { + const pathKey = await runGitRead(['config', '--path', '--get', 'user.signingKey'], root) + if (pathKey.code === 0) { + const inlineKey = await resolveInlineSshPublicSigningKey(pathKey.stdout.trim()) + if (inlineKey) gitConfigArgs.push('-c', `user.signingKey=${inlineKey}`) + } + } + + return { + gitConfigArgs, + runOptions: { + sandboxConfig: gitCommitSigningSandboxOverlay(root, socketPaths), + }, + } +} + /** * Create a commit, appending the Copse attribution trailer (co-author + the * `models` that ran in this thread). Optionally stages all changes first. @@ -1462,7 +1522,9 @@ export async function commitWithAttribution( } const fullMessage = appendCommitAttribution(message, models) - const { stdout, stderr, code } = await runGit(['commit', '-m', fullMessage], root) + const signing = await resolveGitCommitSigningInvocation(root) + const commitArgs = [...(signing?.gitConfigArgs ?? []), 'commit', '-m', fullMessage] + const { stdout, stderr, code } = await runGit(commitArgs, root, signing?.runOptions) if (code !== 0) { // `git commit` reports "nothing to commit" and similar on stdout, not stderr. return stderr.trim() || stdout.trim() || `git commit exited with code ${String(code)}` diff --git a/src/main/services/storage/settings-writable.ts b/src/main/services/storage/settings-writable.ts index e80dc51994..ca9b666080 100644 --- a/src/main/services/storage/settings-writable.ts +++ b/src/main/services/storage/settings-writable.ts @@ -293,6 +293,12 @@ export const RENDERER_WRITABLE_SETTING_SCHEMAS = { // so a working Cloud Agent selection is never redirected behind the user's // back. Off means the turn falls back to a local chat model instead. preferAcpOverCloudAgent: z.boolean(), + // Let Copse's native git_commit subprocess reach the one macOS ssh-agent + // socket named by SSH_AUTH_SOCK. Off by default: the socket can use every key + // loaded into the agent, and Git hooks inherit the commit process's sandbox. + // The private key remains unreadable; git-commit-signing.ts supplies only its + // public identity when the configured signingKey names the private-key path. + gitCommitSshAgentSocketAccess: z.boolean(), // External ACP agents Copse drives as a client (model value `acp:`). registeredAcpAgents: registeredAcpAgentsSchema, browserToolsEnabled: z.boolean(), diff --git a/src/renderer/views/settings-dialog.ts b/src/renderer/views/settings-dialog.ts index 076ec6eea2..3107ac0b9c 100644 --- a/src/renderer/views/settings-dialog.ts +++ b/src/renderer/views/settings-dialog.ts @@ -259,6 +259,7 @@ const SIMPLE_FIELDS: readonly SettingField[] = [ { name: 'remoteAgentAutoCreatePR', kind: 'checkbox', default: true, save: true }, { name: 'remoteAgentWorkOnCurrentBranch', kind: 'checkbox', default: false, save: true }, { name: 'preferAcpOverCloudAgent', kind: 'checkbox', default: true, save: true }, + { name: 'gitCommitSshAgentSocketAccess', kind: 'checkbox', default: false, save: true }, { name: 'localSubagentsEnabled', kind: 'checkbox', default: true, save: true }, { name: 'subagentsEnabled', @@ -921,6 +922,22 @@ export function mountSettingsDialog(store: AppStore, api: ApiClient): void { + +
+ Commit signing + +

+ Off by default. Turn this on when Git uses a passphrase-protected SSH key and signed + commits fail inside Copse's sandbox. The grant applies only to Copse's native + git_commit subprocess, but Git hooks run inside that process and can + also ask ssh-agent to use any key it holds. The private key remains + unreadable. Pair this with ssh-add -c to confirm each use. macOS only: + Linux cannot admit one socket without admitting every Unix socket. +

+
diff --git a/tests/e2e/git-commit-signing-permission.e2e.ts b/tests/e2e/git-commit-signing-permission.e2e.ts new file mode 100644 index 0000000000..21becee985 --- /dev/null +++ b/tests/e2e/git-commit-signing-permission.e2e.ts @@ -0,0 +1,51 @@ +import { $, browser, expect } from '@wdio/globals' +import { resetUserData, seedE2eViewport, seedEmptyProject } from './helpers/seed-config.ts' +import { saveElementScreenshot } from './helpers/screenshot.ts' + +const PERMISSIONS = '.settings-section[data-section="permissions"]' +const TOGGLE = 'input[name="gitCommitSshAgentSocketAccess"]' +const FIELDSET = `${PERMISSIONS} fieldset:has(${TOGGLE})` + +describe('native git commit signing permission', function () { + this.timeout(90_000) + + beforeEach(async () => { + process.env.COPSE_PANEL_MOCK_LLM = '1' + process.env.ANTHROPIC_API_KEY = '' + process.env.OPENAI_API_KEY = '' + resetUserData() + seedEmptyProject(process.cwd(), 'e2e-git-commit-signing-permission') + seedE2eViewport({ width: 1200, height: 800 }, { theme: 'dark' }) + await browser.reloadSession() + }) + + afterEach(() => { + resetUserData() + }) + + it('shows the commit-scoped grant off by default with its security boundary', async () => { + await $('.prompt-input').waitForExist({ timeout: 30_000 }) + await $('[aria-label="Settings"]').click() + await $('.settings-nav-btn[data-section="permissions"]').click() + await $(TOGGLE).waitForDisplayed({ timeout: 30_000 }) + + expect(await $(TOGGLE).isSelected()).toBe(false) + const copy = await $(FIELDSET).getText() + expect(copy).toContain("Copse's native git_commit subprocess") + expect(copy).toContain('Git hooks') + expect(copy).toContain('any key it holds') + expect(copy).toContain('ssh-add -c') + + await saveElementScreenshot(FIELDSET, 'git-commit-signing-permission.png') + }) + + it('takes the grant only after an explicit click', async () => { + await $('.prompt-input').waitForExist({ timeout: 30_000 }) + await $('[aria-label="Settings"]').click() + await $('.settings-nav-btn[data-section="permissions"]').click() + await $(TOGGLE).waitForDisplayed({ timeout: 30_000 }) + + await $(TOGGLE).click() + expect(await $(TOGGLE).isSelected()).toBe(true) + }) +}) diff --git a/tests/e2e/screenshots/accent-light-strong.png b/tests/e2e/screenshots/accent-light-strong.png index ca693241ad..ce9eaef012 100644 Binary files a/tests/e2e/screenshots/accent-light-strong.png and b/tests/e2e/screenshots/accent-light-strong.png differ diff --git a/tests/e2e/screenshots/accent-strong-green.png b/tests/e2e/screenshots/accent-strong-green.png index f75237bb98..67db20c342 100644 Binary files a/tests/e2e/screenshots/accent-strong-green.png and b/tests/e2e/screenshots/accent-strong-green.png differ diff --git a/tests/e2e/screenshots/acp-usage-update-context.png b/tests/e2e/screenshots/acp-usage-update-context.png index 8a0c78da1c..8d00ea58ca 100644 Binary files a/tests/e2e/screenshots/acp-usage-update-context.png and b/tests/e2e/screenshots/acp-usage-update-context.png differ diff --git a/tests/e2e/screenshots/approval-chat-scoped.png b/tests/e2e/screenshots/approval-chat-scoped.png index 78e73ecbba..a267891146 100644 Binary files a/tests/e2e/screenshots/approval-chat-scoped.png and b/tests/e2e/screenshots/approval-chat-scoped.png differ diff --git a/tests/e2e/screenshots/archive-attachment-chip.png b/tests/e2e/screenshots/archive-attachment-chip.png index 0689f986b0..9f686a7cdf 100644 Binary files a/tests/e2e/screenshots/archive-attachment-chip.png and b/tests/e2e/screenshots/archive-attachment-chip.png differ diff --git a/tests/e2e/screenshots/archive-attachment-restored-draft.png b/tests/e2e/screenshots/archive-attachment-restored-draft.png index 3c8637915e..5319eb0bbc 100644 Binary files a/tests/e2e/screenshots/archive-attachment-restored-draft.png and b/tests/e2e/screenshots/archive-attachment-restored-draft.png differ diff --git a/tests/e2e/screenshots/archive-attachment-transcript.png b/tests/e2e/screenshots/archive-attachment-transcript.png index e1c4ff104d..3bb0c421ae 100644 Binary files a/tests/e2e/screenshots/archive-attachment-transcript.png and b/tests/e2e/screenshots/archive-attachment-transcript.png differ diff --git a/tests/e2e/screenshots/ask-user-dialog-cancelled.png b/tests/e2e/screenshots/ask-user-dialog-cancelled.png index 077cfc4a0f..bdfcc73da9 100644 Binary files a/tests/e2e/screenshots/ask-user-dialog-cancelled.png and b/tests/e2e/screenshots/ask-user-dialog-cancelled.png differ diff --git a/tests/e2e/screenshots/ask-user-dialog.png b/tests/e2e/screenshots/ask-user-dialog.png index 629c558f34..ffd1d0eb01 100644 Binary files a/tests/e2e/screenshots/ask-user-dialog.png and b/tests/e2e/screenshots/ask-user-dialog.png differ diff --git a/tests/e2e/screenshots/attachment-preview-text.png b/tests/e2e/screenshots/attachment-preview-text.png index d9fa25fd15..4f74f64ba5 100644 Binary files a/tests/e2e/screenshots/attachment-preview-text.png and b/tests/e2e/screenshots/attachment-preview-text.png differ diff --git a/tests/e2e/screenshots/automation-attention-group.png b/tests/e2e/screenshots/automation-attention-group.png index a7f3c9b11b..e3b28ffb7d 100644 Binary files a/tests/e2e/screenshots/automation-attention-group.png and b/tests/e2e/screenshots/automation-attention-group.png differ diff --git a/tests/e2e/screenshots/automation-thread-group.png b/tests/e2e/screenshots/automation-thread-group.png index 491d1f95ea..61137a9006 100644 Binary files a/tests/e2e/screenshots/automation-thread-group.png and b/tests/e2e/screenshots/automation-thread-group.png differ diff --git a/tests/e2e/screenshots/automation-trigger.png b/tests/e2e/screenshots/automation-trigger.png index 853664aac1..3959a79408 100644 Binary files a/tests/e2e/screenshots/automation-trigger.png and b/tests/e2e/screenshots/automation-trigger.png differ diff --git a/tests/e2e/screenshots/background-task-completion-wake.png b/tests/e2e/screenshots/background-task-completion-wake.png index 5c7a5e9add..ee9cc4cd3e 100644 Binary files a/tests/e2e/screenshots/background-task-completion-wake.png and b/tests/e2e/screenshots/background-task-completion-wake.png differ diff --git a/tests/e2e/screenshots/benchmark-catalog.png b/tests/e2e/screenshots/benchmark-catalog.png index c5402ce5fa..fd493e3e95 100644 Binary files a/tests/e2e/screenshots/benchmark-catalog.png and b/tests/e2e/screenshots/benchmark-catalog.png differ diff --git a/tests/e2e/screenshots/browser-cursor-agent-url-navigation.png b/tests/e2e/screenshots/browser-cursor-agent-url-navigation.png index 9a587c5616..fb52c1d042 100644 Binary files a/tests/e2e/screenshots/browser-cursor-agent-url-navigation.png and b/tests/e2e/screenshots/browser-cursor-agent-url-navigation.png differ diff --git a/tests/e2e/screenshots/browser-session-restore-after-relaunch.png b/tests/e2e/screenshots/browser-session-restore-after-relaunch.png index e74e9823fd..b022fcfc9a 100644 Binary files a/tests/e2e/screenshots/browser-session-restore-after-relaunch.png and b/tests/e2e/screenshots/browser-session-restore-after-relaunch.png differ diff --git a/tests/e2e/screenshots/browser-session-restore-before-quit.png b/tests/e2e/screenshots/browser-session-restore-before-quit.png index 195f160658..f4300d0a4a 100644 Binary files a/tests/e2e/screenshots/browser-session-restore-before-quit.png and b/tests/e2e/screenshots/browser-session-restore-before-quit.png differ diff --git a/tests/e2e/screenshots/browser-thread-sharing-attachments.png b/tests/e2e/screenshots/browser-thread-sharing-attachments.png index 7546b189d0..3621bb8873 100644 Binary files a/tests/e2e/screenshots/browser-thread-sharing-attachments.png and b/tests/e2e/screenshots/browser-thread-sharing-attachments.png differ diff --git a/tests/e2e/screenshots/browser-tools-collapsed.png b/tests/e2e/screenshots/browser-tools-collapsed.png index b7f99394cc..0c241d4905 100644 Binary files a/tests/e2e/screenshots/browser-tools-collapsed.png and b/tests/e2e/screenshots/browser-tools-collapsed.png differ diff --git a/tests/e2e/screenshots/canvas-artefact-preview.png b/tests/e2e/screenshots/canvas-artefact-preview.png index ff43b60d49..7247c1941d 100644 Binary files a/tests/e2e/screenshots/canvas-artefact-preview.png and b/tests/e2e/screenshots/canvas-artefact-preview.png differ diff --git a/tests/e2e/screenshots/canvas-artefact-promoted.png b/tests/e2e/screenshots/canvas-artefact-promoted.png index d83966c4ab..7247c1941d 100644 Binary files a/tests/e2e/screenshots/canvas-artefact-promoted.png and b/tests/e2e/screenshots/canvas-artefact-promoted.png differ diff --git a/tests/e2e/screenshots/canvas-artefact-refresh.png b/tests/e2e/screenshots/canvas-artefact-refresh.png index b07e15d388..04e278acbc 100644 Binary files a/tests/e2e/screenshots/canvas-artefact-refresh.png and b/tests/e2e/screenshots/canvas-artefact-refresh.png differ diff --git a/tests/e2e/screenshots/canvas-preview-thread-isolation.png b/tests/e2e/screenshots/canvas-preview-thread-isolation.png index ec69b938f0..8769178e13 100644 Binary files a/tests/e2e/screenshots/canvas-preview-thread-isolation.png and b/tests/e2e/screenshots/canvas-preview-thread-isolation.png differ diff --git a/tests/e2e/screenshots/capability-lease-approval.png b/tests/e2e/screenshots/capability-lease-approval.png index aa83a5aa1d..546f13221f 100644 Binary files a/tests/e2e/screenshots/capability-lease-approval.png and b/tests/e2e/screenshots/capability-lease-approval.png differ diff --git a/tests/e2e/screenshots/chat-layout-gradient-empty.png b/tests/e2e/screenshots/chat-layout-gradient-empty.png index 071be048da..47e7e2a16d 100644 Binary files a/tests/e2e/screenshots/chat-layout-gradient-empty.png and b/tests/e2e/screenshots/chat-layout-gradient-empty.png differ diff --git a/tests/e2e/screenshots/chat-reasoning-in-transcript.png b/tests/e2e/screenshots/chat-reasoning-in-transcript.png index d2392c30d7..93073b32e6 100644 Binary files a/tests/e2e/screenshots/chat-reasoning-in-transcript.png and b/tests/e2e/screenshots/chat-reasoning-in-transcript.png differ diff --git a/tests/e2e/screenshots/checkup-skill-picked.png b/tests/e2e/screenshots/checkup-skill-picked.png index dd321524e7..97ddf8faa8 100644 Binary files a/tests/e2e/screenshots/checkup-skill-picked.png and b/tests/e2e/screenshots/checkup-skill-picked.png differ diff --git a/tests/e2e/screenshots/ci-investigator-display-collapsed.png b/tests/e2e/screenshots/ci-investigator-display-collapsed.png index e81b752e41..65ebfd65ff 100644 Binary files a/tests/e2e/screenshots/ci-investigator-display-collapsed.png and b/tests/e2e/screenshots/ci-investigator-display-collapsed.png differ diff --git a/tests/e2e/screenshots/code-block-copy-hover.png b/tests/e2e/screenshots/code-block-copy-hover.png index d508555158..5fad99de26 100644 Binary files a/tests/e2e/screenshots/code-block-copy-hover.png and b/tests/e2e/screenshots/code-block-copy-hover.png differ diff --git a/tests/e2e/screenshots/command-palette-pr-number.png b/tests/e2e/screenshots/command-palette-pr-number.png index 5ebf0111f7..6f44597f52 100644 Binary files a/tests/e2e/screenshots/command-palette-pr-number.png and b/tests/e2e/screenshots/command-palette-pr-number.png differ diff --git a/tests/e2e/screenshots/command-palette.png b/tests/e2e/screenshots/command-palette.png index 72f7aa437d..ffd357f8fe 100644 Binary files a/tests/e2e/screenshots/command-palette.png and b/tests/e2e/screenshots/command-palette.png differ diff --git a/tests/e2e/screenshots/comparison-dismiss-before.png b/tests/e2e/screenshots/comparison-dismiss-before.png index 2fdff3f006..1e5fab6ef7 100644 Binary files a/tests/e2e/screenshots/comparison-dismiss-before.png and b/tests/e2e/screenshots/comparison-dismiss-before.png differ diff --git a/tests/e2e/screenshots/comparison-inline-transcript.png b/tests/e2e/screenshots/comparison-inline-transcript.png index 4869fbfe75..570f1dfc19 100644 Binary files a/tests/e2e/screenshots/comparison-inline-transcript.png and b/tests/e2e/screenshots/comparison-inline-transcript.png differ diff --git a/tests/e2e/screenshots/context-wheel-running-hover.png b/tests/e2e/screenshots/context-wheel-running-hover.png index 1230132c66..9bfb30070a 100644 Binary files a/tests/e2e/screenshots/context-wheel-running-hover.png and b/tests/e2e/screenshots/context-wheel-running-hover.png differ diff --git a/tests/e2e/screenshots/create-pr-dialog.png b/tests/e2e/screenshots/create-pr-dialog.png index 4375047a31..3a4bf7fdd4 100644 Binary files a/tests/e2e/screenshots/create-pr-dialog.png and b/tests/e2e/screenshots/create-pr-dialog.png differ diff --git a/tests/e2e/screenshots/cursor-acp-auth-error.png b/tests/e2e/screenshots/cursor-acp-auth-error.png index 135cd36cd2..9c452ba395 100644 Binary files a/tests/e2e/screenshots/cursor-acp-auth-error.png and b/tests/e2e/screenshots/cursor-acp-auth-error.png differ diff --git a/tests/e2e/screenshots/custom-agent-slash-picker.png b/tests/e2e/screenshots/custom-agent-slash-picker.png index 47fd1c8f40..a08f87f783 100644 Binary files a/tests/e2e/screenshots/custom-agent-slash-picker.png and b/tests/e2e/screenshots/custom-agent-slash-picker.png differ diff --git a/tests/e2e/screenshots/debug-trace-composer.png b/tests/e2e/screenshots/debug-trace-composer.png index c08c17eef8..fcc6c79739 100644 Binary files a/tests/e2e/screenshots/debug-trace-composer.png and b/tests/e2e/screenshots/debug-trace-composer.png differ diff --git a/tests/e2e/screenshots/deepseek-dsml-recovery.png b/tests/e2e/screenshots/deepseek-dsml-recovery.png index a674ea33bd..9e6ae7ebbf 100644 Binary files a/tests/e2e/screenshots/deepseek-dsml-recovery.png and b/tests/e2e/screenshots/deepseek-dsml-recovery.png differ diff --git a/tests/e2e/screenshots/developer-mode-footer-menu-default.png b/tests/e2e/screenshots/developer-mode-footer-menu-default.png index c70098c5d9..41bce6b213 100644 Binary files a/tests/e2e/screenshots/developer-mode-footer-menu-default.png and b/tests/e2e/screenshots/developer-mode-footer-menu-default.png differ diff --git a/tests/e2e/screenshots/double-submit-drained.png b/tests/e2e/screenshots/double-submit-drained.png index 3e2fd7e6da..ebfa67f5d0 100644 Binary files a/tests/e2e/screenshots/double-submit-drained.png and b/tests/e2e/screenshots/double-submit-drained.png differ diff --git a/tests/e2e/screenshots/double-submit-single-queued.png b/tests/e2e/screenshots/double-submit-single-queued.png index 68b2f8b827..48980f8e8d 100644 Binary files a/tests/e2e/screenshots/double-submit-single-queued.png and b/tests/e2e/screenshots/double-submit-single-queued.png differ diff --git a/tests/e2e/screenshots/file-open-css-worker-no-error.png b/tests/e2e/screenshots/file-open-css-worker-no-error.png index 679d7da3f5..e2147b54aa 100644 Binary files a/tests/e2e/screenshots/file-open-css-worker-no-error.png and b/tests/e2e/screenshots/file-open-css-worker-no-error.png differ diff --git a/tests/e2e/screenshots/file-open-worker-no-error.png b/tests/e2e/screenshots/file-open-worker-no-error.png index c6afa13975..f6de5c5603 100644 Binary files a/tests/e2e/screenshots/file-open-worker-no-error.png and b/tests/e2e/screenshots/file-open-worker-no-error.png differ diff --git a/tests/e2e/screenshots/file-search-palette.png b/tests/e2e/screenshots/file-search-palette.png index 4d6f3dbcdb..7b423efba3 100644 Binary files a/tests/e2e/screenshots/file-search-palette.png and b/tests/e2e/screenshots/file-search-palette.png differ diff --git a/tests/e2e/screenshots/file-tree-open-in-browser-menu.png b/tests/e2e/screenshots/file-tree-open-in-browser-menu.png index 4f9bc509a7..73ce6bab2b 100644 Binary files a/tests/e2e/screenshots/file-tree-open-in-browser-menu.png and b/tests/e2e/screenshots/file-tree-open-in-browser-menu.png differ diff --git a/tests/e2e/screenshots/file-viewer-changes-clean-file.png b/tests/e2e/screenshots/file-viewer-changes-clean-file.png index 798c05e6f7..2e96b6e91a 100644 Binary files a/tests/e2e/screenshots/file-viewer-changes-clean-file.png and b/tests/e2e/screenshots/file-viewer-changes-clean-file.png differ diff --git a/tests/e2e/screenshots/file-viewer-changes-diff.png b/tests/e2e/screenshots/file-viewer-changes-diff.png index 36be44aaab..8a4fe5e672 100644 Binary files a/tests/e2e/screenshots/file-viewer-changes-diff.png and b/tests/e2e/screenshots/file-viewer-changes-diff.png differ diff --git a/tests/e2e/screenshots/file-viewer-changes-source.png b/tests/e2e/screenshots/file-viewer-changes-source.png index 709dc54043..33cc0e9dfd 100644 Binary files a/tests/e2e/screenshots/file-viewer-changes-source.png and b/tests/e2e/screenshots/file-viewer-changes-source.png differ diff --git a/tests/e2e/screenshots/file-viewer-default-browser-menu.png b/tests/e2e/screenshots/file-viewer-default-browser-menu.png index 8bf4dfb88f..83ef3bc82d 100644 Binary files a/tests/e2e/screenshots/file-viewer-default-browser-menu.png and b/tests/e2e/screenshots/file-viewer-default-browser-menu.png differ diff --git a/tests/e2e/screenshots/footer-index-scale-guard-skipped.png b/tests/e2e/screenshots/footer-index-scale-guard-skipped.png index 24782491ad..f5bc499687 100644 Binary files a/tests/e2e/screenshots/footer-index-scale-guard-skipped.png and b/tests/e2e/screenshots/footer-index-scale-guard-skipped.png differ diff --git a/tests/e2e/screenshots/git-changes-committed-diff.png b/tests/e2e/screenshots/git-changes-committed-diff.png index b203a5c160..e2f6190735 100644 Binary files a/tests/e2e/screenshots/git-changes-committed-diff.png and b/tests/e2e/screenshots/git-changes-committed-diff.png differ diff --git a/tests/e2e/screenshots/git-changes-diff-collapsed.png b/tests/e2e/screenshots/git-changes-diff-collapsed.png index 1fe5b4d707..ebb62d8a92 100644 Binary files a/tests/e2e/screenshots/git-changes-diff-collapsed.png and b/tests/e2e/screenshots/git-changes-diff-collapsed.png differ diff --git a/tests/e2e/screenshots/git-changes-diff-expanded.png b/tests/e2e/screenshots/git-changes-diff-expanded.png index 2449ff3ec6..6d22cf2ad3 100644 Binary files a/tests/e2e/screenshots/git-changes-diff-expanded.png and b/tests/e2e/screenshots/git-changes-diff-expanded.png differ diff --git a/tests/e2e/screenshots/git-changes-image-list.png b/tests/e2e/screenshots/git-changes-image-list.png index baab138234..2ff0361e60 100644 Binary files a/tests/e2e/screenshots/git-changes-image-list.png and b/tests/e2e/screenshots/git-changes-image-list.png differ diff --git a/tests/e2e/screenshots/git-changes-image-staged.png b/tests/e2e/screenshots/git-changes-image-staged.png index 01f6cc97fb..30586616d8 100644 Binary files a/tests/e2e/screenshots/git-changes-image-staged.png and b/tests/e2e/screenshots/git-changes-image-staged.png differ diff --git a/tests/e2e/screenshots/git-changes-image-unstaged.png b/tests/e2e/screenshots/git-changes-image-unstaged.png index 07b5bc7f25..6f1fd9644b 100644 Binary files a/tests/e2e/screenshots/git-changes-image-unstaged.png and b/tests/e2e/screenshots/git-changes-image-unstaged.png differ diff --git a/tests/e2e/screenshots/git-changes-image-untracked.png b/tests/e2e/screenshots/git-changes-image-untracked.png index f30bb4030e..ee89aee1ad 100644 Binary files a/tests/e2e/screenshots/git-changes-image-untracked.png and b/tests/e2e/screenshots/git-changes-image-untracked.png differ diff --git a/tests/e2e/screenshots/git-changes-list.png b/tests/e2e/screenshots/git-changes-list.png index 0fba3798f4..254a41d960 100644 Binary files a/tests/e2e/screenshots/git-changes-list.png and b/tests/e2e/screenshots/git-changes-list.png differ diff --git a/tests/e2e/screenshots/git-changes-popout-proposed-forced.png b/tests/e2e/screenshots/git-changes-popout-proposed-forced.png index c1a73737e8..2aaadae7b9 100644 Binary files a/tests/e2e/screenshots/git-changes-popout-proposed-forced.png and b/tests/e2e/screenshots/git-changes-popout-proposed-forced.png differ diff --git a/tests/e2e/screenshots/git-changes-worker-restart.png b/tests/e2e/screenshots/git-changes-worker-restart.png index cfe6019002..c688ba33b2 100644 Binary files a/tests/e2e/screenshots/git-changes-worker-restart.png and b/tests/e2e/screenshots/git-changes-worker-restart.png differ diff --git a/tests/e2e/screenshots/git-commit-signing-permission.png b/tests/e2e/screenshots/git-commit-signing-permission.png new file mode 100644 index 0000000000..b9e188b34e Binary files /dev/null and b/tests/e2e/screenshots/git-commit-signing-permission.png differ diff --git a/tests/e2e/screenshots/image-expand-composer-new-thread.png b/tests/e2e/screenshots/image-expand-composer-new-thread.png index 8379de402f..306be24c01 100644 Binary files a/tests/e2e/screenshots/image-expand-composer-new-thread.png and b/tests/e2e/screenshots/image-expand-composer-new-thread.png differ diff --git a/tests/e2e/screenshots/image-expand-roadmap.png b/tests/e2e/screenshots/image-expand-roadmap.png index e5bd037322..50a000edc0 100644 Binary files a/tests/e2e/screenshots/image-expand-roadmap.png and b/tests/e2e/screenshots/image-expand-roadmap.png differ diff --git a/tests/e2e/screenshots/image-expand-thread-dismissed.png b/tests/e2e/screenshots/image-expand-thread-dismissed.png index b706177327..0218b712aa 100644 Binary files a/tests/e2e/screenshots/image-expand-thread-dismissed.png and b/tests/e2e/screenshots/image-expand-thread-dismissed.png differ diff --git a/tests/e2e/screenshots/image-expand-thread.png b/tests/e2e/screenshots/image-expand-thread.png index 7d6985bc1b..c184a2de01 100644 Binary files a/tests/e2e/screenshots/image-expand-thread.png and b/tests/e2e/screenshots/image-expand-thread.png differ diff --git a/tests/e2e/screenshots/install-approval-dialog.png b/tests/e2e/screenshots/install-approval-dialog.png index 620c1ec88a..2c8a77e93f 100644 Binary files a/tests/e2e/screenshots/install-approval-dialog.png and b/tests/e2e/screenshots/install-approval-dialog.png differ diff --git a/tests/e2e/screenshots/landing-cupcake-browser.png b/tests/e2e/screenshots/landing-cupcake-browser.png index 4002986069..c467de9631 100644 Binary files a/tests/e2e/screenshots/landing-cupcake-browser.png and b/tests/e2e/screenshots/landing-cupcake-browser.png differ diff --git a/tests/e2e/screenshots/landing-cupcake-changes.png b/tests/e2e/screenshots/landing-cupcake-changes.png index 9ae09be179..128e351e48 100644 Binary files a/tests/e2e/screenshots/landing-cupcake-changes.png and b/tests/e2e/screenshots/landing-cupcake-changes.png differ diff --git a/tests/e2e/screenshots/landing-cupcake-writing.png b/tests/e2e/screenshots/landing-cupcake-writing.png index ed613537d7..7e5ba1a906 100644 Binary files a/tests/e2e/screenshots/landing-cupcake-writing.png and b/tests/e2e/screenshots/landing-cupcake-writing.png differ diff --git a/tests/e2e/screenshots/lm-studio-prompt-progress.png b/tests/e2e/screenshots/lm-studio-prompt-progress.png index 0bcaf93a8e..fa56cc3ca4 100644 Binary files a/tests/e2e/screenshots/lm-studio-prompt-progress.png and b/tests/e2e/screenshots/lm-studio-prompt-progress.png differ diff --git a/tests/e2e/screenshots/machine-turn-attribution.png b/tests/e2e/screenshots/machine-turn-attribution.png index 5ad2853305..06fe29074a 100644 Binary files a/tests/e2e/screenshots/machine-turn-attribution.png and b/tests/e2e/screenshots/machine-turn-attribution.png differ diff --git a/tests/e2e/screenshots/markdown-conformance-quickwins.png b/tests/e2e/screenshots/markdown-conformance-quickwins.png index cb884c4171..6dac0b07d8 100644 Binary files a/tests/e2e/screenshots/markdown-conformance-quickwins.png and b/tests/e2e/screenshots/markdown-conformance-quickwins.png differ diff --git a/tests/e2e/screenshots/markdown-jd-metadata.png b/tests/e2e/screenshots/markdown-jd-metadata.png index 0f8bf6a931..e4c65ae2b9 100644 Binary files a/tests/e2e/screenshots/markdown-jd-metadata.png and b/tests/e2e/screenshots/markdown-jd-metadata.png differ diff --git a/tests/e2e/screenshots/markdown-ordered-list-after.png b/tests/e2e/screenshots/markdown-ordered-list-after.png index c2d908f1b8..34c5c7da0b 100644 Binary files a/tests/e2e/screenshots/markdown-ordered-list-after.png and b/tests/e2e/screenshots/markdown-ordered-list-after.png differ diff --git a/tests/e2e/screenshots/markdown-streaming-heading-pending.png b/tests/e2e/screenshots/markdown-streaming-heading-pending.png index 8b099046bb..a51438f4a2 100644 Binary files a/tests/e2e/screenshots/markdown-streaming-heading-pending.png and b/tests/e2e/screenshots/markdown-streaming-heading-pending.png differ diff --git a/tests/e2e/screenshots/markdown-streaming-list-continuation.png b/tests/e2e/screenshots/markdown-streaming-list-continuation.png index 29189c095d..9ec127a3d0 100644 Binary files a/tests/e2e/screenshots/markdown-streaming-list-continuation.png and b/tests/e2e/screenshots/markdown-streaming-list-continuation.png differ diff --git a/tests/e2e/screenshots/markdown-streaming-list-pending-bullet.png b/tests/e2e/screenshots/markdown-streaming-list-pending-bullet.png index 89f2387737..91b7f64740 100644 Binary files a/tests/e2e/screenshots/markdown-streaming-list-pending-bullet.png and b/tests/e2e/screenshots/markdown-streaming-list-pending-bullet.png differ diff --git a/tests/e2e/screenshots/markdown-streaming-table-pending-row.png b/tests/e2e/screenshots/markdown-streaming-table-pending-row.png index 5e621b1b15..c3f0fcf3b8 100644 Binary files a/tests/e2e/screenshots/markdown-streaming-table-pending-row.png and b/tests/e2e/screenshots/markdown-streaming-table-pending-row.png differ diff --git a/tests/e2e/screenshots/markdown-workspace-links.png b/tests/e2e/screenshots/markdown-workspace-links.png index ec362d54bc..2c885e5830 100644 Binary files a/tests/e2e/screenshots/markdown-workspace-links.png and b/tests/e2e/screenshots/markdown-workspace-links.png differ diff --git a/tests/e2e/screenshots/marketing-site-tour-anchor.png b/tests/e2e/screenshots/marketing-site-tour-anchor.png index 97b64555e5..01be562e86 100644 Binary files a/tests/e2e/screenshots/marketing-site-tour-anchor.png and b/tests/e2e/screenshots/marketing-site-tour-anchor.png differ diff --git a/tests/e2e/screenshots/mcp-tool-labels.png b/tests/e2e/screenshots/mcp-tool-labels.png index 4464ab1192..2ae57eab39 100644 Binary files a/tests/e2e/screenshots/mcp-tool-labels.png and b/tests/e2e/screenshots/mcp-tool-labels.png differ diff --git a/tests/e2e/screenshots/message-fork-resend-resent.png b/tests/e2e/screenshots/message-fork-resend-resent.png index ccfe3f7cb5..037cd1592f 100644 Binary files a/tests/e2e/screenshots/message-fork-resend-resent.png and b/tests/e2e/screenshots/message-fork-resend-resent.png differ diff --git a/tests/e2e/screenshots/model-compare-approval-dialog.png b/tests/e2e/screenshots/model-compare-approval-dialog.png index c16a9554c6..fc0dd09178 100644 Binary files a/tests/e2e/screenshots/model-compare-approval-dialog.png and b/tests/e2e/screenshots/model-compare-approval-dialog.png differ diff --git a/tests/e2e/screenshots/model-compare-bubble-dialog.png b/tests/e2e/screenshots/model-compare-bubble-dialog.png index 0277731884..3a7b85bcd3 100644 Binary files a/tests/e2e/screenshots/model-compare-bubble-dialog.png and b/tests/e2e/screenshots/model-compare-bubble-dialog.png differ diff --git a/tests/e2e/screenshots/model-compare-bubble-result.png b/tests/e2e/screenshots/model-compare-bubble-result.png index dd65047f16..76603f0c36 100644 Binary files a/tests/e2e/screenshots/model-compare-bubble-result.png and b/tests/e2e/screenshots/model-compare-bubble-result.png differ diff --git a/tests/e2e/screenshots/monaco-python-fstring-highlight.png b/tests/e2e/screenshots/monaco-python-fstring-highlight.png index de94ea1131..51510dfd4b 100644 Binary files a/tests/e2e/screenshots/monaco-python-fstring-highlight.png and b/tests/e2e/screenshots/monaco-python-fstring-highlight.png differ diff --git a/tests/e2e/screenshots/multiple-main-windows-secondary.png b/tests/e2e/screenshots/multiple-main-windows-secondary.png index 983c131d0d..8e684cd585 100644 Binary files a/tests/e2e/screenshots/multiple-main-windows-secondary.png and b/tests/e2e/screenshots/multiple-main-windows-secondary.png differ diff --git a/tests/e2e/screenshots/nested-tool-activity-colors.png b/tests/e2e/screenshots/nested-tool-activity-colors.png index 7b070db9f7..4185a6fd29 100644 Binary files a/tests/e2e/screenshots/nested-tool-activity-colors.png and b/tests/e2e/screenshots/nested-tool-activity-colors.png differ diff --git a/tests/e2e/screenshots/new-project-active.png b/tests/e2e/screenshots/new-project-active.png index a6a140b60c..10cdfca94c 100644 Binary files a/tests/e2e/screenshots/new-project-active.png and b/tests/e2e/screenshots/new-project-active.png differ diff --git a/tests/e2e/screenshots/new-thread-composer-centered.png b/tests/e2e/screenshots/new-thread-composer-centered.png index 6acc4c55cf..734baea2ca 100644 Binary files a/tests/e2e/screenshots/new-thread-composer-centered.png and b/tests/e2e/screenshots/new-thread-composer-centered.png differ diff --git a/tests/e2e/screenshots/next-step-hint-accepted.png b/tests/e2e/screenshots/next-step-hint-accepted.png index a481204a4f..27c0f960db 100644 Binary files a/tests/e2e/screenshots/next-step-hint-accepted.png and b/tests/e2e/screenshots/next-step-hint-accepted.png differ diff --git a/tests/e2e/screenshots/next-step-hint.png b/tests/e2e/screenshots/next-step-hint.png index 92f3bba1e7..7ab056b164 100644 Binary files a/tests/e2e/screenshots/next-step-hint.png and b/tests/e2e/screenshots/next-step-hint.png differ diff --git a/tests/e2e/screenshots/npx-approval-dialog.png b/tests/e2e/screenshots/npx-approval-dialog.png index 80b1986b05..7da4a202ea 100644 Binary files a/tests/e2e/screenshots/npx-approval-dialog.png and b/tests/e2e/screenshots/npx-approval-dialog.png differ diff --git a/tests/e2e/screenshots/onboarding-scan-results.png b/tests/e2e/screenshots/onboarding-scan-results.png index 780a6556cc..6efe4357c9 100644 Binary files a/tests/e2e/screenshots/onboarding-scan-results.png and b/tests/e2e/screenshots/onboarding-scan-results.png differ diff --git a/tests/e2e/screenshots/pane-loading-changes.png b/tests/e2e/screenshots/pane-loading-changes.png index b5020ec4ea..fd1e6eb530 100644 Binary files a/tests/e2e/screenshots/pane-loading-changes.png and b/tests/e2e/screenshots/pane-loading-changes.png differ diff --git a/tests/e2e/screenshots/pane-loading-memories.png b/tests/e2e/screenshots/pane-loading-memories.png index 3381caca4a..46b86631dc 100644 Binary files a/tests/e2e/screenshots/pane-loading-memories.png and b/tests/e2e/screenshots/pane-loading-memories.png differ diff --git a/tests/e2e/screenshots/pane-loading-pull-requests.png b/tests/e2e/screenshots/pane-loading-pull-requests.png index b7534583e0..89a2a39a52 100644 Binary files a/tests/e2e/screenshots/pane-loading-pull-requests.png and b/tests/e2e/screenshots/pane-loading-pull-requests.png differ diff --git a/tests/e2e/screenshots/pane-loading-roadmap.png b/tests/e2e/screenshots/pane-loading-roadmap.png index 961558741c..6614cce470 100644 Binary files a/tests/e2e/screenshots/pane-loading-roadmap.png and b/tests/e2e/screenshots/pane-loading-roadmap.png differ diff --git a/tests/e2e/screenshots/pane-popout-changes.png b/tests/e2e/screenshots/pane-popout-changes.png index b05b3faa7e..e3289b0ca3 100644 Binary files a/tests/e2e/screenshots/pane-popout-changes.png and b/tests/e2e/screenshots/pane-popout-changes.png differ diff --git a/tests/e2e/screenshots/pane-popout-explorer.png b/tests/e2e/screenshots/pane-popout-explorer.png index 848194ef2c..5671f7bc2a 100644 Binary files a/tests/e2e/screenshots/pane-popout-explorer.png and b/tests/e2e/screenshots/pane-popout-explorer.png differ diff --git a/tests/e2e/screenshots/pane-rules-and-roadmap-spacing.png b/tests/e2e/screenshots/pane-rules-and-roadmap-spacing.png index 9f8978c1ad..47ad68b535 100644 Binary files a/tests/e2e/screenshots/pane-rules-and-roadmap-spacing.png and b/tests/e2e/screenshots/pane-rules-and-roadmap-spacing.png differ diff --git a/tests/e2e/screenshots/panel-position-bottom.png b/tests/e2e/screenshots/panel-position-bottom.png index 7ee7cf4313..023632c65f 100644 Binary files a/tests/e2e/screenshots/panel-position-bottom.png and b/tests/e2e/screenshots/panel-position-bottom.png differ diff --git a/tests/e2e/screenshots/paste-attachment-chip.png b/tests/e2e/screenshots/paste-attachment-chip.png index 5c58a8370e..660821d89a 100644 Binary files a/tests/e2e/screenshots/paste-attachment-chip.png and b/tests/e2e/screenshots/paste-attachment-chip.png differ diff --git a/tests/e2e/screenshots/paste-attachment-composer-preview.png b/tests/e2e/screenshots/paste-attachment-composer-preview.png index 805ab9c16e..885d27e8f6 100644 Binary files a/tests/e2e/screenshots/paste-attachment-composer-preview.png and b/tests/e2e/screenshots/paste-attachment-composer-preview.png differ diff --git a/tests/e2e/screenshots/paste-attachment-transcript-preview.png b/tests/e2e/screenshots/paste-attachment-transcript-preview.png index d093fa7593..02c906164a 100644 Binary files a/tests/e2e/screenshots/paste-attachment-transcript-preview.png and b/tests/e2e/screenshots/paste-attachment-transcript-preview.png differ diff --git a/tests/e2e/screenshots/paste-attachment-transcript.png b/tests/e2e/screenshots/paste-attachment-transcript.png index edd9a9defb..e54a93365e 100644 Binary files a/tests/e2e/screenshots/paste-attachment-transcript.png and b/tests/e2e/screenshots/paste-attachment-transcript.png differ diff --git a/tests/e2e/screenshots/portrait-panel-controls-footer-row.png b/tests/e2e/screenshots/portrait-panel-controls-footer-row.png index f20db32751..fe341a2278 100644 Binary files a/tests/e2e/screenshots/portrait-panel-controls-footer-row.png and b/tests/e2e/screenshots/portrait-panel-controls-footer-row.png differ diff --git a/tests/e2e/screenshots/portrait-panel-controls-with-panel.png b/tests/e2e/screenshots/portrait-panel-controls-with-panel.png index 94d354ee73..9f9e6499e8 100644 Binary files a/tests/e2e/screenshots/portrait-panel-controls-with-panel.png and b/tests/e2e/screenshots/portrait-panel-controls-with-panel.png differ diff --git a/tests/e2e/screenshots/pr-panel-gh-unavailable.png b/tests/e2e/screenshots/pr-panel-gh-unavailable.png index 441fda36bb..0f66d96c44 100644 Binary files a/tests/e2e/screenshots/pr-panel-gh-unavailable.png and b/tests/e2e/screenshots/pr-panel-gh-unavailable.png differ diff --git a/tests/e2e/screenshots/pr-panel-linked-list.png b/tests/e2e/screenshots/pr-panel-linked-list.png index 383c612108..d7aed56cbf 100644 Binary files a/tests/e2e/screenshots/pr-panel-linked-list.png and b/tests/e2e/screenshots/pr-panel-linked-list.png differ diff --git a/tests/e2e/screenshots/pr-panel-viewer.png b/tests/e2e/screenshots/pr-panel-viewer.png index 383c612108..d7aed56cbf 100644 Binary files a/tests/e2e/screenshots/pr-panel-viewer.png and b/tests/e2e/screenshots/pr-panel-viewer.png differ diff --git a/tests/e2e/screenshots/project-switch-b.png b/tests/e2e/screenshots/project-switch-b.png index 0b8d116930..dd2a15d6fa 100644 Binary files a/tests/e2e/screenshots/project-switch-b.png and b/tests/e2e/screenshots/project-switch-b.png differ diff --git a/tests/e2e/screenshots/queued-held.png b/tests/e2e/screenshots/queued-held.png index fe76349169..32b897e54a 100644 Binary files a/tests/e2e/screenshots/queued-held.png and b/tests/e2e/screenshots/queued-held.png differ diff --git a/tests/e2e/screenshots/queued-message-delete-before.png b/tests/e2e/screenshots/queued-message-delete-before.png index 542f77cf80..09207789dd 100644 Binary files a/tests/e2e/screenshots/queued-message-delete-before.png and b/tests/e2e/screenshots/queued-message-delete-before.png differ diff --git a/tests/e2e/screenshots/read-outside-approval-collapsed.png b/tests/e2e/screenshots/read-outside-approval-collapsed.png index b7698ea7a7..875863369c 100644 Binary files a/tests/e2e/screenshots/read-outside-approval-collapsed.png and b/tests/e2e/screenshots/read-outside-approval-collapsed.png differ diff --git a/tests/e2e/screenshots/read-outside-approval-expanded.png b/tests/e2e/screenshots/read-outside-approval-expanded.png index dfa3570219..55aa2dab04 100644 Binary files a/tests/e2e/screenshots/read-outside-approval-expanded.png and b/tests/e2e/screenshots/read-outside-approval-expanded.png differ diff --git a/tests/e2e/screenshots/roadmap-attachments-pending.png b/tests/e2e/screenshots/roadmap-attachments-pending.png index 32f9d2dfcd..36aef42712 100644 Binary files a/tests/e2e/screenshots/roadmap-attachments-pending.png and b/tests/e2e/screenshots/roadmap-attachments-pending.png differ diff --git a/tests/e2e/screenshots/roadmap-attachments-saved.png b/tests/e2e/screenshots/roadmap-attachments-saved.png index 3e04ab3048..45fee196f8 100644 Binary files a/tests/e2e/screenshots/roadmap-attachments-saved.png and b/tests/e2e/screenshots/roadmap-attachments-saved.png differ diff --git a/tests/e2e/screenshots/roadmap-category-filter.png b/tests/e2e/screenshots/roadmap-category-filter.png index 3fbabbbec5..fc2e9e57df 100644 Binary files a/tests/e2e/screenshots/roadmap-category-filter.png and b/tests/e2e/screenshots/roadmap-category-filter.png differ diff --git a/tests/e2e/screenshots/roadmap-done-filtered.png b/tests/e2e/screenshots/roadmap-done-filtered.png index d52efaeb3f..ed600bcbc1 100644 Binary files a/tests/e2e/screenshots/roadmap-done-filtered.png and b/tests/e2e/screenshots/roadmap-done-filtered.png differ diff --git a/tests/e2e/screenshots/roadmap-done-hidden-default.png b/tests/e2e/screenshots/roadmap-done-hidden-default.png index db949c97e4..d6d3b359fb 100644 Binary files a/tests/e2e/screenshots/roadmap-done-hidden-default.png and b/tests/e2e/screenshots/roadmap-done-hidden-default.png differ diff --git a/tests/e2e/screenshots/roadmap-done-revealed.png b/tests/e2e/screenshots/roadmap-done-revealed.png index b08cf41dbd..38bb0ddd1d 100644 Binary files a/tests/e2e/screenshots/roadmap-done-revealed.png and b/tests/e2e/screenshots/roadmap-done-revealed.png differ diff --git a/tests/e2e/screenshots/roadmap-form-buttons.png b/tests/e2e/screenshots/roadmap-form-buttons.png index 55f986efaf..9dd47fcadb 100644 Binary files a/tests/e2e/screenshots/roadmap-form-buttons.png and b/tests/e2e/screenshots/roadmap-form-buttons.png differ diff --git a/tests/e2e/screenshots/roadmap-list-rows.png b/tests/e2e/screenshots/roadmap-list-rows.png index 1e6c4cd32f..76c55b9b7a 100644 Binary files a/tests/e2e/screenshots/roadmap-list-rows.png and b/tests/e2e/screenshots/roadmap-list-rows.png differ diff --git a/tests/e2e/screenshots/roadmap-review-badges.png b/tests/e2e/screenshots/roadmap-review-badges.png index ee5c59b64a..0bc4709aae 100644 Binary files a/tests/e2e/screenshots/roadmap-review-badges.png and b/tests/e2e/screenshots/roadmap-review-badges.png differ diff --git a/tests/e2e/screenshots/roadmap-review-failure.png b/tests/e2e/screenshots/roadmap-review-failure.png index 3c596b3a27..9921a6683d 100644 Binary files a/tests/e2e/screenshots/roadmap-review-failure.png and b/tests/e2e/screenshots/roadmap-review-failure.png differ diff --git a/tests/e2e/screenshots/roadmap-search-palette-opened.png b/tests/e2e/screenshots/roadmap-search-palette-opened.png index 9d9b554dfa..e811999f3b 100644 Binary files a/tests/e2e/screenshots/roadmap-search-palette-opened.png and b/tests/e2e/screenshots/roadmap-search-palette-opened.png differ diff --git a/tests/e2e/screenshots/roadmap-search-palette.png b/tests/e2e/screenshots/roadmap-search-palette.png index c81e83beac..366f2d079a 100644 Binary files a/tests/e2e/screenshots/roadmap-search-palette.png and b/tests/e2e/screenshots/roadmap-search-palette.png differ diff --git a/tests/e2e/screenshots/roadmap-thread-reopen-tracked.png b/tests/e2e/screenshots/roadmap-thread-reopen-tracked.png index 34fc7af16d..db84dfadfb 100644 Binary files a/tests/e2e/screenshots/roadmap-thread-reopen-tracked.png and b/tests/e2e/screenshots/roadmap-thread-reopen-tracked.png differ diff --git a/tests/e2e/screenshots/roadmap-thread-reopened.png b/tests/e2e/screenshots/roadmap-thread-reopened.png index 13a9e40360..a744888c29 100644 Binary files a/tests/e2e/screenshots/roadmap-thread-reopened.png and b/tests/e2e/screenshots/roadmap-thread-reopened.png differ diff --git a/tests/e2e/screenshots/selection-highlight-light.png b/tests/e2e/screenshots/selection-highlight-light.png index dee629b30e..690d625206 100644 Binary files a/tests/e2e/screenshots/selection-highlight-light.png and b/tests/e2e/screenshots/selection-highlight-light.png differ diff --git a/tests/e2e/screenshots/settings-browser-tools.png b/tests/e2e/screenshots/settings-browser-tools.png index eee5ade6b3..892aa498cf 100644 Binary files a/tests/e2e/screenshots/settings-browser-tools.png and b/tests/e2e/screenshots/settings-browser-tools.png differ diff --git a/tests/e2e/screenshots/settings-customise.png b/tests/e2e/screenshots/settings-customise.png index 8b68ecbb6b..9cc8d8c355 100644 Binary files a/tests/e2e/screenshots/settings-customise.png and b/tests/e2e/screenshots/settings-customise.png differ diff --git a/tests/e2e/screenshots/settings-perplexity-agent-provider.png b/tests/e2e/screenshots/settings-perplexity-agent-provider.png index 97b574b3dc..2c083f8408 100644 Binary files a/tests/e2e/screenshots/settings-perplexity-agent-provider.png and b/tests/e2e/screenshots/settings-perplexity-agent-provider.png differ diff --git a/tests/e2e/screenshots/settings-provider-host-approval.png b/tests/e2e/screenshots/settings-provider-host-approval.png index af7cd3e3d8..6343299715 100644 Binary files a/tests/e2e/screenshots/settings-provider-host-approval.png and b/tests/e2e/screenshots/settings-provider-host-approval.png differ diff --git a/tests/e2e/screenshots/settings-read-terminal.png b/tests/e2e/screenshots/settings-read-terminal.png index eee5ade6b3..892aa498cf 100644 Binary files a/tests/e2e/screenshots/settings-read-terminal.png and b/tests/e2e/screenshots/settings-read-terminal.png differ diff --git a/tests/e2e/screenshots/settings-selected-plugin.png b/tests/e2e/screenshots/settings-selected-plugin.png index c0c488dc35..e5e1b93602 100644 Binary files a/tests/e2e/screenshots/settings-selected-plugin.png and b/tests/e2e/screenshots/settings-selected-plugin.png differ diff --git a/tests/e2e/screenshots/settings-sources-hook-test.png b/tests/e2e/screenshots/settings-sources-hook-test.png index a48610c2f7..007a16a093 100644 Binary files a/tests/e2e/screenshots/settings-sources-hook-test.png and b/tests/e2e/screenshots/settings-sources-hook-test.png differ diff --git a/tests/e2e/screenshots/settings-sources-untrusted-instructions.png b/tests/e2e/screenshots/settings-sources-untrusted-instructions.png index 2bcc591ca7..21c06f854f 100644 Binary files a/tests/e2e/screenshots/settings-sources-untrusted-instructions.png and b/tests/e2e/screenshots/settings-sources-untrusted-instructions.png differ diff --git a/tests/e2e/screenshots/settings-storage-project-picker.png b/tests/e2e/screenshots/settings-storage-project-picker.png index 21e2bb9d1e..79be808dfe 100644 Binary files a/tests/e2e/screenshots/settings-storage-project-picker.png and b/tests/e2e/screenshots/settings-storage-project-picker.png differ diff --git a/tests/e2e/screenshots/settings-usage-plan-auth-errors.png b/tests/e2e/screenshots/settings-usage-plan-auth-errors.png index 224976a5a1..f29793849b 100644 Binary files a/tests/e2e/screenshots/settings-usage-plan-auth-errors.png and b/tests/e2e/screenshots/settings-usage-plan-auth-errors.png differ diff --git a/tests/e2e/screenshots/settings-worktree-actions.png b/tests/e2e/screenshots/settings-worktree-actions.png index 665c9ff1f7..28c01d0010 100644 Binary files a/tests/e2e/screenshots/settings-worktree-actions.png and b/tests/e2e/screenshots/settings-worktree-actions.png differ diff --git a/tests/e2e/screenshots/settings-worktree-cleanup-confirm.png b/tests/e2e/screenshots/settings-worktree-cleanup-confirm.png index 6b26b67ea6..5c41a26357 100644 Binary files a/tests/e2e/screenshots/settings-worktree-cleanup-confirm.png and b/tests/e2e/screenshots/settings-worktree-cleanup-confirm.png differ diff --git a/tests/e2e/screenshots/settings-worktree-edits.png b/tests/e2e/screenshots/settings-worktree-edits.png index 137d21530d..2c1929f255 100644 Binary files a/tests/e2e/screenshots/settings-worktree-edits.png and b/tests/e2e/screenshots/settings-worktree-edits.png differ diff --git a/tests/e2e/screenshots/ssh-project-add-menu.png b/tests/e2e/screenshots/ssh-project-add-menu.png index b7481685d5..decc229c2c 100644 Binary files a/tests/e2e/screenshots/ssh-project-add-menu.png and b/tests/e2e/screenshots/ssh-project-add-menu.png differ diff --git a/tests/e2e/screenshots/ssh-projects-pane-after-enable.png b/tests/e2e/screenshots/ssh-projects-pane-after-enable.png index ef89f2a382..8d8da25437 100644 Binary files a/tests/e2e/screenshots/ssh-projects-pane-after-enable.png and b/tests/e2e/screenshots/ssh-projects-pane-after-enable.png differ diff --git a/tests/e2e/screenshots/supervised-tasks-waiting.png b/tests/e2e/screenshots/supervised-tasks-waiting.png index 0000079923..0ab4bbca4f 100644 Binary files a/tests/e2e/screenshots/supervised-tasks-waiting.png and b/tests/e2e/screenshots/supervised-tasks-waiting.png differ diff --git a/tests/e2e/screenshots/terminal-shell-prompt.png b/tests/e2e/screenshots/terminal-shell-prompt.png index 12eb1a0acc..1bd0d1ef35 100644 Binary files a/tests/e2e/screenshots/terminal-shell-prompt.png and b/tests/e2e/screenshots/terminal-shell-prompt.png differ diff --git a/tests/e2e/screenshots/theme-boot-light.png b/tests/e2e/screenshots/theme-boot-light.png index 248cd20b25..02dfc18b9a 100644 Binary files a/tests/e2e/screenshots/theme-boot-light.png and b/tests/e2e/screenshots/theme-boot-light.png differ diff --git a/tests/e2e/screenshots/thread-cycle-shortcuts.png b/tests/e2e/screenshots/thread-cycle-shortcuts.png index 09a1605db1..d7be0ded5a 100644 Binary files a/tests/e2e/screenshots/thread-cycle-shortcuts.png and b/tests/e2e/screenshots/thread-cycle-shortcuts.png differ diff --git a/tests/e2e/screenshots/thread-hydration-failed-notice.png b/tests/e2e/screenshots/thread-hydration-failed-notice.png index 7247875309..166fbf0d34 100644 Binary files a/tests/e2e/screenshots/thread-hydration-failed-notice.png and b/tests/e2e/screenshots/thread-hydration-failed-notice.png differ diff --git a/tests/e2e/screenshots/thread-hydration-running-notice.png b/tests/e2e/screenshots/thread-hydration-running-notice.png index fd9d798d1a..9600e3b715 100644 Binary files a/tests/e2e/screenshots/thread-hydration-running-notice.png and b/tests/e2e/screenshots/thread-hydration-running-notice.png differ diff --git a/tests/e2e/screenshots/thread-proposal-started-isolated.png b/tests/e2e/screenshots/thread-proposal-started-isolated.png index 158fb9d19a..45155b93e9 100644 Binary files a/tests/e2e/screenshots/thread-proposal-started-isolated.png and b/tests/e2e/screenshots/thread-proposal-started-isolated.png differ diff --git a/tests/e2e/screenshots/thread-running-status-dots.png b/tests/e2e/screenshots/thread-running-status-dots.png index 4645fe1dc6..4622423ae2 100644 Binary files a/tests/e2e/screenshots/thread-running-status-dots.png and b/tests/e2e/screenshots/thread-running-status-dots.png differ diff --git a/tests/e2e/screenshots/thread-worktree-checkout-error.png b/tests/e2e/screenshots/thread-worktree-checkout-error.png index 2057a7a307..c9cdfd1bdc 100644 Binary files a/tests/e2e/screenshots/thread-worktree-checkout-error.png and b/tests/e2e/screenshots/thread-worktree-checkout-error.png differ diff --git a/tests/e2e/screenshots/thread-worktree-checkout-menu.png b/tests/e2e/screenshots/thread-worktree-checkout-menu.png index b2cef7484a..4b6a692688 100644 Binary files a/tests/e2e/screenshots/thread-worktree-checkout-menu.png and b/tests/e2e/screenshots/thread-worktree-checkout-menu.png differ diff --git a/tests/e2e/screenshots/titlebar-outline-icons.png b/tests/e2e/screenshots/titlebar-outline-icons.png index ac474b3ca1..4c38b7cae5 100644 Binary files a/tests/e2e/screenshots/titlebar-outline-icons.png and b/tests/e2e/screenshots/titlebar-outline-icons.png differ diff --git a/tests/e2e/screenshots/titlebar-workspace-name.png b/tests/e2e/screenshots/titlebar-workspace-name.png index 99bd777509..9f2115b947 100644 Binary files a/tests/e2e/screenshots/titlebar-workspace-name.png and b/tests/e2e/screenshots/titlebar-workspace-name.png differ diff --git a/tests/e2e/screenshots/todo-inline-panel.png b/tests/e2e/screenshots/todo-inline-panel.png index 34da45ea83..4957d92505 100644 Binary files a/tests/e2e/screenshots/todo-inline-panel.png and b/tests/e2e/screenshots/todo-inline-panel.png differ diff --git a/tests/e2e/screenshots/tool-activity-icon-alignment.png b/tests/e2e/screenshots/tool-activity-icon-alignment.png index 2c36c3ddb8..de66f0f959 100644 Binary files a/tests/e2e/screenshots/tool-activity-icon-alignment.png and b/tests/e2e/screenshots/tool-activity-icon-alignment.png differ diff --git a/tests/e2e/screenshots/tool-display-live-mock.png b/tests/e2e/screenshots/tool-display-live-mock.png index b0d9c7a934..321f90617c 100644 Binary files a/tests/e2e/screenshots/tool-display-live-mock.png and b/tests/e2e/screenshots/tool-display-live-mock.png differ diff --git a/tests/e2e/screenshots/tool-display-rollup-collapsed.png b/tests/e2e/screenshots/tool-display-rollup-collapsed.png index 2a82398e93..8ea5a43c5a 100644 Binary files a/tests/e2e/screenshots/tool-display-rollup-collapsed.png and b/tests/e2e/screenshots/tool-display-rollup-collapsed.png differ diff --git a/tests/e2e/screenshots/ui-scale-native-window-default.png b/tests/e2e/screenshots/ui-scale-native-window-default.png index f1c62cba7e..5e8c0e84fa 100644 Binary files a/tests/e2e/screenshots/ui-scale-native-window-default.png and b/tests/e2e/screenshots/ui-scale-native-window-default.png differ diff --git a/tests/e2e/screenshots/user-prompt-fold-expanded.png b/tests/e2e/screenshots/user-prompt-fold-expanded.png index 3167c30bfc..911f96450e 100644 Binary files a/tests/e2e/screenshots/user-prompt-fold-expanded.png and b/tests/e2e/screenshots/user-prompt-fold-expanded.png differ diff --git a/tests/e2e/screenshots/user-prompt-markdown.png b/tests/e2e/screenshots/user-prompt-markdown.png index 0239629ea7..610dc5a842 100644 Binary files a/tests/e2e/screenshots/user-prompt-markdown.png and b/tests/e2e/screenshots/user-prompt-markdown.png differ diff --git a/tests/e2e/screenshots/video-attachment-chip.png b/tests/e2e/screenshots/video-attachment-chip.png index d249d835b1..875e3532a1 100644 Binary files a/tests/e2e/screenshots/video-attachment-chip.png and b/tests/e2e/screenshots/video-attachment-chip.png differ diff --git a/tests/e2e/screenshots/video-attachment-transcript.png b/tests/e2e/screenshots/video-attachment-transcript.png index 85d354cec2..4d1f305342 100644 Binary files a/tests/e2e/screenshots/video-attachment-transcript.png and b/tests/e2e/screenshots/video-attachment-transcript.png differ diff --git a/tests/e2e/screenshots/vnc-viewer-shared-screen.png b/tests/e2e/screenshots/vnc-viewer-shared-screen.png index cdbc1e01b9..113625f13b 100644 Binary files a/tests/e2e/screenshots/vnc-viewer-shared-screen.png and b/tests/e2e/screenshots/vnc-viewer-shared-screen.png differ