Files
wild-pc/docs/developing-castle.md
Paul Payne ef588cb806 gateway: converge, don't start/stop/reload
The gateway is a deployment (castle-gateway) — its lifecycle is the same
convergence as everything else, so the bespoke start/stop/reload verbs
(which still routed through the retired enable/disable path) are gone.

- CLI: `castle gateway` is now inspection only — bare or `status` shows
  the route table. Start/stop/reload it via `castle apply` (render routes
  + reload) or `castle restart castle-gateway`. Deletes the last users of
  `_service_enable`/`_service_disable`, so those are removed too.
- API: retire `POST /gateway/reload` (making routes live is `POST /apply`);
  `PUT /gateway/config` message + docstring now say apply, not deploy.
- UI: GatewayPanel's "Reload" button → "Apply" (useApply); drop the
  useGatewayReload hook; GatewaySettings points at `castle apply`.
- docs: `castle gateway reload|status` → `castle gateway` (status lens).

core 129 / cli 31 / api 59 pass; dashboard builds clean; live `castle
gateway` shows routes with no start/stop/reload; /gateway/reload removed
from the API.
2026-07-02 12:12:00 -07:00

3.3 KiB

Developing Castle

How to work on Castle's own code — the CLI, core library, control-plane API, and dashboard. For using Castle to manage software (create/deploy/expose programs), see the operator guide in AGENTS.md.

The Castle monorepo

Castle's own programs live in this git repo (source: repo:<name>) — distinct from the programs you manage, which live under /data/repos/<name>/:

  • cli/ — the castle CLI (installed via uv tool install --editable cli/)
  • core/castle_core: manifest models, config loader, generators
  • castle-api/ — the FastAPI control-plane service (port 9020)
  • app/ — the dashboard frontend (React/Vite; program name castle, served at castle.<gateway.domain>)

The root pyproject.toml is the uv workspace (core, cli, castle-api).

Key files

  • core/src/castle_core/manifest.py — Pydantic models: ProgramSpec, DeploymentSpec (discriminated union on manager), LaunchSpec, AgentSpec, …
  • core/src/castle_core/config.py — config loader (castle.yaml + programs/ + deployments/CastleConfig), the two roots, source: resolution
  • core/src/castle_core/generators/ — systemd unit/timer + Caddyfile generation
  • cli/src/castle_cli/ — resource-first CLI commands; templates/scaffold.py
  • castle-api/src/castle_api/ — routes, health polling, SSE stream.py, mesh, and the agent terminal UX (agents.py, pty_session.py, agent_sessions.py, agent_registry.py)
  • ruff.toml / pyrightconfig.json — shared lint/type config

Commands (all projects use uv)

uv sync
uv run pytest tests/ -v
uv run ruff check . && uv run ruff format .
uv run pyright <paths>

Running uv sync from a member subdir trims the shared workspace venv — resync from the repo root to restore all members. Code style: ruff (100-char lines), pyright, pytest + pytest-asyncio; Python 3.13 for services, 3.11+ for tools/libraries.

castle-api endpoints (port 9020)

  • Core: GET /health, GET /stream (SSE: health, service-action, mesh)
  • Deployments: GET /deployments[/{name}], GET /status
  • Catalog + editing: GET /programs[/{name}], POST /programs/{name}/{action}, PUT|DELETE /programs/{name}; likewise /services, /jobs
  • Config: GET|PUT /, POST /apply, POST /deploy
  • Gateway: GET /gateway, GET /gateway/caddyfile (reload is convergence: POST /apply)
  • Mesh: GET /mesh/status, GET /nodes[/{hostname}]
  • Agents (dashboard terminal UX): GET /agents, GET /agents/sessions, GET /agents/history, DELETE /agents/sessions/{id}, WS /agents/{name}/session
  • Service actions: POST /services/{name}/{action}, GET /services/{name}/unit

Infrastructure internals

  • Generated Caddyfile at ~/.castle/artifacts/specs/Caddyfile; a plugin Caddy at /usr/local/bin/caddy when tls: acme.
  • Systemd user units at ~/.config/systemd/user/castle-*.service (+ .timer); the unit for program X is castle-X.service. Use drop-in *.service.d/*.conf for extra env castle apply shouldn't overwrite.
  • The container launcher resolves docker via shutil.which("docker") (preferred over rootless podman on this box).
  • Service data at $CASTLE_DATA_DIR/<name>/; secrets at ~/.castle/secrets/ (mode 700) — never in project directories.