- 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>
218 lines
7.4 KiB
Python
218 lines
7.4 KiB
Python
"""
|
|
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
|