mirror of
https://github.com/Av0idD4rk/firegex.git
synced 2026-10-23 14:31:00 +00:00
Expose 28 tools with shared REST operations, role-scoped bearer authentication, revisions, durable retries, snapshots, jobs and timed rollback. Add Docker verification harnesses and setup/workflow documentation in docs/mcp.md and inst.md. Validated with 330 unit tests, two consecutive 25-case MCP/import runs, and restart recovery; the full upstream regression run remains incomplete.
124 lines
5.2 KiB
Python
124 lines
5.2 KiB
Python
"""MCP metadata, separate from the schema-hashed product databases and their backups."""
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import secrets
|
|
import sqlite3
|
|
import time
|
|
from pathlib import Path
|
|
|
|
|
|
def encode(value):
|
|
return json.dumps(value, sort_keys=True, ensure_ascii=True, separators=(",", ":"))
|
|
|
|
|
|
def digest(value):
|
|
return hashlib.sha256(encode(value).encode()).hexdigest()
|
|
|
|
|
|
class Store:
|
|
def __init__(self, path=None):
|
|
self.path = Path(path or os.getenv("FIREGEX_MCP_DB", "db/mcp/control.sqlite3"))
|
|
self.conn = None
|
|
|
|
def open(self):
|
|
if self.conn is not None:
|
|
return
|
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
self.conn = sqlite3.connect(self.path, timeout=10)
|
|
self.conn.row_factory = sqlite3.Row
|
|
self.conn.executescript("""
|
|
CREATE TABLE IF NOT EXISTS tokens (
|
|
id TEXT PRIMARY KEY, name TEXT NOT NULL, hash TEXT UNIQUE NOT NULL,
|
|
role TEXT NOT NULL, expires REAL NOT NULL, revoked INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
CREATE TABLE IF NOT EXISTS records (
|
|
kind TEXT NOT NULL, id TEXT NOT NULL, created REAL NOT NULL,
|
|
value TEXT NOT NULL, PRIMARY KEY(kind, id)
|
|
);
|
|
CREATE TABLE IF NOT EXISTS requests (
|
|
actor TEXT NOT NULL, id TEXT NOT NULL, fingerprint TEXT NOT NULL,
|
|
change_id TEXT NOT NULL, PRIMARY KEY(actor, id)
|
|
);
|
|
""")
|
|
self.conn.commit()
|
|
os.chmod(self.path, 0o600)
|
|
|
|
def close(self):
|
|
if self.conn is not None:
|
|
self.conn.close()
|
|
self.conn = None
|
|
|
|
def token_create(self, name, role="operator", hours=168):
|
|
if role not in {"observer", "operator", "admin"} or not 0 < hours <= 8760:
|
|
raise ValueError("Use observer/operator/admin and a TTL between 0 and 8760 hours")
|
|
raw = "fgx_" + secrets.token_urlsafe(32)
|
|
token_id = secrets.token_hex(8)
|
|
with self.conn:
|
|
self.conn.execute("INSERT INTO tokens VALUES (?, ?, ?, ?, ?, 0)",
|
|
(token_id, name, hashlib.sha256(raw.encode()).hexdigest(),
|
|
role, time.time() + hours * 3600))
|
|
return {"id": token_id, "name": name, "role": role, "token": raw}
|
|
|
|
def token_verify(self, raw):
|
|
row = self.conn.execute(
|
|
"SELECT id,name,role,expires FROM tokens WHERE hash=? AND revoked=0 AND expires>?",
|
|
(hashlib.sha256(raw.encode()).hexdigest(), time.time()),
|
|
).fetchone()
|
|
return dict(row) if row else None
|
|
|
|
def token_list(self):
|
|
return [dict(r) for r in self.conn.execute("SELECT id,name,role,expires,revoked FROM tokens")]
|
|
|
|
def token_revoke(self, token_id):
|
|
with self.conn:
|
|
cur = self.conn.execute("UPDATE tokens SET revoked=1 WHERE id=?", (token_id,))
|
|
return bool(cur.rowcount)
|
|
|
|
def put(self, kind, record_id, value):
|
|
with self.conn:
|
|
self.conn.execute("INSERT INTO records VALUES (?, ?, ?, ?) ON CONFLICT(kind,id) "
|
|
"DO UPDATE SET value=excluded.value", (kind, record_id, time.time(), encode(value)))
|
|
|
|
def get(self, kind, record_id):
|
|
row = self.conn.execute("SELECT value FROM records WHERE kind=? AND id=?", (kind, record_id)).fetchone()
|
|
return json.loads(row[0]) if row else None
|
|
|
|
def list(self, kind, limit=100, offset=0):
|
|
return [json.loads(r[0]) for r in self.conn.execute(
|
|
"SELECT value FROM records WHERE kind=? ORDER BY created DESC,id LIMIT ? OFFSET ?",
|
|
(kind, limit, offset))]
|
|
|
|
def delete(self, kind, record_id):
|
|
with self.conn:
|
|
self.conn.execute("DELETE FROM records WHERE kind=? AND id=?", (kind, record_id))
|
|
|
|
def pending(self, *states):
|
|
return [json.loads(r[0]) for r in self.conn.execute(
|
|
"SELECT value FROM records WHERE kind='change' AND json_extract(value,'$.status') IN ("
|
|
+ ",".join("?" for _ in states) + ") ORDER BY created", states)]
|
|
|
|
def expire_artifacts(self):
|
|
with self.conn:
|
|
self.conn.execute("DELETE FROM records WHERE kind='artifact' AND json_extract(value,'$.expires_at') < ?",
|
|
(time.time(),))
|
|
|
|
def request_get(self, actor, request_id):
|
|
row = self.conn.execute("SELECT fingerprint,change_id FROM requests WHERE actor=? AND id=?",
|
|
(actor, request_id)).fetchone()
|
|
return dict(row) if row else None
|
|
|
|
def request_put(self, actor, request_id, fingerprint, change_id):
|
|
with self.conn:
|
|
self.conn.execute("INSERT INTO requests VALUES (?, ?, ?, ?)",
|
|
(actor, request_id, fingerprint, change_id))
|
|
|
|
def queue_change(self, change, request):
|
|
"""Persist the work and its retry key in the same SQLite transaction."""
|
|
with self.conn:
|
|
self.conn.execute("INSERT INTO records VALUES ('change', ?, ?, ?)",
|
|
(change["id"], time.time(), encode(change)))
|
|
if request:
|
|
self.conn.execute("INSERT INTO requests VALUES (?, ?, ?, ?)",
|
|
(request["actor"], request["id"], request["fingerprint"], change["id"]))
|