Fix/bypass root confirm (#1275)

* fix(ink): 主屏幕模式周期性终端重绘, 防止长时间运行 TUI 显示腐蚀

- 添加 lastMainScreenHealTime 字段, 每 5 秒触发一次全量终端重绘
- 使用 wall-clock 时间替代帧计数, 避免 drain frames (250fps) 加速周期
- 添加 isTTY 守卫, 防止非 TTY 环境泄漏 ANSI 转义序列
- 扩展 needsEraseBeforePaint 到主屏幕模式, BSU/ESU 确保原子性无闪烁
- 修复 log-update cursor 漂移和 blit ghosting 导致的文字重叠/残留

Co-Authored-By: deepseek-v4-pro <deepseek-ai@claude-code-best.win>

* fix(messages): lookups 缓存感知 progress tick 替换, 修复 Bash 进度时间卡死

REPL.tsx 用原地替换处理 ephemeral progress (Bash/PowerShell/MCP) 以
限制 messages 数组增长, 但 computeMessageStructureKey 只把 parentToolUseID
计入 key, 替换前后 key 完全相同, Messages.tsx 的 lookups 缓存命中,
updateMessageLookupsIncremental 长度相同时又直接返回 existing, 导致
progressMessagesByToolUseID 永远停在首条 tick, ShellProgressMessage 的
elapsed time 卡在首次显示值不动.

- computeMessageStructureKey: 加入 progress.uuid, tick 替换后 key 必变
- updateMessageLookupsIncremental: 长度相同 + 末尾为 progress 时返回 null
  触发 full rebuild, 让新 tick 进入 progressMessagesByToolUseID

补充 4 个测试覆盖 bug 行为与 fast path 保护.

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

* fix: bypass 模式在 root/sudo 下改为警告 + y 确认而非直接退出

交互式 TTY 下打印风险警告并等待用户输入 y 才进入 bypass 模式;
非 TTY (pipe/ACP/CI) 维持原 exit(1) 行为,因为无法交互确认。

Co-Authored-By: glm-5.2 <zai-org@claude-code-best.win>

---------

Co-authored-by: deepseek-v4-pro <deepseek-ai@claude-code-best.win>
Co-authored-by: glm-5.2 <zai-org@claude-code-best.win>
This commit is contained in:
claude-code-best
2026-06-19 10:17:47 +08:00
committed by GitHub
parent bca27589c2
commit f69c705166
4 changed files with 255 additions and 6 deletions

View File

@@ -401,10 +401,39 @@ export async function setup(
process.env.IS_SANDBOX !== '1' &&
!isEnvTruthy(process.env.CLAUDE_CODE_BUBBLEWRAP)
) {
console.error(
`--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons`,
)
process.exit(1)
// Root + bypass = every tool call executes without review at uid 0.
// Interactive TTY: warn and require explicit "y" to proceed.
// Non-interactive (pipe, ACP, CI, no TTY): cannot prompt, must abort.
if (process.stdin.isTTY) {
console.error(
chalk.bold.red(
'WARNING: Running as root/sudo with bypass permissions mode is dangerous.',
),
)
console.error(
chalk.yellow(
'Bypass mode skips ALL permission checks. Combined with root, any command (rm -rf /, chmod, dd) executes without review.',
),
)
const readline = await import('readline')
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
const answer = await new Promise<string>(resolve => {
rl.question('\nI understand the risks. Continue? [y/N] ', resolve)
})
rl.close()
if (answer.trim().toLowerCase() !== 'y') {
console.error('Aborted.')
process.exit(1)
}
} else {
console.error(
`--dangerously-skip-permissions cannot be used with root/sudo privileges for security reasons`,
)
process.exit(1)
}
}
if (

View File

@@ -16,6 +16,7 @@ import {
createUserInterruptionMessage,
prepareUserContent,
createToolResultStopMessage,
createProgressMessage,
extractTag,
isNotEmptyMessage,
deriveUUID,
@@ -28,6 +29,9 @@ import {
DONT_ASK_REJECT_MESSAGE,
SYNTHETIC_MODEL,
ensureToolResultPairing,
buildMessageLookups,
updateMessageLookupsIncremental,
computeMessageStructureKey,
} from '../messages'
import type {
Message,
@@ -786,3 +790,168 @@ describe('normalizeMessagesForAPI thinking + tool_use same turn (CC-1215)',
}
})
})
// ─── Progress tick replace (Bash/PowerShell elapsed-time freeze) ──────────
describe('computeMessageStructureKey + updateMessageLookupsIncremental: progress replace', () => {
// REPL.tsx replaces ephemeral progress ticks (Bash/PowerShell/MCP) in-place
// to bound the messages array. The lookups cache must invalidate when the
// trailing progress tick changes, or ShellProgressMessage's elapsed time
// freezes at the first tick forever.
type BashProgress = {
type: 'bash_progress'
elapsedTimeSeconds: number
output: string
fullOutput: string
}
function makeAssistantWithToolUse(toolUseID: string): Message {
return createAssistantMessage({
content: [
{
type: 'tool_use',
id: toolUseID,
name: 'Bash',
input: { command: 'sleep 10' },
} as any,
],
})
}
function makeProgress(
parentToolUseID: string,
uuid: `${string}-${string}-${string}-${string}-${string}`,
elapsedTimeSeconds: number,
) {
const msg = createProgressMessage<BashProgress>({
toolUseID: `bash-progress-${elapsedTimeSeconds}`,
parentToolUseID,
data: {
type: 'bash_progress',
elapsedTimeSeconds,
output: '',
fullOutput: '',
},
})
// Override uuid so the test is deterministic (createProgressMessage
// generates a random uuid).
return { ...msg, uuid }
}
test('computeMessageStructureKey distinguishes progress ticks by uuid', () => {
const assistant = makeAssistantWithToolUse('bash-1')
const normalized = normalizeMessages([assistant])
const progress1 = makeProgress(
'bash-1',
'00000000-0000-0000-0000-000000000001',
3,
)
const progress2 = makeProgress(
'bash-1',
'00000000-0000-0000-0000-000000000002',
4,
)
const keyBefore = computeMessageStructureKey(
[...normalized, progress1 as any],
[...normalized, progress1 as any] as any,
)
const keyAfter = computeMessageStructureKey(
[...normalized, progress2 as any],
[...normalized, progress2 as any] as any,
)
// Same parentToolUseID, same length, but different uuid (tick replace).
// Without uuid in the key, these would be identical and the lookups cache
// would freeze on the first tick.
expect(keyBefore).not.toEqual(keyAfter)
})
test('updateMessageLookupsIncremental returns null when trailing progress was replaced (same length)', () => {
const assistant = makeAssistantWithToolUse('bash-1')
const normalized = normalizeMessages([assistant])
const progress1 = makeProgress(
'bash-1',
'00000000-0000-0000-0000-000000000001',
3,
)
const progress2 = makeProgress(
'bash-1',
'00000000-0000-0000-0000-000000000002',
4,
)
const withProgress1 = [...normalized, progress1 as any]
const withProgress2 = [...normalized, progress2 as any]
const existing = buildMessageLookups(
withProgress1 as any,
withProgress1 as any,
)
// Same length, but the trailing progress is a fresh tick. Returning
// `existing` here would leave progressMessagesByToolUseID stuck on u1.
const result = updateMessageLookupsIncremental(
existing,
withProgress1.length,
withProgress1.length,
withProgress2 as any,
withProgress2 as any,
)
expect(result).toBeNull()
})
test('updateMessageLookupsIncremental still returns existing when length same and trailing is NOT progress', () => {
// Protect the original streaming-delta fast path: content-only changes
// on a non-progress trailing message should not trigger a full rebuild.
const assistant = makeAssistantWithToolUse('bash-1')
const normalized = normalizeMessages([assistant])
const existing = buildMessageLookups(normalized as any, normalized as any)
const result = updateMessageLookupsIncremental(
existing,
normalized.length,
normalized.length,
normalized as any,
normalized as any,
)
expect(result).toBe(existing)
})
test('full rebuild after progress replace yields the new tick in progressMessagesByToolUseID', () => {
// End-to-end: buildMessageLookups after a tick replace must reflect the
// fresh progress, not the stale one. This is what Messages.tsx falls back
// to when updateMessageLookupsIncremental returns null.
const assistant = makeAssistantWithToolUse('bash-1')
const normalized = normalizeMessages([assistant])
const progress1 = makeProgress(
'bash-1',
'00000000-0000-0000-0000-000000000001',
3,
)
const progress2 = makeProgress(
'bash-1',
'00000000-0000-0000-0000-000000000002',
4,
)
const withProgress2 = [...normalized, progress2 as any]
const rebuilt = buildMessageLookups(
withProgress2 as any,
withProgress2 as any,
)
const arr = rebuilt.progressMessagesByToolUseID.get('bash-1')
expect(arr).toBeDefined()
expect(arr).toHaveLength(1)
expect(arr![0].uuid).toBe('00000000-0000-0000-0000-000000000002')
expect((arr![0].data as BashProgress).elapsedTimeSeconds).toBe(4)
})
})

View File

@@ -1417,11 +1417,21 @@ export function updateMessageLookupsIncremental(
return null
}
// No new messages — nothing to do
// No new messages — nothing to do, UNLESS the trailing message is a
// progress tick. REPL.tsx replaces ephemeral progress (Bash/PowerShell/MCP)
// in-place to bound the messages array — same length, but the trailing
// progress is a fresh tick. Returning `existing` here would leave
// progressMessagesByToolUseID stuck on the first tick and elapsed-time
// displays (ShellProgressMessage) would freeze. Force a full rebuild so
// the fresh tick propagates.
if (
normalizedMessages.length === previousNormalizedCount &&
messages.length === previousMessageCount
) {
const lastNormalized = normalizedMessages[normalizedMessages.length - 1]
if (lastNormalized && lastNormalized.type === 'progress') {
return null
}
return existing
}
@@ -1605,7 +1615,13 @@ export function computeMessageStructureKey(
}
for (const msg of normalizedMessages) {
if (msg.type === 'progress') {
parts.push('p', (msg as ProgressMessage).parentToolUseID as string)
const pMsg = msg as ProgressMessage
// Include uuid so ephemeral progress tick replacements
// (Bash/PowerShell/MCP) invalidate the lookups cache. Without this,
// REPL.tsx's in-place tick replacement (same parentToolUseID, same
// length) yields an identical key, lookups cache the first tick
// forever, and ShellProgressMessage's elapsed time freezes.
parts.push('p', pMsg.parentToolUseID as string, pMsg.uuid)
}
}
return parts.join(',')