mirror of
https://github.com/0xWheatyz/handler.git
synced 2026-08-30 04:26:24 +00:00
4f05d09c2b
Make credentials/hosts, projects, agents, and approvals manageable from the dashboard. The API and control layer are separate containers, so the API can't run control actions directly (no git/tmux/claude, doesn't own the tmux sessions). Instead the API enqueues a command and a worker in the control container executes it and writes the result back. Data model (migration 0003): - `commands` queue/audit table; `forge_hosts` registry; `approvals` gains a nullable approver id + `actor` so operator verdicts are first-class. Control worker: - `control/worker.py` claims commands and dispatches to the existing control functions (spawn/kill/resume/record_approval/write_skills/poller.sweep), plus a periodic CI sweep. New `handler worker` CLI subcommand; it becomes the control image's default command (subsumes `poll-ci --watch`). API: - `require_admin` gate + `ADMIN_TOKEN`; project GET/PATCH/DELETE; agent spawn/kill/delete; resume now enqueues (fixes a cross-container bug where the API tried to send tmux keys to a session in the control container); new approvals/commands/hosts routes; forge-init and poll-ci enqueue endpoints. Credentials/hosts: - host->token-env lookup consults the `forge_hosts` registry first (built-in map is the fallback); `resolve()` refactored to a scheme dispatch reserving `db:` for a future encrypted store. Web input restricts credential_ref to env:/file:/db: (cmd: stays CLI-only — it would run arbitrary commands). Dashboard: - New tabs for projects, agents (spawn/kill with live command-status polling), approvals, hosts, and an activity/audit view; shared context is now writable. Tests: +33 (queue atomicity, worker dispatch, CRUD, hosts, admin gating, cmd: rejection, host-aware credentials, and an API->queue->worker->spawn end-to-end). README gains a Web management section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CrhrBToauu4L2qG6jdnuFP
61 lines
2.3 KiB
Python
61 lines
2.3 KiB
Python
"""End-to-end web management: the dashboard's HTTP calls -> command queue -> worker ->
|
|
real ``spawn.spawn`` -> tmux seam. Proves the full container-split flow works with only the
|
|
tmux/claude boundary faked, not the control layer itself."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from handler.control import worker
|
|
from handler.db import repository as repo
|
|
from handler.db.engine import get_engine
|
|
|
|
|
|
def _spawnable_project(root):
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
(root / ".mise.toml").write_text("[tasks.test]\nrun = 'pytest'\n")
|
|
with get_engine().begin() as conn:
|
|
repo.create_project(conn, "proj", str(root))
|
|
|
|
|
|
def test_spawn_via_api_then_worker_creates_agent_and_session(client, auth, env, fake_tmux):
|
|
_spawnable_project(env["tmp"] / "proj")
|
|
|
|
# 1. The dashboard enqueues a spawn (202 + a queued command).
|
|
r = client.post(
|
|
"/projects/proj/agents/spawn",
|
|
json={"name": "api", "task": "build the thing"},
|
|
headers=auth,
|
|
)
|
|
assert r.status_code == 202
|
|
command_id = r.json()["id"]
|
|
assert r.json()["status"] == "queued"
|
|
|
|
# No agent yet — the worker hasn't run.
|
|
assert client.get("/projects/proj/agents", headers=auth).json() == []
|
|
|
|
# 2. The control worker drains the queue (runs the real spawn.spawn).
|
|
assert worker.drain("test-worker") == 1
|
|
|
|
# 3. The command is done and the agent + tmux session now exist.
|
|
got = client.get(f"/commands/{command_id}", headers=auth).json()
|
|
assert got["status"] == "done"
|
|
assert got["result"]["name"] == "api"
|
|
|
|
agents = client.get("/projects/proj/agents", headers=auth).json()
|
|
assert [a["name"] for a in agents] == ["api"]
|
|
assert fake_tmux["calls"]["new_session"][0]["name"] == "proj__api"
|
|
|
|
|
|
def test_kill_via_api_then_worker(client, auth, env, fake_tmux):
|
|
_spawnable_project(env["tmp"] / "proj")
|
|
client.post("/projects/proj/agents/spawn", json={"name": "api"}, headers=auth)
|
|
worker.drain("w")
|
|
|
|
r = client.post("/projects/proj/agents/api/kill", headers=auth)
|
|
assert r.status_code == 202
|
|
worker.drain("w")
|
|
|
|
assert client.get(f"/commands/{r.json()['id']}", headers=auth).json()["status"] == "done"
|
|
assert "proj__api" in fake_tmux["calls"]["kill_session"]
|
|
with get_engine().begin() as conn:
|
|
assert repo.get_agent_by_name(conn, "proj", "api")["status"] == "done"
|