AUTARCH v1.9 — remote monitoring, SSH manager, daemon, vault, cleanup

- Add Remote Monitoring Station with PIAP device profile system
- Add SSH/SSHD manager with fail2ban integration
- Add privileged daemon architecture for safe root operations
- Add encrypted vault, HAL memory, HAL auto-analyst
- Add network security suite, module creator, codex training
- Add start.sh launcher script and GTK3 desktop launcher
- Remove Output/ build artifacts, installer files, loose docs
- Update .gitignore for runtime data and build artifacts
- Update README for v1.9 with new launch method, screenshots, and features

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
SsSnake
2026-03-24 06:59:06 -07:00
parent 1092689f45
commit da53899f66
382 changed files with 15277 additions and 493964 deletions

217
core/hal_memory.py Normal file
View File

@@ -0,0 +1,217 @@
"""
AUTARCH HAL Memory Cache
Encrypted conversation history for the HAL AI agent.
Stores all HAL conversations in an AES-encrypted file.
Only the AI agent system can read them — the decryption key is
derived from the machine ID + a HAL-specific salt, same pattern
as the vault but with a separate keyspace.
Max size: configurable, default 4GB. Trims oldest entries when exceeded.
Usage:
from core.hal_memory import get_hal_memory
mem = get_hal_memory()
mem.add('user', 'scan my network')
mem.add('hal', 'Running nmap on 10.0.0.0/24...')
history = mem.get_history(last_n=50)
mem.add_context('scan_result', {'tool': 'nmap', 'output': '...'})
"""
import hashlib
import json
import logging
import os
import struct
import time
from pathlib import Path
from typing import Optional
_log = logging.getLogger('autarch.hal_memory')
_MEMORY_DIR = Path(__file__).parent.parent / 'data'
_MEMORY_FILE = _MEMORY_DIR / 'hal_memory.enc'
_MEMORY_MAGIC = b'HALM'
_MEMORY_VERSION = 1
_DEFAULT_MAX_BYTES = 4 * 1024 * 1024 * 1024 # 4GB
def _derive_key(salt: bytes) -> bytes:
"""Derive AES key from machine identity + HAL-specific material."""
machine_id = b''
for path in ('/etc/machine-id', '/var/lib/dbus/machine-id'):
try:
with open(path) as f:
machine_id = f.read().strip().encode()
break
except Exception:
continue
if not machine_id:
import socket
machine_id = f"hal-{socket.gethostname()}".encode()
return hashlib.pbkdf2_hmac('sha256', machine_id + b'HAL-MEMORY-KEY', salt, 100_000, dklen=32)
def _encrypt(plaintext: bytes, key: bytes) -> tuple:
iv = os.urandom(16)
try:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.padding import PKCS7
padder = PKCS7(128).padder()
padded = padder.update(plaintext) + padder.finalize()
enc = Cipher(algorithms.AES(key), modes.CBC(iv)).encryptor()
return iv, enc.update(padded) + enc.finalize()
except ImportError:
pass
try:
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
return iv, AES.new(key, AES.MODE_CBC, iv).encrypt(pad(plaintext, 16))
except ImportError:
raise RuntimeError('No crypto backend available')
def _decrypt(iv: bytes, ciphertext: bytes, key: bytes) -> bytes:
try:
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.padding import PKCS7
dec = Cipher(algorithms.AES(key), modes.CBC(iv)).decryptor()
padded = dec.update(ciphertext) + dec.finalize()
return PKCS7(128).unpadder().update(padded) + PKCS7(128).unpadder().finalize()
except ImportError:
pass
try:
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
return unpad(AES.new(key, AES.MODE_CBC, iv).decrypt(ciphertext), 16)
except ImportError:
raise RuntimeError('No crypto backend available')
class HalMemory:
"""Encrypted conversation memory for HAL."""
def __init__(self, max_bytes: int = _DEFAULT_MAX_BYTES):
self._max_bytes = max_bytes
self._salt = b''
self._entries = []
self._load()
def _load(self):
if not _MEMORY_FILE.exists():
self._salt = os.urandom(32)
self._entries = []
return
try:
with open(_MEMORY_FILE, 'rb') as f:
magic = f.read(4)
if magic != _MEMORY_MAGIC:
self._salt = os.urandom(32)
self._entries = []
return
f.read(1) # version
self._salt = f.read(32)
iv = f.read(16)
ciphertext = f.read()
key = _derive_key(self._salt)
plaintext = _decrypt(iv, ciphertext, key)
self._entries = json.loads(plaintext.decode('utf-8'))
_log.info(f'[HAL Memory] Loaded {len(self._entries)} entries')
except Exception as e:
_log.error(f'[HAL Memory] Load failed: {e}')
self._salt = os.urandom(32)
self._entries = []
def _save(self):
try:
_MEMORY_DIR.mkdir(parents=True, exist_ok=True)
key = _derive_key(self._salt)
plaintext = json.dumps(self._entries).encode('utf-8')
# Trim if over max size
while len(plaintext) > self._max_bytes and len(self._entries) > 10:
self._entries = self._entries[len(self._entries) // 4:] # Drop oldest 25%
plaintext = json.dumps(self._entries).encode('utf-8')
_log.info(f'[HAL Memory] Trimmed to {len(self._entries)} entries ({len(plaintext)} bytes)')
iv, ciphertext = _encrypt(plaintext, key)
with open(_MEMORY_FILE, 'wb') as f:
f.write(_MEMORY_MAGIC)
f.write(struct.pack('B', _MEMORY_VERSION))
f.write(self._salt)
f.write(iv)
f.write(ciphertext)
os.chmod(_MEMORY_FILE, 0o600)
except Exception as e:
_log.error(f'[HAL Memory] Save failed: {e}')
def add(self, role: str, content: str, metadata: dict = None):
"""Add a conversation entry."""
entry = {
'role': role,
'content': content,
'timestamp': time.time(),
}
if metadata:
entry['metadata'] = metadata
self._entries.append(entry)
# Auto-save every 20 entries
if len(self._entries) % 20 == 0:
self._save()
def add_context(self, context_type: str, data: dict):
"""Add a context entry (scan result, fix result, IR, etc.)."""
self.add('context', json.dumps(data), metadata={'type': context_type})
def get_history(self, last_n: int = 50) -> list:
"""Get recent conversation history."""
return self._entries[-last_n:] if self._entries else []
def get_full_history(self) -> list:
"""Get all entries."""
return self._entries
def search(self, query: str, max_results: int = 20) -> list:
"""Search memory for entries containing query string."""
query_lower = query.lower()
results = []
for entry in reversed(self._entries):
if query_lower in entry.get('content', '').lower():
results.append(entry)
if len(results) >= max_results:
break
return results
def clear(self):
"""Clear all memory."""
self._entries = []
self._save()
def save(self):
"""Force save to disk."""
self._save()
def stats(self) -> dict:
"""Get memory stats."""
total_bytes = len(json.dumps(self._entries).encode())
return {
'entries': len(self._entries),
'bytes': total_bytes,
'max_bytes': self._max_bytes,
'percent_used': round(total_bytes / self._max_bytes * 100, 2) if self._max_bytes else 0,
}
# Singleton
_instance: Optional[HalMemory] = None
def get_hal_memory(max_bytes: int = None) -> HalMemory:
global _instance
if _instance is None:
from core.config import get_config
config = get_config()
if max_bytes is None:
max_bytes = config.get_int('hal_memory', 'max_bytes', _DEFAULT_MAX_BYTES)
_instance = HalMemory(max_bytes=max_bytes)
return _instance