The apply/converge + gateway refactors let the docs collapse a lot:
- design.md §Component Lifecycle: the build→install→deploy three-step
("enables and starts services") becomes two moves — develop, then
`castle apply` (activation polymorphic over the manager). Filesystem
layout de-duped and corrected (program source is /data/repos/<name>,
not the long-removed ~/.castle/code).
- registry.md §Lifecycle: three near-identical per-kind flows (service/
tool/job) collapse into one flow + a "what apply does per manager"
table. Manager table's "start/stop" column → "how apply activates".
Fixed stale paths-table rows (code/<name>) and a stale `service run`.
- developing-castle.md endpoint list: `POST /apply` is the one lifecycle
endpoint; dropped retired `/deploy`; service actions are restart-only;
added `/config/deployments/{name}/enabled`.
Net −37 lines; no retired-verb or stale-path references remain in docs.
3.4 KiB
3.4 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/— thecastleCLI (installed viauv tool install --editable cli/)core/—castle_core: manifest models, config loader, generatorscastle-api/— the FastAPI control-plane service (port 9020)app/— the dashboard frontend (React/Vite; program namecastle, served atcastle.<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 onmanager),LaunchSpec,AgentSpec, …core/src/castle_core/config.py— config loader (castle.yaml+programs/+deployments/→CastleConfig), the two roots,source:resolutioncore/src/castle_core/generators/— systemd unit/timer + Caddyfile generationcli/src/castle_cli/— resource-first CLI commands;templates/scaffold.pycastle-api/src/castle_api/— routes, health polling, SSEstream.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) - Converge:
POST /apply({name?, plan?}) — the one lifecycle endpoint - Deployments:
GET /deployments[/{name}],GET /status - Catalog + editing:
GET /programs[/{name}],POST /programs/{name}/{action}(dev verbs only),PUT|DELETE /programs/{name}; likewise/services,/jobs - Config:
GET|PUT /,POST /config/apply,PUT /config/deployments/{name}/enabled - Gateway:
GET /gateway,GET /gateway/caddyfile,PUT /gateway/config(making routes live 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}/restart,GET /services/{name}/unit
Infrastructure internals
- Generated Caddyfile at
~/.castle/artifacts/specs/Caddyfile; a plugin Caddy at/usr/local/bin/caddywhentls: acme. - Systemd user units at
~/.config/systemd/user/castle-*.service(+.timer); the unit for programXiscastle-X.service. Use drop-in*.service.d/*.conffor extra envcastle applyshouldn't overwrite. - The
containerlauncher resolves docker viashutil.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.