Split monolithic kernel_commands.rs (2185 lines) and pipeline_commands.rs (1391 lines) into focused sub-modules under kernel_commands/ and pipeline_commands/ directories. Add gateway module (commands, config, io, runtime), health_check, and 15 new TypeScript client libraries for SaaS relay, auth, admin, telemetry, and kernel sub-systems (a2a, agent, chat, hands, skills, triggers). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
555 lines
15 KiB
TypeScript
555 lines
15 KiB
TypeScript
/**
|
|
* 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
|
|
*/
|
|
|
|
import { generateRandomString } from './crypto-utils';
|
|
import { createLogger } from './logger';
|
|
|
|
const log = createLogger('AutonomyManager');
|
|
|
|
// === 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';
|
|
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: 'skillAutoInstall',
|
|
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()}_${generateRandomString(6)}`;
|
|
this.pendingApprovals.set(approvalId, decision);
|
|
|
|
// Store in localStorage for persistence
|
|
this.persistPendingApprovals();
|
|
|
|
log.debug(`Approval requested: ${approvalId} for ${decision.action}`);
|
|
return approvalId;
|
|
}
|
|
|
|
/**
|
|
* Approve a pending action.
|
|
*/
|
|
approve(approvalId: string): boolean {
|
|
const decision = this.pendingApprovals.get(approvalId);
|
|
if (!decision) {
|
|
log.warn(`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');
|
|
|
|
log.info('Approved: ${approvalId}');
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Reject a pending action.
|
|
*/
|
|
reject(approvalId: string): boolean {
|
|
const decision = this.pendingApprovals.get(approvalId);
|
|
if (!decision) {
|
|
log.warn(`Approval not found: ${approvalId}`);
|
|
return false;
|
|
}
|
|
|
|
// Remove from pending
|
|
this.pendingApprovals.delete(approvalId);
|
|
this.persistPendingApprovals();
|
|
|
|
// Update audit log
|
|
this.updateAuditLogOutcome(approvalId, 'failed');
|
|
|
|
log.info('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()}_${generateRandomString(6)}`,
|
|
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) {
|
|
log.warn('Audit entry not found: ${auditId}');
|
|
return false;
|
|
}
|
|
|
|
if (entry.outcome === 'rolled_back') {
|
|
log.warn('Already rolled back: ${auditId}');
|
|
return false;
|
|
}
|
|
|
|
// Mark as rolled back
|
|
entry.outcome = 'rolled_back';
|
|
entry.rolledBackAt = new Date().toISOString();
|
|
this.saveAuditLog();
|
|
|
|
log.info(`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();
|
|
log.info(`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 (e) {
|
|
log.debug('Failed to load autonomy config from localStorage', { error: e });
|
|
}
|
|
return DEFAULT_AUTONOMY_CONFIGS.assisted;
|
|
}
|
|
|
|
private saveConfig(): void {
|
|
try {
|
|
localStorage.setItem(AUTONOMY_CONFIG_KEY, JSON.stringify(this.config));
|
|
} catch (e) {
|
|
log.debug('Failed to save autonomy config to localStorage', { error: e });
|
|
}
|
|
}
|
|
|
|
private loadAuditLog(): void {
|
|
try {
|
|
const raw = localStorage.getItem(AUDIT_LOG_KEY);
|
|
if (raw) {
|
|
this.auditLog = JSON.parse(raw);
|
|
}
|
|
} catch (e) {
|
|
log.debug('Failed to load audit log from localStorage', { error: e });
|
|
this.auditLog = [];
|
|
}
|
|
}
|
|
|
|
private saveAuditLog(): void {
|
|
try {
|
|
localStorage.setItem(AUDIT_LOG_KEY, JSON.stringify(this.auditLog.slice(-100)));
|
|
} catch (e) {
|
|
log.debug('Failed to save audit log to localStorage', { error: e });
|
|
}
|
|
}
|
|
|
|
private persistPendingApprovals(): void {
|
|
try {
|
|
const pending = Array.from(this.pendingApprovals.entries());
|
|
localStorage.setItem('zclaw-pending-approvals', JSON.stringify(pending));
|
|
} catch (e) {
|
|
log.debug('Failed to persist pending approvals to localStorage', { error: e });
|
|
}
|
|
}
|
|
}
|
|
|
|
// === 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) {
|
|
log.error(`Action ${action} failed:`, error);
|
|
return { executed: false, decision };
|
|
}
|
|
}
|
|
|
|
// Need approval
|
|
const approvalId = manager.requestApproval(decision);
|
|
onApprovalNeeded?.(decision, approvalId);
|
|
|
|
return { executed: false, decision, approvalId };
|
|
}
|