Specs stay at repo root (cross-VM). Move deploy and code into logical projects with README per domain, updated manifest.yaml, and symlinks at legacy paths for VM122 backward compatibility.
104 lines
5.9 KiB
Python
104 lines
5.9 KiB
Python
"""Persistence Agentic Ops."""
|
|
from __future__ import annotations
|
|
import json, sqlite3
|
|
from datetime import datetime, timezone
|
|
from typing import Any
|
|
|
|
def _now():
|
|
return datetime.now(timezone.utc).isoformat()
|
|
|
|
def init_agent_schema(conn):
|
|
conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS agent_scenarios (
|
|
id TEXT PRIMARY KEY, title TEXT NOT NULL, schedule TEXT,
|
|
severity_default TEXT NOT NULL DEFAULT 'warn', config_json TEXT NOT NULL,
|
|
enabled INTEGER NOT NULL DEFAULT 1, updated_at TEXT NOT NULL);
|
|
CREATE TABLE IF NOT EXISTS agent_runs (
|
|
id INTEGER PRIMARY KEY, scenario_id TEXT NOT NULL, trigger TEXT NOT NULL DEFAULT 'cron',
|
|
status TEXT NOT NULL, summary_text TEXT, llm_model TEXT, metadata_json TEXT,
|
|
started_at TEXT NOT NULL, finished_at TEXT);
|
|
CREATE TABLE IF NOT EXISTS agent_findings (
|
|
id INTEGER PRIMARY KEY, run_id INTEGER NOT NULL, severity TEXT NOT NULL,
|
|
category TEXT NOT NULL DEFAULT 'api', title TEXT NOT NULL, detail_md TEXT,
|
|
evidence_json TEXT, suggested_human_action TEXT, kb_refs_json TEXT,
|
|
acknowledged_at TEXT, acknowledged_by TEXT, created_at TEXT NOT NULL);
|
|
CREATE TABLE IF NOT EXISTS agent_action_log (
|
|
id INTEGER PRIMARY KEY, ts TEXT NOT NULL, agent_id TEXT NOT NULL DEFAULT 'sentinel',
|
|
run_id INTEGER, event_type TEXT NOT NULL, message TEXT, payload_json TEXT);
|
|
CREATE TABLE IF NOT EXISTS agent_kb_chunks (
|
|
id INTEGER PRIMARY KEY, source_path TEXT NOT NULL, chunk_text TEXT NOT NULL, indexed_at TEXT NOT NULL);
|
|
CREATE INDEX IF NOT EXISTS idx_agent_runs_scenario ON agent_runs(scenario_id);
|
|
""")
|
|
|
|
def log_event(conn, *, event_type, message, agent_id="sentinel", run_id=None, payload=None):
|
|
conn.execute("INSERT INTO agent_action_log (ts,agent_id,run_id,event_type,message,payload_json) VALUES (?,?,?,?,?,?)",
|
|
(_now(), agent_id, run_id, event_type, message, json.dumps(payload or {})))
|
|
try:
|
|
conn.execute("INSERT INTO desk_security_audit (username,event_type,client_ip,payload,created_at) VALUES (?,?,?,?,?)",
|
|
("agentic", f"agent.{event_type}", "vm122", json.dumps({"message": message, **(payload or {})}), _now()))
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
|
|
def upsert_scenario(conn, scenario):
|
|
conn.execute("""INSERT INTO agent_scenarios (id,title,schedule,severity_default,config_json,enabled,updated_at)
|
|
VALUES (?,?,?,?,?,1,?) ON CONFLICT(id) DO UPDATE SET title=excluded.title, schedule=excluded.schedule,
|
|
severity_default=excluded.severity_default, config_json=excluded.config_json, updated_at=excluded.updated_at""",
|
|
(scenario["id"], scenario["title"], scenario.get("schedule","*/5 * * * *"),
|
|
scenario.get("severity_default","warn"), json.dumps(scenario), _now()))
|
|
|
|
def list_scenarios(conn):
|
|
out = []
|
|
for row in conn.execute("SELECT * FROM agent_scenarios WHERE enabled=1 ORDER BY id"):
|
|
item = dict(row)
|
|
item["config"] = json.loads(item.pop("config_json") or "{}")
|
|
last = conn.execute("SELECT status,started_at FROM agent_runs WHERE scenario_id=? ORDER BY id DESC LIMIT 1", (row["id"],)).fetchone()
|
|
item["last_run_status"] = last["status"] if last else None
|
|
item["last_run_at"] = last["started_at"] if last else None
|
|
out.append(item)
|
|
return out
|
|
|
|
def get_scenario(conn, scenario_id):
|
|
row = conn.execute("SELECT * FROM agent_scenarios WHERE id=? AND enabled=1", (scenario_id,)).fetchone()
|
|
if not row: return None
|
|
item = dict(row); item["config"] = json.loads(item.pop("config_json") or "{}"); return item
|
|
|
|
def create_run(conn, scenario_id, trigger):
|
|
return int(conn.execute("INSERT INTO agent_runs (scenario_id,trigger,status,started_at) VALUES (?,?,?,?)",
|
|
(scenario_id, trigger, "running", _now())).lastrowid)
|
|
|
|
def finish_run(conn, run_id, *, status, summary, llm_model=None, metadata=None):
|
|
conn.execute("UPDATE agent_runs SET status=?,summary_text=?,llm_model=?,metadata_json=?,finished_at=? WHERE id=?",
|
|
(status, summary, llm_model, json.dumps(metadata or {}), _now(), run_id))
|
|
|
|
def add_finding(conn, run_id, *, severity, category, title, detail_md="", evidence=None, human_action="", kb_refs=None):
|
|
return int(conn.execute("""INSERT INTO agent_findings (run_id,severity,category,title,detail_md,evidence_json,
|
|
suggested_human_action,kb_refs_json,created_at) VALUES (?,?,?,?,?,?,?,?,?)""",
|
|
(run_id, severity, category, title, detail_md, json.dumps(evidence or {}), human_action,
|
|
json.dumps(kb_refs or []), _now())).lastrowid)
|
|
|
|
def list_findings(conn, *, severity=None, limit=50, open_only=True):
|
|
q, params = "SELECT * FROM agent_findings WHERE 1=1", []
|
|
if severity: q += " AND severity=?"; params.append(severity)
|
|
if open_only: q += " AND acknowledged_at IS NULL"
|
|
q += " ORDER BY id DESC LIMIT ?"; params.append(limit)
|
|
return [dict(r) for r in conn.execute(q, params)]
|
|
|
|
def list_action_log(conn, limit=100):
|
|
return [dict(r) for r in conn.execute("SELECT * FROM agent_action_log ORDER BY id DESC LIMIT ?", (limit,))]
|
|
|
|
def index_kb_file(conn, source_path, text):
|
|
conn.execute("DELETE FROM agent_kb_chunks WHERE source_path=?", (source_path,))
|
|
now = _now()
|
|
for i in range(0, len(text), 1200):
|
|
conn.execute("INSERT INTO agent_kb_chunks (source_path,chunk_text,indexed_at) VALUES (?,?,?)",
|
|
(source_path, text[i:i+1200], now))
|
|
|
|
def search_kb(conn, query, limit=8):
|
|
terms = [t.strip().lower() for t in query.split() if len(t.strip()) > 2]
|
|
if not terms: return []
|
|
scored = []
|
|
for row in conn.execute("SELECT source_path,chunk_text FROM agent_kb_chunks"):
|
|
score = sum(1 for t in terms if t in row["chunk_text"].lower())
|
|
if score: scored.append((score, {"source": row["source_path"], "snippet": row["chunk_text"][:400]}))
|
|
scored.sort(key=lambda x: -x[0])
|
|
return [s[1] for s in scored[:limit]]
|