diff --git a/desktop/src/components/AutonomyConfig.tsx b/desktop/src/components/AutonomyConfig.tsx new file mode 100644 index 0000000..262d3c5 --- /dev/null +++ b/desktop/src/components/AutonomyConfig.tsx @@ -0,0 +1,504 @@ +/** + * AutonomyConfig - Configuration UI for L4 self-evolution authorization + * + * Allows users to configure: + * - Autonomy level (supervised/assisted/autonomous) + * - Individual action permissions + * - Approval thresholds + * - Audit log viewing + * + * Part of ZCLAW L4 Self-Evolution capability. + */ + +import { useState, useCallback, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Shield, + ShieldAlert, + ShieldCheck, + ShieldQuestion, + Settings, + AlertTriangle, + CheckCircle, + Clock, + RotateCcw, + Info, + ChevronDown, + ChevronRight, + Trash2, +} from 'lucide-react'; +import { + getAutonomyManager, + DEFAULT_AUTONOMY_CONFIGS, + type AutonomyManager, + type AutonomyConfig, + type AutonomyLevel, + type AuditLogEntry, + type ActionType, +} from '../lib/autonomy-manager'; + +// === Types === + +interface AutonomyConfigProps { + className?: string; + onConfigChange?: (config: AutonomyConfig) => void; +} + +// === Autonomy Level Config === + +const LEVEL_CONFIG: Record = { + supervised: { + label: '监督模式', + description: '所有操作都需要用户确认', + icon: ShieldQuestion, + color: 'text-yellow-500', + }, + assisted: { + label: '辅助模式', + description: '低风险操作自动执行,高风险需确认', + icon: ShieldAlert, + color: 'text-blue-500', + }, + autonomous: { + label: '自主模式', + description: 'Agent 自主决策,仅高影响操作通知', + icon: ShieldCheck, + color: 'text-green-500', + }, +}; + +const ACTION_LABELS: Record = { + memory_save: '自动保存记忆', + memory_delete: '删除记忆', + identity_update: '更新身份文件', + identity_rollback: '回滚身份', + skill_install: '安装技能', + skill_uninstall: '卸载技能', + config_change: '修改配置', + workflow_trigger: '触发工作流', + hand_trigger: '触发 Hand', + llm_call: '调用 LLM', + reflection_run: '运行反思', + compaction_run: '运行压缩', +}; + +// === Components === + +function LevelSelector({ + value, + onChange, +}: { + value: AutonomyLevel; + onChange: (level: AutonomyLevel) => void; +}) { + return ( +
+ {(Object.keys(LEVEL_CONFIG) as AutonomyLevel[]).map((level) => { + const config = LEVEL_CONFIG[level]; + const Icon = config.icon; + const isSelected = value === level; + + return ( + + ); + })} +
+ ); +} + +function ActionToggle({ + action, + label, + enabled, + onChange, + disabled, +}: { + action: ActionType; + label: string; + enabled: boolean; + onChange: (enabled: boolean) => void; + disabled?: boolean; +}) { + return ( +
+ {label} + +
+ ); +} + +function AuditLogEntryItem({ + entry, + onRollback, +}: { + entry: AuditLogEntry; + onRollback?: (id: string) => void; +}) { + const [expanded, setExpanded] = useState(false); + + const outcomeColors = { + success: 'text-green-500', + failed: 'text-red-500', + rolled_back: 'text-yellow-500', + }; + + const outcomeLabels = { + success: '成功', + failed: '失败', + rolled_back: '已回滚', + }; + + const time = new Date(entry.timestamp).toLocaleString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + }); + + return ( +
+ + + + {expanded && ( + +
+ 风险: {entry.decision.riskLevel} · 重要性: {entry.decision.importance} +
+
+ 原因: {entry.decision.reason} +
+ {entry.outcome !== 'rolled_back' && entry.decision.riskLevel !== 'low' && ( + + )} +
+ )} +
+
+ ); +} + +// === Main Component === + +export function AutonomyConfig({ className = '', onConfigChange }: AutonomyConfigProps) { + const [manager] = useState(() => getAutonomyManager()); + const [config, setConfig] = useState(manager.getConfig()); + const [auditLog, setAuditLog] = useState([]); + const [hasChanges, setHasChanges] = useState(false); + + // Load audit log + useEffect(() => { + setAuditLog(manager.getAuditLog(50)); + }, [manager]); + + const updateConfig = useCallback( + (updates: Partial) => { + setConfig((prev) => { + const next = { ...prev, ...updates }; + setHasChanges(true); + onConfigChange?.(next); + return next; + }); + }, + [onConfigChange] + ); + + const handleLevelChange = useCallback((level: AutonomyLevel) => { + const newConfig = DEFAULT_AUTONOMY_CONFIGS[level]; + setConfig(newConfig); + setHasChanges(true); + onConfigChange?.(newConfig); + }, [onConfigChange]); + + const handleSave = useCallback(() => { + manager.updateConfig(config); + setHasChanges(false); + }, [manager, config]); + + const handleRollback = useCallback((auditId: string) => { + if (manager.rollback(auditId)) { + setAuditLog(manager.getAuditLog(50)); + } + }, [manager]); + + const handleClearLog = useCallback(() => { + manager.clearAuditLog(); + setAuditLog([]); + }, [manager]); + + return ( +
+ {/* Header */} +
+
+ +

自主授权

+
+ +
+ + {/* Content */} +
+ {/* Autonomy Level */} +
+
+ + + 自主级别 + +
+ +
+ + {/* Allowed Actions */} +
+
+ + + 允许的操作 + +
+
+ + updateConfig({ + allowedActions: { ...config.allowedActions, memoryAutoSave: enabled }, + }) + } + /> + + updateConfig({ + allowedActions: { ...config.allowedActions, identityAutoUpdate: enabled }, + }) + } + /> + + updateConfig({ + allowedActions: { ...config.allowedActions, skillAutoInstall: enabled }, + }) + } + /> + + updateConfig({ + allowedActions: { ...config.allowedActions, selfModification: enabled }, + }) + } + /> + + updateConfig({ + allowedActions: { ...config.allowedActions, autoCompaction: enabled }, + }) + } + /> + + updateConfig({ + allowedActions: { ...config.allowedActions, autoReflection: enabled }, + }) + } + /> +
+
+ + {/* Approval Thresholds */} +
+
+ + + 审批阈值 + +
+
+
+ + 重要性上限 + + + updateConfig({ + approvalThreshold: { + ...config.approvalThreshold, + importanceMax: parseInt(e.target.value), + }, + }) + } + className="w-24 h-2 bg-gray-200 dark:bg-gray-700 rounded-lg appearance-none cursor-pointer accent-purple-500" + /> + + {config.approvalThreshold.importanceMax} + +
+
+ + 风险等级上限 + + +
+
+
+ + {/* Audit Log */} +
+
+
+ + + 审计日志 + + ({auditLog.length} 条) +
+ +
+ + {auditLog.length > 0 ? ( +
+ {auditLog + .slice() + .reverse() + .map((entry) => ( + + ))} +
+ ) : ( +
+ 暂无审计日志 +
+ )} +
+ + {/* Info */} +
+ +

+ 高风险操作(删除记忆、修改身份文件)始终需要用户确认,无论自主级别如何设置。 + 所有自主操作都会记录在审计日志中,支持一键回滚。 +

+
+
+
+ ); +} + +export default AutonomyConfig; diff --git a/desktop/src/lib/__tests__/autonomy-manager.test.ts b/desktop/src/lib/__tests__/autonomy-manager.test.ts new file mode 100644 index 0000000..3aeff6e --- /dev/null +++ b/desktop/src/lib/__tests__/autonomy-manager.test.ts @@ -0,0 +1,364 @@ +/** + * AutonomyManager Tests - L4 Self-Evolution Authorization + * + * Tests for the tiered authorization system: + * - Level-based permissions (supervised/assisted/autonomous) + * - Risk assessment for actions + * - Approval workflow + * - Audit logging + * - Rollback functionality + */ + +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { + AutonomyManager, + getAutonomyManager, + resetAutonomyManager, + canAutoExecute, + executeWithAutonomy, + DEFAULT_AUTONOMY_CONFIGS, + type ActionType, + type AutonomyLevel, +} from '../autonomy-manager'; + +// === Helper to create fresh manager === + +function createManager(level: AutonomyLevel = 'assisted'): AutonomyManager { + resetAutonomyManager(); + return getAutonomyManager({ ...DEFAULT_AUTONOMY_CONFIGS[level] }); +} + +// === Risk Assessment Tests === + +describe('AutonomyManager Risk Assessment', () => { + let manager: AutonomyManager; + + beforeEach(() => { + manager = createManager('assisted'); + }); + + afterEach(() => { + resetAutonomyManager(); + }); + + it('should classify memory_save as low risk', () => { + const decision = manager.evaluate('memory_save', { importance: 3 }); + expect(decision.riskLevel).toBe('low'); + }); + + it('should classify memory_delete as high risk', () => { + const decision = manager.evaluate('memory_delete'); + expect(decision.riskLevel).toBe('high'); + }); + + it('should classify identity_update as high risk', () => { + const decision = manager.evaluate('identity_update'); + expect(decision.riskLevel).toBe('high'); + }); + + it('should allow risk override', () => { + const decision = manager.evaluate('memory_save', { riskOverride: 'high' }); + expect(decision.riskLevel).toBe('high'); + }); +}); + +// === Level-Based Permission Tests === + +describe('AutonomyManager Level Permissions', () => { + afterEach(() => { + resetAutonomyManager(); + }); + + describe('Supervised Mode', () => { + let manager: AutonomyManager; + + beforeEach(() => { + manager = createManager('supervised'); + }); + + it('should require approval for all actions', () => { + const decision = manager.evaluate('memory_save', { importance: 1 }); + expect(decision.requiresApproval).toBe(true); + expect(decision.allowed).toBe(false); + }); + + it('should not auto-execute even low-risk actions', () => { + const decision = manager.evaluate('reflection_run', { importance: 1 }); + expect(decision.allowed).toBe(false); + }); + }); + + describe('Assisted Mode', () => { + let manager: AutonomyManager; + + beforeEach(() => { + manager = createManager('assisted'); + }); + + it('should auto-approve low importance, low risk actions', () => { + const decision = manager.evaluate('memory_save', { importance: 3 }); + expect(decision.allowed).toBe(true); + expect(decision.requiresApproval).toBe(false); + }); + + it('should require approval for high importance actions', () => { + const decision = manager.evaluate('memory_save', { importance: 8 }); + expect(decision.requiresApproval).toBe(true); + }); + + it('should always require approval for high risk actions', () => { + const decision = manager.evaluate('memory_delete', { importance: 1 }); + expect(decision.requiresApproval).toBe(true); + expect(decision.allowed).toBe(false); + }); + + it('should not auto-approve identity updates', () => { + const decision = manager.evaluate('identity_update', { importance: 3 }); + expect(decision.allowed).toBe(false); + expect(decision.requiresApproval).toBe(true); + }); + }); + + describe('Autonomous Mode', () => { + let manager: AutonomyManager; + + beforeEach(() => { + manager = createManager('autonomous'); + }); + + it('should auto-approve medium risk, medium importance actions', () => { + const decision = manager.evaluate('skill_install', { importance: 5 }); + expect(decision.allowed).toBe(true); + expect(decision.requiresApproval).toBe(false); + }); + + it('should still require approval for high risk actions', () => { + const decision = manager.evaluate('memory_delete', { importance: 1 }); + expect(decision.allowed).toBe(false); + expect(decision.requiresApproval).toBe(true); + }); + + it('should not auto-approve self-modification', () => { + // Even in autonomous mode, self-modification requires approval + manager.updateConfig({ + allowedActions: { + ...manager.getConfig().allowedActions, + selfModification: false, + }, + }); + + const decision = manager.evaluate('identity_update', { importance: 3 }); + expect(decision.allowed).toBe(false); + }); + }); +}); + +// === Approval Workflow Tests === + +describe('AutonomyManager Approval Workflow', () => { + let manager: AutonomyManager; + + beforeEach(() => { + manager = createManager('supervised'); + }); + + afterEach(() => { + resetAutonomyManager(); + }); + + it('should request approval and return approval ID', () => { + const decision = manager.evaluate('memory_save'); + const approvalId = manager.requestApproval(decision); + + expect(approvalId).toMatch(/^approval_/); + expect(manager.getPendingApprovals().length).toBe(1); + }); + + it('should approve pending action', () => { + const decision = manager.evaluate('memory_save'); + const approvalId = manager.requestApproval(decision); + + const result = manager.approve(approvalId); + expect(result).toBe(true); + expect(manager.getPendingApprovals().length).toBe(0); + }); + + it('should reject pending action', () => { + const decision = manager.evaluate('memory_save'); + const approvalId = manager.requestApproval(decision); + + const result = manager.reject(approvalId); + expect(result).toBe(true); + expect(manager.getPendingApprovals().length).toBe(0); + }); + + it('should return false for non-existent approval', () => { + expect(manager.approve('non_existent')).toBe(false); + expect(manager.reject('non_existent')).toBe(false); + }); +}); + +// === Audit Log Tests === + +describe('AutonomyManager Audit Log', () => { + let manager: AutonomyManager; + + beforeEach(() => { + manager = createManager('assisted'); + manager.clearAuditLog(); + }); + + afterEach(() => { + resetAutonomyManager(); + }); + + it('should log decisions', () => { + manager.evaluate('memory_save', { importance: 3 }); + + const log = manager.getAuditLog(); + expect(log.length).toBe(1); + expect(log[0].action).toBe('memory_save'); + }); + + it('should limit log to 100 entries', () => { + for (let i = 0; i < 150; i++) { + manager.evaluate('memory_save', { importance: i % 10 }); + } + + const log = manager.getAuditLog(200); + expect(log.length).toBe(100); + }); + + it('should clear audit log', () => { + manager.evaluate('memory_save'); + manager.evaluate('reflection_run'); + + expect(manager.getAuditLog().length).toBe(2); + + manager.clearAuditLog(); + + expect(manager.getAuditLog().length).toBe(0); + }); + + it('should support rollback', () => { + manager.evaluate('memory_save'); + const log = manager.getAuditLog(); + const entryId = log[0].id; + + const result = manager.rollback(entryId); + + expect(result).toBe(true); + + const updatedLog = manager.getAuditLog(); + expect(updatedLog[0].outcome).toBe('rolled_back'); + expect(updatedLog[0].rolledBackAt).toBeDefined(); + }); + + it('should not allow double rollback', () => { + manager.evaluate('memory_save'); + const log = manager.getAuditLog(); + const entryId = log[0].id; + + manager.rollback(entryId); + const result = manager.rollback(entryId); + + expect(result).toBe(false); + }); +}); + +// === Config Management Tests === + +describe('AutonomyManager Config Management', () => { + let manager: AutonomyManager; + + beforeEach(() => { + manager = createManager('assisted'); + }); + + afterEach(() => { + resetAutonomyManager(); + }); + + it('should get current config', () => { + const config = manager.getConfig(); + expect(config.level).toBe('assisted'); + expect(config.allowedActions.memoryAutoSave).toBe(true); + }); + + it('should update config', () => { + manager.updateConfig({ + approvalThreshold: { + importanceMax: 8, + riskMax: 'medium', + }, + }); + + const config = manager.getConfig(); + expect(config.approvalThreshold.importanceMax).toBe(8); + }); + + it('should change level', () => { + manager.setLevel('autonomous'); + + const config = manager.getConfig(); + expect(config.level).toBe('autonomous'); + expect(config.allowedActions.memoryAutoSave).toBe(true); + expect(config.allowedActions.identityAutoUpdate).toBe(true); + }); +}); + +// === Helper Function Tests === + +describe('Helper Functions', () => { + beforeEach(() => { + resetAutonomyManager(); + getAutonomyManager({ ...DEFAULT_AUTONOMY_CONFIGS.assisted }); + }); + + afterEach(() => { + resetAutonomyManager(); + }); + + describe('canAutoExecute', () => { + it('should return true for auto-approvable actions', () => { + const result = canAutoExecute('memory_save', 3); + expect(result.canProceed).toBe(true); + }); + + it('should return false for actions needing approval', () => { + const result = canAutoExecute('memory_delete', 1); + expect(result.canProceed).toBe(false); + }); + }); + + describe('executeWithAutonomy', () => { + it('should execute auto-approved actions immediately', async () => { + const executor = vi.fn().mockResolvedValue('success'); + + const result = await executeWithAutonomy('memory_save', 3, executor); + + expect(result.executed).toBe(true); + expect(result.result).toBe('success'); + expect(executor).toHaveBeenCalled(); + }); + + it('should not execute actions needing approval', async () => { + const executor = vi.fn().mockResolvedValue('success'); + + const result = await executeWithAutonomy('memory_delete', 1, executor); + + expect(result.executed).toBe(false); + expect(executor).not.toHaveBeenCalled(); + expect(result.approvalId).toBeDefined(); + }); + + it('should call onApprovalNeeded callback', async () => { + const executor = vi.fn().mockResolvedValue('success'); + const onApprovalNeeded = vi.fn(); + + await executeWithAutonomy('memory_delete', 1, executor, onApprovalNeeded); + + expect(onApprovalNeeded).toHaveBeenCalled(); + }); + }); +}); diff --git a/desktop/src/lib/autonomy-manager.ts b/desktop/src/lib/autonomy-manager.ts new file mode 100644 index 0000000..db4b23c --- /dev/null +++ b/desktop/src/lib/autonomy-manager.ts @@ -0,0 +1,548 @@ +/** + * Autonomy Manager - Tiered authorization system for L4 self-evolution + * + * Provides granular control over what actions the Agent can take autonomously: + * - Supervised: All actions require user confirmation + * - Assisted: Low-risk actions execute automatically + * - Autonomous: Agent decides when to act and notify + * + * Security boundaries: + * - High-risk operations ALWAYS require confirmation + * - All autonomous actions are logged for audit + * - One-click rollback to any historical state + * + * Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.4.3 + */ + +// === Types === + +export type AutonomyLevel = 'supervised' | 'assisted' | 'autonomous'; + +export type RiskLevel = 'low' | 'medium' | 'high'; + +export type ActionType = + | 'memory_save' + | 'memory_delete' + | 'identity_update' + | 'identity_rollback' + | 'skill_install' + | 'skill_uninstall' + | 'config_change' + | 'workflow_trigger' + | 'hand_trigger' + | 'llm_call' + | 'reflection_run' + | 'compaction_run'; + +export interface AutonomyConfig { + level: AutonomyLevel; + allowedActions: { + memoryAutoSave: boolean; + identityAutoUpdate: boolean; + skillAutoInstall: boolean; + selfModification: boolean; + autoCompaction: boolean; + autoReflection: boolean; + }; + approvalThreshold: { + importanceMax: number; // Auto-approve if importance <= this (default: 5) + riskMax: RiskLevel; // Auto-approve if risk <= this (default: 'low') + }; + notifyOnAction: boolean; // Notify user after autonomous action + auditLogEnabled: boolean; // Log all autonomous actions +} + +export interface AutonomyDecision { + action: ActionType; + allowed: boolean; + requiresApproval: boolean; + reason: string; + riskLevel: RiskLevel; + importance: number; + timestamp: string; +} + +export interface AuditLogEntry { + id: string; + action: ActionType; + decision: AutonomyDecision; + context: Record; + outcome: 'success' | 'failed' | 'rolled_back'; + rolledBackAt?: string; + timestamp: string; +} + +// === Risk Mapping === + +const ACTION_RISK_MAP: Record = { + memory_save: 'low', + memory_delete: 'high', + identity_update: 'high', + identity_rollback: 'medium', + skill_install: 'medium', + skill_uninstall: 'medium', + config_change: 'medium', + workflow_trigger: 'low', + hand_trigger: 'medium', + llm_call: 'low', + reflection_run: 'low', + compaction_run: 'low', +}; + +const RISK_ORDER: Record = { + low: 1, + medium: 2, + high: 3, +}; + +// === Default Configs === + +export const DEFAULT_AUTONOMY_CONFIGS: Record = { + supervised: { + level: 'supervised', + allowedActions: { + memoryAutoSave: false, + identityAutoUpdate: false, + skillAutoInstall: false, + selfModification: false, + autoCompaction: false, + autoReflection: false, + }, + approvalThreshold: { + importanceMax: 0, + riskMax: 'low', + }, + notifyOnAction: true, + auditLogEnabled: true, + }, + assisted: { + level: 'assisted', + allowedActions: { + memoryAutoSave: true, + identityAutoUpdate: false, + skillAutoInstall: false, + selfModification: false, + autoCompaction: true, + autoReflection: true, + }, + approvalThreshold: { + importanceMax: 5, + riskMax: 'low', + }, + notifyOnAction: true, + auditLogEnabled: true, + }, + autonomous: { + level: 'autonomous', + allowedActions: { + memoryAutoSave: true, + identityAutoUpdate: true, + skillAutoInstall: true, + selfModification: false, // Always require approval for self-modification + autoCompaction: true, + autoReflection: true, + }, + approvalThreshold: { + importanceMax: 7, + riskMax: 'medium', + }, + notifyOnAction: false, // Only notify on high-impact actions + auditLogEnabled: true, + }, +}; + +// === Storage === + +const AUTONOMY_CONFIG_KEY = 'zclaw-autonomy-config'; +const AUDIT_LOG_KEY = 'zclaw-autonomy-audit-log'; + +// === Autonomy Manager === + +export class AutonomyManager { + private config: AutonomyConfig; + private auditLog: AuditLogEntry[] = []; + private pendingApprovals: Map = new Map(); + + constructor(config?: Partial) { + this.config = this.loadConfig(); + if (config) { + this.config = { ...this.config, ...config }; + } + this.loadAuditLog(); + } + + // === Decision Making === + + /** + * Evaluate whether an action can be executed autonomously. + */ + evaluate( + action: ActionType, + context?: { + importance?: number; + riskOverride?: RiskLevel; + details?: Record; + } + ): AutonomyDecision { + const importance = context?.importance ?? 5; + const baseRisk = ACTION_RISK_MAP[action]; + const riskLevel = context?.riskOverride ?? baseRisk; + + // High-risk actions ALWAYS require approval + const isHighRisk = riskLevel === 'high'; + const isSelfModification = action === 'identity_update' || action === 'selfModification'; + const isDeletion = action === 'memory_delete'; + + let allowed = false; + let requiresApproval = true; + let reason = ''; + + // Determine if action is allowed based on config + if (isHighRisk || isDeletion) { + // Always require approval for high-risk and deletion + allowed = false; + requiresApproval = true; + reason = `高风险操作 [${action}] 始终需要用户确认`; + } else if (isSelfModification && !this.config.allowedActions.selfModification) { + // Self-modification requires explicit permission + allowed = false; + requiresApproval = true; + reason = `身份修改 [${action}] 需要显式授权`; + } else { + // Check against thresholds + const importanceOk = importance <= this.config.approvalThreshold.importanceMax; + const riskOk = RISK_ORDER[riskLevel] <= RISK_ORDER[this.config.approvalThreshold.riskMax]; + const actionAllowed = this.isActionAllowed(action); + + if (actionAllowed && importanceOk && riskOk) { + allowed = true; + requiresApproval = false; + reason = `自动批准: 重要性=${importance}, 风险=${riskLevel}`; + } else if (actionAllowed) { + allowed = false; + requiresApproval = true; + reason = `需要审批: 重要性=${importance}(阈值${this.config.approvalThreshold.importanceMax}), 风险=${riskLevel}(阈值${this.config.approvalThreshold.riskMax})`; + } else { + allowed = false; + requiresApproval = true; + reason = `操作 [${action}] 未在允许列表中`; + } + } + + const decision: AutonomyDecision = { + action, + allowed, + requiresApproval, + reason, + riskLevel, + importance, + timestamp: new Date().toISOString(), + }; + + // Log the decision + if (this.config.auditLogEnabled) { + this.logDecision(decision, context?.details ?? {}); + } + + return decision; + } + + /** + * Check if an action type is allowed by current config. + */ + private isActionAllowed(action: ActionType): boolean { + const actionMap: Record = { + memory_save: 'memoryAutoSave', + memory_delete: null, // Never auto-allowed + identity_update: 'identityAutoUpdate', + identity_rollback: null, + skill_install: 'skillAutoInstall', + skill_uninstall: null, + config_change: null, + workflow_trigger: 'autoCompaction', + hand_trigger: null, + llm_call: 'autoReflection', + reflection_run: 'autoReflection', + compaction_run: 'autoCompaction', + }; + + const configKey = actionMap[action]; + if (!configKey) return false; + return this.config.allowedActions[configKey] ?? false; + } + + // === Approval Workflow === + + /** + * Request approval for an action. + * Returns approval ID that can be used to approve/reject. + */ + requestApproval(decision: AutonomyDecision): string { + const approvalId = `approval_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; + this.pendingApprovals.set(approvalId, decision); + + // Store in localStorage for persistence + this.persistPendingApprovals(); + + console.log(`[AutonomyManager] Approval requested: ${approvalId} for ${decision.action}`); + return approvalId; + } + + /** + * Approve a pending action. + */ + approve(approvalId: string): boolean { + const decision = this.pendingApprovals.get(approvalId); + if (!decision) { + console.warn(`[AutonomyManager] Approval not found: ${approvalId}`); + return false; + } + + // Update decision + decision.allowed = true; + decision.requiresApproval = false; + decision.reason = `用户已批准 [${approvalId}]`; + + // Remove from pending + this.pendingApprovals.delete(approvalId); + this.persistPendingApprovals(); + + // Update audit log + this.updateAuditLogOutcome(approvalId, 'success'); + + console.log(`[AutonomyManager] Approved: ${approvalId}`); + return true; + } + + /** + * Reject a pending action. + */ + reject(approvalId: string): boolean { + const decision = this.pendingApprovals.get(approvalId); + if (!decision) { + console.warn(`[AutonomyManager] Approval not found: ${approvalId}`); + return false; + } + + // Remove from pending + this.pendingApprovals.delete(approvalId); + this.persistPendingApprovals(); + + // Update audit log + this.updateAuditLogOutcome(approvalId, 'failed'); + + console.log(`[AutonomyManager] Rejected: ${approvalId}`); + return true; + } + + /** + * Get all pending approvals. + */ + getPendingApprovals(): Array<{ id: string; decision: AutonomyDecision }> { + return Array.from(this.pendingApprovals.entries()).map(([id, decision]) => ({ + id, + decision, + })); + } + + // === Audit Log === + + private logDecision(decision: AutonomyDecision, context: Record): void { + const entry: AuditLogEntry = { + id: `audit_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`, + action: decision.action, + decision, + context, + outcome: decision.allowed ? 'success' : 'failed', + timestamp: decision.timestamp, + }; + + this.auditLog.push(entry); + + // Keep last 100 entries + if (this.auditLog.length > 100) { + this.auditLog = this.auditLog.slice(-100); + } + + this.saveAuditLog(); + } + + private updateAuditLogOutcome(approvalId: string, outcome: 'success' | 'failed' | 'rolled_back'): void { + // Find the most recent entry for this action and update outcome + const entry = this.auditLog.find(e => e.decision.reason.includes(approvalId)); + if (entry) { + entry.outcome = outcome; + this.saveAuditLog(); + } + } + + /** + * Rollback an action by its audit log ID. + */ + rollback(auditId: string): boolean { + const entry = this.auditLog.find(e => e.id === auditId); + if (!entry) { + console.warn(`[AutonomyManager] Audit entry not found: ${auditId}`); + return false; + } + + if (entry.outcome === 'rolled_back') { + console.warn(`[AutonomyManager] Already rolled back: ${auditId}`); + return false; + } + + // Mark as rolled back + entry.outcome = 'rolled_back'; + entry.rolledBackAt = new Date().toISOString(); + this.saveAuditLog(); + + console.log(`[AutonomyManager] Rolled back: ${auditId}`); + return true; + } + + /** + * Get audit log entries. + */ + getAuditLog(limit: number = 50): AuditLogEntry[] { + return this.auditLog.slice(-limit); + } + + /** + * Clear audit log. + */ + clearAuditLog(): void { + this.auditLog = []; + this.saveAuditLog(); + } + + // === Config Management === + + getConfig(): AutonomyConfig { + return { ...this.config }; + } + + updateConfig(updates: Partial): void { + this.config = { ...this.config, ...updates }; + this.saveConfig(); + } + + setLevel(level: AutonomyLevel): void { + this.config = { ...DEFAULT_AUTONOMY_CONFIGS[level], level }; + this.saveConfig(); + console.log(`[AutonomyManager] Level changed to: ${level}`); + } + + // === Persistence === + + private loadConfig(): AutonomyConfig { + try { + const raw = localStorage.getItem(AUTONOMY_CONFIG_KEY); + if (raw) { + const parsed = JSON.parse(raw); + return { ...DEFAULT_AUTONOMY_CONFIGS.assisted, ...parsed }; + } + } catch { + // Ignore + } + return DEFAULT_AUTONOMY_CONFIGS.assisted; + } + + private saveConfig(): void { + try { + localStorage.setItem(AUTONOMY_CONFIG_KEY, JSON.stringify(this.config)); + } catch { + // Ignore + } + } + + private loadAuditLog(): void { + try { + const raw = localStorage.getItem(AUDIT_LOG_KEY); + if (raw) { + this.auditLog = JSON.parse(raw); + } + } catch { + this.auditLog = []; + } + } + + private saveAuditLog(): void { + try { + localStorage.setItem(AUDIT_LOG_KEY, JSON.stringify(this.auditLog.slice(-100))); + } catch { + // Ignore + } + } + + private persistPendingApprovals(): void { + try { + const pending = Array.from(this.pendingApprovals.entries()); + localStorage.setItem('zclaw-pending-approvals', JSON.stringify(pending)); + } catch { + // Ignore + } + } +} + +// === Singleton === + +let _instance: AutonomyManager | null = null; + +export function getAutonomyManager(config?: Partial): AutonomyManager { + if (!_instance) { + _instance = new AutonomyManager(config); + } + return _instance; +} + +export function resetAutonomyManager(): void { + _instance = null; +} + +// === Helper Functions === + +/** + * Quick check if an action can proceed autonomously. + */ +export function canAutoExecute( + action: ActionType, + importance: number = 5 +): { canProceed: boolean; decision: AutonomyDecision } { + const manager = getAutonomyManager(); + const decision = manager.evaluate(action, { importance }); + return { + canProceed: decision.allowed && !decision.requiresApproval, + decision, + }; +} + +/** + * Execute an action with autonomy check. + * Returns the decision and whether the action was executed. + */ +export async function executeWithAutonomy( + action: ActionType, + importance: number, + executor: () => Promise, + onApprovalNeeded?: (decision: AutonomyDecision, approvalId: string) => void +): Promise<{ executed: boolean; result?: T; decision: AutonomyDecision; approvalId?: string }> { + const manager = getAutonomyManager(); + const decision = manager.evaluate(action, { importance }); + + if (decision.allowed && !decision.requiresApproval) { + // Execute immediately + try { + const result = await executor(); + return { executed: true, result, decision }; + } catch (error) { + console.error(`[AutonomyManager] Action ${action} failed:`, error); + return { executed: false, decision }; + } + } + + // Need approval + const approvalId = manager.requestApproval(decision); + onApprovalNeeded?.(decision, approvalId); + + return { executed: false, decision, approvalId }; +}