Revert "feat: 桌面端危险命令分级授权"

This commit is contained in:
liuzhq
2026-03-25 19:59:28 +08:00
committed by GitHub
parent 06477f2eec
commit 75fc304bf4
7 changed files with 29 additions and 309 deletions
-8
View File
@@ -48,10 +48,6 @@ const translations: Record<LanguageType, Record<string, string>> = {
coworkErrorUnknown: '任务执行出错,请重试。如果问题持续出现,请检查模型配置。',
imErrorPrefix: '处理消息时出错',
// Exec approval continuation
execApprovalApproved: '用户已确认执行该命令,请检查执行结果并继续。',
execApprovalDenied: '用户已拒绝执行该命令。',
// Skill manager errors
skillErrNoSkillMd: '来源中未找到 SKILL.md',
@@ -93,10 +89,6 @@ const translations: Record<LanguageType, Record<string, string>> = {
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',
@@ -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,
-89
View File
@@ -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: '' };
}
+8 -26
View File
@@ -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<string, unknown>
): Promise<PermissionResult | null> {
// 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;
+2 -22
View File
@@ -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<string, unknown>;
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.
@@ -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<string, string> = {
'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<CoworkPermissionModalProps> = ({
}
};
const { dangerLevel, dangerReasonText } = useMemo(() => {
if (permission.toolName !== 'Bash') {
return { dangerLevel: 'safe' as DangerLevel, dangerReasonText: '' };
}
const input = permission.toolInput as Record<string, unknown>;
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<string, unknown>)?.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<CoworkPermissionModalProps> = ({
</div>
{/* Warning for dangerous operations - 固定在滚动区域外,始终可见 */}
{!isQuestionTool && dangerLevel === 'destructive' && (
{!isQuestionTool && isDangerousBash && (
<div className="flex items-start gap-2 p-3 mx-6 my-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
<ExclamationTriangleIcon className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-red-700 dark:text-red-400">
{i18nService.t('coworkDestructiveOperation')}
</p>
{dangerReasonText && (
<p className="text-xs text-red-600 dark:text-red-500 mt-0.5">{dangerReasonText}</p>
)}
</div>
</div>
)}
{!isQuestionTool && dangerLevel === 'caution' && (
<div className="flex items-start gap-2 p-3 mx-6 my-4 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800">
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-500 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-medium text-yellow-700 dark:text-yellow-400">
{i18nService.t('coworkCautionOperation')}
</p>
{dangerReasonText && (
<p className="text-xs text-yellow-600 dark:text-yellow-500 mt-0.5">{dangerReasonText}</p>
)}
</div>
<p className="text-sm text-red-700 dark:text-red-400">
{i18nService.t('coworkDangerousOperation')}
</p>
</div>
)}
-22
View File
@@ -411,17 +411,6 @@ const translations: Record<LanguageType, Record<string, string>> = {
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<LanguageType, Record<string, string>> = {
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',