| """Local metadata-only trajectories; raw values are deliberately not persisted. |
| |
| Hashes can still reveal low-entropy secrets via guessing. Persist HMACs instead, |
| using a per-store key kept outside the database. Rich training records require a |
| separate reviewed, redacted ingestion path. |
| """ |
| import hashlib |
| import hmac |
| import json |
| from pathlib import Path |
| import secrets |
| import sqlite3 |
| from time import time |
|
|
|
|
| class Recorder: |
| def __init__(self, path): |
| path = Path(path) |
| path.parent.mkdir(parents=True, exist_ok=True) |
| keyfile = path.with_suffix('.key') |
| try: |
| with keyfile.open('xb') as handle: |
| handle.write(secrets.token_bytes(32)) |
| keyfile.chmod(0o600) |
| except FileExistsError: |
| pass |
| self.key = keyfile.read_bytes() |
| if len(self.key) != 32: |
| raise ValueError('invalid recording key') |
| self.db = sqlite3.connect(path) |
| self.db.execute('''CREATE TABLE IF NOT EXISTS events ( |
| id INTEGER PRIMARY KEY, created REAL, task TEXT, epoch INTEGER, |
| sequence INTEGER, state_key TEXT, action_key TEXT, kind TEXT, |
| status TEXT, code TEXT, wall_ms REAL, cpu_ms REAL)''') |
|
|
| def private_key(self, value): |
| return hmac.new(self.key, value.encode('utf-8'), hashlib.sha256).hexdigest() |
|
|
| def record(self, decision, state, outcome): |
| ticket = decision.ticket |
| self.db.execute('INSERT INTO events VALUES (NULL,?,?,?,?,?,?,?,?,?,?,?)', |
| (time(), ticket.task_id, ticket.epoch, ticket.sequence, |
| self.private_key(state.state_hash if state else ''), |
| self.private_key(decision.action.encode()), decision.action.kind.name, |
| outcome.status, outcome.code, outcome.wall_ms, outcome.cpu_ms)) |
| self.db.commit() |
|
|
| def close(self): |
| self.db.close() |
|
|