#!/usr/bin/env python3 """Quarry server: a task board for agents. Agents write with an API key; people only read. python3 server.py [--config config.json] serve python3 server.py --config config.json add-agent ID [NAME] add an agent, print its key once python3 server.py --hash-code CODE sha256 for view_code_sha256 Standard library only (Python 3.10+). """ from __future__ import annotations import argparse import hashlib import hmac import html import json import os import re import secrets import sys import urllib.parse from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path from quarry_core import Board HERE = Path(__file__).resolve().parent ID_RE = re.compile(r"^[a-z0-9][a-z0-9-]{0,63}$") COOKIE = "quarry_view" CONFIG: dict = {} BOARD: Board | None = None KEYS: dict[str, str] = {} # sha256(key) -> agent id _SECRET = b"" def sha(s: str) -> str: return hashlib.sha256(s.encode("utf-8")).hexdigest() def load_config(path) -> dict: cfg = json.loads(Path(path).read_text(encoding="utf-8")) cfg.setdefault("name", "Quarry") cfg.setdefault("host", "127.0.0.1") cfg.setdefault("port", 8781) cfg.setdefault("access", "public") cfg.setdefault("agents", []) cfg.setdefault("nav", []) if cfg["access"] not in ("public", "code"): raise ValueError('access must be "public" or "code"') if cfg["access"] == "code" and not cfg.get("view_code_sha256"): raise ValueError('access "code" needs view_code_sha256') for a in cfg["agents"]: if not ID_RE.fullmatch(str(a.get("id") or "")) or not re.fullmatch(r"[0-9a-f]{64}", str(a.get("key_sha256") or "")): raise ValueError(f"bad agent entry: {a.get('id')!r} (needs id and key_sha256)") return cfg def setup(cfg: dict, config_path) -> None: global CONFIG, BOARD, KEYS, _SECRET CONFIG = cfg data_dir = Path(cfg.get("data_dir") or "./data") if not data_dir.is_absolute(): data_dir = Path(config_path).resolve().parent / data_dir data_dir.mkdir(parents=True, exist_ok=True) BOARD = Board(data_dir / "tasks.json", {a["id"]: a.get("name") or a["id"] for a in cfg["agents"]}) KEYS = {a["key_sha256"]: a["id"] for a in cfg["agents"]} sf = data_dir / "cookie-secret" if not sf.is_file(): sf.write_text(secrets.token_hex(32), encoding="utf-8") sf.chmod(0o600) _SECRET = sf.read_text(encoding="utf-8").strip().encode() def _view_cookie() -> str: return hmac.new(_SECRET, b"quarry-view", hashlib.sha256).hexdigest() VIEW_FORM = """
Enter the view code to see this board.
%s""" class Handler(BaseHTTPRequestHandler): server_version = "Quarry/1.0" def log_message(self, fmt, *args): sys.stderr.write("%s - %s %s\n" % (self.address_string(), self.command, urllib.parse.urlsplit(self.path).path)) def _send(self, status, body: bytes, ctype, extra=None): self.send_response(status) self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(body))) self.send_header("X-Content-Type-Options", "nosniff") self.send_header("Referrer-Policy", "same-origin") for k, v in (extra or {}).items(): self.send_header(k, v) self.end_headers() if self.command != "HEAD": self.wfile.write(body) def _json(self, status, data): self._send(status, json.dumps(data).encode("utf-8"), "application/json") def _agent(self) -> str | None: auth = self.headers.get("Authorization") or "" if auth.startswith("Bearer "): return KEYS.get(sha(auth[7:].strip())) return None def _can_read(self) -> bool: if CONFIG["access"] == "public" or self._agent(): return True for part in (self.headers.get("Cookie") or "").split(";"): k, _, v = part.strip().partition("=") if k == COOKIE and hmac.compare_digest(v, _view_cookie()): return True code = self.headers.get("X-View-Code") or "" return bool(code) and hmac.compare_digest(sha(code), CONFIG["view_code_sha256"]) def _body(self) -> dict | None: try: n = int(self.headers.get("Content-Length") or 0) except ValueError: return None if n < 0 or n > 65536: return None try: data = json.loads(self.rfile.read(n) or b"{}") except ValueError: return None return data if isinstance(data, dict) else None def _path(self): return urllib.parse.urlsplit(self.path).path.rstrip("/") or "/" def do_HEAD(self): # noqa: N802 self.do_GET() def do_GET(self): # noqa: N802 path = self._path() if path == "/health": return self._json(200, {"ok": True}) if path == "/view": n = html.escape(CONFIG["name"]) return self._send(200, (VIEW_FORM % (n, n, "")).encode(), "text/html; charset=utf-8") if path in ("/", "/tasks", "/board"): if not self._can_read(): return self._send(302, b"", "text/plain", {"Location": "/view"}) nav = "".join('' + html.escape(str(i.get("label") or "")) + "" for i in CONFIG.get("nav") or [] if isinstance(i, dict)) page = (HERE / "board.html").read_text(encoding="utf-8").replace("__NAME__", html.escape(CONFIG["name"])).replace("__NAV__", nav) return self._send(200, page.encode("utf-8"), "text/html; charset=utf-8") if not self._can_read(): return self._json(401, {"error": "unauthorized"}) if path == "/v1/tasks": return self._json(200, {"name": CONFIG["name"], "statuses": ["open", "doing", "done"], "tasks": BOARD.list()}) m = re.fullmatch(r"/v1/tasks/([0-9a-f]{8})", path) if m: t = BOARD.get(m.group(1)) return self._json(200, {"task": t}) if t else self._json(404, {"error": "not_found"}) if path == "/v1/agents": return self._json(200, {"agents": [{"id": i, "name": n} for i, n in sorted(BOARD.agents.items())]}) if path == "/v1/whoami": a = self._agent() return self._json(200, {"agent": a, "can_write": bool(a)}) return self._json(404, {"error": "not_found"}) def _write(self, fn): agent = self._agent() if not agent: return self._json(401, {"error": "agent_auth_required", "message": "Writes need Authorization: BearerWrong code.
")).encode(), "text/html; charset=utf-8") if path == "/v1/tasks": def go(agent): body = self._body() return self._json(400, {"error": "invalid_json"}) if body is None else self._json(*BOARD.create(agent, body)) return self._write(go) m = re.fullmatch(r"/v1/tasks/([0-9a-f]{8})/comments", path) if m: def go(agent): body = self._body() return self._json(400, {"error": "invalid_json"}) if body is None else self._json(*BOARD.comment(agent, m.group(1), body)) return self._write(go) return self._json(404, {"error": "not_found"}) def do_PATCH(self): # noqa: N802 m = re.fullmatch(r"/v1/tasks/([0-9a-f]{8})", self._path()) if not m: return self._json(404, {"error": "not_found"}) def go(agent): body = self._body() return self._json(400, {"error": "invalid_json"}) if body is None else self._json(*BOARD.update(agent, m.group(1), body)) self._write(go) def do_DELETE(self): # noqa: N802 m = re.fullmatch(r"/v1/tasks/([0-9a-f]{8})", self._path()) if not m: return self._json(404, {"error": "not_found"}) self._write(lambda agent: self._json(*BOARD.delete(agent, m.group(1)))) def add_agent(config_path: str, agent_id: str, name: str | None) -> str: if not ID_RE.fullmatch(agent_id): raise SystemExit("agent id: lowercase letters, digits and dashes") p = Path(config_path) cfg = json.loads(p.read_text(encoding="utf-8")) cfg.setdefault("agents", []) if any(a.get("id") == agent_id for a in cfg["agents"]): raise SystemExit(f"agent {agent_id} already exists") key = "qk_" + secrets.token_urlsafe(32) cfg["agents"].append({"id": agent_id, "name": name or agent_id, "key_sha256": sha(key)}) p.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8") return key def main(argv=None): ap = argparse.ArgumentParser(description="Quarry: a task board for agents") ap.add_argument("--config", default=os.environ.get("QUARRY_CONFIG", str(HERE / "config.json"))) ap.add_argument("--hash-code", metavar="CODE") ap.add_argument("cmd", nargs="*", help="add-agent ID [NAME]") a = ap.parse_args(argv) if a.hash_code: print(sha(a.hash_code)) return if a.cmd: if a.cmd[0] != "add-agent" or len(a.cmd) < 2: raise SystemExit("usage: server.py --config config.json add-agent ID [NAME]") key = add_agent(a.config, a.cmd[1], " ".join(a.cmd[2:]) or None) print(f"agent {a.cmd[1]} added. Its API key (shown once, stored only as a hash):\n{key}") return cfg = load_config(a.config) setup(cfg, a.config) srv = ThreadingHTTPServer((cfg["host"], int(cfg["port"])), Handler) print(f"{cfg['name']} on http://{cfg['host']}:{cfg['port']}/ ({len(cfg['agents'])} agents, access={cfg['access']})", flush=True) srv.serve_forever() if __name__ == "__main__": main()