From bd88bbc96ab2e2ad8fd9a2daf478e527fd2b23a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Fri, 27 Mar 2026 07:39:17 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20Kill=5FLIFE=20service=20skeleton=20?= =?UTF-8?q?=E2=80=94=20FastAPI=20+=20Docker=20+=20mascarade=20bridge?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - kill_life/ Python package (FastAPI v0.1.0) - server.py: 6 endpoints (health, agents, specs, agent/run, mcp/servers, bridge) - worker.py: background task worker stub - Dockerfile: python:3.12-slim + uv - docker-compose.yml: kill-life-api (port 8200) + kill-life-worker - mcp.json: added mascarade-bridge + kill-life-api server entries - 6 BMAD agents exposed, ~25 specs discoverable, MCP bridge to mascarade-core Co-Authored-By: Claude Opus 4.6 (1M context) --- Dockerfile | 19 +++++ docker-compose.yml | 44 ++++++++++++ kill_life/__init__.py | 3 + kill_life/server.py | 156 ++++++++++++++++++++++++++++++++++++++++++ kill_life/worker.py | 46 +++++++++++++ mcp.json | 15 ++++ pyproject.toml | 26 +++++++ 7 files changed, 309 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 kill_life/__init__.py create mode 100644 kill_life/server.py create mode 100644 kill_life/worker.py create mode 100644 pyproject.toml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b8acc0b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +FROM python:3.12-slim + +WORKDIR /app + +# Install uv for fast dependency management +RUN pip install --no-cache-dir uv + +# Copy dependency definition first (cache layer) +COPY pyproject.toml . + +# Install dependencies +RUN uv pip install --system -e "." + +# Copy source +COPY . . + +EXPOSE 8200 + +CMD ["uvicorn", "kill_life.server:app", "--host", "0.0.0.0", "--port", "8200"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..fd6fbee --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +version: "3.9" + +services: + kill-life-api: + build: . + container_name: kill-life-api + ports: + - "8200:8200" + volumes: + - .:/app + environment: + - MASCARADE_CORE_URL=http://mascarade-core:8100 + - MASCARADE_API_KEY=${MASCARADE_API_KEY:-} + - KILL_LIFE_ENV=production + healthcheck: + test: ["CMD", "python", "-c", "import httpx; r=httpx.get('http://localhost:8200/health'); r.raise_for_status()"] + interval: 30s + timeout: 10s + retries: 3 + restart: unless-stopped + networks: + - kill-life-net + + kill-life-worker: + build: . + container_name: kill-life-worker + command: ["python", "-m", "kill_life.worker"] + volumes: + - .:/app + environment: + - MASCARADE_CORE_URL=http://mascarade-core:8100 + - MASCARADE_API_KEY=${MASCARADE_API_KEY:-} + - KILL_LIFE_ENV=production + depends_on: + kill-life-api: + condition: service_healthy + restart: unless-stopped + networks: + - kill-life-net + +networks: + kill-life-net: + driver: bridge + name: kill-life-net diff --git a/kill_life/__init__.py b/kill_life/__init__.py new file mode 100644 index 0000000..5022bfd --- /dev/null +++ b/kill_life/__init__.py @@ -0,0 +1,3 @@ +"""Kill_LIFE — AI-native control plane for embedded systems.""" + +__version__ = "0.1.0" diff --git a/kill_life/server.py b/kill_life/server.py new file mode 100644 index 0000000..d4e4eac --- /dev/null +++ b/kill_life/server.py @@ -0,0 +1,156 @@ +"""Kill_LIFE FastAPI service — minimal skeleton for mascarade ecosystem integration.""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from fastapi import FastAPI, HTTPException +from pydantic import BaseModel + +from kill_life import __version__ + +REPO_ROOT = Path(__file__).resolve().parent.parent + +app = FastAPI( + title="Kill_LIFE API", + version=__version__, + description="Control plane API for Kill_LIFE embedded systems framework", +) + +# --------------------------------------------------------------------------- +# Agent definitions (from agents/*.md) +# --------------------------------------------------------------------------- + +BMAD_AGENTS: dict[str, dict[str, str]] = { + "pm": { + "name": "PM Agent", + "file": "agents/pm_agent.md", + "role": "Project management, intake, prioritization", + }, + "architect": { + "name": "Architect Agent", + "file": "agents/architect_agent.md", + "role": "System architecture, component design, tech decisions", + }, + "firmware": { + "name": "Firmware Agent", + "file": "agents/firmware_agent.md", + "role": "Embedded firmware development, HAL, drivers", + }, + "hw_schematic": { + "name": "HW Schematic Agent", + "file": "agents/hw_schematic_agent.md", + "role": "Hardware schematics, KiCad, PCB layout, BOM", + }, + "qa": { + "name": "QA Agent", + "file": "agents/qa_agent.md", + "role": "Testing, compliance, evidence collection", + }, + "doc": { + "name": "Doc Agent", + "file": "agents/doc_agent.md", + "role": "Documentation, runbooks, operator guides", + }, +} + + +def _list_specs() -> list[dict[str, str]]: + """Scan specs/ directory for markdown files.""" + specs_dir = REPO_ROOT / "specs" + if not specs_dir.is_dir(): + return [] + specs = [] + for p in sorted(specs_dir.glob("*.md")): + specs.append({"name": p.stem, "file": f"specs/{p.name}"}) + return specs + + +def _load_mcp_json() -> dict[str, Any]: + """Load mcp.json from repo root.""" + mcp_path = REPO_ROOT / "mcp.json" + if not mcp_path.exists(): + return {} + return json.loads(mcp_path.read_text()) + + +# --------------------------------------------------------------------------- +# Endpoints +# --------------------------------------------------------------------------- + + +@app.get("/health") +async def health(): + return {"status": "ok", "service": "kill-life", "version": __version__} + + +@app.get("/agents") +async def list_agents(): + return {"agents": BMAD_AGENTS} + + +@app.get("/specs") +async def list_specs(): + return {"specs": _list_specs(), "count": len(_list_specs())} + + +class AgentRunRequest(BaseModel): + input: str + context: dict[str, Any] | None = None + + +@app.post("/agents/{name}/run") +async def run_agent(name: str, req: AgentRunRequest): + if name not in BMAD_AGENTS: + raise HTTPException(status_code=404, detail=f"Agent '{name}' not found. Available: {list(BMAD_AGENTS.keys())}") + + agent = BMAD_AGENTS[name] + + # Load agent definition + agent_file = REPO_ROOT / agent["file"] + definition = "" + if agent_file.exists(): + definition = agent_file.read_text() + + # Stub response — real execution would route through mascarade LLM + return { + "agent": name, + "role": agent["role"], + "input": req.input, + "status": "stub", + "message": f"Agent '{name}' received input. Connect mascarade-core for LLM execution.", + "definition_loaded": bool(definition), + "mascarade_endpoint": "http://mascarade-core:8100/v1/chat/completions", + } + + +@app.get("/mcp/servers") +async def list_mcp_servers(): + mcp = _load_mcp_json() + servers = mcp.get("mcpServers", {}) + return { + "servers": {k: {"type": v.get("type", "local"), "tools": v.get("tools", [])} for k, v in servers.items()}, + "count": len(servers), + } + + +# --------------------------------------------------------------------------- +# Mascarade bridge info +# --------------------------------------------------------------------------- + + +@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", + "mcp_servers_count": len(_load_mcp_json().get("mcpServers", {})), + "integration": { + "specs_as_datasets": "mascarade finetune/ reads Kill_LIFE specs/", + "mcp_tools": "mascarade mcp/client.py loads Kill_LIFE MCP servers", + "agent_execution": "POST /agents/{name}/run routes through mascarade-core LLM", + }, + } diff --git a/kill_life/worker.py b/kill_life/worker.py new file mode 100644 index 0000000..44c9c55 --- /dev/null +++ b/kill_life/worker.py @@ -0,0 +1,46 @@ +"""Kill_LIFE background worker — processes agent execution tasks. + +Stub implementation. In production, this would: +- Poll a task queue (Redis/mascarade P2P) +- Execute BMAD agent pipelines +- Report results back to the API +""" + +from __future__ import annotations + +import asyncio +import logging +import signal +import sys + +logging.basicConfig(level=logging.INFO, format="%(asctime)s [kill-life-worker] %(message)s") +log = logging.getLogger(__name__) + +_running = True + + +def _shutdown(signum, frame): + global _running + log.info("Received signal %s, shutting down...", signum) + _running = False + + +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)")) + + while _running: + # Stub: sleep and wait. Real implementation would poll task queue. + await asyncio.sleep(5) + + log.info("Worker stopped.") + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + sys.exit(0) diff --git a/mcp.json b/mcp.json index 36bda8a..c5e32c7 100644 --- a/mcp.json +++ b/mcp.json @@ -64,6 +64,21 @@ "Authorization": "Bearer ${HUGGINGFACE_API_KEY}" }, "note": "Use https://huggingface.co/mcp?login for OAuth browser login instead of token auth" + }, + "mascarade-bridge": { + "type": "url", + "url": "http://192.168.0.119:8100/mcp", + "headers": { + "Authorization": "Bearer ${MASCARADE_API_KEY}" + }, + "tools": ["llm-chat", "llm-complete", "p2p-status", "agent-run"], + "note": "Bridge to mascarade-core for LLM execution of Kill_LIFE agents" + }, + "kill-life-api": { + "type": "url", + "url": "http://localhost:8200", + "tools": ["agents", "specs", "mcp-servers"], + "note": "Local Kill_LIFE API (docker-compose up)" } } } diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b766bf9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "kill-life" +version = "0.1.0" +description = "Kill_LIFE control plane — AI-native embedded systems framework" +requires-python = ">=3.12" +license = "MIT" +dependencies = [ + "fastapi>=0.115", + "uvicorn[standard]>=0.34", + "pydantic>=2.10", + "httpx>=0.28", +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "ruff>=0.9", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.ruff] +target-version = "py312" +line-length = 120