v2.0 — Re-integrate autonomy framework from Linux non-public build

Add multi-model autonomous threat response system (SLM/SAM/LAM):
- ModelRouter: concurrent model tiers with fallback chains
- RulesEngine: condition-action automation with 11 condition/action types
- AutonomyDaemon: background threat monitoring and rule dispatch
- Web UI: 4-tab dashboard (Dashboard, Rules, Activity Log, Models)
- Config: [slm], [sam], [lam], [autonomy] settings sections

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
DigiJ 2026-03-03 00:51:18 -08:00
parent 1789a07c2b
commit 6d4bef8d24
11 changed files with 2393 additions and 1 deletions

3
.gitignore vendored
View File

@ -61,6 +61,9 @@ build/
build_temp/ build_temp/
*.spec.bak *.spec.bak
# Local utility scripts
kill_autarch.bat
# OS files # OS files
.DS_Store .DS_Store
Thumbs.db Thumbs.db

View File

@ -64,6 +64,7 @@ hidden_imports = [
'core.upnp', 'core.wireshark', 'core.wireguard', 'core.upnp', 'core.wireshark', 'core.wireguard',
'core.mcp_server', 'core.discovery', 'core.mcp_server', 'core.discovery',
'core.osint_db', 'core.nvd', 'core.osint_db', 'core.nvd',
'core.model_router', 'core.rules', 'core.autonomy',
# Web routes (Flask blueprints) # Web routes (Flask blueprints)
'web.app', 'web.auth', 'web.app', 'web.auth',
@ -90,6 +91,7 @@ hidden_imports = [
'web.routes.targets', 'web.routes.targets',
'web.routes.encmodules', 'web.routes.encmodules',
'web.routes.llm_trainer', 'web.routes.llm_trainer',
'web.routes.autonomy',
# Standard library (sometimes missed on Windows) # Standard library (sometimes missed on Windows)
'email.mime.text', 'email.mime.multipart', 'email.mime.text', 'email.mime.multipart',

View File

@ -117,3 +117,35 @@ host = 0.0.0.0
port = 17322 port = 17322
auto_start = false auto_start = false
[slm]
enabled = true
backend = local
model_path =
n_ctx = 512
n_gpu_layers = -1
n_threads = 2
[sam]
enabled = true
backend = local
model_path =
n_ctx = 2048
n_gpu_layers = -1
n_threads = 4
[lam]
enabled = true
backend = local
model_path =
n_ctx = 4096
n_gpu_layers = -1
n_threads = 4
[autonomy]
enabled = false
monitor_interval = 3
rule_eval_interval = 5
max_concurrent_agents = 3
threat_threshold_auto_respond = 40
log_max_entries = 1000

665
core/autonomy.py Normal file
View File

@ -0,0 +1,665 @@
"""
AUTARCH Autonomy Daemon
Background loop that monitors threats, evaluates rules, and dispatches
AI-driven responses across all categories (defense, offense, counter,
analyze, OSINT, simulate).
The daemon ties together:
- ThreatMonitor (threat data gathering)
- RulesEngine (condition-action evaluation)
- ModelRouter (SLM/SAM/LAM model tiers)
- Agent (autonomous task execution)
"""
import json
import logging
import threading
import time
import uuid
from collections import deque
from dataclasses import dataclass, field, asdict
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Any, Optional, Deque
from .config import get_config
from .rules import RulesEngine, Rule
from .model_router import get_model_router, ModelTier
_logger = logging.getLogger('autarch.autonomy')
@dataclass
class ActivityEntry:
"""Single entry in the autonomy activity log."""
id: str
timestamp: str
rule_id: Optional[str] = None
rule_name: Optional[str] = None
tier: Optional[str] = None
action_type: str = ''
action_detail: str = ''
result: str = ''
success: bool = True
duration_ms: Optional[int] = None
def to_dict(self) -> dict:
return asdict(self)
class AutonomyDaemon:
"""Background daemon for autonomous threat response.
Lifecycle: start() -> pause()/resume() -> stop()
"""
LOG_PATH = Path(__file__).parent.parent / 'data' / 'autonomy_log.json'
def __init__(self, config=None):
self.config = config or get_config()
self.rules_engine = RulesEngine()
self._router = None # Lazy — get_model_router() on start
# State
self._thread: Optional[threading.Thread] = None
self._running = False
self._paused = False
self._stop_event = threading.Event()
# Agent tracking
self._active_agents: Dict[str, threading.Thread] = {}
self._agent_lock = threading.Lock()
# Activity log (ring buffer)
settings = self.config.get_autonomy_settings()
max_entries = settings.get('log_max_entries', 1000)
self._activity: Deque[ActivityEntry] = deque(maxlen=max_entries)
self._activity_lock = threading.Lock()
# SSE subscribers
self._subscribers: List = []
self._sub_lock = threading.Lock()
# Load persisted log
self._load_log()
# ------------------------------------------------------------------
# Lifecycle
# ------------------------------------------------------------------
@property
def status(self) -> dict:
"""Current daemon status."""
settings = self.config.get_autonomy_settings()
with self._agent_lock:
active = len(self._active_agents)
return {
'running': self._running,
'paused': self._paused,
'enabled': settings['enabled'],
'monitor_interval': settings['monitor_interval'],
'rule_eval_interval': settings['rule_eval_interval'],
'active_agents': active,
'max_agents': settings['max_concurrent_agents'],
'rules_count': len(self.rules_engine.get_all_rules()),
'activity_count': len(self._activity),
}
def start(self) -> bool:
"""Start the autonomy daemon background thread."""
if self._running:
_logger.warning('[Autonomy] Already running')
return False
self._router = get_model_router()
self._running = True
self._paused = False
self._stop_event.clear()
self._thread = threading.Thread(
target=self._run_loop,
name='AutonomyDaemon',
daemon=True,
)
self._thread.start()
self._log_activity('system', 'Autonomy daemon started')
_logger.info('[Autonomy] Daemon started')
return True
def stop(self):
"""Stop the daemon and wait for thread exit."""
if not self._running:
return
self._running = False
self._stop_event.set()
if self._thread and self._thread.is_alive():
self._thread.join(timeout=10)
self._log_activity('system', 'Autonomy daemon stopped')
_logger.info('[Autonomy] Daemon stopped')
def pause(self):
"""Pause rule evaluation (monitoring continues)."""
self._paused = True
self._log_activity('system', 'Autonomy paused')
_logger.info('[Autonomy] Paused')
def resume(self):
"""Resume rule evaluation."""
self._paused = False
self._log_activity('system', 'Autonomy resumed')
_logger.info('[Autonomy] Resumed')
# ------------------------------------------------------------------
# Main loop
# ------------------------------------------------------------------
def _run_loop(self):
"""Background loop: gather context, evaluate rules, dispatch."""
settings = self.config.get_autonomy_settings()
monitor_interval = settings['monitor_interval']
rule_eval_interval = settings['rule_eval_interval']
last_rule_eval = 0
while self._running and not self._stop_event.is_set():
try:
# Gather threat context every cycle
context = self._gather_context()
# Evaluate rules at a slower cadence
now = time.time()
if not self._paused and (now - last_rule_eval) >= rule_eval_interval:
last_rule_eval = now
self._evaluate_and_dispatch(context)
except Exception as e:
_logger.error(f'[Autonomy] Loop error: {e}')
self._log_activity('error', f'Loop error: {e}', success=False)
# Sleep in short increments so stop is responsive
self._stop_event.wait(timeout=monitor_interval)
def _gather_context(self) -> Dict[str, Any]:
"""Gather current threat context from ThreatMonitor."""
try:
from modules.defender_monitor import get_threat_monitor
tm = get_threat_monitor()
except ImportError:
_logger.warning('[Autonomy] ThreatMonitor not available')
return {'timestamp': datetime.now().isoformat()}
context: Dict[str, Any] = {
'timestamp': datetime.now().isoformat(),
}
try:
context['connections'] = tm.get_connections()
context['connection_count'] = len(context['connections'])
except Exception:
context['connections'] = []
context['connection_count'] = 0
try:
context['bandwidth'] = {}
bw = tm.get_bandwidth()
if bw:
total_rx = sum(iface.get('rx_delta', 0) for iface in bw)
total_tx = sum(iface.get('tx_delta', 0) for iface in bw)
context['bandwidth'] = {
'rx_mbps': (total_rx * 8) / 1_000_000,
'tx_mbps': (total_tx * 8) / 1_000_000,
'interfaces': bw,
}
except Exception:
context['bandwidth'] = {'rx_mbps': 0, 'tx_mbps': 0}
try:
context['arp_alerts'] = tm.check_arp_spoofing()
except Exception:
context['arp_alerts'] = []
try:
context['new_ports'] = tm.check_new_listening_ports()
except Exception:
context['new_ports'] = []
try:
context['threat_score'] = tm.calculate_threat_score()
except Exception:
context['threat_score'] = {'score': 0, 'level': 'LOW', 'details': []}
try:
context['ddos'] = tm.detect_ddos()
except Exception:
context['ddos'] = {'under_attack': False}
try:
context['scan_indicators'] = tm.check_port_scan_indicators()
if isinstance(context['scan_indicators'], list):
context['scan_indicators'] = len(context['scan_indicators'])
except Exception:
context['scan_indicators'] = 0
return context
# ------------------------------------------------------------------
# Rule evaluation and dispatch
# ------------------------------------------------------------------
def _evaluate_and_dispatch(self, context: Dict[str, Any]):
"""Evaluate rules and dispatch matching actions."""
matches = self.rules_engine.evaluate(context)
for rule, resolved_actions in matches:
for action in resolved_actions:
action_type = action.get('type', '')
_logger.info(f'[Autonomy] Rule "{rule.name}" triggered -> {action_type}')
if self._is_agent_action(action_type):
self._dispatch_agent(rule, action, context)
else:
self._dispatch_direct(rule, action, context)
def _is_agent_action(self, action_type: str) -> bool:
"""Check if an action requires an AI agent."""
return action_type in ('run_module', 'counter_scan', 'escalate_to_lam')
def _dispatch_direct(self, rule: Rule, action: dict, context: dict):
"""Execute a simple action directly (no LLM needed)."""
action_type = action.get('type', '')
start = time.time()
success = True
result = ''
try:
if action_type == 'block_ip':
result = self._action_block_ip(action.get('ip', ''))
elif action_type == 'unblock_ip':
result = self._action_unblock_ip(action.get('ip', ''))
elif action_type == 'rate_limit_ip':
result = self._action_rate_limit(
action.get('ip', ''),
action.get('rate', '10/s'),
)
elif action_type == 'block_port':
result = self._action_block_port(
action.get('port', ''),
action.get('direction', 'inbound'),
)
elif action_type == 'kill_process':
result = self._action_kill_process(action.get('pid', ''))
elif action_type in ('alert', 'log_event'):
result = action.get('message', 'No message')
elif action_type == 'run_shell':
result = self._action_run_shell(action.get('command', ''))
else:
result = f'Unknown action type: {action_type}'
success = False
except Exception as e:
result = f'Error: {e}'
success = False
duration = int((time.time() - start) * 1000)
detail = action.get('ip', '') or action.get('port', '') or action.get('message', '')[:80]
self._log_activity(
action_type, detail,
rule_id=rule.id, rule_name=rule.name,
result=result, success=success, duration_ms=duration,
)
def _dispatch_agent(self, rule: Rule, action: dict, context: dict):
"""Spawn an AI agent to handle a complex action."""
settings = self.config.get_autonomy_settings()
max_agents = settings['max_concurrent_agents']
# Clean finished agents
with self._agent_lock:
self._active_agents = {
k: v for k, v in self._active_agents.items()
if v.is_alive()
}
if len(self._active_agents) >= max_agents:
_logger.warning('[Autonomy] Max agents reached, skipping')
self._log_activity(
action.get('type', 'agent'), 'Skipped: max agents reached',
rule_id=rule.id, rule_name=rule.name,
success=False,
)
return
agent_id = str(uuid.uuid4())[:8]
action_type = action.get('type', '')
# Determine tier
if action_type == 'escalate_to_lam':
tier = ModelTier.LAM
else:
tier = ModelTier.SAM
t = threading.Thread(
target=self._run_agent,
args=(agent_id, tier, rule, action, context),
name=f'Agent-{agent_id}',
daemon=True,
)
with self._agent_lock:
self._active_agents[agent_id] = t
t.start()
self._log_activity(
action_type, f'Agent {agent_id} spawned ({tier.value})',
rule_id=rule.id, rule_name=rule.name, tier=tier.value,
)
def _run_agent(self, agent_id: str, tier: ModelTier, rule: Rule,
action: dict, context: dict):
"""Execute an agent task in a background thread."""
from .agent import Agent
from .tools import get_tool_registry
action_type = action.get('type', '')
start = time.time()
# Build task prompt
if action_type == 'run_module':
module = action.get('module', '')
args = action.get('args', '')
task = f'Run the AUTARCH module "{module}" with arguments: {args}'
elif action_type == 'counter_scan':
target = action.get('target', '')
task = f'Perform a counter-scan against {target}. Gather reconnaissance and identify vulnerabilities.'
elif action_type == 'escalate_to_lam':
task = action.get('task', 'Analyze the current threat landscape and recommend actions.')
else:
task = f'Execute action: {action_type} with params: {json.dumps(action)}'
# Get LLM instance for the tier
router = self._router or get_model_router()
llm_inst = router.get_instance(tier)
if llm_inst is None or not llm_inst.is_loaded:
# Try fallback
for fallback in (ModelTier.SAM, ModelTier.LAM):
llm_inst = router.get_instance(fallback)
if llm_inst and llm_inst.is_loaded:
tier = fallback
break
else:
self._log_activity(
action_type, f'Agent {agent_id}: no model loaded',
rule_id=rule.id, rule_name=rule.name,
tier=tier.value, success=False,
result='No model available for agent execution',
)
return
try:
agent = Agent(
llm=llm_inst,
tools=get_tool_registry(),
max_steps=15,
verbose=False,
)
result = agent.run(task)
duration = int((time.time() - start) * 1000)
self._log_activity(
action_type,
f'Agent {agent_id}: {result.summary[:100]}',
rule_id=rule.id, rule_name=rule.name,
tier=tier.value, success=result.success,
result=result.summary, duration_ms=duration,
)
except Exception as e:
duration = int((time.time() - start) * 1000)
_logger.error(f'[Autonomy] Agent {agent_id} failed: {e}')
self._log_activity(
action_type, f'Agent {agent_id} failed: {e}',
rule_id=rule.id, rule_name=rule.name,
tier=tier.value, success=False,
result=str(e), duration_ms=duration,
)
finally:
with self._agent_lock:
self._active_agents.pop(agent_id, None)
# ------------------------------------------------------------------
# Direct action implementations
# ------------------------------------------------------------------
def _action_block_ip(self, ip: str) -> str:
if not ip:
return 'No IP specified'
try:
from modules.defender_monitor import get_threat_monitor
tm = get_threat_monitor()
tm.auto_block_ip(ip)
return f'Blocked {ip}'
except Exception as e:
return f'Block failed: {e}'
def _action_unblock_ip(self, ip: str) -> str:
if not ip:
return 'No IP specified'
try:
import subprocess, platform
if platform.system() == 'Windows':
cmd = f'netsh advfirewall firewall delete rule name="AUTARCH Block {ip}"'
else:
cmd = f'iptables -D INPUT -s {ip} -j DROP 2>/dev/null; iptables -D OUTPUT -d {ip} -j DROP 2>/dev/null'
subprocess.run(cmd, shell=True, capture_output=True, timeout=10)
return f'Unblocked {ip}'
except Exception as e:
return f'Unblock failed: {e}'
def _action_rate_limit(self, ip: str, rate: str) -> str:
if not ip:
return 'No IP specified'
try:
from modules.defender_monitor import get_threat_monitor
tm = get_threat_monitor()
tm.apply_rate_limit(ip)
return f'Rate limited {ip} at {rate}'
except Exception as e:
return f'Rate limit failed: {e}'
def _action_block_port(self, port: str, direction: str) -> str:
if not port:
return 'No port specified'
try:
import subprocess, platform
if platform.system() == 'Windows':
d = 'in' if direction == 'inbound' else 'out'
cmd = f'netsh advfirewall firewall add rule name="AUTARCH Block Port {port}" dir={d} action=block protocol=TCP localport={port}'
else:
chain = 'INPUT' if direction == 'inbound' else 'OUTPUT'
cmd = f'iptables -A {chain} -p tcp --dport {port} -j DROP'
subprocess.run(cmd, shell=True, capture_output=True, timeout=10)
return f'Blocked port {port} ({direction})'
except Exception as e:
return f'Block port failed: {e}'
def _action_kill_process(self, pid: str) -> str:
if not pid:
return 'No PID specified'
try:
import subprocess, platform
if platform.system() == 'Windows':
cmd = f'taskkill /F /PID {pid}'
else:
cmd = f'kill -9 {pid}'
subprocess.run(cmd, shell=True, capture_output=True, timeout=10)
return f'Killed process {pid}'
except Exception as e:
return f'Kill failed: {e}'
def _action_run_shell(self, command: str) -> str:
if not command:
return 'No command specified'
try:
import subprocess
result = subprocess.run(
command, shell=True, capture_output=True,
text=True, timeout=30,
)
output = result.stdout[:500]
if result.returncode != 0:
output += f'\n[exit {result.returncode}]'
return output.strip() or '[no output]'
except Exception as e:
return f'Shell failed: {e}'
# ------------------------------------------------------------------
# Activity log
# ------------------------------------------------------------------
def _log_activity(self, action_type: str, detail: str, *,
rule_id: str = None, rule_name: str = None,
tier: str = None, result: str = '',
success: bool = True, duration_ms: int = None):
"""Add an entry to the activity log and notify SSE subscribers."""
entry = ActivityEntry(
id=str(uuid.uuid4())[:8],
timestamp=datetime.now().isoformat(),
rule_id=rule_id,
rule_name=rule_name,
tier=tier,
action_type=action_type,
action_detail=detail,
result=result,
success=success,
duration_ms=duration_ms,
)
with self._activity_lock:
self._activity.append(entry)
# Notify SSE subscribers
self._notify_subscribers(entry)
# Persist periodically (every 10 entries)
if len(self._activity) % 10 == 0:
self._save_log()
def get_activity(self, limit: int = 50, offset: int = 0) -> List[dict]:
"""Get recent activity entries."""
with self._activity_lock:
entries = list(self._activity)
entries.reverse() # Newest first
return [e.to_dict() for e in entries[offset:offset + limit]]
def get_activity_count(self) -> int:
return len(self._activity)
# ------------------------------------------------------------------
# SSE streaming
# ------------------------------------------------------------------
def subscribe(self):
"""Create an SSE subscriber queue."""
import queue
q = queue.Queue(maxsize=100)
with self._sub_lock:
self._subscribers.append(q)
return q
def unsubscribe(self, q):
"""Remove an SSE subscriber."""
with self._sub_lock:
try:
self._subscribers.remove(q)
except ValueError:
pass
def _notify_subscribers(self, entry: ActivityEntry):
"""Push an activity entry to all SSE subscribers."""
data = json.dumps(entry.to_dict())
with self._sub_lock:
dead = []
for q in self._subscribers:
try:
q.put_nowait(data)
except Exception:
dead.append(q)
for q in dead:
try:
self._subscribers.remove(q)
except ValueError:
pass
# ------------------------------------------------------------------
# Persistence
# ------------------------------------------------------------------
def _save_log(self):
"""Persist activity log to JSON file."""
try:
self.LOG_PATH.parent.mkdir(parents=True, exist_ok=True)
with self._activity_lock:
entries = [e.to_dict() for e in self._activity]
self.LOG_PATH.write_text(
json.dumps({'entries': entries[-200:]}, indent=2),
encoding='utf-8',
)
except Exception as e:
_logger.error(f'[Autonomy] Failed to save log: {e}')
def _load_log(self):
"""Load persisted activity log."""
if not self.LOG_PATH.exists():
return
try:
data = json.loads(self.LOG_PATH.read_text(encoding='utf-8'))
for entry_dict in data.get('entries', []):
entry = ActivityEntry(
id=entry_dict.get('id', str(uuid.uuid4())[:8]),
timestamp=entry_dict.get('timestamp', ''),
rule_id=entry_dict.get('rule_id'),
rule_name=entry_dict.get('rule_name'),
tier=entry_dict.get('tier'),
action_type=entry_dict.get('action_type', ''),
action_detail=entry_dict.get('action_detail', ''),
result=entry_dict.get('result', ''),
success=entry_dict.get('success', True),
duration_ms=entry_dict.get('duration_ms'),
)
self._activity.append(entry)
_logger.info(f'[Autonomy] Loaded {len(self._activity)} log entries')
except Exception as e:
_logger.error(f'[Autonomy] Failed to load log: {e}')
# ------------------------------------------------------------------
# Singleton
# ------------------------------------------------------------------
_daemon_instance: Optional[AutonomyDaemon] = None
def get_autonomy_daemon() -> AutonomyDaemon:
"""Get the global AutonomyDaemon instance."""
global _daemon_instance
if _daemon_instance is None:
_daemon_instance = AutonomyDaemon()
return _daemon_instance
def reset_autonomy_daemon():
"""Stop and reset the global daemon."""
global _daemon_instance
if _daemon_instance is not None:
_daemon_instance.stop()
_daemon_instance = None

View File

@ -87,7 +87,39 @@ class Config:
'host': '0.0.0.0', 'host': '0.0.0.0',
'port': '17322', 'port': '17322',
'auto_start': 'false', 'auto_start': 'false',
} },
'slm': {
'enabled': 'true',
'backend': 'local',
'model_path': '',
'n_ctx': '512',
'n_gpu_layers': '-1',
'n_threads': '2',
},
'sam': {
'enabled': 'true',
'backend': 'local',
'model_path': '',
'n_ctx': '2048',
'n_gpu_layers': '-1',
'n_threads': '4',
},
'lam': {
'enabled': 'true',
'backend': 'local',
'model_path': '',
'n_ctx': '4096',
'n_gpu_layers': '-1',
'n_threads': '4',
},
'autonomy': {
'enabled': 'false',
'monitor_interval': '3',
'rule_eval_interval': '5',
'max_concurrent_agents': '3',
'threat_threshold_auto_respond': '40',
'log_max_entries': '1000',
},
} }
def __init__(self, config_path: str = None): def __init__(self, config_path: str = None):
@ -332,6 +364,40 @@ class Config:
'auto_start': self.get_bool('revshell', 'auto_start', False), 'auto_start': self.get_bool('revshell', 'auto_start', False),
} }
def get_tier_settings(self, tier: str) -> dict:
"""Get settings for a model tier (slm, sam, lam)."""
return {
'enabled': self.get_bool(tier, 'enabled', True),
'backend': self.get(tier, 'backend', 'local'),
'model_path': self.get(tier, 'model_path', ''),
'n_ctx': self.get_int(tier, 'n_ctx', 2048),
'n_gpu_layers': self.get_int(tier, 'n_gpu_layers', -1),
'n_threads': self.get_int(tier, 'n_threads', 4),
}
def get_slm_settings(self) -> dict:
"""Get Small Language Model tier settings."""
return self.get_tier_settings('slm')
def get_sam_settings(self) -> dict:
"""Get Small Action Model tier settings."""
return self.get_tier_settings('sam')
def get_lam_settings(self) -> dict:
"""Get Large Action Model tier settings."""
return self.get_tier_settings('lam')
def get_autonomy_settings(self) -> dict:
"""Get autonomy daemon settings."""
return {
'enabled': self.get_bool('autonomy', 'enabled', False),
'monitor_interval': self.get_int('autonomy', 'monitor_interval', 3),
'rule_eval_interval': self.get_int('autonomy', 'rule_eval_interval', 5),
'max_concurrent_agents': self.get_int('autonomy', 'max_concurrent_agents', 3),
'threat_threshold_auto_respond': self.get_int('autonomy', 'threat_threshold_auto_respond', 40),
'log_max_entries': self.get_int('autonomy', 'log_max_entries', 1000),
}
@staticmethod @staticmethod
def get_templates_dir() -> Path: def get_templates_dir() -> Path:
"""Get the path to the configuration templates directory.""" """Get the path to the configuration templates directory."""

305
core/model_router.py Normal file
View File

@ -0,0 +1,305 @@
"""
AUTARCH Model Router
Manages concurrent SLM/LAM/SAM model instances for autonomous operation.
Model Tiers:
SLM (Small Language Model) Fast classification, routing, yes/no decisions
SAM (Small Action Model) Quick tool execution, simple automated responses
LAM (Large Action Model) Complex multi-step agent tasks, strategic planning
"""
import json
import logging
import threading
from typing import Optional, Dict, Any
from enum import Enum
from .config import get_config
_logger = logging.getLogger('autarch.model_router')
class ModelTier(Enum):
SLM = 'slm'
SAM = 'sam'
LAM = 'lam'
# Fallback chain: if a tier fails, try the next one
_FALLBACK = {
ModelTier.SLM: [ModelTier.SAM, ModelTier.LAM],
ModelTier.SAM: [ModelTier.LAM],
ModelTier.LAM: [],
}
class _TierConfigProxy:
"""Proxies Config but overrides the backend section for a specific model tier.
When a tier says backend=local with model_path=X, this proxy makes the LLM
class (which reads [llama]) see the tier's model_path/n_ctx/etc instead.
"""
def __init__(self, base_config, tier_name: str):
self._base = base_config
self._tier = tier_name
self._overrides: Dict[str, Dict[str, str]] = {}
self._build_overrides()
def _build_overrides(self):
backend = self._base.get(self._tier, 'backend', 'local')
model_path = self._base.get(self._tier, 'model_path', '')
n_ctx = self._base.get(self._tier, 'n_ctx', '2048')
n_gpu_layers = self._base.get(self._tier, 'n_gpu_layers', '-1')
n_threads = self._base.get(self._tier, 'n_threads', '4')
if backend == 'local':
self._overrides['llama'] = {
'model_path': model_path,
'n_ctx': n_ctx,
'n_gpu_layers': n_gpu_layers,
'n_threads': n_threads,
}
elif backend == 'transformers':
self._overrides['transformers'] = {
'model_path': model_path,
}
# claude and huggingface are API-based — no path override needed
def get(self, section: str, key: str, fallback=None):
overrides = self._overrides.get(section, {})
if key in overrides:
return overrides[key]
return self._base.get(section, key, fallback)
def get_int(self, section: str, key: str, fallback: int = 0) -> int:
overrides = self._overrides.get(section, {})
if key in overrides:
try:
return int(overrides[key])
except (ValueError, TypeError):
return fallback
return self._base.get_int(section, key, fallback)
def get_float(self, section: str, key: str, fallback: float = 0.0) -> float:
overrides = self._overrides.get(section, {})
if key in overrides:
try:
return float(overrides[key])
except (ValueError, TypeError):
return fallback
return self._base.get_float(section, key, fallback)
def get_bool(self, section: str, key: str, fallback: bool = False) -> bool:
overrides = self._overrides.get(section, {})
if key in overrides:
val = str(overrides[key]).lower()
return val in ('true', '1', 'yes', 'on')
return self._base.get_bool(section, key, fallback)
# Delegate all settings getters to base (they call self.get internally)
def get_llama_settings(self) -> dict:
from .config import Config
return Config.get_llama_settings(self)
def get_transformers_settings(self) -> dict:
from .config import Config
return Config.get_transformers_settings(self)
def get_claude_settings(self) -> dict:
return self._base.get_claude_settings()
def get_huggingface_settings(self) -> dict:
return self._base.get_huggingface_settings()
class ModelRouter:
"""Manages up to 3 concurrent LLM instances (SLM, SAM, LAM).
Each tier can use a different backend (local GGUF, transformers, Claude API,
HuggingFace). The router handles loading, unloading, fallback, and thread-safe
access.
"""
def __init__(self, config=None):
self.config = config or get_config()
self._instances: Dict[ModelTier, Any] = {}
self._locks: Dict[ModelTier, threading.Lock] = {
tier: threading.Lock() for tier in ModelTier
}
self._load_lock = threading.Lock()
@property
def status(self) -> Dict[str, dict]:
"""Return load status of all tiers."""
result = {}
for tier in ModelTier:
inst = self._instances.get(tier)
settings = self.config.get_tier_settings(tier.value)
result[tier.value] = {
'loaded': inst is not None and inst.is_loaded,
'model_name': inst.model_name if inst and inst.is_loaded else None,
'backend': settings['backend'],
'enabled': settings['enabled'],
'model_path': settings['model_path'],
}
return result
def load_tier(self, tier: ModelTier, verbose: bool = False) -> bool:
"""Load a single tier's model. Thread-safe."""
settings = self.config.get_tier_settings(tier.value)
if not settings['enabled']:
_logger.info(f"[Router] Tier {tier.value} is disabled, skipping")
return False
if not settings['model_path'] and settings['backend'] == 'local':
_logger.warning(f"[Router] No model_path configured for {tier.value}")
return False
with self._load_lock:
# Unload existing if any
if tier in self._instances:
self.unload_tier(tier)
try:
inst = self._create_instance(tier, verbose)
self._instances[tier] = inst
_logger.info(f"[Router] Loaded {tier.value}: {inst.model_name}")
return True
except Exception as e:
_logger.error(f"[Router] Failed to load {tier.value}: {e}")
return False
def unload_tier(self, tier: ModelTier):
"""Unload a tier's model and free resources."""
inst = self._instances.pop(tier, None)
if inst:
try:
inst.unload_model()
_logger.info(f"[Router] Unloaded {tier.value}")
except Exception as e:
_logger.error(f"[Router] Error unloading {tier.value}: {e}")
def load_all(self, verbose: bool = False) -> Dict[str, bool]:
"""Load all enabled tiers. Returns {tier_name: success}."""
results = {}
for tier in ModelTier:
results[tier.value] = self.load_tier(tier, verbose)
return results
def unload_all(self):
"""Unload all tiers."""
for tier in list(self._instances.keys()):
self.unload_tier(tier)
def get_instance(self, tier: ModelTier):
"""Get the LLM instance for a tier (may be None if not loaded)."""
return self._instances.get(tier)
def is_tier_loaded(self, tier: ModelTier) -> bool:
"""Check if a tier has a loaded model."""
inst = self._instances.get(tier)
return inst is not None and inst.is_loaded
def classify(self, text: str) -> Dict[str, Any]:
"""Use SLM to classify/triage an event or task.
Returns: {'tier': 'sam'|'lam', 'category': str, 'urgency': str, 'reasoning': str}
Falls back to SAM tier if SLM is not loaded.
"""
classify_prompt = f"""Classify this event/task for autonomous handling.
Respond with ONLY a JSON object, no other text:
{{"tier": "sam" or "lam", "category": "defense|offense|counter|analyze|osint|simulate", "urgency": "high|medium|low", "reasoning": "brief explanation"}}
Event: {text}"""
# Try SLM first, then fallback
for tier in [ModelTier.SLM, ModelTier.SAM, ModelTier.LAM]:
inst = self._instances.get(tier)
if inst and inst.is_loaded:
try:
with self._locks[tier]:
response = inst.generate(classify_prompt, max_tokens=200, temperature=0.1)
# Parse JSON from response
response = response.strip()
# Find JSON in response
start = response.find('{')
end = response.rfind('}')
if start >= 0 and end > start:
return json.loads(response[start:end + 1])
except Exception as e:
_logger.warning(f"[Router] Classification failed on {tier.value}: {e}")
continue
# Default if all tiers fail
return {'tier': 'sam', 'category': 'defense', 'urgency': 'medium',
'reasoning': 'Default classification (no model available)'}
def generate(self, tier: ModelTier, prompt: str, **kwargs) -> str:
"""Generate with a specific tier, falling back to higher tiers on failure.
Fallback chain: SLM -> SAM -> LAM, SAM -> LAM
"""
chain = [tier] + _FALLBACK.get(tier, [])
for t in chain:
inst = self._instances.get(t)
if inst and inst.is_loaded:
try:
with self._locks[t]:
return inst.generate(prompt, **kwargs)
except Exception as e:
_logger.warning(f"[Router] Generate failed on {t.value}: {e}")
continue
from .llm import LLMError
raise LLMError(f"All tiers exhausted for generation (started at {tier.value})")
def _create_instance(self, tier: ModelTier, verbose: bool = False):
"""Create an LLM instance from tier config."""
from .llm import LLM, TransformersLLM, ClaudeLLM, HuggingFaceLLM
section = tier.value
backend = self.config.get(section, 'backend', 'local')
proxy = _TierConfigProxy(self.config, section)
if verbose:
model_path = self.config.get(section, 'model_path', '')
_logger.info(f"[Router] Creating {tier.value} instance: backend={backend}, model={model_path}")
if backend == 'local':
inst = LLM(proxy)
elif backend == 'transformers':
inst = TransformersLLM(proxy)
elif backend == 'claude':
inst = ClaudeLLM(proxy)
elif backend == 'huggingface':
inst = HuggingFaceLLM(proxy)
else:
from .llm import LLMError
raise LLMError(f"Unknown backend '{backend}' for tier {tier.value}")
inst.load_model(verbose=verbose)
return inst
# Singleton
_router_instance = None
def get_model_router() -> ModelRouter:
"""Get the global ModelRouter instance."""
global _router_instance
if _router_instance is None:
_router_instance = ModelRouter()
return _router_instance
def reset_model_router():
"""Reset the global ModelRouter (unloads all models)."""
global _router_instance
if _router_instance is not None:
_router_instance.unload_all()
_router_instance = None

333
core/rules.py Normal file
View File

@ -0,0 +1,333 @@
"""
AUTARCH Automation Rules Engine
Condition-action rules for autonomous threat response.
Rules are JSON-serializable and stored in data/automation_rules.json.
The engine evaluates conditions against a threat context dict and returns
matching rules with resolved action parameters.
"""
import json
import logging
import re
import ipaddress
import uuid
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Any, Optional, Tuple
from dataclasses import dataclass, field, asdict
_logger = logging.getLogger('autarch.rules')
@dataclass
class Rule:
"""A single automation rule."""
id: str
name: str
enabled: bool = True
priority: int = 50 # 0=highest, 100=lowest
conditions: List[Dict] = field(default_factory=list) # AND-combined
actions: List[Dict] = field(default_factory=list)
cooldown_seconds: int = 60
last_triggered: Optional[str] = None # ISO timestamp
created: Optional[str] = None
description: str = ''
def to_dict(self) -> dict:
return asdict(self)
@classmethod
def from_dict(cls, d: dict) -> 'Rule':
return cls(
id=d.get('id', str(uuid.uuid4())[:8]),
name=d.get('name', 'Untitled'),
enabled=d.get('enabled', True),
priority=d.get('priority', 50),
conditions=d.get('conditions', []),
actions=d.get('actions', []),
cooldown_seconds=d.get('cooldown_seconds', 60),
last_triggered=d.get('last_triggered'),
created=d.get('created'),
description=d.get('description', ''),
)
class RulesEngine:
"""Evaluates automation rules against a threat context."""
RULES_PATH = Path(__file__).parent.parent / 'data' / 'automation_rules.json'
CONDITION_TYPES = {
'threat_score_above', 'threat_score_below', 'threat_level_is',
'port_scan_detected', 'ddos_detected', 'ddos_attack_type',
'connection_from_ip', 'connection_count_above',
'new_listening_port', 'bandwidth_rx_above_mbps',
'arp_spoof_detected', 'schedule', 'always',
}
ACTION_TYPES = {
'block_ip', 'unblock_ip', 'rate_limit_ip', 'block_port',
'kill_process', 'alert', 'log_event', 'run_shell',
'run_module', 'counter_scan', 'escalate_to_lam',
}
def __init__(self):
self._rules: List[Rule] = []
self._load()
def _load(self):
"""Load rules from JSON file."""
if not self.RULES_PATH.exists():
self._rules = []
return
try:
data = json.loads(self.RULES_PATH.read_text(encoding='utf-8'))
self._rules = [Rule.from_dict(r) for r in data.get('rules', [])]
_logger.info(f"[Rules] Loaded {len(self._rules)} rules")
except Exception as e:
_logger.error(f"[Rules] Failed to load rules: {e}")
self._rules = []
def save(self):
"""Save rules to JSON file."""
self.RULES_PATH.parent.mkdir(parents=True, exist_ok=True)
data = {
'version': 1,
'rules': [r.to_dict() for r in self._rules],
}
self.RULES_PATH.write_text(json.dumps(data, indent=2), encoding='utf-8')
def add_rule(self, rule: Rule) -> Rule:
if not rule.created:
rule.created = datetime.now().isoformat()
self._rules.append(rule)
self._rules.sort(key=lambda r: r.priority)
self.save()
return rule
def update_rule(self, rule_id: str, updates: dict) -> Optional[Rule]:
for rule in self._rules:
if rule.id == rule_id:
for key, value in updates.items():
if hasattr(rule, key) and key != 'id':
setattr(rule, key, value)
self._rules.sort(key=lambda r: r.priority)
self.save()
return rule
return None
def delete_rule(self, rule_id: str) -> bool:
before = len(self._rules)
self._rules = [r for r in self._rules if r.id != rule_id]
if len(self._rules) < before:
self.save()
return True
return False
def get_rule(self, rule_id: str) -> Optional[Rule]:
for rule in self._rules:
if rule.id == rule_id:
return rule
return None
def get_all_rules(self) -> List[Rule]:
return list(self._rules)
def evaluate(self, context: Dict[str, Any]) -> List[Tuple[Rule, List[Dict]]]:
"""Evaluate all enabled rules against a threat context.
Args:
context: Dict with keys from ThreatMonitor / AutonomyDaemon:
- threat_score: {'score': int, 'level': str, 'details': [...]}
- connection_count: int
- connections: [...]
- ddos: {'under_attack': bool, 'attack_type': str, ...}
- new_ports: [{'port': int, 'process': str}, ...]
- arp_alerts: [...]
- bandwidth: {'rx_mbps': float, 'tx_mbps': float}
- scan_indicators: int
- timestamp: str
Returns:
List of (Rule, resolved_actions) for rules that match and aren't in cooldown.
"""
matches = []
now = datetime.now()
for rule in self._rules:
if not rule.enabled:
continue
# Check cooldown
if rule.last_triggered:
try:
last = datetime.fromisoformat(rule.last_triggered)
if (now - last).total_seconds() < rule.cooldown_seconds:
continue
except (ValueError, TypeError):
pass
# Evaluate all conditions (AND logic)
if not rule.conditions:
continue
all_match = all(
self._evaluate_condition(cond, context)
for cond in rule.conditions
)
if all_match:
# Resolve action variables
resolved = [self._resolve_variables(a, context) for a in rule.actions]
matches.append((rule, resolved))
# Mark triggered
rule.last_triggered = now.isoformat()
# Save updated trigger times
if matches:
self.save()
return matches
def _evaluate_condition(self, condition: dict, context: dict) -> bool:
"""Evaluate a single condition against context."""
ctype = condition.get('type', '')
value = condition.get('value')
if ctype == 'threat_score_above':
return context.get('threat_score', {}).get('score', 0) > (value or 0)
elif ctype == 'threat_score_below':
return context.get('threat_score', {}).get('score', 0) < (value or 100)
elif ctype == 'threat_level_is':
return context.get('threat_score', {}).get('level', 'LOW') == (value or 'HIGH')
elif ctype == 'port_scan_detected':
return context.get('scan_indicators', 0) > 0
elif ctype == 'ddos_detected':
return context.get('ddos', {}).get('under_attack', False)
elif ctype == 'ddos_attack_type':
return context.get('ddos', {}).get('attack_type', '') == (value or '')
elif ctype == 'connection_from_ip':
return self._check_ip_match(value, context.get('connections', []))
elif ctype == 'connection_count_above':
return context.get('connection_count', 0) > (value or 0)
elif ctype == 'new_listening_port':
return len(context.get('new_ports', [])) > 0
elif ctype == 'bandwidth_rx_above_mbps':
return context.get('bandwidth', {}).get('rx_mbps', 0) > (value or 0)
elif ctype == 'arp_spoof_detected':
return len(context.get('arp_alerts', [])) > 0
elif ctype == 'schedule':
return self._check_cron(condition.get('cron', ''))
elif ctype == 'always':
return True
_logger.warning(f"[Rules] Unknown condition type: {ctype}")
return False
def _check_ip_match(self, pattern: str, connections: list) -> bool:
"""Check if any connection's remote IP matches a pattern (IP or CIDR)."""
if not pattern:
return False
try:
network = ipaddress.ip_network(pattern, strict=False)
for conn in connections:
remote = conn.get('remote_addr', '')
if remote and remote not in ('0.0.0.0', '::', '127.0.0.1', '::1', '*'):
try:
if ipaddress.ip_address(remote) in network:
return True
except ValueError:
continue
except ValueError:
# Not a valid IP/CIDR, try exact match
return any(conn.get('remote_addr') == pattern for conn in connections)
return False
def _check_cron(self, cron_expr: str) -> bool:
"""Minimal 5-field cron matcher: minute hour day month weekday.
Supports * and */N. Does not support ranges or lists.
"""
if not cron_expr:
return False
parts = cron_expr.strip().split()
if len(parts) != 5:
return False
now = datetime.now()
current = [now.minute, now.hour, now.day, now.month, now.isoweekday() % 7]
for field_val, pattern in zip(current, parts):
if pattern == '*':
continue
if pattern.startswith('*/'):
try:
step = int(pattern[2:])
if step > 0 and field_val % step != 0:
return False
except ValueError:
return False
else:
try:
if field_val != int(pattern):
return False
except ValueError:
return False
return True
def _resolve_variables(self, action: dict, context: dict) -> dict:
"""Replace $variable placeholders in action parameters with context values."""
resolved = {}
# Build variable map from context
variables = {
'$threat_score': str(context.get('threat_score', {}).get('score', 0)),
'$threat_level': context.get('threat_score', {}).get('level', 'LOW'),
}
# Source IP = top talker (most connections)
connections = context.get('connections', [])
if connections:
ip_counts = {}
for c in connections:
rip = c.get('remote_addr', '')
if rip and rip not in ('0.0.0.0', '::', '127.0.0.1', '::1', '*'):
ip_counts[rip] = ip_counts.get(rip, 0) + 1
if ip_counts:
variables['$source_ip'] = max(ip_counts, key=ip_counts.get)
# New port
new_ports = context.get('new_ports', [])
if new_ports:
variables['$new_port'] = str(new_ports[0].get('port', ''))
variables['$suspicious_pid'] = str(new_ports[0].get('pid', ''))
# DDoS attack type
ddos = context.get('ddos', {})
if ddos:
variables['$attack_type'] = ddos.get('attack_type', 'unknown')
# Resolve in all string values
for key, val in action.items():
if isinstance(val, str):
for var_name, var_val in variables.items():
val = val.replace(var_name, var_val)
resolved[key] = val
return resolved

View File

@ -65,6 +65,7 @@ def create_app():
from web.routes.targets import targets_bp from web.routes.targets import targets_bp
from web.routes.encmodules import encmodules_bp from web.routes.encmodules import encmodules_bp
from web.routes.llm_trainer import llm_trainer_bp from web.routes.llm_trainer import llm_trainer_bp
from web.routes.autonomy import autonomy_bp
app.register_blueprint(auth_bp) app.register_blueprint(auth_bp)
app.register_blueprint(dashboard_bp) app.register_blueprint(dashboard_bp)
@ -89,6 +90,7 @@ def create_app():
app.register_blueprint(targets_bp) app.register_blueprint(targets_bp)
app.register_blueprint(encmodules_bp) app.register_blueprint(encmodules_bp)
app.register_blueprint(llm_trainer_bp) app.register_blueprint(llm_trainer_bp)
app.register_blueprint(autonomy_bp)
# Start network discovery advertising (mDNS + Bluetooth) # Start network discovery advertising (mDNS + Bluetooth)
try: try:

241
web/routes/autonomy.py Normal file
View File

@ -0,0 +1,241 @@
"""Autonomy routes — daemon control, model management, rules CRUD, activity log."""
import json
from flask import Blueprint, render_template, request, jsonify, Response, stream_with_context
from web.auth import login_required
autonomy_bp = Blueprint('autonomy', __name__, url_prefix='/autonomy')
def _get_daemon():
from core.autonomy import get_autonomy_daemon
return get_autonomy_daemon()
def _get_router():
from core.model_router import get_model_router
return get_model_router()
# ==================== PAGES ====================
@autonomy_bp.route('/')
@login_required
def index():
return render_template('autonomy.html')
# ==================== DAEMON CONTROL ====================
@autonomy_bp.route('/status')
@login_required
def status():
daemon = _get_daemon()
router = _get_router()
return jsonify({
'daemon': daemon.status,
'models': router.status,
})
@autonomy_bp.route('/start', methods=['POST'])
@login_required
def start():
daemon = _get_daemon()
ok = daemon.start()
return jsonify({'success': ok, 'status': daemon.status})
@autonomy_bp.route('/stop', methods=['POST'])
@login_required
def stop():
daemon = _get_daemon()
daemon.stop()
return jsonify({'success': True, 'status': daemon.status})
@autonomy_bp.route('/pause', methods=['POST'])
@login_required
def pause():
daemon = _get_daemon()
daemon.pause()
return jsonify({'success': True, 'status': daemon.status})
@autonomy_bp.route('/resume', methods=['POST'])
@login_required
def resume():
daemon = _get_daemon()
daemon.resume()
return jsonify({'success': True, 'status': daemon.status})
# ==================== MODELS ====================
@autonomy_bp.route('/models')
@login_required
def models():
return jsonify(_get_router().status)
@autonomy_bp.route('/models/load/<tier>', methods=['POST'])
@login_required
def models_load(tier):
from core.model_router import ModelTier
try:
mt = ModelTier(tier)
except ValueError:
return jsonify({'error': f'Invalid tier: {tier}'}), 400
ok = _get_router().load_tier(mt, verbose=True)
return jsonify({'success': ok, 'models': _get_router().status})
@autonomy_bp.route('/models/unload/<tier>', methods=['POST'])
@login_required
def models_unload(tier):
from core.model_router import ModelTier
try:
mt = ModelTier(tier)
except ValueError:
return jsonify({'error': f'Invalid tier: {tier}'}), 400
_get_router().unload_tier(mt)
return jsonify({'success': True, 'models': _get_router().status})
# ==================== RULES ====================
@autonomy_bp.route('/rules')
@login_required
def rules_list():
daemon = _get_daemon()
rules = daemon.rules_engine.get_all_rules()
return jsonify({'rules': [r.to_dict() for r in rules]})
@autonomy_bp.route('/rules', methods=['POST'])
@login_required
def rules_create():
from core.rules import Rule
data = request.get_json(silent=True) or {}
rule = Rule.from_dict(data)
daemon = _get_daemon()
daemon.rules_engine.add_rule(rule)
return jsonify({'success': True, 'rule': rule.to_dict()})
@autonomy_bp.route('/rules/<rule_id>', methods=['PUT'])
@login_required
def rules_update(rule_id):
data = request.get_json(silent=True) or {}
daemon = _get_daemon()
rule = daemon.rules_engine.update_rule(rule_id, data)
if rule:
return jsonify({'success': True, 'rule': rule.to_dict()})
return jsonify({'error': 'Rule not found'}), 404
@autonomy_bp.route('/rules/<rule_id>', methods=['DELETE'])
@login_required
def rules_delete(rule_id):
daemon = _get_daemon()
ok = daemon.rules_engine.delete_rule(rule_id)
return jsonify({'success': ok})
@autonomy_bp.route('/templates')
@login_required
def rule_templates():
"""Pre-built rule templates for common scenarios."""
templates = [
{
'name': 'Auto-Block Port Scanners',
'description': 'Block IPs that trigger port scan detection',
'conditions': [{'type': 'port_scan_detected'}],
'actions': [
{'type': 'block_ip', 'ip': '$source_ip'},
{'type': 'alert', 'message': 'Blocked scanner: $source_ip'},
],
'priority': 10,
'cooldown_seconds': 300,
},
{
'name': 'DDoS Auto-Response',
'description': 'Rate-limit top talkers during DDoS attacks',
'conditions': [{'type': 'ddos_detected'}],
'actions': [
{'type': 'rate_limit_ip', 'ip': '$source_ip', 'rate': '10/s'},
{'type': 'alert', 'message': 'DDoS mitigated: $attack_type from $source_ip'},
],
'priority': 5,
'cooldown_seconds': 60,
},
{
'name': 'High Threat Alert',
'description': 'Send alert when threat score exceeds threshold',
'conditions': [{'type': 'threat_score_above', 'value': 60}],
'actions': [
{'type': 'alert', 'message': 'Threat score: $threat_score ($threat_level)'},
],
'priority': 20,
'cooldown_seconds': 120,
},
{
'name': 'New Port Investigation',
'description': 'Use SAM agent to investigate new listening ports',
'conditions': [{'type': 'new_listening_port'}],
'actions': [
{'type': 'escalate_to_lam', 'task': 'Investigate new listening port $new_port (PID $suspicious_pid). Determine if this is legitimate or suspicious.'},
],
'priority': 30,
'cooldown_seconds': 300,
},
{
'name': 'Bandwidth Spike Alert',
'description': 'Alert on unusual inbound bandwidth',
'conditions': [{'type': 'bandwidth_rx_above_mbps', 'value': 100}],
'actions': [
{'type': 'alert', 'message': 'Bandwidth spike detected (>100 Mbps RX)'},
],
'priority': 25,
'cooldown_seconds': 60,
},
]
return jsonify({'templates': templates})
# ==================== ACTIVITY LOG ====================
@autonomy_bp.route('/activity')
@login_required
def activity():
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
daemon = _get_daemon()
entries = daemon.get_activity(limit=limit, offset=offset)
return jsonify({'entries': entries, 'total': daemon.get_activity_count()})
@autonomy_bp.route('/activity/stream')
@login_required
def activity_stream():
"""SSE stream of live activity entries."""
daemon = _get_daemon()
q = daemon.subscribe()
def generate():
try:
while True:
try:
data = q.get(timeout=30)
yield f'data: {data}\n\n'
except Exception:
# Send keepalive
yield f'data: {{"type":"keepalive"}}\n\n'
finally:
daemon.unsubscribe(q)
return Response(
stream_with_context(generate()),
mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'},
)

742
web/templates/autonomy.html Normal file
View File

@ -0,0 +1,742 @@
{% extends "base.html" %}
{% block title %}Autonomy - AUTARCH{% endblock %}
{% block content %}
<div class="page-header">
<h1 style="color:var(--accent)">Autonomy</h1>
<p style="margin:0;font-size:0.85rem;color:var(--text-secondary)">
Multi-model autonomous threat response — SLM / SAM / LAM
</p>
</div>
<!-- Tab Navigation -->
<div style="display:flex;gap:0;border-bottom:2px solid var(--border);margin-bottom:1.5rem">
<button class="auto-tab active" onclick="autoTab('dashboard')" id="tab-dashboard">Dashboard</button>
<button class="auto-tab" onclick="autoTab('rules')" id="tab-rules">Rules</button>
<button class="auto-tab" onclick="autoTab('activity')" id="tab-activity">Activity Log</button>
<button class="auto-tab" onclick="autoTab('models')" id="tab-models">Models</button>
</div>
<!-- ==================== DASHBOARD TAB ==================== -->
<div id="panel-dashboard" class="auto-panel">
<!-- Controls -->
<div class="section">
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
<div id="daemon-status-badge" style="padding:6px 16px;border-radius:20px;font-size:0.8rem;font-weight:600;background:var(--bg-input);border:1px solid var(--border)">
STOPPED
</div>
<button class="btn btn-primary" onclick="autoStart()" id="btn-start">Start</button>
<button class="btn btn-danger" onclick="autoStop()" id="btn-stop" disabled>Stop</button>
<button class="btn" onclick="autoPause()" id="btn-pause" disabled style="background:var(--bg-input);border:1px solid var(--border)">Pause</button>
<button class="btn" onclick="autoResume()" id="btn-resume" disabled style="background:var(--bg-input);border:1px solid var(--border)">Resume</button>
</div>
</div>
<!-- Stats Cards -->
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1rem;margin-bottom:1.5rem">
<div class="tool-card" style="text-align:center">
<div style="font-size:2rem;font-weight:700;color:var(--accent)" id="stat-agents">0</div>
<div style="font-size:0.8rem;color:var(--text-secondary)">Active Agents</div>
</div>
<div class="tool-card" style="text-align:center">
<div style="font-size:2rem;font-weight:700;color:var(--success)" id="stat-rules">0</div>
<div style="font-size:0.8rem;color:var(--text-secondary)">Rules</div>
</div>
<div class="tool-card" style="text-align:center">
<div style="font-size:2rem;font-weight:700;color:var(--warning)" id="stat-activity">0</div>
<div style="font-size:0.8rem;color:var(--text-secondary)">Activity Entries</div>
</div>
</div>
<!-- Model Tier Cards -->
<div class="section">
<h2>Model Tiers</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:1rem">
<div class="tool-card" id="card-slm">
<div style="display:flex;justify-content:space-between;align-items:center">
<h4>SLM <span style="font-size:0.7rem;color:var(--text-muted);font-weight:400">Small Language Model</span></h4>
<span class="tier-dot" id="dot-slm"></span>
</div>
<p style="font-size:0.8rem;color:var(--text-secondary);margin:0.5rem 0">Fast classification, routing, yes/no decisions</p>
<div style="font-size:0.75rem;color:var(--text-muted)" id="slm-model">No model configured</div>
<div style="margin-top:0.5rem;display:flex;gap:0.5rem">
<button class="btn btn-small btn-primary" onclick="autoLoadTier('slm')">Load</button>
<button class="btn btn-small" onclick="autoUnloadTier('slm')" style="background:var(--bg-input);border:1px solid var(--border)">Unload</button>
</div>
</div>
<div class="tool-card" id="card-sam">
<div style="display:flex;justify-content:space-between;align-items:center">
<h4>SAM <span style="font-size:0.7rem;color:var(--text-muted);font-weight:400">Small Action Model</span></h4>
<span class="tier-dot" id="dot-sam"></span>
</div>
<p style="font-size:0.8rem;color:var(--text-secondary);margin:0.5rem 0">Quick tool execution, simple automated responses</p>
<div style="font-size:0.75rem;color:var(--text-muted)" id="sam-model">No model configured</div>
<div style="margin-top:0.5rem;display:flex;gap:0.5rem">
<button class="btn btn-small btn-primary" onclick="autoLoadTier('sam')">Load</button>
<button class="btn btn-small" onclick="autoUnloadTier('sam')" style="background:var(--bg-input);border:1px solid var(--border)">Unload</button>
</div>
</div>
<div class="tool-card" id="card-lam">
<div style="display:flex;justify-content:space-between;align-items:center">
<h4>LAM <span style="font-size:0.7rem;color:var(--text-muted);font-weight:400">Large Action Model</span></h4>
<span class="tier-dot" id="dot-lam"></span>
</div>
<p style="font-size:0.8rem;color:var(--text-secondary);margin:0.5rem 0">Complex multi-step agent tasks, strategic planning</p>
<div style="font-size:0.75rem;color:var(--text-muted)" id="lam-model">No model configured</div>
<div style="margin-top:0.5rem;display:flex;gap:0.5rem">
<button class="btn btn-small btn-primary" onclick="autoLoadTier('lam')">Load</button>
<button class="btn btn-small" onclick="autoUnloadTier('lam')" style="background:var(--bg-input);border:1px solid var(--border)">Unload</button>
</div>
</div>
</div>
</div>
</div>
<!-- ==================== RULES TAB ==================== -->
<div id="panel-rules" class="auto-panel" style="display:none">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem">
<h2>Automation Rules</h2>
<div style="display:flex;gap:0.5rem">
<button class="btn btn-primary" onclick="autoShowRuleModal()">+ Add Rule</button>
<button class="btn" onclick="autoShowTemplates()" style="background:var(--bg-input);border:1px solid var(--border)">Templates</button>
</div>
</div>
<table class="data-table" id="rules-table">
<thead>
<tr>
<th style="width:30px"></th>
<th>Name</th>
<th>Priority</th>
<th>Conditions</th>
<th>Actions</th>
<th>Cooldown</th>
<th style="width:100px">Actions</th>
</tr>
</thead>
<tbody id="rules-tbody"></tbody>
</table>
<div id="rules-empty" style="text-align:center;padding:2rem;color:var(--text-muted);display:none">
No rules configured. Add a rule or use a template to get started.
</div>
</div>
<!-- ==================== ACTIVITY TAB ==================== -->
<div id="panel-activity" class="auto-panel" style="display:none">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
<h2>Activity Log</h2>
<div style="display:flex;gap:0.5rem;align-items:center">
<span id="activity-live-dot" class="live-dot"></span>
<span style="font-size:0.8rem;color:var(--text-secondary)" id="activity-count">0 entries</span>
</div>
</div>
<div id="activity-log" style="max-height:600px;overflow-y:auto;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-secondary)">
<table class="data-table" style="margin:0">
<thead><tr>
<th style="width:150px">Time</th>
<th style="width:80px">Status</th>
<th style="width:100px">Action</th>
<th>Detail</th>
<th style="width:80px">Rule</th>
<th style="width:50px">Tier</th>
</tr></thead>
<tbody id="activity-tbody"></tbody>
</table>
</div>
</div>
<!-- ==================== MODELS TAB ==================== -->
<div id="panel-models" class="auto-panel" style="display:none">
<h2 style="margin-bottom:1rem">Model Configuration</h2>
<p style="font-size:0.85rem;color:var(--text-secondary);margin-bottom:1.5rem">
Configure model tiers in <code>autarch_settings.conf</code> under <code>[slm]</code>, <code>[sam]</code>, and <code>[lam]</code> sections.
Each tier supports backends: <code>local</code> (GGUF), <code>transformers</code>, <code>claude</code>, <code>huggingface</code>.
</p>
<div id="models-detail" style="display:grid;grid-template-columns:1fr;gap:1rem"></div>
</div>
<!-- ==================== RULE EDITOR MODAL ==================== -->
<div id="rule-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:1000;display:none;align-items:center;justify-content:center">
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:1.5rem;width:90%;max-width:700px;max-height:85vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
<h3 id="rule-modal-title">Add Rule</h3>
<button onclick="autoCloseRuleModal()" style="background:none;border:none;color:var(--text-secondary);font-size:1.2rem;cursor:pointer">&#x2715;</button>
</div>
<input type="hidden" id="rule-edit-id">
<div style="display:grid;gap:1rem">
<div>
<label style="font-size:0.8rem;color:var(--text-secondary);display:block;margin-bottom:4px">Name</label>
<input type="text" id="rule-name" class="form-input" placeholder="Rule name">
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:1rem">
<div>
<label style="font-size:0.8rem;color:var(--text-secondary);display:block;margin-bottom:4px">Priority (0=highest)</label>
<input type="number" id="rule-priority" class="form-input" value="50" min="0" max="100">
</div>
<div>
<label style="font-size:0.8rem;color:var(--text-secondary);display:block;margin-bottom:4px">Cooldown (seconds)</label>
<input type="number" id="rule-cooldown" class="form-input" value="60" min="0">
</div>
</div>
<div>
<label style="font-size:0.8rem;color:var(--text-secondary);display:block;margin-bottom:4px">Description</label>
<input type="text" id="rule-description" class="form-input" placeholder="Optional description">
</div>
<!-- Conditions -->
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
<label style="font-size:0.8rem;color:var(--text-secondary)">Conditions (AND logic)</label>
<button class="btn btn-small" onclick="autoAddCondition()" style="background:var(--bg-input);border:1px solid var(--border);font-size:0.75rem">+ Condition</button>
</div>
<div id="conditions-list"></div>
</div>
<!-- Actions -->
<div>
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
<label style="font-size:0.8rem;color:var(--text-secondary)">Actions</label>
<button class="btn btn-small" onclick="autoAddAction()" style="background:var(--bg-input);border:1px solid var(--border);font-size:0.75rem">+ Action</button>
</div>
<div id="actions-list"></div>
</div>
<div style="display:flex;gap:0.5rem;justify-content:flex-end;margin-top:0.5rem">
<button class="btn" onclick="autoCloseRuleModal()" style="background:var(--bg-input);border:1px solid var(--border)">Cancel</button>
<button class="btn btn-primary" onclick="autoSaveRule()">Save Rule</button>
</div>
</div>
</div>
</div>
<!-- ==================== TEMPLATES MODAL ==================== -->
<div id="templates-modal" style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:1000;align-items:center;justify-content:center">
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:1.5rem;width:90%;max-width:600px;max-height:80vh;overflow-y:auto">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:1rem">
<h3>Rule Templates</h3>
<button onclick="autoCloseTemplates()" style="background:none;border:none;color:var(--text-secondary);font-size:1.2rem;cursor:pointer">&#x2715;</button>
</div>
<div id="templates-list"></div>
</div>
</div>
<style>
.auto-tab {
padding: 10px 20px;
background: none;
border: none;
color: var(--text-secondary);
font-size: 0.85rem;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: color 0.2s, border-color 0.2s;
}
.auto-tab:hover { color: var(--text-primary); }
.auto-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
.tier-dot {
width: 10px; height: 10px; border-radius: 50%;
background: var(--text-muted);
display: inline-block;
}
.tier-dot.loaded { background: var(--success); box-shadow: 0 0 6px var(--success); }
.live-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--text-muted);
display: inline-block;
}
.live-dot.active { background: var(--success); animation: pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
.form-input {
width: 100%;
padding: 8px 12px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-primary);
font-size: 0.85rem;
}
.form-input:focus { outline: none; border-color: var(--accent); }
.cond-row, .action-row {
display: flex;
gap: 0.5rem;
align-items: center;
margin-bottom: 0.5rem;
padding: 8px;
background: var(--bg-input);
border-radius: var(--radius);
}
.cond-row select, .action-row select {
padding: 6px 8px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-primary);
font-size: 0.8rem;
}
.cond-row input, .action-row input {
padding: 6px 8px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 4px;
color: var(--text-primary);
font-size: 0.8rem;
flex: 1;
}
.row-remove {
background: none; border: none; color: var(--danger); cursor: pointer; font-size: 1rem; padding: 0 4px;
}
.activity-success { color: var(--success); }
.activity-fail { color: var(--danger); }
.activity-system { color: var(--text-muted); font-style: italic; }
.template-card {
padding: 1rem;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: var(--radius);
margin-bottom: 0.75rem;
cursor: pointer;
transition: border-color 0.2s;
}
.template-card:hover { border-color: var(--accent); }
</style>
<script>
// ==================== TAB NAVIGATION ====================
function autoTab(tab) {
document.querySelectorAll('.auto-panel').forEach(p => p.style.display = 'none');
document.querySelectorAll('.auto-tab').forEach(t => t.classList.remove('active'));
document.getElementById('panel-' + tab).style.display = 'block';
document.getElementById('tab-' + tab).classList.add('active');
if (tab === 'activity') autoLoadActivity();
if (tab === 'models') autoLoadModelsDetail();
}
// ==================== STATUS REFRESH ====================
let _autoRefreshTimer = null;
function autoRefreshStatus() {
fetch('/autonomy/status').then(r => r.json()).then(data => {
const d = data.daemon;
const badge = document.getElementById('daemon-status-badge');
if (d.running && !d.paused) {
badge.textContent = 'RUNNING';
badge.style.background = 'rgba(34,197,94,0.15)';
badge.style.borderColor = 'var(--success)';
badge.style.color = 'var(--success)';
} else if (d.running && d.paused) {
badge.textContent = 'PAUSED';
badge.style.background = 'rgba(245,158,11,0.15)';
badge.style.borderColor = 'var(--warning)';
badge.style.color = 'var(--warning)';
} else {
badge.textContent = 'STOPPED';
badge.style.background = 'var(--bg-input)';
badge.style.borderColor = 'var(--border)';
badge.style.color = 'var(--text-muted)';
}
document.getElementById('btn-start').disabled = d.running;
document.getElementById('btn-stop').disabled = !d.running;
document.getElementById('btn-pause').disabled = !d.running || d.paused;
document.getElementById('btn-resume').disabled = !d.running || !d.paused;
document.getElementById('stat-agents').textContent = d.active_agents;
document.getElementById('stat-rules').textContent = d.rules_count;
document.getElementById('stat-activity').textContent = d.activity_count;
// Update model dots
const models = data.models;
for (const tier of ['slm', 'sam', 'lam']) {
const dot = document.getElementById('dot-' + tier);
const label = document.getElementById(tier + '-model');
if (models[tier]) {
if (models[tier].loaded) {
dot.classList.add('loaded');
label.textContent = models[tier].model_name || models[tier].model_path || 'Loaded';
} else {
dot.classList.remove('loaded');
label.textContent = models[tier].model_path || 'No model configured';
}
}
}
}).catch(() => {});
}
function autoStartRefresh() {
autoRefreshStatus();
_autoRefreshTimer = setInterval(autoRefreshStatus, 5000);
}
// ==================== DAEMON CONTROLS ====================
function autoStart() {
fetch('/autonomy/start', {method:'POST'}).then(r => r.json()).then(() => autoRefreshStatus());
}
function autoStop() {
fetch('/autonomy/stop', {method:'POST'}).then(r => r.json()).then(() => autoRefreshStatus());
}
function autoPause() {
fetch('/autonomy/pause', {method:'POST'}).then(r => r.json()).then(() => autoRefreshStatus());
}
function autoResume() {
fetch('/autonomy/resume', {method:'POST'}).then(r => r.json()).then(() => autoRefreshStatus());
}
// ==================== MODEL CONTROLS ====================
function autoLoadTier(tier) {
fetch('/autonomy/models/load/' + tier, {method:'POST'}).then(r => r.json()).then(data => {
autoRefreshStatus();
if (data.success === false) alert('Failed to load ' + tier.toUpperCase() + ' tier. Check model configuration.');
});
}
function autoUnloadTier(tier) {
fetch('/autonomy/models/unload/' + tier, {method:'POST'}).then(r => r.json()).then(() => autoRefreshStatus());
}
function autoLoadModelsDetail() {
fetch('/autonomy/models').then(r => r.json()).then(data => {
const container = document.getElementById('models-detail');
let html = '';
for (const [tier, info] of Object.entries(data)) {
const status = info.loaded ? '<span style="color:var(--success)">Loaded</span>' : '<span style="color:var(--text-muted)">Not loaded</span>';
html += `<div class="tool-card">
<h4>${tier.toUpperCase()}</h4>
<table class="data-table" style="max-width:100%;margin-top:0.5rem">
<tr><td>Status</td><td>${status}</td></tr>
<tr><td>Backend</td><td><code>${info.backend}</code></td></tr>
<tr><td>Model</td><td style="word-break:break-all">${info.model_name || info.model_path || '<em>Not configured</em>'}</td></tr>
<tr><td>Enabled</td><td>${info.enabled ? 'Yes' : 'No'}</td></tr>
</table>
<div style="margin-top:0.5rem;display:flex;gap:0.5rem">
<button class="btn btn-small btn-primary" onclick="autoLoadTier('${tier}')">Load</button>
<button class="btn btn-small" onclick="autoUnloadTier('${tier}')" style="background:var(--bg-input);border:1px solid var(--border)">Unload</button>
</div>
</div>`;
}
container.innerHTML = html;
});
}
// ==================== RULES ====================
let _rules = [];
function autoLoadRules() {
fetch('/autonomy/rules').then(r => r.json()).then(data => {
_rules = data.rules || [];
renderRules();
});
}
function renderRules() {
const tbody = document.getElementById('rules-tbody');
const empty = document.getElementById('rules-empty');
if (_rules.length === 0) {
tbody.innerHTML = '';
empty.style.display = 'block';
return;
}
empty.style.display = 'none';
tbody.innerHTML = _rules.map(r => {
const enabled = r.enabled ? '<span style="color:var(--success)">&#x25cf;</span>' : '<span style="color:var(--text-muted)">&#x25cb;</span>';
const conds = (r.conditions||[]).map(c => c.type).join(', ') || '-';
const acts = (r.actions||[]).map(a => a.type).join(', ') || '-';
return `<tr>
<td>${enabled}</td>
<td><strong>${esc(r.name)}</strong></td>
<td>${r.priority}</td>
<td style="font-size:0.8rem">${esc(conds)}</td>
<td style="font-size:0.8rem">${esc(acts)}</td>
<td>${r.cooldown_seconds}s</td>
<td>
<button class="btn btn-small" onclick="autoEditRule('${r.id}')" style="background:var(--bg-input);border:1px solid var(--border);font-size:0.7rem">Edit</button>
<button class="btn btn-small" onclick="autoDeleteRule('${r.id}')" style="background:var(--bg-input);border:1px solid var(--danger);color:var(--danger);font-size:0.7rem">Del</button>
</td>
</tr>`;
}).join('');
}
function autoDeleteRule(id) {
if (!confirm('Delete this rule?')) return;
fetch('/autonomy/rules/' + id, {method:'DELETE'}).then(r => r.json()).then(() => {
autoLoadRules();
autoRefreshStatus();
});
}
// ==================== RULE EDITOR ====================
const CONDITION_TYPES = [
{value:'threat_score_above', label:'Threat Score Above', hasValue:true, valueType:'number'},
{value:'threat_score_below', label:'Threat Score Below', hasValue:true, valueType:'number'},
{value:'threat_level_is', label:'Threat Level Is', hasValue:true, valueType:'text'},
{value:'port_scan_detected', label:'Port Scan Detected', hasValue:false},
{value:'ddos_detected', label:'DDoS Detected', hasValue:false},
{value:'ddos_attack_type', label:'DDoS Attack Type', hasValue:true, valueType:'text'},
{value:'connection_from_ip', label:'Connection From IP', hasValue:true, valueType:'text'},
{value:'connection_count_above', label:'Connection Count Above', hasValue:true, valueType:'number'},
{value:'new_listening_port', label:'New Listening Port', hasValue:false},
{value:'bandwidth_rx_above_mbps', label:'Bandwidth RX Above (Mbps)', hasValue:true, valueType:'number'},
{value:'arp_spoof_detected', label:'ARP Spoof Detected', hasValue:false},
{value:'schedule', label:'Schedule (Cron)', hasValue:true, valueType:'text'},
{value:'always', label:'Always', hasValue:false},
];
const ACTION_TYPES = [
{value:'block_ip', label:'Block IP', fields:['ip']},
{value:'unblock_ip', label:'Unblock IP', fields:['ip']},
{value:'rate_limit_ip', label:'Rate Limit IP', fields:['ip','rate']},
{value:'block_port', label:'Block Port', fields:['port','direction']},
{value:'kill_process', label:'Kill Process', fields:['pid']},
{value:'alert', label:'Alert', fields:['message']},
{value:'log_event', label:'Log Event', fields:['message']},
{value:'run_shell', label:'Run Shell', fields:['command']},
{value:'run_module', label:'Run Module (SAM)', fields:['module','args']},
{value:'counter_scan', label:'Counter Scan (SAM)', fields:['target']},
{value:'escalate_to_lam', label:'Escalate to LAM', fields:['task']},
];
function autoShowRuleModal(editRule) {
const modal = document.getElementById('rule-modal');
modal.style.display = 'flex';
document.getElementById('rule-modal-title').textContent = editRule ? 'Edit Rule' : 'Add Rule';
document.getElementById('rule-edit-id').value = editRule ? editRule.id : '';
document.getElementById('rule-name').value = editRule ? editRule.name : '';
document.getElementById('rule-priority').value = editRule ? editRule.priority : 50;
document.getElementById('rule-cooldown').value = editRule ? editRule.cooldown_seconds : 60;
document.getElementById('rule-description').value = editRule ? (editRule.description||'') : '';
const condList = document.getElementById('conditions-list');
const actList = document.getElementById('actions-list');
condList.innerHTML = '';
actList.innerHTML = '';
if (editRule) {
(editRule.conditions||[]).forEach(c => autoAddCondition(c));
(editRule.actions||[]).forEach(a => autoAddAction(a));
}
}
function autoCloseRuleModal() {
document.getElementById('rule-modal').style.display = 'none';
}
function autoEditRule(id) {
const rule = _rules.find(r => r.id === id);
if (rule) autoShowRuleModal(rule);
}
function autoAddCondition(existing) {
const list = document.getElementById('conditions-list');
const row = document.createElement('div');
row.className = 'cond-row';
const sel = document.createElement('select');
sel.innerHTML = CONDITION_TYPES.map(c => `<option value="${c.value}">${c.label}</option>`).join('');
if (existing) sel.value = existing.type;
const inp = document.createElement('input');
inp.placeholder = 'Value';
inp.style.display = 'none';
if (existing && existing.value !== undefined) { inp.value = existing.value; inp.style.display = ''; }
if (existing && existing.cron) { inp.value = existing.cron; inp.style.display = ''; }
sel.onchange = () => {
const ct = CONDITION_TYPES.find(c => c.value === sel.value);
inp.style.display = ct && ct.hasValue ? '' : 'none';
inp.placeholder = sel.value === 'schedule' ? 'Cron: */5 * * * *' : 'Value';
};
sel.onchange();
const rm = document.createElement('button');
rm.className = 'row-remove';
rm.textContent = '\u2715';
rm.onclick = () => row.remove();
row.append(sel, inp, rm);
list.appendChild(row);
}
function autoAddAction(existing) {
const list = document.getElementById('actions-list');
const row = document.createElement('div');
row.className = 'action-row';
row.style.flexWrap = 'wrap';
const sel = document.createElement('select');
sel.innerHTML = ACTION_TYPES.map(a => `<option value="${a.value}">${a.label}</option>`).join('');
if (existing) sel.value = existing.type;
const fieldsDiv = document.createElement('div');
fieldsDiv.style.cssText = 'display:flex;gap:0.5rem;flex:1;min-width:200px';
function renderFields() {
fieldsDiv.innerHTML = '';
const at = ACTION_TYPES.find(a => a.value === sel.value);
if (at) {
at.fields.forEach(f => {
const inp = document.createElement('input');
inp.placeholder = f;
inp.dataset.field = f;
if (existing && existing[f]) inp.value = existing[f];
fieldsDiv.appendChild(inp);
});
}
}
sel.onchange = renderFields;
renderFields();
const rm = document.createElement('button');
rm.className = 'row-remove';
rm.textContent = '\u2715';
rm.onclick = () => row.remove();
row.append(sel, fieldsDiv, rm);
list.appendChild(row);
}
function autoSaveRule() {
const id = document.getElementById('rule-edit-id').value;
const name = document.getElementById('rule-name').value.trim();
if (!name) { alert('Rule name is required'); return; }
const conditions = [];
document.querySelectorAll('#conditions-list .cond-row').forEach(row => {
const type = row.querySelector('select').value;
const inp = row.querySelector('input');
const cond = {type};
if (inp.style.display !== 'none' && inp.value) {
if (type === 'schedule') cond.cron = inp.value;
else {
const num = Number(inp.value);
cond.value = isNaN(num) ? inp.value : num;
}
}
conditions.push(cond);
});
const actions = [];
document.querySelectorAll('#actions-list .action-row').forEach(row => {
const type = row.querySelector('select').value;
const action = {type};
row.querySelectorAll('input[data-field]').forEach(inp => {
if (inp.value) action[inp.dataset.field] = inp.value;
});
actions.push(action);
});
const rule = {
name,
priority: parseInt(document.getElementById('rule-priority').value) || 50,
cooldown_seconds: parseInt(document.getElementById('rule-cooldown').value) || 60,
description: document.getElementById('rule-description').value.trim(),
conditions,
actions,
enabled: true,
};
const url = id ? '/autonomy/rules/' + id : '/autonomy/rules';
const method = id ? 'PUT' : 'POST';
fetch(url, {method, headers:{'Content-Type':'application/json'}, body:JSON.stringify(rule)})
.then(r => r.json())
.then(() => {
autoCloseRuleModal();
autoLoadRules();
autoRefreshStatus();
});
}
// ==================== TEMPLATES ====================
function autoShowTemplates() {
const modal = document.getElementById('templates-modal');
modal.style.display = 'flex';
fetch('/autonomy/templates').then(r => r.json()).then(data => {
const list = document.getElementById('templates-list');
list.innerHTML = (data.templates||[]).map(t => `
<div class="template-card" onclick='autoApplyTemplate(${JSON.stringify(t).replace(/'/g,"&#39;")})'>
<strong>${esc(t.name)}</strong>
<p style="font-size:0.8rem;color:var(--text-secondary);margin:0.25rem 0">${esc(t.description)}</p>
<span style="font-size:0.7rem;color:var(--text-muted)">Priority: ${t.priority} | Cooldown: ${t.cooldown_seconds}s</span>
</div>
`).join('');
});
}
function autoCloseTemplates() {
document.getElementById('templates-modal').style.display = 'none';
}
function autoApplyTemplate(template) {
autoCloseTemplates();
fetch('/autonomy/rules', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(template),
}).then(r => r.json()).then(() => {
autoLoadRules();
autoRefreshStatus();
});
}
// ==================== ACTIVITY LOG ====================
let _activitySSE = null;
function autoLoadActivity() {
fetch('/autonomy/activity?limit=100').then(r => r.json()).then(data => {
renderActivity(data.entries || []);
document.getElementById('activity-count').textContent = (data.total||0) + ' entries';
});
// Start SSE
if (!_activitySSE) {
_activitySSE = new EventSource('/autonomy/activity/stream');
_activitySSE.onmessage = (e) => {
try {
const entry = JSON.parse(e.data);
if (entry.type === 'keepalive') return;
prependActivity(entry);
document.getElementById('activity-live-dot').classList.add('active');
} catch(ex) {}
};
}
}
function renderActivity(entries) {
const tbody = document.getElementById('activity-tbody');
tbody.innerHTML = entries.map(e => activityRow(e)).join('');
}
function prependActivity(entry) {
const tbody = document.getElementById('activity-tbody');
tbody.insertAdjacentHTML('afterbegin', activityRow(entry));
// Limit rows
while (tbody.children.length > 200) tbody.removeChild(tbody.lastChild);
}
function activityRow(e) {
const time = e.timestamp ? new Date(e.timestamp).toLocaleTimeString() : '';
const cls = e.action_type === 'system' ? 'activity-system' : (e.success ? 'activity-success' : 'activity-fail');
const icon = e.success ? '&#x2713;' : '&#x2717;';
return `<tr class="${cls}">
<td style="font-size:0.75rem;font-family:monospace">${time}</td>
<td>${e.action_type === 'system' ? '-' : icon}</td>
<td style="font-size:0.8rem">${esc(e.action_type||'')}</td>
<td style="font-size:0.8rem">${esc(e.action_detail||'')}</td>
<td style="font-size:0.75rem">${esc(e.rule_name||'')}</td>
<td style="font-size:0.75rem">${esc(e.tier||'')}</td>
</tr>`;
}
// ==================== UTILS ====================
function esc(s) {
if (!s) return '';
const d = document.createElement('div');
d.textContent = s;
return d.innerHTML;
}
// ==================== INIT ====================
document.addEventListener('DOMContentLoaded', () => {
autoStartRefresh();
autoLoadRules();
});
</script>
{% endblock %}

View File

@ -35,6 +35,7 @@
<div class="nav-section"> <div class="nav-section">
<div class="nav-section-title">Categories</div> <div class="nav-section-title">Categories</div>
<ul class="nav-links"> <ul class="nav-links">
<li><a href="{{ url_for('autonomy.index') }}" class="{% if request.blueprint == 'autonomy' %}active{% endif %}" style="color:var(--accent)">Autonomy</a></li>
<li><a href="{{ url_for('defense.index') }}" class="{% if request.blueprint == 'defense' and request.endpoint not in ('defense.linux_index', 'defense.windows_index', 'defense.monitor_index') %}active{% endif %}">Defense</a></li> <li><a href="{{ url_for('defense.index') }}" class="{% if request.blueprint == 'defense' and request.endpoint not in ('defense.linux_index', 'defense.windows_index', 'defense.monitor_index') %}active{% endif %}">Defense</a></li>
<li><a href="{{ url_for('defense.linux_index') }}" class="{% if request.endpoint == 'defense.linux_index' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">&#x2514; Linux</a></li> <li><a href="{{ url_for('defense.linux_index') }}" class="{% if request.endpoint == 'defense.linux_index' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">&#x2514; Linux</a></li>
<li><a href="{{ url_for('defense.windows_index') }}" class="{% if request.endpoint == 'defense.windows_index' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">&#x2514; Windows</a></li> <li><a href="{{ url_for('defense.windows_index') }}" class="{% if request.endpoint == 'defense.windows_index' %}active{% endif %}" style="padding-left:1.5rem;font-size:0.85rem">&#x2514; Windows</a></li>