|
1 | 1 | # SPDX-License-Identifier: MIT |
2 | 2 | # Copyright (c) 2026 PiQrypt Inc. |
3 | 3 | # e-Soleau: DSO2026006483 (19/02/2026) -- DSO2026009143 (12/03/2026) |
4 | | -# sync: generated from piqrypt@v1.8.7 — do not edit manually |
| 4 | +# sync: generated from piqrypt@v1.8.10 — do not edit manually |
5 | 5 | # Source: https://github.com/piqrypt/piqrypt/blob/main//home/runner/work/piqrypt/piqrypt/piqrypt/vigil/vigil_server.py |
6 | 6 | # To modify: edit in piqrypt, changes will be synced on next release |
7 | 7 | """ |
|
39 | 39 | import re |
40 | 40 | import sys |
41 | 41 | import threading |
| 42 | +import subprocess |
42 | 43 | import time |
43 | 44 | import shutil |
44 | 45 | import signal |
|
133 | 134 | # ── Auth instance (partagée par tous les handlers) ──────────────────────────── |
134 | 135 | _AUTH = AuthMiddleware("VIGIL_TOKEN", service="vigil") |
135 | 136 |
|
| 137 | +# ── Demo process handle (singleton) ─────────────────────────────────────────── |
| 138 | +_demo_proc: Optional["subprocess.Popen"] = None |
| 139 | +_demo_active: bool = False |
| 140 | +_DEMO_LOCKFILE = PIQRYPT_DIR / ".demo_active" |
| 141 | + |
136 | 142 |
|
137 | 143 | # ── TrustGate push (fire-and-forget, thread séparé) ────────────────────────── |
138 | 144 | def _push_to_trustgate( |
@@ -363,6 +369,11 @@ def do_GET(self): |
363 | 369 | self._send_json(200, _AUTH.tier_info()) |
364 | 370 | return |
365 | 371 |
|
| 372 | + # ── Demo status (publique — pour l'UI) ── |
| 373 | + if path == "/api/demo/status": |
| 374 | + self._send_json(200, {"active": _demo_active}) |
| 375 | + return |
| 376 | + |
366 | 377 | # ── Features (publique — pour l'UI) ── |
367 | 378 | if path == "/api/features": |
368 | 379 | vf = _AUTH.vigil_features() |
@@ -446,6 +457,23 @@ def do_POST(self): |
446 | 457 | # ── Auth + feature gating ── |
447 | 458 | if not _AUTH.check(self): |
448 | 459 | return |
| 460 | + |
| 461 | + # ── Demo routes — auth only, no record feature required ── |
| 462 | + if path == "/api/demo/start": |
| 463 | + length = int(self.headers.get("Content-Length", 0)) |
| 464 | + body = self.rfile.read(length) if length else b"{}" |
| 465 | + try: |
| 466 | + payload = json.loads(body) |
| 467 | + except json.JSONDecodeError: |
| 468 | + self._send_error(400, "Invalid JSON") |
| 469 | + return |
| 470 | + self._api_demo_start(payload) |
| 471 | + return |
| 472 | + |
| 473 | + if path == "/api/demo/stop": |
| 474 | + self._api_demo_stop() |
| 475 | + return |
| 476 | + |
449 | 477 | if not _AUTH.check_feature(self, "record"): |
450 | 478 | return |
451 | 479 |
|
@@ -950,6 +978,34 @@ def _api_create_agent(self, payload: Dict): |
950 | 978 | self._send_error(503, "Backend non disponible — mode DEMO") |
951 | 979 | return |
952 | 980 |
|
| 981 | + # ── Agent count limit (Free tier: 3 agents max) ─────────────────────── |
| 982 | + try: |
| 983 | + from aiss.license import get_tier as _get_license_tier, TIERS as _TIERS |
| 984 | + _tier = _get_license_tier() |
| 985 | + _agents_max = _TIERS.get(_tier, _TIERS["free"]).get("agents_max") |
| 986 | + if _agents_max is not None: |
| 987 | + _agents_dir = PIQRYPT_DIR / "agents" |
| 988 | + _existing = 0 |
| 989 | + if _agents_dir.exists(): |
| 990 | + _existing = len([ |
| 991 | + d for d in _agents_dir.iterdir() if d.is_dir() |
| 992 | + ]) |
| 993 | + if _existing >= _agents_max: |
| 994 | + self._send_json(403, { |
| 995 | + "error": "agent_limit_reached", |
| 996 | + "message": ( |
| 997 | + f"Free tier limit reached ({_agents_max} agents) " |
| 998 | + "— upgrade at piqrypt.com" |
| 999 | + ), |
| 1000 | + "limit": _agents_max, |
| 1001 | + "current": _existing, |
| 1002 | + "tier": _tier, |
| 1003 | + "upgrade": "https://piqrypt.com/pricing", |
| 1004 | + }) |
| 1005 | + return |
| 1006 | + except Exception as e: |
| 1007 | + log.debug("[Vigil] agent_limit check failed (non-blocking): %s", e) |
| 1008 | + |
953 | 1009 | # ── Bridge limit enforcement (Free tier: 2 bridges max) ────────────── |
954 | 1010 | bridge_limit = _AUTH.get_bridge_limit() |
955 | 1011 | if bridge_limit is not None and bridge: |
@@ -1098,6 +1154,94 @@ def _api_delete_agent(self, name: str, confirmed: bool = False): |
1098 | 1154 | log.error("[Vigil] delete_agent(%s) failed: %s", name, e) |
1099 | 1155 | self._send_json(500, {"error": str(e)}) |
1100 | 1156 |
|
| 1157 | + def _api_demo_start(self, payload: Dict): |
| 1158 | + """POST /api/demo/start — launch demo_families.py for the given family.""" |
| 1159 | + global _demo_proc, _demo_active |
| 1160 | + |
| 1161 | + family = (payload.get("family") or "nexus").strip() |
| 1162 | + if family not in ("nexus", "pixelflow", "alphacore"): |
| 1163 | + family = "nexus" |
| 1164 | + |
| 1165 | + # Kill any running demo |
| 1166 | + if _demo_proc is not None: |
| 1167 | + try: |
| 1168 | + _demo_proc.terminate() |
| 1169 | + except Exception: |
| 1170 | + pass |
| 1171 | + _demo_proc = None |
| 1172 | + |
| 1173 | + # Purge agent state |
| 1174 | + agents_dir = PIQRYPT_DIR / "agents" |
| 1175 | + if agents_dir.exists(): |
| 1176 | + shutil.rmtree(agents_dir, ignore_errors=True) |
| 1177 | + agents_dir.mkdir(parents=True, exist_ok=True) |
| 1178 | + |
| 1179 | + peers_file = PIQRYPT_DIR / "peers.json" |
| 1180 | + if peers_file.exists(): |
| 1181 | + try: |
| 1182 | + peers_file.unlink() |
| 1183 | + except Exception: |
| 1184 | + pass |
| 1185 | + |
| 1186 | + # Locate demo script |
| 1187 | + demo_script = Path(__file__).resolve().parent.parent / "demos" / "demo_families.py" |
| 1188 | + if not demo_script.exists(): |
| 1189 | + self._send_error(404, f"demo_families.py not found at {demo_script}") |
| 1190 | + return |
| 1191 | + |
| 1192 | + token = _AUTH.token or "" |
| 1193 | + _demo_proc = subprocess.Popen( |
| 1194 | + [sys.executable, str(demo_script), "--family", family, "--loop", "--fast"], |
| 1195 | + env={ |
| 1196 | + **os.environ, |
| 1197 | + "VIGIL_TOKEN": token, |
| 1198 | + "PIQRYPT_SCRYPT_N": "16384", |
| 1199 | + "VIGIL_DEV_DELETE": "1", |
| 1200 | + "PYTHONIOENCODING": "utf-8", |
| 1201 | + }, |
| 1202 | + stdout=subprocess.DEVNULL, |
| 1203 | + stderr=subprocess.DEVNULL, |
| 1204 | + ) |
| 1205 | + _demo_active = True |
| 1206 | + try: |
| 1207 | + _DEMO_LOCKFILE.write_text(family) |
| 1208 | + except Exception: |
| 1209 | + pass |
| 1210 | + log.info("[Vigil] Demo started — family=%s pid=%d", family, _demo_proc.pid) |
| 1211 | + self._send_json(200, {"status": "ok", "family": family}) |
| 1212 | + |
| 1213 | + def _api_demo_stop(self): |
| 1214 | + """POST /api/demo/stop — terminate demo process and purge agent state.""" |
| 1215 | + global _demo_proc, _demo_active |
| 1216 | + |
| 1217 | + if _demo_proc is not None: |
| 1218 | + try: |
| 1219 | + _demo_proc.terminate() |
| 1220 | + except Exception: |
| 1221 | + pass |
| 1222 | + _demo_proc = None |
| 1223 | + |
| 1224 | + agents_dir = PIQRYPT_DIR / "agents" |
| 1225 | + if agents_dir.exists(): |
| 1226 | + shutil.rmtree(agents_dir, ignore_errors=True) |
| 1227 | + agents_dir.mkdir(parents=True, exist_ok=True) |
| 1228 | + |
| 1229 | + peers_file = PIQRYPT_DIR / "peers.json" |
| 1230 | + if peers_file.exists(): |
| 1231 | + try: |
| 1232 | + peers_file.unlink() |
| 1233 | + except Exception: |
| 1234 | + pass |
| 1235 | + |
| 1236 | + _demo_active = False |
| 1237 | + try: |
| 1238 | + if _DEMO_LOCKFILE.exists(): |
| 1239 | + _DEMO_LOCKFILE.unlink() |
| 1240 | + except Exception: |
| 1241 | + pass |
| 1242 | + log.info("[Vigil] Demo stopped") |
| 1243 | + self._send_json(200, {"status": "ok"}) |
| 1244 | + |
1101 | 1245 | def _api_record(self, name: str, event: Dict): |
1102 | 1246 | """Receive a stamped event from a bridge and feed it to Vigil.""" |
1103 | 1247 | if BACKEND_AVAILABLE: |
@@ -1498,6 +1642,21 @@ def __init__(self, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT): |
1498 | 1642 | self._thread: Optional[threading.Thread] = None |
1499 | 1643 |
|
1500 | 1644 | def start(self, blocking: bool = True): |
| 1645 | + # ── Demo lockfile check — purge stale demo state on restart ────────── |
| 1646 | + if _DEMO_LOCKFILE.exists(): |
| 1647 | + log.info("[Vigil] Demo lockfile found — purging stale demo state") |
| 1648 | + try: |
| 1649 | + agents_dir = PIQRYPT_DIR / "agents" |
| 1650 | + if agents_dir.exists(): |
| 1651 | + shutil.rmtree(agents_dir, ignore_errors=True) |
| 1652 | + agents_dir.mkdir(parents=True, exist_ok=True) |
| 1653 | + peers_file = PIQRYPT_DIR / "peers.json" |
| 1654 | + if peers_file.exists(): |
| 1655 | + peers_file.unlink() |
| 1656 | + _DEMO_LOCKFILE.unlink() |
| 1657 | + except Exception as e: |
| 1658 | + log.warning("[Vigil] Demo cleanup failed: %s", e) |
| 1659 | + |
1501 | 1660 | self._server = HTTPServer((self.host, self.port), VIGILHandler) |
1502 | 1661 | log.info("━" * 56) |
1503 | 1662 | log.info(" VIGIL Server v1.8.4") |
|
0 commit comments