diff --git a/.claude/settings.json b/.claude/settings.json index d1b8006..de1e092 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -8,19 +8,9 @@ "Bash(make aperant-install:*)", "Bash(make aperant-build:*)", "Bash(make aperant-test:*)", - "Bash(/Users/electron/Documents/Lelectron_rare/Kill_LIFE/tools/run_knowledge_base_mcp.sh --doctor)", - "Bash(/Users/electron/Documents/Lelectron_rare/Kill_LIFE/tools/run_github_dispatch_mcp.sh --doctor)", - "Bash(/Users/electron/Documents/Lelectron_rare/Kill_LIFE/tools/hw/run_kicad_mcp.sh --doctor)", - "Bash(ssh -o ConnectTimeout=5 -o BatchMode=yes clems@192.168.0.120 'echo OK')", - "Bash(ssh -o ConnectTimeout=5 -o BatchMode=yes root@192.168.0.119 'echo OK')", - "Bash(ssh -o ConnectTimeout=5 -o BatchMode=yes kxkm@kxkm-ai 'echo OK')", - "Bash(ssh -o ConnectTimeout=5 -o BatchMode=yes cils@192.168.0.210 'echo OK')" - ], - "additionalDirectories": [ - "/Users/electron/Library/Application Support/Code/User/globalStorage/saoudrizwan.claude-dev/settings", - "/Volumes/root/mascarade-main/api/src/routes", - "/Volumes/root/mascarade-main/api/src", - "/Volumes/root/mascarade-main/core/tests" + "Bash(./tools/run_knowledge_base_mcp.sh --doctor)", + "Bash(./tools/run_github_dispatch_mcp.sh --doctor)", + "Bash(./tools/hw/run_kicad_mcp.sh --doctor)" ] } } diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..57a53f3 --- /dev/null +++ b/.env.example @@ -0,0 +1,23 @@ +# Kill_LIFE — Environment variables +# Copy to .env and adjust values for your setup. + +# Mascarade integration (LLM router / agent execution) +MASCARADE_CORE_URL=http://localhost:8100 +MASCARADE_API_URL=http://localhost:8000 +MASCARADE_API_KEY= +MASCARADE_MCP_URL=http://localhost:8100/mcp + +# P2P mesh bootstrap node +P2P_BOOTSTRAP=localhost:4002 + +# Worker config +KILL_LIFE_API_URL=http://localhost:8200 +WORKER_POLL_INTERVAL=10 + +# MCP servers (optional) +APIFY_API_KEY= +HUGGINGFACE_API_KEY= + +# CI/CD (optional) +WOKWI_CLI_TOKEN= +KILL_LIFE_ENV=development diff --git a/.gitignore b/.gitignore index 06c588a..74f8a6b 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ ENV/ .vscode/ .history/ +# Claude Code (machine-specific paths) +.claude/settings.json + # MacOS .DS_Store .AppleDouble diff --git a/CLAUDE.md b/CLAUDE.md index babb39e..b4de2ff 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -97,6 +97,6 @@ KiCad 10 schematics. KiBot for exports (BOM, SVG, PDF, netlist). ERC validation ## External Services -- **Mascarade** (`MASCARADE_CORE_URL`, default `http://192.168.0.119:8100`) — LLM router with agentic RAG +- **Mascarade** (`MASCARADE_CORE_URL`, default `http://localhost:8100`) — LLM router with agentic RAG - **MCP servers** (10 configured in `mcp.json`) — kicad, freecad, openscad, platformio, github-dispatch, knowledge-base, validate-specs, apify, huggingface, mascarade-bridge - **n8n** — workflow automation (ZeroClaw integration) diff --git a/bmad/gates/gate_s2.md b/bmad/gates/gate_s2.md index e69de29..9758a0f 100644 --- a/bmad/gates/gate_s2.md +++ b/bmad/gates/gate_s2.md @@ -0,0 +1,17 @@ +# Gate S2 — Web / PR review + +Web (si concerné): +- [ ] `npm run build` ok (aucune erreur TypeScript) +- [ ] `npx playwright test e2e/smoke.spec.ts` ok +- [ ] Pas de régression visuelle sur les pages modifiées + +API (si concerné): +- [ ] `python -m unittest discover test/` — tests stable verts +- [ ] Endpoints modifiés documentés dans `specs/contracts/` +- [ ] Pas de chemin hardcodé (IPs, paths absolus) + +PR review: +- [ ] Scope guard validé (write_set respecté par agent) +- [ ] Pas de secret dans le diff (`CLAUDE.md`, `.env`, tokens) +- [ ] Handoff template rempli si cross-repo +- [ ] Evidence pack généré (si lot concerné) diff --git a/kill_life/server.py b/kill_life/server.py index 7ab0f36..d2c2fe2 100644 --- a/kill_life/server.py +++ b/kill_life/server.py @@ -15,6 +15,8 @@ from kill_life import __version__ from kill_life.agent_catalog import canonical_agents_by_id, canonical_agents_for_api, legacy_runtime_mapping MASCARADE_CORE_URL = os.environ.get("MASCARADE_CORE_URL", "http://localhost:8100") +MASCARADE_API_URL = os.environ.get("MASCARADE_API_URL", "http://localhost:8000") +P2P_BOOTSTRAP = os.environ.get("P2P_BOOTSTRAP", "localhost:4002") REPO_ROOT = Path(__file__).resolve().parent.parent @@ -161,9 +163,9 @@ async def list_mcp_servers(): @app.get("/bridge/mascarade") async def mascarade_bridge_info(): return { - "mascarade_api": "http://192.168.0.119:8000", - "mascarade_core": "http://192.168.0.119:8100", - "p2p_bootstrap": "192.168.0.119:4002", + "mascarade_api": MASCARADE_API_URL, + "mascarade_core": MASCARADE_CORE_URL, + "p2p_bootstrap": P2P_BOOTSTRAP, "mcp_servers_count": len(_load_mcp_json().get("mcpServers", {})), "integration": { "specs_as_datasets": "mascarade finetune/ reads Kill_LIFE specs/", diff --git a/kill_life/worker.py b/kill_life/worker.py index 44c9c55..3d4103b 100644 --- a/kill_life/worker.py +++ b/kill_life/worker.py @@ -1,21 +1,27 @@ -"""Kill_LIFE background worker — processes agent execution tasks. +"""Kill_LIFE background worker — polls mascarade-core for pending tasks. -Stub implementation. In production, this would: -- Poll a task queue (Redis/mascarade P2P) -- Execute BMAD agent pipelines -- Report results back to the API +Polls the Mascarade task queue and dispatches agent execution requests +to the local Kill_LIFE API. Gracefully degrades when Mascarade is +unreachable (logs and retries). """ from __future__ import annotations import asyncio import logging +import os import signal import sys +import httpx + logging.basicConfig(level=logging.INFO, format="%(asctime)s [kill-life-worker] %(message)s") log = logging.getLogger(__name__) +MASCARADE_CORE_URL = os.environ.get("MASCARADE_CORE_URL", "http://localhost:8100") +KILL_LIFE_API_URL = os.environ.get("KILL_LIFE_API_URL", "http://localhost:8200") +POLL_INTERVAL = int(os.environ.get("WORKER_POLL_INTERVAL", "10")) + _running = True @@ -25,16 +31,76 @@ def _shutdown(signum, frame): _running = False +async def poll_and_dispatch(client: httpx.AsyncClient) -> int: + """Poll Mascarade for pending tasks and dispatch them. Returns count of tasks processed.""" + try: + resp = await client.get( + f"{MASCARADE_CORE_URL}/v1/api/agents/queue", + params={"source": "kill-life"}, + timeout=10.0, + ) + if resp.status_code == 404: + # Endpoint not available yet — not an error + return 0 + resp.raise_for_status() + data = resp.json() + except httpx.ConnectError: + log.debug("Mascarade unreachable at %s — will retry", MASCARADE_CORE_URL) + return 0 + except httpx.TimeoutException: + log.debug("Mascarade poll timed out") + return 0 + except Exception as exc: + log.warning("Poll error: %s", exc) + return 0 + + tasks = data.get("tasks", []) + if not tasks: + return 0 + + processed = 0 + for task in tasks: + agent_name = task.get("agent") + task_input = task.get("input", "") + task_id = task.get("id", "unknown") + + if not agent_name: + log.warning("Task %s has no agent name, skipping", task_id) + continue + + log.info("Dispatching task %s → agent %s", task_id, agent_name) + try: + run_resp = await client.post( + f"{KILL_LIFE_API_URL}/agents/{agent_name}/run", + json={"input": task_input, "context": task.get("context")}, + timeout=60.0, + ) + if run_resp.is_success: + log.info("Task %s completed (agent=%s)", task_id, agent_name) + processed += 1 + else: + log.warning("Task %s failed: %s %s", task_id, run_resp.status_code, run_resp.text[:200]) + except Exception as exc: + log.warning("Task %s dispatch error: %s", task_id, exc) + + return processed + + async def main(): signal.signal(signal.SIGTERM, _shutdown) signal.signal(signal.SIGINT, _shutdown) - log.info("Kill_LIFE worker started. Waiting for tasks...") - log.info(" MASCARADE_CORE_URL: %s", __import__("os").environ.get("MASCARADE_CORE_URL", "(not set)")) + log.info("Kill_LIFE worker started") + log.info(" MASCARADE_CORE_URL: %s", MASCARADE_CORE_URL) + log.info(" KILL_LIFE_API_URL: %s", KILL_LIFE_API_URL) + log.info(" POLL_INTERVAL: %ds", POLL_INTERVAL) - while _running: - # Stub: sleep and wait. Real implementation would poll task queue. - await asyncio.sleep(5) + async with httpx.AsyncClient() as client: + while _running: + count = await poll_and_dispatch(client) + if count: + log.info("Processed %d task(s) this cycle", count) + await asyncio.sleep(POLL_INTERVAL) log.info("Worker stopped.") diff --git a/mcp.json b/mcp.json index bd25fac..ca731b5 100644 --- a/mcp.json +++ b/mcp.json @@ -67,7 +67,7 @@ }, "mascarade-bridge": { "type": "url", - "url": "http://192.168.0.119:8100/mcp", + "url": "http://localhost:8100/mcp", "headers": { "Authorization": "Bearer ${MASCARADE_API_KEY}" }, diff --git a/test/test_kill_life_agent_api.py b/test/test_kill_life_agent_api.py index ea914c5..bf83b3d 100644 --- a/test/test_kill_life_agent_api.py +++ b/test/test_kill_life_agent_api.py @@ -81,5 +81,27 @@ class KillLifeAgentApiTests(unittest.TestCase): self.assertIn('"lot_id": "T-123"', DummyAsyncClient.last_payload["messages"][0]["content"]) + def test_bridge_mascarade_uses_env_vars(self) -> None: + """Ensure /bridge/mascarade returns env-based URLs, not hardcoded IPs.""" + with patch.dict("os.environ", { + "MASCARADE_API_URL": "http://test-api:8000", + "MASCARADE_CORE_URL": "http://test-core:8100", + "P2P_BOOTSTRAP": "test-node:4002", + }): + # Re-import to pick up env vars + import importlib + import kill_life.server as srv + importlib.reload(srv) + client = TestClient(srv.app) + response = client.get("/bridge/mascarade") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertEqual(data["mascarade_api"], "http://test-api:8000") + self.assertEqual(data["mascarade_core"], "http://test-core:8100") + self.assertEqual(data["p2p_bootstrap"], "test-node:4002") + # No hardcoded IPs + self.assertNotIn("192.168.0.119", str(data)) + + if __name__ == "__main__": unittest.main()