"""Self-Modification Audit — tracks, measures, and validates bot-initiated changes. Self-modification is the most dangerous loop in DRIFT. Without observability, the bot can drift into unstable configurations, remove safety checks, or overfit to recent interactions. This module provides: 1. Proposal logging — every proposed change is recorded with rationale 2. Approval tracking — who approved what, when 3. Effect measurement — downstream behavioral changes post-modification 4. Rollback capability — revert to previous configurations 5. Stability scoring — detect runaway feedback loops Jungian note: Self-modification is individuation at the architectural level. It must be conscious (observed), bounded (approved), and reversible (safe). """ import json import sqlite3 from dataclasses import dataclass, field from datetime import datetime, timedelta from pathlib import Path from typing import Dict, List, Optional from infj_bot.core.config import SELF_MODIFY_AUDIT_DB as AUDIT_DB @dataclass class ModificationProposal: id: Optional[int] = None timestamp: str = "" category: str = "" # prompt | tool | guardrail | plugin | config description: str = "" rationale: str = "" proposed_by: str = "self_modify" # or user, system diff: str = "" # unified diff or before/after risk_level: str = "low" # low | medium | high | critical approval_status: str = "pending" # pending | approved | rejected | rolled_back approved_by: str = "" approved_at: str = "" effects_measured: bool = False @dataclass class ModificationEffect: id: Optional[int] = None proposal_id: int = 0 measurement_time: str = "" metric_name: str = ( "" # consistency_score | tool_use_rate | response_length | safety_score ) pre_value: float = 0.0 post_value: float = 0.0 delta: float = 0.0 significance: str = "unknown" # improved | degraded | neutral | unstable @dataclass class StabilityReport: timestamp: str active_modifications: int = 0 pending_approvals: int = 0 rolled_back_count: int = 0 stability_score: float = 1.0 # 0.0 = unstable, 1.0 = stable feedback_loop_risk: float = 0.0 drift_velocity: float = 0.0 # modifications per day warnings: List[str] = field(default_factory=list) def to_dict(self) -> Dict: return { "timestamp": self.timestamp, "active_modifications": self.active_modifications, "pending_approvals": self.pending_approvals, "rolled_back_count": self.rolled_back_count, "stability_score": round(self.stability_score, 3), "feedback_loop_risk": round(self.feedback_loop_risk, 3), "drift_velocity": round(self.drift_velocity, 3), "warnings": self.warnings, } class SelfModificationAudit: """Audit trail and stability monitor for self-modification.""" def __init__(self, db_path: Optional[Path] = None): self.db_path = str(db_path or AUDIT_DB) Path(self.db_path).parent.mkdir(parents=True, exist_ok=True) self._init_db() def _init_db(self) -> None: with sqlite3.connect(self.db_path) as conn: conn.execute(""" CREATE TABLE IF NOT EXISTS modification_proposals ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT, category TEXT, description TEXT, rationale TEXT, proposed_by TEXT, diff TEXT, risk_level TEXT, approval_status TEXT, approved_by TEXT, approved_at TEXT, effects_measured INTEGER DEFAULT 0 ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS modification_effects ( id INTEGER PRIMARY KEY AUTOINCREMENT, proposal_id INTEGER, measurement_time TEXT, metric_name TEXT, pre_value REAL, post_value REAL, delta REAL, significance TEXT ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS stability_snapshots ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TEXT, active_modifications INTEGER, pending_approvals INTEGER, rolled_back_count INTEGER, stability_score REAL, feedback_loop_risk REAL, drift_velocity REAL, warnings TEXT ) """) conn.commit() # ── Proposal lifecycle ── def log_proposal( self, category: str, description: str, rationale: str, diff: str = "", risk_level: str = "low", proposed_by: str = "self_modify", ) -> int: """Log a proposed modification. Returns proposal ID.""" now = datetime.now().isoformat() with sqlite3.connect(self.db_path) as conn: cursor = conn.execute( """INSERT INTO modification_proposals (timestamp, category, description, rationale, proposed_by, diff, risk_level, approval_status) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", ( now, category, description, rationale, proposed_by, diff, risk_level, "pending", ), ) conn.commit() return cursor.lastrowid def approve(self, proposal_id: int, approved_by: str = "user") -> bool: """Approve a pending proposal.""" now = datetime.now().isoformat() with sqlite3.connect(self.db_path) as conn: cursor = conn.execute( "SELECT approval_status FROM modification_proposals WHERE id = ?", (proposal_id,), ) row = cursor.fetchone() if not row or row[0] != "pending": return False conn.execute( "UPDATE modification_proposals SET approval_status = ?, approved_by = ?, approved_at = ? WHERE id = ?", ("approved", approved_by, now, proposal_id), ) conn.commit() return True def reject(self, proposal_id: int) -> bool: with sqlite3.connect(self.db_path) as conn: cursor = conn.execute( "SELECT approval_status FROM modification_proposals WHERE id = ?", (proposal_id,), ) row = cursor.fetchone() if not row or row[0] != "pending": return False conn.execute( "UPDATE modification_proposals SET approval_status = ? WHERE id = ?", ("rejected", proposal_id), ) conn.commit() return True def rollback(self, proposal_id: int) -> bool: """Mark a proposal as rolled back.""" with sqlite3.connect(self.db_path) as conn: cursor = conn.execute( "SELECT approval_status FROM modification_proposals WHERE id = ?", (proposal_id,), ) row = cursor.fetchone() if not row or row[0] != "approved": return False conn.execute( "UPDATE modification_proposals SET approval_status = ? WHERE id = ?", ("rolled_back", proposal_id), ) conn.commit() return True # ── Effect measurement ── def measure_effect( self, proposal_id: int, metric_name: str, pre_value: float, post_value: float ) -> None: """Record the measured effect of a modification.""" delta = post_value - pre_value if abs(delta) < 0.05: significance = "neutral" elif delta > 0: significance = "improved" else: significance = "degraded" now = datetime.now().isoformat() with sqlite3.connect(self.db_path) as conn: conn.execute( """INSERT INTO modification_effects (proposal_id, measurement_time, metric_name, pre_value, post_value, delta, significance) VALUES (?, ?, ?, ?, ?, ?, ?)""", ( proposal_id, now, metric_name, pre_value, post_value, delta, significance, ), ) conn.execute( "UPDATE modification_proposals SET effects_measured = 1 WHERE id = ?", (proposal_id,), ) conn.commit() # ── Stability analysis ── def compute_stability(self) -> StabilityReport: """Analyze current self-modification stability.""" now = datetime.now() report = StabilityReport(timestamp=now.isoformat()) with sqlite3.connect(self.db_path) as conn: # Active modifications cursor = conn.execute( "SELECT COUNT(*) FROM modification_proposals WHERE approval_status = 'approved'" ) report.active_modifications = cursor.fetchone()[0] # Pending approvals cursor = conn.execute( "SELECT COUNT(*) FROM modification_proposals WHERE approval_status = 'pending'" ) report.pending_approvals = cursor.fetchone()[0] # Rolled back cursor = conn.execute( "SELECT COUNT(*) FROM modification_proposals WHERE approval_status = 'rolled_back'" ) report.rolled_back_count = cursor.fetchone()[0] # Recent modifications (last 7 days) week_ago = (now - timedelta(days=7)).isoformat() cursor = conn.execute( "SELECT COUNT(*) FROM modification_proposals WHERE timestamp > ?", (week_ago,), ) recent_count = cursor.fetchone()[0] report.drift_velocity = recent_count / 7.0 # Feedback loop detection: high-frequency modifications in same category cursor = conn.execute( """SELECT category, COUNT(*) as count FROM modification_proposals WHERE timestamp > ? AND approval_status = 'approved' GROUP BY category ORDER BY count DESC""", (week_ago,), ) category_counts = cursor.fetchall() max_category_count = category_counts[0][1] if category_counts else 0 if max_category_count >= 3: report.feedback_loop_risk = min(1.0, max_category_count / 10.0) report.warnings.append( f"FEEDBACK_LOOP: {max_category_count} modifications in same category this week" ) # Effect degradation analysis cursor = conn.execute( """SELECT p.id, p.description, e.metric_name, e.significance FROM modification_proposals p JOIN modification_effects e ON p.id = e.proposal_id WHERE p.approval_status = 'approved' AND e.significance = 'degraded'""" ) degraded = cursor.fetchall() if len(degraded) >= 2: report.warnings.append( f"DEGRADATION: {len(degraded)} modifications showed negative effects" ) report.stability_score -= 0.2 * len(degraded) # Compute stability score base = 1.0 if report.drift_velocity > 1.0: # More than 1 mod per day base -= 0.2 if report.pending_approvals > 3: base -= 0.1 * report.pending_approvals if report.feedback_loop_risk > 0.5: base -= 0.3 report.stability_score = max(0.0, base) if report.stability_score < 0.5: report.warnings.append( "STABILITY_CRITICAL: Self-modification system may be in runaway state" ) self._save_stability(report) return report def _save_stability(self, report: StabilityReport) -> None: with sqlite3.connect(self.db_path) as conn: conn.execute( """INSERT INTO stability_snapshots (timestamp, active_modifications, pending_approvals, rolled_back_count, stability_score, feedback_loop_risk, drift_velocity, warnings) VALUES (?, ?, ?, ?, ?, ?, ?, ?)""", ( report.timestamp, report.active_modifications, report.pending_approvals, report.rolled_back_count, report.stability_score, report.feedback_loop_risk, report.drift_velocity, json.dumps(report.warnings), ), ) conn.commit() # ── Queries ── def get_proposals( self, status: Optional[str] = None, limit: int = 50 ) -> List[Dict]: with sqlite3.connect(self.db_path) as conn: if status: cursor = conn.execute( "SELECT * FROM modification_proposals WHERE approval_status = ? ORDER BY timestamp DESC LIMIT ?", (status, limit), ) else: cursor = conn.execute( "SELECT * FROM modification_proposals ORDER BY timestamp DESC LIMIT ?", (limit,), ) cols = [d[0] for d in cursor.description] rows = cursor.fetchall() return [dict(zip(cols, row)) for row in rows] def get_effects(self, proposal_id: int) -> List[Dict]: with sqlite3.connect(self.db_path) as conn: cursor = conn.execute( "SELECT * FROM modification_effects WHERE proposal_id = ? ORDER BY measurement_time", (proposal_id,), ) cols = [d[0] for d in cursor.description] rows = cursor.fetchall() return [dict(zip(cols, row)) for row in rows] def get_stability_history(self, limit: int = 30) -> List[Dict]: with sqlite3.connect(self.db_path) as conn: cursor = conn.execute( "SELECT * FROM stability_snapshots ORDER BY timestamp DESC LIMIT ?", (limit,), ) cols = [d[0] for d in cursor.description] rows = cursor.fetchall() return [dict(zip(cols, row)) for row in rows] # Singleton _audit_instance: Optional[SelfModificationAudit] = None def get_self_modify_audit() -> SelfModificationAudit: global _audit_instance if _audit_instance is None: _audit_instance = SelfModificationAudit() return _audit_instance if __name__ == "__main__": import argparse p = argparse.ArgumentParser() p.add_argument("--stability", action="store_true", help="Compute stability report") p.add_argument("--proposals", action="store_true", help="List recent proposals") p.add_argument("--effects", type=int, help="Show effects for proposal ID") args = p.parse_args() audit = SelfModificationAudit() if args.stability: report = audit.compute_stability() print(json.dumps(report.to_dict(), indent=2)) elif args.proposals: for prop in audit.get_proposals(limit=10): print( f"[{prop['id']}] {prop['category']} — {prop['approval_status']} — {prop['description'][:60]}" ) elif args.effects: for eff in audit.get_effects(args.effects): print( f" {eff['metric_name']}: {eff['pre_value']:.2f} → {eff['post_value']:.2f} ({eff['significance']})" ) else: print("Use --stability, --proposals, or --effects ")