# Component Registry How castle tracks, configures, and manages components. This is the central reference for `castle.yaml` structure and the manifest architecture. ## castle.yaml The single source of truth for all components. Lives at the repo root. ```yaml gateway: port: 9000 components: my-service: description: Does something useful run: runner: python tool: my-service expose: http: internal: { port: 9001 } health_path: /health proxy: caddy: { path_prefix: /my-service } manage: systemd: {} ``` ## Manifest blocks Each component declares **what it does** through these optional blocks: ### `run` — How to start it Discriminated union on `runner`. The runner encodes both the language/toolchain (used by `castle sync`) and the deployment resolution (used by `castle deploy`): | Runner | Sync | Deploy | Key fields | |--------|------|--------|------------| | `python` | `uv sync` | `which(tool)` → installed binary | `tool`, `args` | | `command` | *(none)* | `which(argv[0])` → resolved path | `argv` | | `container` | *(none)* | `podman run` | `image`, `command`, `ports`, `volumes` | | `node` | `package_manager install` | `package_manager run script` | `script`, `package_manager` | | `remote` | *(none)* | *(none — no local process)* | `base_url`, `health_url` | **Services** use `python`: ```yaml run: runner: python tool: my-service # name in [project.scripts] ``` **Tools invoked by castle** (jobs, scheduled tasks) use `command`: ```yaml run: runner: command argv: ["protonmail", "sync"] cwd: protonmail env: PROTONMAIL_USERNAME: user@example.com ``` **Standalone tools** that users invoke directly often have no `run` block at all — castle just installs them to PATH. ### `expose` — What it exposes ```yaml expose: http: internal: port: 9001 # Required for services health_path: /health # Used by health polling ``` Having `expose.http` gives the component the **service** role. ### `proxy` — How to proxy it ```yaml proxy: caddy: path_prefix: /my-service # Proxied at gateway:9000/my-service/ ``` Castle generates a Caddyfile from these entries. Only needed for services accessible through the gateway. ### `manage` — How to manage it ```yaml manage: systemd: {} ``` Enables `castle service enable/disable` and `castle logs`. An empty `{}` uses defaults (enable=true, restart=on-failure, restart_sec=5). Full options: ```yaml manage: systemd: description: Custom unit description restart: always # on-failure | always | no restart_sec: 2 no_new_privileges: true after: [network.target, castle-other.service] wanted_by: [default.target] ``` ### `install` — How to install it ```yaml install: path: alias: my-tool # Command name in PATH ``` Creates a shim so the tool is available system-wide after `uv tool install --editable .`. ### `tool` — Tool metadata ```yaml tool: source: components/my-tool/ # Source directory version: "1.0.0" system_dependencies: [pandoc, poppler-utils] ``` This block provides metadata for `castle tool list` and the dashboard. It's separate from `install` (which handles PATH registration) and `run` (which handles execution). The install method (uv tool install vs symlink) is inferred from the source directory: if `pyproject.toml` exists, it's a Python package; if the source is a file, it's symlinked. ### `build` — How to build it ```yaml build: commands: - ["pnpm", "build"] outputs: - dist/ ``` Having build outputs gives the component the **frontend** role. ### `triggers` — What triggers it ```yaml triggers: - type: schedule cron: "*/5 * * * *" timezone: America/Los_Angeles # default ``` Having a schedule trigger gives the component the **job** role. Castle generates a systemd .timer file alongside the .service unit. Other trigger types: `manual`, `event` (source + topic), `request` (protocol). ### `env` with secrets Environment variables can reference secrets stored in `~/.castle/secrets/`: ```yaml run: env: API_KEY: ${secret:MY_API_KEY} ``` Castle resolves `${secret:NAME}` by reading `~/.castle/secrets/NAME`. Never store secrets in castle.yaml or project directories. ## Role derivation Roles are **computed** from manifest declarations, never set manually: | Role | Derived when | |------|-------------| | **service** | Has `expose.http` | | **tool** | Has `install.path` or has `tool` spec (fallback) | | **worker** | Has `manage.systemd` but no `expose.http` | | **job** | Has trigger with `type: schedule` | | **frontend** | Has `build` with outputs or commands | | **containerized** | Runner is `container` | | **remote** | Runner is `remote` | A component can have multiple roles. For example, `protonmail` is both a **tool** (installed to PATH) and a **job** (runs on a cron schedule). ## Registering a new component ### Via `castle create` (recommended) ```bash # Service — scaffolds project, assigns port, registers in castle.yaml castle create my-service --type service --description "Does something" # Tool — scaffolds under components/ castle create my-tool --type tool --description "Does something" ``` ### Manually Add an entry to the `components:` section of `castle.yaml`: ```yaml components: my-tool: description: Does something useful tool: source: components/my-tool/ install: path: alias: my-tool ``` ## Lifecycle ### Service lifecycle ```bash castle create my-service --type service # 1. Scaffold + register cd components/my-service && uv sync # 2. Install deps # ... implement ... castle test my-service # 3. Run tests castle service enable my-service # 4. Generate systemd unit, start castle gateway reload # 5. Update Caddy routes ``` After `service enable`, the service starts automatically on boot and restarts on failure. Manage with: ```bash castle logs my-service -f # Tail logs castle run my-service # Run in foreground (for debugging) castle service disable my-service # Stop and remove systemd unit ``` ### Tool lifecycle ```bash castle create my-tool --type tool # 1. Scaffold + register cd components/my-tool && uv sync # 2. Install deps # ... implement ... castle test my-tool # 3. Run tests uv tool install --editable components/my-tool/ # 4. Install to PATH ``` ### Job lifecycle Jobs are tools or services with a schedule trigger. They need both `run` (so castle knows how to execute them) and `manage.systemd` (so systemd handles the timer): ```yaml my-job: description: Runs nightly run: runner: command argv: ["my-job"] triggers: - type: schedule cron: "0 2 * * *" manage: systemd: {} ``` `castle service enable my-job` generates both a `.service` (Type=oneshot) and a `.timer` file. ## Infrastructure paths | What | Where | |------|-------| | Component registry | `castle.yaml` (repo root) | | Service data | `/data/castle//` | | Secrets | `~/.castle/secrets/` | | Generated Caddyfile | `~/.castle/generated/Caddyfile` | | Systemd units | `~/.config/systemd/user/castle-*.service` | | Systemd timers | `~/.config/systemd/user/castle-*.timer` | ## Manifest models The Pydantic models live in `core/src/castle_core/manifest.py`. Key classes: - `ComponentManifest` — top-level model, has `roles` computed property - `RunSpec` — discriminated union (RunPython, RunCommand, RunContainer, RunNode, RunRemote) - `TriggerSpec` — union (TriggerSchedule, TriggerManual, TriggerEvent, TriggerRequest) - `ExposeSpec`, `ProxySpec`, `ManageSpec`, `InstallSpec`, `ToolSpec`, `BuildSpec` - `CaddySpec`, `SystemdSpec`, `HttpExposeSpec`, `HttpInternal` Config loading: `core/src/castle_core/config.py` — `load_config()` parses castle.yaml into `CastleConfig` with typed `components` dict. Infrastructure generators: `core/src/castle_core/generators/` — systemd unit/timer generation (`systemd.py`) and Caddyfile generation (`caddyfile.py`).