weed-sim / key_os /server.py
tostido's picture
WEED-SIM: evolutionary genetics sandbox with embedded Observer Bus
ae853c1
Raw
History Blame Contribute Delete
23.1 kB
from __future__ import annotations
import logging
import os
import re
import sys
import time
import urllib.parse
from contextlib import asynccontextmanager
from pathlib import Path
from typing import Any
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from key_os.facilities import kernel_facilities
from key_os.models import Receipt, ServiceSpec, utc_now
from key_os.probe import ProbeError, call_endpoint, enumerate_space
from key_os.routes import default_links, route_query
from key_os.security import contracts_report, redact, require_confirmation
from key_os.store import KeyOSStore
log = logging.getLogger("key_os")
from pydantic import BaseModel, Field
class ConnectRequest(BaseModel):
target: str = Field(..., description="Target Space ID (owner/name) or full URL to connect to", json_schema_extra={"example": "tostido/meshscale"})
class ProbeRequest(BaseModel):
target: str = Field(..., description="Target Space ID or URL", json_schema_extra={"example": "http://127.0.0.1:7861"})
path: str = Field(..., description="API endpoint path to query", json_schema_extra={"example": "/health"})
method: str = Field("GET", description="HTTP method to use", json_schema_extra={"example": "GET"})
query: dict[str, Any] | None = Field(None, description="Optional query parameters")
body: Any | None = Field(None, description="Optional request body to send as JSON")
class ReceiptRequest(BaseModel):
event_type: str = Field("operator_note", description="Event category / type", json_schema_extra={"example": "diagnostic_smoke"})
subject: str = Field("key_os", description="Event subject or target key", json_schema_extra={"example": "local_server"})
source: str = Field("operator", description="Creator or source identifier", json_schema_extra={"example": "codex_agent"})
payload: dict[str, Any] = Field(default_factory=dict, description="Custom payload data")
class NotepadAddRequest(BaseModel):
type: str = Field("observation", description="Note type (observation, hypothesis, action, outcome, question, guide, issue)", json_schema_extra={"example": "issue"})
title: str | None = Field(None, description="Title of the note (defaults to type if omitted)")
content: str = Field(..., description="The main text/content of the note", json_schema_extra={"example": "Local probes fail due to HF host checks."})
tags: list[str] = Field(default_factory=list, description="Tags to organize the notes")
links: list[str] = Field(default_factory=list, description="Associated links or file paths")
confidence: float | None = Field(None, description="Confidence level between 0 and 1")
source: str = Field("operator", description="Source indicating who added the note", json_schema_extra={"example": "browser"})
class NotepadClearRequest(BaseModel):
confirm: str = Field(..., description="Set to 'CONFIRM' to authorize deletion", json_schema_extra={"example": "CONFIRM"})
class FactoryResetRequest(BaseModel):
confirm: str = Field(..., description="Set to 'CONFIRM' to authorize reset", json_schema_extra={"example": "CONFIRM"})
clear_receipts: bool = Field(True, description="Whether to purge all receipts")
clear_notepad: bool = Field(False, description="Whether to purge the notepad")
clear_dispatch: bool = Field(True, description="Whether to clear dispatch state")
class SignalAckRequest(BaseModel):
state: str = Field(..., description="New acknowledgement state (new, seen, deferred, handled, blocked, superseded)", json_schema_extra={"example": "handled"})
note: str | None = Field(None, description="Optional comment regarding the status change", json_schema_extra={"example": "fixed in 0.1.3"})
class ContinuityCheckpointRequest(BaseModel):
subject: str = Field("codex_continuity", description="Optional subject annotation")
note: str = Field("Codex continuity checked.", description="Optional note text")
source: str = Field("operator", description="Optional source label")
class DispatchStartRequest(BaseModel):
seed: str = Field("status", description="The initial dispatch seed")
allow_mutating: bool = Field(True, description="Whether to permit mutating routes")
allow_external: bool = Field(True, description="Whether to permit external HTTP routes")
allow_destructive: bool = Field(True, description="Whether to permit destructive routes")
max_steps: int = Field(60, description="Max execution steps allowed")
step_delay: float = Field(0.45, description="Delay between execution steps in seconds")
ROOT = Path(__file__).parent
STATIC = ROOT / "static"
SERVICE_KEY_RE = re.compile(r"^[a-z0-9][a-z0-9_.-]{0,80}$")
RESERVED_SERVICE_KEYS = frozenset(facility.key for facility in kernel_facilities())
def _bool_env(name: str, default: bool = False) -> bool:
value = os.environ.get(name)
if value is None:
return default
return value.strip().lower() in {"1", "true", "yes", "on"}
def _store() -> KeyOSStore:
return KeyOSStore(os.environ.get("KEY_OS_ROOT", "/data/key_os"))
def _links() -> dict[str, str]:
links = default_links()
if os.environ.get("KEY_OS_HOME_URL"):
links["home"] = os.environ["KEY_OS_HOME_URL"]
if os.environ.get("KEY_OS_BUCKET_URL"):
links["bucket"] = os.environ["KEY_OS_BUCKET_URL"]
if os.environ.get("KEY_OS_DOCS_URL"):
links["docs"] = os.environ["KEY_OS_DOCS_URL"]
return links
def _runtime_flags() -> dict[str, Any]:
return {
"operator_auth_required": False,
"operator_auth_configured": False,
"unauth_mutation_allowed": True,
}
def _boot_diagnostics() -> None:
"""Log a clear summary of storage config at startup."""
root = os.environ.get("KEY_OS_ROOT", "/data/key_os")
log.info("──────────────────────────────────────────────")
log.info(" KEY OS boot diagnostics")
log.info("──────────────────────────────────────────────")
log.info(
" ROOT : %s (writable=%s)",
root,
os.access(root, os.W_OK) if os.path.isdir(root) else "dir-missing",
)
log.info(" AUTH : Wholesale Removed (Open Mutability)")
log.info("──────────────────────────────────────────────")
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Startup/shutdown lifecycle — runs after env is fully available."""
logging.basicConfig(
level=logging.INFO,
format="%(levelname)s: %(name)s - %(message)s",
stream=sys.stderr,
)
_boot_diagnostics()
yield
app = FastAPI(
title=os.environ.get("KEY_OS_NAME", "KEY OS"),
lifespan=lifespan,
)
app.mount("/static", StaticFiles(directory=str(STATIC)), name="static")
@app.get("/")
async def index() -> FileResponse:
return FileResponse(STATIC / "index.html")
@app.get("/health")
async def health() -> dict[str, Any]:
return {"ok": True, "surface": "key_os", "time": utc_now()}
@app.get("/api/status")
async def status(detail: str = "compact") -> dict[str, Any]:
return _store().status(_links(), runtime_flags=_runtime_flags(), detail=detail)
@app.get("/api/os")
async def resource_os() -> dict[str, Any]:
return _store().resource_snapshot(_links(), runtime_flags=_runtime_flags())
@app.get("/api/os/search")
async def resource_search(q: str = "", limit: int = 40) -> dict[str, Any]:
return _store().resource_search(q, limit=limit, links=_links(), runtime_flags=_runtime_flags())
@app.get("/api/os/oculus")
async def oculus() -> dict[str, Any]:
return _store().status(_links(), runtime_flags=_runtime_flags()).get("oculus", {})
@app.get("/api/continuity")
async def continuity() -> dict[str, Any]:
return _store().continuity()
@app.get("/api/signals")
async def signals() -> dict[str, Any]:
return _store().signals()
@app.get("/api/signals/{signal_id}")
async def signal_detail(signal_id: str) -> dict[str, Any]:
signal = _store().get_signal(signal_id)
if signal is None:
raise HTTPException(status_code=404, detail="signal not found")
return signal
@app.post("/api/signals/{signal_id}/ack")
async def signal_ack(signal_id: str, payload: SignalAckRequest) -> dict[str, Any]:
try:
return _store().acknowledge_signal(signal_id, payload.model_dump())
except KeyError as exc:
raise HTTPException(status_code=404, detail="signal not found") from exc
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.post("/api/continuity/checkpoint")
async def continuity_checkpoint(payload: ContinuityCheckpointRequest) -> dict[str, Any]:
return _store().acknowledge_continuity(payload.model_dump())
@app.get("/api/services")
async def services() -> dict[str, Any]:
custom_count = len(_store().load_services(include_kernel=False))
items = [service.to_dict(redact_payload=True) for service in _store().load_services()]
groups: dict[str, list[dict[str, Any]]] = {}
for item in items:
groups.setdefault(str(item.get("group") or "custom"), []).append(item)
return {"count": len(items), "custom_count": custom_count, "groups": groups, "services": items}
@app.get("/api/facilities")
async def facilities() -> dict[str, Any]:
return _store().registry().manifest()
@app.get("/api/facilities/graph")
async def facility_graph() -> dict[str, Any]:
return _store().registry().graph()
@app.get("/api/facility/{key}")
async def facility_detail(key: str) -> dict[str, Any]:
return _store().registry().node_detail(key)
@app.post("/api/services")
async def register_service(request: Request) -> dict[str, Any]:
payload = await request.json()
key = str(payload.get("key") or "").strip().lower()
label = str(payload.get("label") or key).strip()
group = str(payload.get("group") or "custom").strip().lower()
route = str(payload.get("route") or "").strip()
if not key:
raise HTTPException(status_code=400, detail="service key is required")
if not SERVICE_KEY_RE.match(key):
raise HTTPException(status_code=400, detail="service key must match ^[a-z0-9][a-z0-9_.-]{0,80}$")
if key in RESERVED_SERVICE_KEYS:
raise HTTPException(status_code=409, detail=f"service key '{key}' is reserved by the kernel")
if group == "kernel":
raise HTTPException(status_code=400, detail="custom services may not use reserved group 'kernel'")
if not route:
raise HTTPException(status_code=400, detail="service route is required")
service = ServiceSpec(
key=key,
label=label,
group=group or "custom",
description=str(payload.get("description") or ""),
route=route,
method=str(payload.get("method") or "GET").upper(),
risk_class=str(payload.get("risk_class") or "inspect"),
tags=tuple(payload.get("tags") or ()),
input_schema=payload.get("input_schema") or {},
output_schema=payload.get("output_schema") or {},
payload=payload.get("payload") or {},
)
return {"ok": True, "service": _store().put_service(service).to_dict(redact_payload=True)}
@app.post("/api/route")
async def route(request: Request) -> dict[str, Any]:
payload = await request.json()
query = str(payload.get("query") or "")
report = _store().status(_links(), runtime_flags=_runtime_flags())
return {"query": query, "route": route_query(query, report, _links()), "summary": report.get("kernel", {})}
def _is_self_target(url: str, request: Request) -> bool:
try:
parsed_target = urllib.parse.urlparse(url)
target_host = parsed_target.hostname or ""
target_port = parsed_target.port
parsed_self = urllib.parse.urlparse(str(request.base_url))
self_host = parsed_self.hostname or ""
self_port = parsed_self.port or 80
# Normalize localhost/127.0.0.1/::1/127.*
is_target_local = target_host.lower() in {"localhost", "127.0.0.1", "::1"} or target_host.startswith("127.")
is_self_local = self_host.lower() in {"localhost", "127.0.0.1", "::1"} or self_host.startswith("127.")
if is_target_local and is_self_local:
return (target_port or 80) == self_port
return target_host.lower() == self_host.lower() and (target_port or 80) == self_port
except Exception:
return False
@app.post("/api/connect")
async def connect_space(payload: ConnectRequest, request: Request) -> dict[str, Any]:
"""Surgeon's tools: quantify a parallel Space's entire API array. No token —
this is an observation surface, scoped to the HF constellation in probe.py."""
target = payload.target.strip()
if not target:
raise HTTPException(status_code=400, detail="target space url or id is required")
try:
from key_os.probe import normalize_target
normalized_target = normalize_target(target)
except ProbeError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if _is_self_target(normalized_target, request):
import httpx
try:
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
t0 = time.perf_counter()
health_resp = await client.get("/health")
elapsed = round((time.perf_counter() - t0) * 1000, 1)
spec_resp = await client.get("/openapi.json")
spec = spec_resp.json() if spec_resp.status_code == 200 else None
if spec and "paths" in spec:
from key_os.probe import _parse_openapi
endpoints = _parse_openapi(spec)
info = spec.get("info") or {}
result = {
"target": target,
"base_url": normalized_target,
"fetched_at": utc_now(),
"health": {
"reachable": True,
"probe_path": "/health",
"status": health_resp.status_code,
"stage": "ok" if health_resp.status_code == 200 else "error",
"elapsed_ms": elapsed,
},
"status": "connected",
"title": info.get("title"),
"version": info.get("version"),
"openapi": spec.get("openapi"),
"count": len(endpoints),
"endpoints": endpoints,
}
else:
result = {
"target": target,
"base_url": normalized_target,
"fetched_at": utc_now(),
"health": {
"reachable": True,
"probe_path": "/health",
"status": health_resp.status_code,
"stage": "ok" if health_resp.status_code == 200 else "error",
"elapsed_ms": elapsed,
},
"status": "no_openapi",
"raw_status": spec_resp.status_code,
"message": f"No OpenAPI spec at {normalized_target}/openapi.json",
"endpoints": [],
}
except Exception as exc:
result = {
"target": target,
"base_url": normalized_target,
"fetched_at": utc_now(),
"health": {"reachable": False, "stage": "unreachable", "status": 0},
"status": "unreachable",
"message": f"Self-connection error: {exc}",
"endpoints": [],
}
else:
try:
result = enumerate_space(target)
except ProbeError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
try:
_store().append_event_receipt(
"space_connect",
str(result.get("base_url") or target),
{"status": result.get("status"), "count": result.get("count")},
source="probe",
)
except Exception: # provenance is best-effort; never block the observation
pass
return result
@app.post("/api/probe")
async def probe_space(payload: ProbeRequest, request: Request) -> dict[str, Any]:
"""Fire a single request at a connected Space and return the raw result."""
target = payload.target.strip()
path = payload.path.strip()
method = payload.method.upper()
if not target or not path:
raise HTTPException(status_code=400, detail="target and path are required")
try:
from key_os.probe import normalize_target
normalized_target = normalize_target(target)
except ProbeError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if _is_self_target(normalized_target, request):
import httpx
if not path.startswith("/"):
path = "/" + path
try:
transport = httpx.ASGITransport(app=app)
async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
t0 = time.perf_counter()
resp = await client.request(
method=method,
url=path,
params=payload.query,
json=payload.body,
timeout=10.0,
)
elapsed = round((time.perf_counter() - t0) * 1000, 1)
try:
body_json = resp.json()
except Exception:
body_json = None
result = {
"ok": 200 <= resp.status_code < 400,
"status": resp.status_code,
"method": method,
"url": normalized_target + path,
"elapsed_ms": elapsed,
"content_type": resp.headers.get("content-type", ""),
"json": body_json,
"text": None if body_json is not None else resp.text[:20000],
"truncated": len(resp.text) > 20000,
}
except Exception as exc:
result = {
"ok": False,
"status": 0,
"method": method,
"url": normalized_target + path,
"error": f"{type(exc).__name__}: {exc}",
"elapsed_ms": 0.0,
}
else:
try:
result = call_endpoint(
target,
path,
method=method,
query=payload.query,
body=payload.body,
)
except ProbeError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
try:
_store().append_event_receipt(
"space_probe",
f"{method} {path}",
{"target": target, "status": result.get("status"), "elapsed_ms": result.get("elapsed_ms")},
source="probe",
)
except Exception:
pass
return result
@app.get("/api/receipts")
async def receipts(limit: int = 50) -> dict[str, Any]:
items = _store().load_receipts(limit=limit)
return {"count": len(items), "receipts": items}
@app.post("/api/receipts")
async def write_receipt(payload: ReceiptRequest) -> dict[str, Any]:
receipt = Receipt(
event_type=payload.event_type,
subject=payload.subject,
source=payload.source,
payload=redact(payload.payload),
)
_store().append_receipt(receipt)
return {"ok": True, "receipt": receipt.to_dict()}
@app.get("/api/notepad")
async def notepad(query: str = "", limit: int = 80, type: str = "") -> dict[str, Any]:
items = _store().notepad().list(query=query, limit=limit, note_type=type)
return {"schema": "key_os.research_notepad/v1", "count": len(items), "entries": items}
@app.get("/api/notepad/summary")
async def notepad_summary() -> dict[str, Any]:
return _store().notepad().summary()
@app.post("/api/notepad")
async def notepad_add(payload: NotepadAddRequest) -> dict[str, Any]:
entry = _store().notepad().add(payload.model_dump(), source=payload.source)
receipt = _store().append_event_receipt("notepad_add", str(entry.get("id")), entry, source="notepad")
return {"ok": True, "entry": entry, "receipt": receipt.to_dict()}
@app.post("/api/notepad/clear")
async def notepad_clear(payload: NotepadClearRequest) -> dict[str, Any]:
try:
require_confirmation("notepad_clear", payload.model_dump())
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
result = _store().notepad().clear()
receipt = _store().append_event_receipt("notepad_clear", "research_notepad", result, source="notepad")
return {**result, "receipt": receipt.to_dict()}
@app.get("/api/dispatch/state")
async def dispatch_state() -> dict[str, Any]:
store = _store()
state = store.dispatch().read()
state["plan"] = store.dispatch().plan(store.registry(), policy=state.get("policy") or {})
return state
@app.post("/api/dispatch/start")
async def dispatch_start(payload: DispatchStartRequest) -> dict[str, Any]:
store = _store()
state = store.dispatch().start(payload.model_dump(), store.registry())
receipt = store.append_event_receipt("dispatch_start", str(payload.seed), payload.model_dump(), source="dispatch")
return {"ok": True, "state": state, "receipt": receipt.to_dict()}
@app.post("/api/dispatch/stop")
async def dispatch_stop(request: Request) -> dict[str, Any]:
store = _store()
state = store.dispatch().stop()
receipt = store.append_event_receipt("dispatch_stop", "dispatcher", {}, source="dispatch")
return {"ok": True, "state": state, "receipt": receipt.to_dict()}
@app.get("/api/security/contracts")
async def security_contracts() -> dict[str, Any]:
return contracts_report()
@app.post("/api/factory-reset")
async def factory_reset(payload: FactoryResetRequest) -> dict[str, Any]:
try:
require_confirmation("factory_reset", payload.model_dump())
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
clear_receipts = payload.clear_receipts
clear_notepad = payload.clear_notepad
clear_dispatch = payload.clear_dispatch
return _store().factory_reset(clear_receipts=clear_receipts, clear_notepad=clear_notepad, clear_dispatch=clear_dispatch)
@app.post("/api/reset")
async def legacy_factory_reset(payload: FactoryResetRequest) -> dict[str, Any]:
return await factory_reset(payload)