/** * 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 }; }