Worker agent (decnet.agent): - mTLS FastAPI service exposing /deploy, /teardown, /status, /health, /mutate. uvicorn enforces CERT_REQUIRED with the DECNET CA pinned. - executor.py offloads the blocking deployer onto asyncio.to_thread so the event loop stays responsive. - server.py refuses to start without an enrolled bundle in ~/.decnet/agent/ — unauthenticated agents are not a supported mode. - docs/openapi disabled on the agent — narrow attack surface. tests/test_base_repo.py: DummyRepo was missing get_attacker_artifacts (pre-existing abstractmethod) and so could not be instantiated. Added the stub + coverage for the new swarm CRUD surface on BaseRepository.
46 lines
1.6 KiB
Python
46 lines
1.6 KiB
Python
"""Thin adapter between the agent's HTTP endpoints and the existing
|
|
``decnet.engine.deployer`` code path.
|
|
|
|
Kept deliberately small: the agent does not re-implement deployment logic,
|
|
it only translates a master RPC into the same function calls the unihost
|
|
CLI already uses. Everything runs in a worker thread (the deployer is
|
|
blocking) so the FastAPI event loop stays responsive.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from typing import Any
|
|
|
|
from decnet.engine import deployer as _deployer
|
|
from decnet.config import DecnetConfig, load_state, clear_state
|
|
from decnet.logging import get_logger
|
|
|
|
log = get_logger("agent.executor")
|
|
|
|
|
|
async def deploy(config: DecnetConfig, dry_run: bool = False, no_cache: bool = False) -> None:
|
|
"""Run the blocking deployer off-loop. The deployer itself calls
|
|
save_state() internally once the compose file is materialised."""
|
|
log.info("agent.deploy name=%s deckies=%d", config.name, len(config.deckies))
|
|
await asyncio.to_thread(_deployer.deploy, config, dry_run, no_cache, False)
|
|
|
|
|
|
async def teardown(decky_id: str | None = None) -> None:
|
|
log.info("agent.teardown decky_id=%s", decky_id)
|
|
await asyncio.to_thread(_deployer.teardown, decky_id)
|
|
if decky_id is None:
|
|
await asyncio.to_thread(clear_state)
|
|
|
|
|
|
async def status() -> dict[str, Any]:
|
|
state = await asyncio.to_thread(load_state)
|
|
if state is None:
|
|
return {"deployed": False, "deckies": []}
|
|
config, _compose_path = state
|
|
return {
|
|
"deployed": True,
|
|
"name": getattr(config, "name", None),
|
|
"compose_path": str(_compose_path),
|
|
"deckies": [d.model_dump() for d in config.deckies],
|
|
}
|