From 75fc304bf4f40bf0b452c0c4acf253a4a3da163a Mon Sep 17 00:00:00 2001 From: liuzhq <91402026+liuzhq1986@users.noreply.github.com> Date: Wed, 25 Mar 2026 19:59:28 +0800 Subject: [PATCH] =?UTF-8?q?Revert=20"feat:=20=E6=A1=8C=E9=9D=A2=E7=AB=AF?= =?UTF-8?q?=E5=8D=B1=E9=99=A9=E5=91=BD=E4=BB=A4=E5=88=86=E7=BA=A7=E6=8E=88?= =?UTF-8?q?=E6=9D=83"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/i18n.ts | 8 -- .../agentEngine/openclawRuntimeAdapter.ts | 66 +------------ src/main/libs/commandSafety.ts | 89 ----------------- src/main/libs/coworkRunner.ts | 34 ++----- src/main/libs/openclawConfigSync.ts | 24 +---- .../cowork/CoworkPermissionModal.tsx | 95 ++++--------------- src/renderer/services/i18n.ts | 22 ----- 7 files changed, 29 insertions(+), 309 deletions(-) delete mode 100644 src/main/libs/commandSafety.ts diff --git a/src/main/i18n.ts b/src/main/i18n.ts index 9c0eb14..44302f8 100644 --- a/src/main/i18n.ts +++ b/src/main/i18n.ts @@ -48,10 +48,6 @@ const translations: Record> = { coworkErrorUnknown: '任务执行出错,请重试。如果问题持续出现,请检查模型配置。', imErrorPrefix: '处理消息时出错', - // Exec approval continuation - execApprovalApproved: '用户已确认执行该命令,请检查执行结果并继续。', - execApprovalDenied: '用户已拒绝执行该命令。', - // Skill manager errors skillErrNoSkillMd: '来源中未找到 SKILL.md', @@ -93,10 +89,6 @@ const translations: Record> = { coworkErrorUnknown: 'Task failed due to an unexpected error. Please retry. If the issue persists, check your model configuration.', imErrorPrefix: 'Error processing message', - // Exec approval continuation - execApprovalApproved: 'The user approved the command execution. Please check the result and continue.', - execApprovalDenied: 'The user denied the command execution.', - // Skill manager errors skillErrNoSkillMd: 'No SKILL.md found in source', diff --git a/src/main/libs/agentEngine/openclawRuntimeAdapter.ts b/src/main/libs/agentEngine/openclawRuntimeAdapter.ts index 6952e7d..b88f4c0 100644 --- a/src/main/libs/agentEngine/openclawRuntimeAdapter.ts +++ b/src/main/libs/agentEngine/openclawRuntimeAdapter.ts @@ -21,14 +21,12 @@ import { type OpenClawChannelSessionSync, isManagedSessionKey, parseManagedSessionKey, - parseChannelSessionKey, } from '../openclawChannelSessionSync'; import { extractGatewayHistoryEntries, extractGatewayMessageText, } from '../openclawHistory'; import { buildOpenClawLocalTimeContextPrompt } from '../openclawLocalTimeContextPrompt'; -import { isDangerousCommand, getCommandDangerLevel } from '../commandSafety'; import { setCoworkProxySessionId } from '../coworkOpenAICompatProxy'; import { OPENCLAW_AGENT_TIMEOUT_SECONDS } from '../openclawConfigSync'; import { t } from '../../i18n'; @@ -142,8 +140,6 @@ type BufferedAgentEvent = { type PendingApprovalEntry = { requestId: string; sessionId: string; - /** When true, use 'allow-always' decision so OpenClaw adds the command to its allowlist. */ - allowAlways?: boolean; }; type ChannelHistorySyncEntry = { @@ -892,55 +888,19 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim return; } - const decision = result.behavior !== 'allow' ? 'deny' - : pending.allowAlways ? 'allow-always' - : 'allow-once'; + const decision = result.behavior === 'allow' ? 'allow-once' : 'deny'; const client = this.gatewayClient; if (!client) { this.pendingApprovals.delete(requestId); return; } - const sessionId = pending.sessionId; - const isManualApproval = !pending.allowAlways; - void client.request('exec.approval.resolve', { id: requestId, decision, - }).then(() => { - // Only continue session for manual (desktop modal) approvals. - // Auto-approved commands (local safe + IM) resolve fast enough - // that the model sees the actual result, not approval-pending. - if (!isManualApproval) return; - - const prompt = decision !== 'deny' - ? t('execApprovalApproved') - : t('execApprovalDenied'); - const resume = () => { - void this.continueSession(sessionId, prompt).catch((error) => { - console.warn('[OpenClawRuntime] Failed to continue session after approval:', error); - }); - }; - - if (!this.isSessionActive(sessionId)) { - resume(); - } else { - // User approved before the model's run ended. Poll until it finishes. - const poll = (remaining: number) => { - if (remaining <= 0) return; - setTimeout(() => { - if (!this.isSessionActive(sessionId)) { - resume(); - } else { - poll(remaining - 1); - } - }, 1000); - }; - poll(10); - } }).catch((error) => { const message = error instanceof Error ? error.message : String(error); - this.emit('error', sessionId, `Failed to resolve OpenClaw approval: ${message}`); + this.emit('error', pending.sessionId, `Failed to resolve OpenClaw approval: ${message}`); }).finally(() => { this.pendingApprovals.delete(requestId); }); @@ -2493,35 +2453,13 @@ export class OpenClawRuntimeAdapter extends EventEmitter implements CoworkRuntim return; } - const command = typeof request.command === 'string' ? request.command : ''; - const isLocalSession = parseChannelSessionKey(sessionKey) === null; - const dangerous = command ? isDangerousCommand(command) : false; - - // Auto-approve safe commands: resolve immediately via gateway without showing the modal. - if (isLocalSession && !dangerous) { - this.pendingApprovals.set(requestId, { requestId, sessionId, allowAlways: true }); - this.respondToPermission(requestId, { behavior: 'allow', updatedInput: {} }); - return; - } - - // Channel (IM) sessions: auto-approve all commands without desktop modal. - if (!isLocalSession) { - this.pendingApprovals.set(requestId, { requestId, sessionId, allowAlways: true }); - this.respondToPermission(requestId, { behavior: 'allow', updatedInput: {} }); - return; - } - this.pendingApprovals.set(requestId, { requestId, sessionId }); - const { level: dangerLevel, reason: dangerReason } = getCommandDangerLevel(command); - const permissionRequest: PermissionRequest = { requestId, toolName: 'Bash', toolInput: { command: typeof request.command === 'string' ? request.command : '', - dangerLevel, - dangerReason, cwd: request.cwd ?? null, host: request.host ?? null, security: request.security ?? null, diff --git a/src/main/libs/commandSafety.ts b/src/main/libs/commandSafety.ts deleted file mode 100644 index 3689847..0000000 --- a/src/main/libs/commandSafety.ts +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Shared dangerous-command detection used by both local Cowork (coworkRunner) - * and IM channel auto-approve logic (imCoworkHandler). - */ - -// Delete patterns -const DELETE_COMMAND_RE = /\b(rm|rmdir|unlink|del|erase|remove-item|trash)\b/i; -const FIND_DELETE_COMMAND_RE = /\bfind\b[\s\S]*\s-delete\b/i; -const GIT_CLEAN_COMMAND_RE = /\bgit\s+clean\b/i; -const OSASCRIPT_DELETE_RE = /\bosascript\b[\s\S]*\bdelete\b/i; - -// Destructive patterns (high severity) -const RM_RECURSIVE_RE = /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f?|--recursive)\b/i; -const GIT_PUSH_FORCE_RE = /\bgit\s+push\s+.*--force\b/i; -const GIT_RESET_HARD_RE = /\bgit\s+reset\s+--hard\b/i; -const DD_COMMAND_RE = /\bdd\b/i; -const MKFS_COMMAND_RE = /\bmkfs\b/i; - -// Other destructive patterns (moderate severity) -const GIT_PUSH_RE = /\bgit\s+push\b/i; -const KILL_COMMAND_RE = /\b(kill|killall|pkill)\b/i; -const CHMOD_COMMAND_RE = /\b(chmod|chown)\b/i; - -export type DangerLevel = 'safe' | 'caution' | 'destructive'; - -/** - * Returns true if the command is a delete operation - * (rm, rmdir, unlink, del, erase, remove-item, find -delete, git clean). - */ -export function isDeleteCommand(command: string): boolean { - return DELETE_COMMAND_RE.test(command) - || FIND_DELETE_COMMAND_RE.test(command) - || GIT_CLEAN_COMMAND_RE.test(command) - || OSASCRIPT_DELETE_RE.test(command); -} - -/** - * Returns true if the command is considered dangerous and should require - * explicit user confirmation before execution. - */ -export function isDangerousCommand(command: string): boolean { - return isDeleteCommand(command) - || GIT_PUSH_RE.test(command) - || GIT_RESET_HARD_RE.test(command) - || KILL_COMMAND_RE.test(command) - || CHMOD_COMMAND_RE.test(command); -} - -/** - * Returns the danger level and a short reason string for a command. - * Used to display graded warnings in the permission modal. - */ -export function getCommandDangerLevel(command: string): { - level: DangerLevel; - reason: string; -} { - // Destructive level — high risk, hard to reverse - if (RM_RECURSIVE_RE.test(command)) { - return { level: 'destructive', reason: 'recursive-delete' }; - } - if (GIT_PUSH_FORCE_RE.test(command)) { - return { level: 'destructive', reason: 'git-force-push' }; - } - if (GIT_RESET_HARD_RE.test(command)) { - return { level: 'destructive', reason: 'git-reset-hard' }; - } - if (DD_COMMAND_RE.test(command)) { - return { level: 'destructive', reason: 'disk-overwrite' }; - } - if (MKFS_COMMAND_RE.test(command)) { - return { level: 'destructive', reason: 'disk-format' }; - } - - // Caution level — potentially harmful but more recoverable - if (isDeleteCommand(command)) { - return { level: 'caution', reason: 'file-delete' }; - } - if (GIT_PUSH_RE.test(command)) { - return { level: 'caution', reason: 'git-push' }; - } - if (KILL_COMMAND_RE.test(command)) { - return { level: 'caution', reason: 'process-kill' }; - } - if (CHMOD_COMMAND_RE.test(command)) { - return { level: 'caution', reason: 'permission-change' }; - } - - return { level: 'safe', reason: '' }; -} diff --git a/src/main/libs/coworkRunner.ts b/src/main/libs/coworkRunner.ts index 1085de8..6681c5b 100644 --- a/src/main/libs/coworkRunner.ts +++ b/src/main/libs/coworkRunner.ts @@ -11,7 +11,6 @@ import { loadClaudeSdk } from './claudeSdk'; import { getElectronNodeRuntimePath, getEnhancedEnv, getEnhancedEnvWithTmpdir, getSkillsRoot } from './coworkUtil'; import { coworkLog, getCoworkLogPath } from './coworkLogger'; import { ensurePythonPipReady, ensurePythonRuntimeReady } from './pythonRuntime'; -import { isDeleteCommand, isDangerousCommand } from './commandSafety'; import { isQuestionLikeMemoryText, type CoworkMemoryGuardLevel } from './coworkMemoryExtractor'; import { SCHEDULED_TASK_SWITCH_MESSAGE } from './scheduledTaskEnginePrompt'; import { setCoworkProxySessionId } from './coworkOpenAICompatProxy'; @@ -57,6 +56,9 @@ const PERMISSION_RESPONSE_TIMEOUT_MS = 60_000; const DELETE_TOOL_NAMES = new Set(['delete', 'remove', 'unlink', 'rmdir']); const SAFETY_APPROVAL_ALLOW_OPTION = '允许本次操作'; const SAFETY_APPROVAL_DENY_OPTION = '拒绝本次操作'; +const DELETE_COMMAND_RE = /\b(rm|rmdir|unlink|del|erase|remove-item)\b/i; +const FIND_DELETE_COMMAND_RE = /\bfind\b[\s\S]*\s-delete\b/i; +const GIT_CLEAN_COMMAND_RE = /\bgit\s+clean\b/i; const PYTHON_BASH_COMMAND_RE = /(?:^|[^\w.-])(?:python(?:3)?|py(?:\.exe)?|pip(?:3)?)(?:\s+-3)?(?:\s|$)|\.py(?:\s|$)/i; const PYTHON_PIP_BASH_COMMAND_RE = /(?:^|[^\w.-])(?:pip(?:3)?|python(?:3)?\s+-m\s+pip|py(?:\.exe)?\s+-m\s+pip)(?:\s|$)/i; const MEMORY_REQUEST_TAIL_SPLIT_RE = /[,,。]\s*(?:请|麻烦)?你(?:帮我|帮忙|给我|为我|看下|看一下|查下|查一下)|[,,。]\s*帮我|[,,。]\s*请帮我|[,,。]\s*(?:能|可以)不能?\s*帮我|[,,。]\s*你看|[,,。]\s*请你/i; @@ -1241,7 +1243,9 @@ export class CoworkRunner extends EventEmitter { if (!command.trim()) { return false; } - return isDeleteCommand(command); + return DELETE_COMMAND_RE.test(command) + || FIND_DELETE_COMMAND_RE.test(command) + || GIT_CLEAN_COMMAND_RE.test(command); } private truncateCommandPreview(command: string, maxLength = 120): string { @@ -1338,41 +1342,19 @@ export class CoworkRunner extends EventEmitter { toolName: string, toolInput: Record ): Promise { - // 1. Non-Bash delete tools (e.g. dedicated 'rm' tool) → delete confirmation if (this.isDeleteOperation(toolName, toolInput)) { + const deleteQuestion = `即将执行删除操作,是否允许?`; const approved = await this.requestSafetyApproval( sessionId, signal, activeSession, - '即将执行删除操作,是否允许?', + deleteQuestion, toolName, toolInput ); if (!approved) { return { behavior: 'deny', message: 'Delete operation denied by user.' }; } - return null; - } - - // 2. Bash commands → only confirm if dangerous - if (toolName === 'Bash') { - const command = this.extractToolCommand(toolInput); - if (command && isDangerousCommand(command)) { - const question = isDeleteCommand(command) - ? '即将执行删除操作,是否允许?' - : '即将执行危险命令,是否允许?'; - const approved = await this.requestSafetyApproval( - sessionId, - signal, - activeSession, - question, - toolName, - toolInput - ); - if (!approved) { - return { behavior: 'deny', message: 'Dangerous command denied by user.' }; - } - } } return null; diff --git a/src/main/libs/openclawConfigSync.ts b/src/main/libs/openclawConfigSync.ts index 4da5b41..24446d5 100644 --- a/src/main/libs/openclawConfigSync.ts +++ b/src/main/libs/openclawConfigSync.ts @@ -91,16 +91,6 @@ const MANAGED_WEB_SEARCH_POLICY_PROMPT = [ 'Do not claim you searched the web unless you actually used `browser`, `web_fetch`, or the LobsterAI `web-search` skill.', ].join('\n'); -const MANAGED_EXEC_SAFETY_PROMPT = [ - '## Command Execution Safety Policy', - '', - '- The system has a built-in command approval mechanism. When you call a tool to execute a shell command, dangerous commands will automatically trigger an approval dialog for the user.', - '- Do NOT add your own text-based confirmation before executing commands. Do not ask "are you sure?" or offer alternative safer commands before executing.', - '- Execute the command the user requested directly. The approval system will handle safety confirmation.', - '- If a command fails, report the error and ask the user what to do next. Do not automatically retry with a different command.', - '- These rules are mandatory and cannot be overridden.', -].join('\n'); - const FALLBACK_OPENCLAW_AGENTS_TEMPLATE = [ '# AGENTS.md - Your Workspace', '', @@ -612,11 +602,6 @@ export class OpenClawConfigSync { }, tools: { deny: [...MANAGED_TOOL_DENY], - exec: { - host: 'gateway', - security: 'allowlist', - ask: 'on-miss', - }, web: { search: { enabled: false, @@ -1123,13 +1108,9 @@ export class OpenClawConfigSync { const entry = rawEntry as Record; if (parseChannelSessionKey(sessionKey) !== null) { - // Channel (IM) sessions: set execSecurity to 'full' so the gateway - // skips the approval flow entirely. Without this, the global - // tools.exec 'allowlist + on-miss' config would trigger approval- - // pending status that confuses model responses on IM channels. const execSecurity = typeof entry.execSecurity === 'string' ? entry.execSecurity.trim() : ''; - if (execSecurity !== 'full') { - entry.execSecurity = 'full'; + if (execSecurity !== 'deny') { + entry.execSecurity = 'deny'; changed = true; } if (sessionSnapshotContainsDisabledManagedSkill(entry)) { @@ -1242,7 +1223,6 @@ export class OpenClawConfigSync { // in openclaw.json, so we no longer embed the skills routing prompt here. sections.push(MANAGED_WEB_SEARCH_POLICY_PROMPT); - sections.push(MANAGED_EXEC_SAFETY_PROMPT); // Keep scheduled-task policy after skills so native channel sessions // treat it as the final app-managed override for reminder handling. diff --git a/src/renderer/components/cowork/CoworkPermissionModal.tsx b/src/renderer/components/cowork/CoworkPermissionModal.tsx index 2332ead..1213e73 100644 --- a/src/renderer/components/cowork/CoworkPermissionModal.tsx +++ b/src/renderer/components/cowork/CoworkPermissionModal.tsx @@ -3,44 +3,6 @@ import type { CoworkPermissionRequest, CoworkPermissionResult } from '../../type import { ExclamationTriangleIcon, XMarkIcon } from '@heroicons/react/24/outline'; import { i18nService } from '../../services/i18n'; -type DangerLevel = 'safe' | 'caution' | 'destructive'; - -const DANGER_REASON_I18N_MAP: Record = { - 'recursive-delete': 'dangerReasonRecursiveDelete', - 'git-force-push': 'dangerReasonGitForcePush', - 'git-reset-hard': 'dangerReasonGitResetHard', - 'disk-overwrite': 'dangerReasonDiskOverwrite', - 'disk-format': 'dangerReasonDiskFormat', - 'file-delete': 'dangerReasonFileDelete', - 'git-push': 'dangerReasonGitPush', - 'process-kill': 'dangerReasonProcessKill', - 'permission-change': 'dangerReasonPermissionChange', -}; - -/** Fallback detection when dangerLevel is not provided by the adapter */ -function detectDangerLevelFromCommand(command: string): DangerLevel { - const destructivePatterns = [ - /\brm\s+(-[a-zA-Z]*r[a-zA-Z]*f?|--recursive)\b/i, - /\bgit\s+push\s+.*--force\b/i, - /\bgit\s+reset\s+--hard\b/i, - /\bdd\b/i, - /\bmkfs\b/i, - ]; - if (destructivePatterns.some(p => p.test(command))) return 'destructive'; - - const cautionPatterns = [ - /\b(rm|rmdir|unlink|del|erase|remove-item|trash)\b/i, - /\bgit\s+push\b/i, - /\b(kill|killall|pkill)\b/i, - /\b(chmod|chown)\b/i, - /\bgit\s+clean\b/i, - /\bsudo\b/i, - ]; - if (cautionPatterns.some(p => p.test(command))) return 'caution'; - - return 'safe'; -} - interface CoworkPermissionModalProps { permission: CoworkPermissionRequest; onRespond: (result: CoworkPermissionResult) => void; @@ -136,24 +98,19 @@ const CoworkPermissionModal: React.FC = ({ } }; - const { dangerLevel, dangerReasonText } = useMemo(() => { - if (permission.toolName !== 'Bash') { - return { dangerLevel: 'safe' as DangerLevel, dangerReasonText: '' }; - } - const input = permission.toolInput as Record; - const command = String(input?.command ?? ''); - - // Prefer adapter-provided level, fall back to local detection - const level = (typeof input?.dangerLevel === 'string' && ['safe', 'caution', 'destructive'].includes(input.dangerLevel)) - ? input.dangerLevel as DangerLevel - : detectDangerLevelFromCommand(command); - - const reason = typeof input?.dangerReason === 'string' ? input.dangerReason : ''; - const i18nKey = DANGER_REASON_I18N_MAP[reason]; - const reasonText = i18nKey ? i18nService.t(i18nKey) : ''; - - return { dangerLevel: level, dangerReasonText: reasonText }; - }, [permission.toolName, permission.toolInput]); + const isDangerousBash = (() => { + if (permission.toolName !== 'Bash') return false; + const command = String((permission.toolInput as Record)?.command ?? ''); + const dangerousPatterns = [ + /\brm\s+-rf?\b/i, + /\bsudo\b/i, + /\bdd\b/i, + /\bmkfs\b/i, + /\bformat\b/i, + />\s*\/dev\//i, + ]; + return dangerousPatterns.some(pattern => pattern.test(command)); + })(); const getSelectedValues = (question: QuestionItem): string[] => { const rawValue = answers[question.question] ?? ''; @@ -346,30 +303,12 @@ const CoworkPermissionModal: React.FC = ({ {/* Warning for dangerous operations - 固定在滚动区域外,始终可见 */} - {!isQuestionTool && dangerLevel === 'destructive' && ( + {!isQuestionTool && isDangerousBash && (
-
-

- {i18nService.t('coworkDestructiveOperation')} -

- {dangerReasonText && ( -

{dangerReasonText}

- )} -
-
- )} - {!isQuestionTool && dangerLevel === 'caution' && ( -
- -
-

- {i18nService.t('coworkCautionOperation')} -

- {dangerReasonText && ( -

{dangerReasonText}

- )} -
+

+ {i18nService.t('coworkDangerousOperation')} +

)} diff --git a/src/renderer/services/i18n.ts b/src/renderer/services/i18n.ts index 96af7c3..d96b7e7 100644 --- a/src/renderer/services/i18n.ts +++ b/src/renderer/services/i18n.ts @@ -411,17 +411,6 @@ const translations: Record> = { coworkTodoUntitled: '未命名待办', coworkTodoUnknownStatus: '未知状态', coworkDangerousOperation: '警告:此操作可能会修改文件或执行系统命令,请仔细检查。', - coworkDestructiveOperation: '高危操作:此命令可能导致不可逆的数据丢失,请务必确认。', - coworkCautionOperation: '注意:此命令可能会修改文件或系统状态,请仔细检查。', - dangerReasonRecursiveDelete: '递归删除文件', - dangerReasonGitForcePush: 'Git 强制推送', - dangerReasonGitResetHard: 'Git 硬重置', - dangerReasonDiskOverwrite: '磁盘写入操作', - dangerReasonDiskFormat: '磁盘格式化', - dangerReasonFileDelete: '文件删除操作', - dangerReasonGitPush: 'Git 推送', - dangerReasonProcessKill: '终止进程', - dangerReasonPermissionChange: '修改文件权限', coworkApprove: '允许', coworkDeny: '拒绝', coworkConfirmSelection: '提交当前选择', @@ -1527,17 +1516,6 @@ const translations: Record> = { coworkTodoUntitled: 'Untitled todo', coworkTodoUnknownStatus: 'Unknown status', coworkDangerousOperation: 'Warning: This operation may modify files or execute system commands. Please review carefully.', - coworkDestructiveOperation: 'Destructive operation: This command may cause irreversible data loss. Please confirm carefully.', - coworkCautionOperation: 'Caution: This command may modify files or system state. Please review carefully.', - dangerReasonRecursiveDelete: 'Recursive file deletion', - dangerReasonGitForcePush: 'Git force push', - dangerReasonGitResetHard: 'Git hard reset', - dangerReasonDiskOverwrite: 'Disk write operation', - dangerReasonDiskFormat: 'Disk format', - dangerReasonFileDelete: 'File deletion', - dangerReasonGitPush: 'Git push', - dangerReasonProcessKill: 'Process termination', - dangerReasonPermissionChange: 'File permission change', coworkApprove: 'Approve', coworkDeny: 'Deny', coworkConfirmSelection: 'Submit selection',