From 2bb6540b2d208c2cf345cacfe064a59d66031b1d Mon Sep 17 00:00:00 2001 From: Winniy Date: Sat, 23 May 2026 20:02:27 +0800 Subject: [PATCH] fix trae solo auto approval --- .../src/providers/approval-utils.d.ts | 2 ++ .../src/providers/approval-utils.ts | 27 +++++++++++++++---- .../src/providers/cli-provider-instance.ts | 11 +++++++- .../src/providers/ide-provider-instance.ts | 24 +++++++++++------ .../test/providers/approval-utils.test.ts | 21 +++++++++++++++ 5 files changed, 71 insertions(+), 14 deletions(-) diff --git a/packages/daemon-core/src/providers/approval-utils.d.ts b/packages/daemon-core/src/providers/approval-utils.d.ts index cd216d9d..bcb6d306 100644 --- a/packages/daemon-core/src/providers/approval-utils.d.ts +++ b/packages/daemon-core/src/providers/approval-utils.d.ts @@ -1,7 +1,9 @@ import type { ProviderModule } from './contracts.js'; +export declare function isUnsafeApprovalButtonLabel(value: string): boolean; export declare function getApprovalPositiveHints(provider?: Pick | null): string[]; export declare function pickApprovalButton(buttons: string[] | null | undefined, provider?: Pick | null): { index: number; label: string; + unsafe?: boolean; }; export declare function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string; diff --git a/packages/daemon-core/src/providers/approval-utils.ts b/packages/daemon-core/src/providers/approval-utils.ts index 41245b24..2b838e12 100644 --- a/packages/daemon-core/src/providers/approval-utils.ts +++ b/packages/daemon-core/src/providers/approval-utils.ts @@ -16,6 +16,8 @@ const DEFAULT_APPROVAL_POSITIVE_HINTS = [ 'always allow', ]; +const UNSAFE_APPROVAL_LABEL_PATTERN = /\b(?:abort|cancel|deny|discard|do\s+not|end|interrupt|no|reject|stop|terminate)\b/i; + function normalizeApprovalLabel(value: string): string { return String(value || '') .toLowerCase() @@ -24,6 +26,10 @@ function normalizeApprovalLabel(value: string): string { .trim(); } +export function isUnsafeApprovalButtonLabel(value: string): boolean { + return UNSAFE_APPROVAL_LABEL_PATTERN.test(normalizeApprovalLabel(value)); +} + export function getApprovalPositiveHints(provider?: Pick | null): string[] { const customHints = Array.isArray(provider?.approvalPositiveHints) ? provider.approvalPositiveHints @@ -36,7 +42,7 @@ export function getApprovalPositiveHints(provider?: Pick | null, -): { index: number; label: string } { +): { index: number; label: string; unsafe?: boolean } { const labels = (buttons || []).map((button) => String(button || '').trim()).filter(Boolean); if (labels.length === 0) { return { index: 0, label: 'Approve' }; @@ -44,19 +50,30 @@ export function pickApprovalButton( const normalizedButtons = labels.map((label) => normalizeApprovalLabel(label)); const hints = getApprovalPositiveHints(provider); + const safeCandidate = (index: number) => ( + index >= 0 && !isUnsafeApprovalButtonLabel(labels[index]) + ? { index, label: labels[index] } + : null + ); for (const hint of hints) { const exactIndex = normalizedButtons.findIndex((label) => label === hint); - if (exactIndex >= 0) return { index: exactIndex, label: labels[exactIndex] }; + const exact = safeCandidate(exactIndex); + if (exact) return exact; const prefixIndex = normalizedButtons.findIndex((label) => label.startsWith(hint)); - if (prefixIndex >= 0) return { index: prefixIndex, label: labels[prefixIndex] }; + const prefix = safeCandidate(prefixIndex); + if (prefix) return prefix; const includeIndex = normalizedButtons.findIndex((label) => label.includes(hint)); - if (includeIndex >= 0) return { index: includeIndex, label: labels[includeIndex] }; + const include = safeCandidate(includeIndex); + if (include) return include; } - return { index: 0, label: labels[0] }; + const nonUnsafeIndex = labels.findIndex((label) => !isUnsafeApprovalButtonLabel(label)); + if (nonUnsafeIndex >= 0) return { index: nonUnsafeIndex, label: labels[nonUnsafeIndex] }; + + return { index: 0, label: labels[0], unsafe: true }; } export function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: string): string { diff --git a/packages/daemon-core/src/providers/cli-provider-instance.ts b/packages/daemon-core/src/providers/cli-provider-instance.ts index 2d2f911d..2c81edcf 100644 --- a/packages/daemon-core/src/providers/cli-provider-instance.ts +++ b/packages/daemon-core/src/providers/cli-provider-instance.ts @@ -926,7 +926,16 @@ export class CliProviderInstance implements ProviderInstance { this.autoApproveBusyTimer = null; }, 2000); const modal = adapterStatus.activeModal; - const { index: buttonIndex, label: buttonLabel } = pickApprovalButton(modal?.buttons, this.provider); + const { index: buttonIndex, label: buttonLabel, unsafe } = pickApprovalButton(modal?.buttons, this.provider); + if (unsafe) { + LOG.warn('CLI', `[${this.type}] auto-approve skipped unsafe button "${buttonLabel}"`); + if (this.autoApproveBusyTimer) { + clearTimeout(this.autoApproveBusyTimer); + this.autoApproveBusyTimer = null; + } + this.autoApproveBusy = false; + return false; + } this.recordAutoApproval(modal?.message, buttonLabel, now); setTimeout(() => { this.adapter.resolveModal(buttonIndex); diff --git a/packages/daemon-core/src/providers/ide-provider-instance.ts b/packages/daemon-core/src/providers/ide-provider-instance.ts index a5c6f871..7bd8b1be 100644 --- a/packages/daemon-core/src/providers/ide-provider-instance.ts +++ b/packages/daemon-core/src/providers/ide-provider-instance.ts @@ -129,7 +129,7 @@ export class IdeProviderInstance implements ProviderInstance { const autoApproveActive = ( this.currentStatus === 'waiting_approval' || this.cachedChat?.status === 'waiting_approval' - ) && this.canAutoApprove(); + ) && this.canAutoApprove(this.cachedChat); const visibleStatus = (autoApproveActive ? 'generating' : this.currentStatus) as ProviderState['status']; // Collect extension status @@ -184,7 +184,7 @@ export class IdeProviderInstance implements ProviderInstance { const autoApproveActive = ( this.currentStatus === 'waiting_approval' || this.cachedChat?.status === 'waiting_approval' - ) && this.canAutoApprove(); + ) && this.canAutoApprove(this.cachedChat); const visibleStatus = autoApproveActive ? 'generating' : this.currentStatus; return { id: this.instanceId, @@ -441,7 +441,7 @@ export class IdeProviderInstance implements ProviderInstance { const rawAgentStatus = (chatStatus === 'streaming' || chatStatus === 'generating') ? 'generating' : chatStatus === 'waiting_approval' ? 'waiting_approval' : 'idle'; - const autoApproveActive = rawAgentStatus === 'waiting_approval' && this.canAutoApprove(); + const autoApproveActive = rawAgentStatus === 'waiting_approval' && this.canAutoApprove(chatData); const agentStatus = autoApproveActive ? 'generating' : rawAgentStatus; const lastMsg = Array.isArray(chatData?.messages) && chatData.messages.length > 0 ? chatData.messages[chatData.messages.length - 1] @@ -673,10 +673,14 @@ export class IdeProviderInstance implements ProviderInstance { if (this.context) this.context.cdp = cdp; } - private canAutoApprove(): boolean { - return this.settings.autoApprove !== false - && typeof this.provider.scripts?.resolveAction === 'function' - && !!this.context?.cdp?.isConnected; + private canAutoApprove(chatData?: any): boolean { + if (this.settings.autoApprove === false) return false; + if (typeof this.provider.scripts?.resolveAction !== 'function') return false; + if (!this.context?.cdp?.isConnected) return false; + if (chatData?.activeModal?.buttons) { + return !pickApprovalButton(chatData.activeModal.buttons, this.provider).unsafe; + } + return true; } // ─── Auto-approve via CDP script ──────────────────── @@ -694,7 +698,11 @@ export class IdeProviderInstance implements ProviderInstance { this.autoApproveBusy = true; try { - const { label: targetButton } = pickApprovalButton(_chatData?.activeModal?.buttons, this.provider); + const { label: targetButton, unsafe } = pickApprovalButton(_chatData?.activeModal?.buttons, this.provider); + if (unsafe) { + LOG.warn('IdeInstance', `[IdeInstance:${this.type}] autoApprove skipped unsafe button "${targetButton}"`); + return; + } const script = scriptFn({ action: 'approve', button: targetButton, buttonText: targetButton }); if (!script) return; diff --git a/packages/daemon-core/test/providers/approval-utils.test.ts b/packages/daemon-core/test/providers/approval-utils.test.ts index 7c8656d0..28369124 100644 --- a/packages/daemon-core/test/providers/approval-utils.test.ts +++ b/packages/daemon-core/test/providers/approval-utils.test.ts @@ -27,4 +27,25 @@ describe('approval-utils', () => { approvalPositiveHints: ['yes', 'allow', 'always allow'], })).toEqual({ index: 0, label: '1 Yes' }) }) + + it('marks all-destructive choices unsafe so auto-approve can leave them alone', () => { + expect(pickApprovalButton([ + 'Terminate current task', + 'Cancel', + ])).toEqual({ index: 0, label: 'Terminate current task', unsafe: true }) + }) + + it('falls back to a non-destructive choice when no positive hint matches', () => { + expect(pickApprovalButton([ + 'Terminate current task', + 'Keep waiting', + ])).toEqual({ index: 1, label: 'Keep waiting' }) + }) + + it('does not let positive words make a destructive button auto-approvable', () => { + expect(pickApprovalButton([ + 'Confirm termination', + 'Keep running', + ])).toEqual({ index: 1, label: 'Keep running' }) + }) })