feat: Kill_LIFE service skeleton — FastAPI + Docker + mascarade bridge

- 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) <noreply@anthropic.com>
This commit is contained in:
L'électron rare
2026-03-27 07:39:17 +01:00
parent 4d82d3ed63
commit bd88bbc96a
7 changed files with 309 additions and 0 deletions
+19
View File
@@ -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"]
+44
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
"""Kill_LIFE — AI-native control plane for embedded systems."""
__version__ = "0.1.0"
+156
View File
@@ -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",
},
}
+46
View File
@@ -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)
+15
View File
@@ -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)"
}
}
}
+26
View File
@@ -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