Files
wild-pc/docs/component-registry.md
Paul Payne 0d36e4f72a Refactor component terminology to programs in config and manifest
- Updated the terminology from "components" to "programs" across the codebase, including in config loading, saving, and manifest specifications.
- Introduced a new `stacks.py` file to handle lifecycle actions for development stacks, implementing handlers for Python and React Vite stacks.
- Adjusted tests to reflect the new program structure and ensure proper functionality.
- Revised documentation to align with the new terminology and structure, ensuring clarity on the purpose and configuration of programs, services, and jobs.
2026-02-23 22:09:41 -08:00

8.2 KiB

Registry

How castle tracks, configures, and manages programs, services, and jobs. This is the central reference for castle.yaml structure and the registry architecture.

castle.yaml

The single source of truth. Lives at the repo root. Three top-level sections:

gateway:
  port: 9000

programs:
  my-tool:
    description: Does something useful
    source: programs/my-tool
    install:
      path: { alias: my-tool }
    tool:
      system_dependencies: [pandoc]

services:
  my-service:
    component: my-service
    run:
      runner: python
      tool: my-service
    expose:
      http:
        internal: { port: 9001 }
        health_path: /health
    proxy:
      caddy: { path_prefix: /my-service }
    manage:
      systemd: {}

jobs:
  my-job:
    component: my-tool
    run:
      runner: command
      argv: [my-tool, sync]
    schedule: "0 2 * * *"
    manage:
      systemd: {}

Section semantics

Section Purpose Category
programs: Software catalog — what exists tool, frontend, program
services: Long-running daemons — how they run service
jobs: Scheduled tasks — when they run job

Services and jobs can reference a program via component: for description fallthrough and source code linking. They can also exist independently (e.g., castle-gateway runs Caddy — not our software).

Program blocks

Programs define what software exists — identity, source, tools, builds.

source — Where the source lives

source: programs/my-tool

Relative path from repo root to the project directory.

install — How to install it

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

tool:
  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). The source directory is set via the top-level source field on the program, not here.

build — How to build it

build:
  commands:
    - ["pnpm", "build"]
  outputs:
    - dist/

Components with build outputs are categorized as frontends in the UI.

Service blocks

Services define how long-running daemons are deployed.

run — How to start it (required)

Discriminated union on runner:

Runner Sync Deploy Key fields
python uv sync which(tool) → installed binary tool, args
command (none) which(argv[0]) → resolved path argv
container (none) docker/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
run:
  runner: python
  tool: my-service        # name in [project.scripts]

expose — What it exposes

expose:
  http:
    internal:
      port: 9001            # Required for HTTP services
    health_path: /health     # Used by health polling

proxy — How to proxy it

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

manage:
  systemd: {}

Enables castle service enable/disable and castle logs. An empty {} uses defaults (enable=true, restart=on-failure, restart_sec=2).

Full options:

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]
    exec_reload: "caddy reload ..."

defaults — Default environment

defaults:
  env:
    CENTRAL_CONTEXT_URL: http://localhost:9001
    API_KEY: ${secret:MY_API_KEY}

Castle resolves ${secret:NAME} by reading ~/.castle/secrets/NAME. Never store secrets in castle.yaml or project directories.

Job blocks

Jobs define how scheduled tasks run. Same blocks as services plus schedule and timezone.

schedule — Cron expression (required)

schedule: "*/5 * * * *"
timezone: America/Los_Angeles    # default

Castle generates a systemd .timer file alongside the .service unit.

Other blocks

Jobs also support run (required), manage, and defaults — same semantics as services.

Registering a new program

# Service — scaffolds project, assigns port, registers in castle.yaml
castle create my-service --stack python-fastapi --description "Does something"

# Tool — scaffolds under programs/
castle create my-tool --stack python-cli --description "Does something"

Manually

Add entries to the appropriate sections of castle.yaml:

# Tool — only needs a program entry
programs:
  my-tool:
    description: Does something useful
    source: programs/my-tool
    install:
      path:
        alias: my-tool

# Service — needs both program and service entries
programs:
  my-service:
    description: Does something useful
    source: programs/my-service

services:
  my-service:
    component: my-service
    run:
      runner: python
      tool: my-service
    expose:
      http:
        internal: { port: 9001 }
        health_path: /health
    proxy:
      caddy: { path_prefix: /my-service }
    manage:
      systemd: {}

Lifecycle

Service lifecycle

castle create my-service --stack python-fastapi   # 1. Scaffold + register
cd programs/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:

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

castle create my-tool --stack python-cli        # 1. Scaffold + register
cd programs/my-tool && uv sync           # 2. Install deps
# ... implement ...
castle test my-tool                      # 3. Run tests
uv tool install --editable programs/my-tool/    # 4. Install to PATH

Job lifecycle

Jobs are defined in the jobs: section with a run spec and schedule:

jobs:
  my-job:
    description: Runs nightly
    run:
      runner: command
      argv: ["my-job"]
    schedule: "0 2 * * *"
    manage:
      systemd: {}

castle service enable my-job generates both a .service (Type=oneshot) and a .timer file.

Infrastructure paths

What Where
Registry castle.yaml (repo root)
Service data /data/castle/<name>/
Secrets ~/.castle/secrets/<NAME>
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:

  • ProgramSpec — software catalog entry (source, install, tool, build)
  • ServiceSpec — long-running daemon (run, expose, proxy, manage, defaults)
  • JobSpec — scheduled task (run, schedule, manage, defaults)
  • RunSpec — discriminated union (RunPython, RunCommand, RunContainer, RunNode, RunRemote)
  • ExposeSpec, ProxySpec, ManageSpec, InstallSpec, ToolSpec, BuildSpec
  • CaddySpec, SystemdSpec, HttpExposeSpec, HttpInternal

Config loading: core/src/castle_core/config.pyload_config() parses castle.yaml into CastleConfig with typed programs, services, and jobs dicts.

Infrastructure generators: core/src/castle_core/generators/ — systemd unit/timer generation (systemd.py) and Caddyfile generation (caddyfile.py).