feat(l4): add AutonomyManager for tiered authorization system (Phase 3)
Implements L4 self-evolution authorization with: Autonomy Levels: - Supervised: All actions require user confirmation - Assisted: Low-risk actions auto-execute, high-risk need approval - Autonomous: Agent decides, only high-impact actions notify Features: - Risk-based action classification (low/medium/high) - Importance threshold for auto-approval - Approval workflow with pending queue - Full audit logging with rollback support - Configurable action permissions per level Security: - High-risk actions ALWAYS require confirmation - Self-modification disabled by default even in autonomous mode - All autonomous actions logged for audit - One-click rollback to any historical state Tests: 30 passing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
548
desktop/src/lib/autonomy-manager.ts
Normal file
548
desktop/src/lib/autonomy-manager.ts
Normal file
@@ -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<string, unknown>;
|
||||
outcome: 'success' | 'failed' | 'rolled_back';
|
||||
rolledBackAt?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
// === Risk Mapping ===
|
||||
|
||||
const ACTION_RISK_MAP: Record<ActionType, RiskLevel> = {
|
||||
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<RiskLevel, number> = {
|
||||
low: 1,
|
||||
medium: 2,
|
||||
high: 3,
|
||||
};
|
||||
|
||||
// === Default Configs ===
|
||||
|
||||
export const DEFAULT_AUTONOMY_CONFIGS: Record<AutonomyLevel, AutonomyConfig> = {
|
||||
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<string, AutonomyDecision> = new Map();
|
||||
|
||||
constructor(config?: Partial<AutonomyConfig>) {
|
||||
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<string, unknown>;
|
||||
}
|
||||
): 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<ActionType, keyof AutonomyConfig['allowedActions'] | null> = {
|
||||
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<string, unknown>): 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<AutonomyConfig>): 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<AutonomyConfig>): 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<T>(
|
||||
action: ActionType,
|
||||
importance: number,
|
||||
executor: () => Promise<T>,
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user