mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 00:05:51 +00:00
feat(acp): bypassPermissions 默认显示,去掉 opt-in 限制
之前 bypassPermissions 需要本地显式 opt-in(ACP_PERMISSION_MODE 环境变量、 CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS 环境变量、或 settings.permissions.defaultMode) 才会出现在 modes 列表里 —— 标准客户端看不到这个 mode,永远没法切换。 去掉 opt-in 后,只要进程级允许(非 root 或 IS_SANDBOX=1)就显示。 - permissionMode: isAcpBypassPermissionModeAvailable 只保留进程级检查,删除 isAcpBypassLocallyEnabled / isSettingsBypassPermissionMode / isTruthyEnv 等 只服务于 opt-in 的辅助函数 - createSessionMethod: 调用方去掉 settingsMode 参数 - agent.test: 反转所有依赖 "bypass 需要 opt-in" 的断言 Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
@@ -411,29 +411,23 @@ describe('AcpAgent', () => {
|
|||||||
expect(res.modes?.currentModeId).toBe('plan')
|
expect(res.modes?.currentModeId).toBe('plan')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('rejects _meta.permissionMode bypass without a local ACP bypass gate', async () => {
|
test('honors _meta.permissionMode bypass without any opt-in (always available when process allows)', async () => {
|
||||||
mockGetSettings.mockImplementationOnce(() => ({
|
// bypass is exposed by default; only the root/sandbox process guard remains.
|
||||||
permissions: { defaultMode: 'acceptEdits' },
|
|
||||||
}))
|
|
||||||
const consoleErrorSpy = spyOn(console, 'error').mockImplementation(
|
|
||||||
() => {},
|
|
||||||
)
|
|
||||||
const agent = new AcpAgent(makeConn())
|
const agent = new AcpAgent(makeConn())
|
||||||
try {
|
const res = await agent.newSession({
|
||||||
await expect(
|
cwd: '/tmp',
|
||||||
agent.newSession({
|
_meta: { permissionMode: 'bypassPermissions' },
|
||||||
cwd: '/tmp',
|
} as any)
|
||||||
_meta: { permissionMode: 'bypassPermissions' },
|
|
||||||
} as any),
|
|
||||||
).rejects.toThrow('Mode not available: bypassPermissions')
|
|
||||||
|
|
||||||
expect(consoleErrorSpy).not.toHaveBeenCalled()
|
expect(res.modes?.currentModeId).toBe('bypassPermissions')
|
||||||
} finally {
|
expect(res.modes?.availableModes.map((mode: any) => mode.id)).toContain(
|
||||||
consoleErrorSpy.mockRestore()
|
'bypassPermissions',
|
||||||
}
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('honors _meta.permissionMode bypass with a local ACP bypass gate', async () => {
|
test('honors _meta.permissionMode bypass regardless of local env gate', async () => {
|
||||||
|
// The old CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS opt-in no longer gates availability,
|
||||||
|
// but setting it should still not break the request.
|
||||||
process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS = '1'
|
process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS = '1'
|
||||||
const agent = new AcpAgent(makeConn())
|
const agent = new AcpAgent(makeConn())
|
||||||
const res = await agent.newSession({
|
const res = await agent.newSession({
|
||||||
@@ -987,28 +981,15 @@ describe('AcpAgent', () => {
|
|||||||
).rejects.toThrow('Session not found')
|
).rejects.toThrow('Session not found')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('availableModes excludes bypassPermissions without a local ACP bypass gate', async () => {
|
test('availableModes includes bypassPermissions by default (no opt-in needed)', async () => {
|
||||||
const agent = new AcpAgent(makeConn())
|
const agent = new AcpAgent(makeConn())
|
||||||
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
const session = agent.sessions.get(sessionId)
|
const session = agent.sessions.get(sessionId)
|
||||||
const modeIds = session?.modes.availableModes.map((m: any) => m.id)
|
const modeIds = session?.modes.availableModes.map((m: any) => m.id)
|
||||||
expect(modeIds).not.toContain('bypassPermissions')
|
expect(modeIds).toContain('bypassPermissions')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('rejects bypassPermissions without a local ACP bypass gate', async () => {
|
test('can switch to bypassPermissions without any opt-in gate', async () => {
|
||||||
const agent = new AcpAgent(makeConn())
|
|
||||||
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
|
||||||
await expect(
|
|
||||||
agent.setSessionMode({ sessionId, modeId: 'bypassPermissions' } as any),
|
|
||||||
).rejects.toThrow('Mode not available')
|
|
||||||
|
|
||||||
const session = agent.sessions.get(sessionId)
|
|
||||||
expect(session?.modes.currentModeId).toBe('default')
|
|
||||||
expect(session?.appState.toolPermissionContext.mode).toBe('default')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('can switch to bypassPermissions mode with a local ACP bypass gate', async () => {
|
|
||||||
process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS = '1'
|
|
||||||
const agent = new AcpAgent(makeConn())
|
const agent = new AcpAgent(makeConn())
|
||||||
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
await agent.setSessionMode({
|
await agent.setSessionMode({
|
||||||
@@ -1023,7 +1004,8 @@ describe('AcpAgent', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('rejects bypassPermissions when the session does not expose it', async () => {
|
test('rejects bypassPermissions when the session does not expose it', async () => {
|
||||||
process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS = '1'
|
// Even though bypass is available by default, removeBypassMode simulates a session
|
||||||
|
// where the mode was stripped (e.g., future custom filter). The rejection still fires.
|
||||||
const agent = new AcpAgent(makeConn())
|
const agent = new AcpAgent(makeConn())
|
||||||
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
const { sessionId } = await agent.newSession({ cwd: '/tmp' } as any)
|
||||||
const session = agent.sessions.get(sessionId)
|
const session = agent.sessions.get(sessionId)
|
||||||
@@ -1069,15 +1051,17 @@ describe('AcpAgent', () => {
|
|||||||
const session = agent.sessions.get(sessionId)
|
const session = agent.sessions.get(sessionId)
|
||||||
removeBypassMode(session)
|
removeBypassMode(session)
|
||||||
|
|
||||||
// The value is rejected because it is not in the option's listed values
|
// bypassPermissions passes the config-option layer (it's still listed in the
|
||||||
// (config-option validation runs before the mode-availability check).
|
// option's options array — removeBypassMode only strips it from modes.availableModes
|
||||||
|
// and isBypassPermissionsModeAvailable), then applySessionMode rejects it with
|
||||||
|
// "Mode not available". This covers the second of the two validation layers.
|
||||||
await expect(
|
await expect(
|
||||||
agent.setSessionConfigOption({
|
agent.setSessionConfigOption({
|
||||||
sessionId,
|
sessionId,
|
||||||
configId: 'mode',
|
configId: 'mode',
|
||||||
value: 'bypassPermissions',
|
value: 'bypassPermissions',
|
||||||
} as any),
|
} as any),
|
||||||
).rejects.toThrow(/Invalid value 'bypassPermissions'/)
|
).rejects.toThrow('Mode not available')
|
||||||
|
|
||||||
expect(session?.modes.currentModeId).toBe('default')
|
expect(session?.modes.currentModeId).toBe('default')
|
||||||
expect(session?.appState.toolPermissionContext.mode).toBe('default')
|
expect(session?.appState.toolPermissionContext.mode).toBe('default')
|
||||||
|
|||||||
@@ -137,10 +137,10 @@ async function createSession(
|
|||||||
// Parse MCP servers from ACP params
|
// Parse MCP servers from ACP params
|
||||||
// MCP server config is handled separately in the tools system
|
// MCP server config is handled separately in the tools system
|
||||||
|
|
||||||
// ACP clients can expose bypass only when both the process and local config allow it.
|
// bypassPermissions is exposed to ACP clients whenever the process itself allows it
|
||||||
const isBypassAvailable = isAcpBypassPermissionModeAvailable(
|
// (non-root or sandbox). The previous additional opt-in gate made the mode invisible
|
||||||
settingsPermissionMode,
|
// to standard clients and defeated the purpose of listing it. See permissionMode.ts.
|
||||||
)
|
const isBypassAvailable = isAcpBypassPermissionModeAvailable()
|
||||||
|
|
||||||
// Create a mutable AppState for the session
|
// Create a mutable AppState for the session
|
||||||
const appState: AppState = {
|
const appState: AppState = {
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ export function resolveSessionPermissionMode(
|
|||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
metaResolved === 'bypassPermissions' &&
|
metaResolved === 'bypassPermissions' &&
|
||||||
!isAcpBypassPermissionModeAvailable(settingsMode)
|
!isAcpBypassPermissionModeAvailable()
|
||||||
) {
|
) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Mode not available: bypassPermissions requires a local ACP bypass opt-in.',
|
'Mode not available: bypassPermissions cannot run as root (start the agent as a non-root user, or set IS_SANDBOX=1).',
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,14 +78,20 @@ export function hasOwnField(
|
|||||||
return !!value && Object.hasOwn(value, key)
|
return !!value && Object.hasOwn(value, key)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAcpBypassPermissionModeAvailable(
|
/**
|
||||||
settingsMode?: unknown,
|
* Whether bypassPermissions is selectable by ACP clients.
|
||||||
): boolean {
|
*
|
||||||
return (
|
* The previous implementation required a local opt-in (ACP_PERMISSION_MODE env var,
|
||||||
isProcessBypassPermissionModeAvailable() &&
|
* CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS env var, or settings.permissions.defaultMode).
|
||||||
(isAcpBypassLocallyEnabled() ||
|
* That gate made the mode invisible to standard clients unless the operator already
|
||||||
isSettingsBypassPermissionMode(settingsMode))
|
* pre-configured it — defeating the point of exposing it through the ACP mode list.
|
||||||
)
|
*
|
||||||
|
* The only remaining guard is the process-level one: bypass must not silently run
|
||||||
|
* as root (where every skipped permission check is a privilege boundary crossed),
|
||||||
|
* unless explicitly marked as a sandbox.
|
||||||
|
*/
|
||||||
|
export function isAcpBypassPermissionModeAvailable(): boolean {
|
||||||
|
return isProcessBypassPermissionModeAvailable()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProcessBypassPermissionModeAvailable(): boolean {
|
function isProcessBypassPermissionModeAvailable(): boolean {
|
||||||
@@ -94,22 +100,3 @@ function isProcessBypassPermissionModeAvailable(): boolean {
|
|||||||
if (typeof process.getuid === 'function') return process.getuid() !== 0
|
if (typeof process.getuid === 'function') return process.getuid() !== 0
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAcpBypassLocallyEnabled(): boolean {
|
|
||||||
return (
|
|
||||||
process.env.ACP_PERMISSION_MODE === 'bypassPermissions' ||
|
|
||||||
isTruthyEnv(process.env.CLAUDE_CODE_ACP_ALLOW_BYPASS_PERMISSIONS)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function isSettingsBypassPermissionMode(settingsMode: unknown): boolean {
|
|
||||||
try {
|
|
||||||
return resolvePermissionMode(settingsMode) === 'bypassPermissions'
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isTruthyEnv(value: string | undefined): boolean {
|
|
||||||
return value === '1' || value?.toLowerCase() === 'true'
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user