Files
wild-pc/docs/component-registry.md
Paul Payne 08c6f3fa83 feat: Enhance tool management and documentation in Castle
- Introduced category tools support in the `castle create` command.
- Added detailed guides for creating components in CLAUDE.md.
- Implemented new API endpoints for listing and retrieving tool details.
- Updated component and tool models to include additional metadata.
- Improved error handling and response structures in service actions.
- Enhanced documentation for component registry and web APIs.
2026-02-21 00:09:34 -08:00

8.0 KiB

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.

gateway:
  port: 9000

components:
  my-service:
    description: Does something useful
    run:
      runner: python_uv_tool
      tool: my-service
      cwd: my-service
      env:
        MY_SERVICE_DATA_DIR: /data/castle/my-service
        MY_SERVICE_PORT: "9001"
    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:

Runner Use case Key fields
python_uv_tool Python service/tool via uv tool, cwd, env
command Shell command argv, cwd, env
python_module Python -m invocation module, args, python
container Docker/Podman image, command, ports, volumes
node Node.js script script, package_manager (npm/pnpm/yarn)
remote External service base_url, health_url

Services use python_uv_tool:

run:
  runner: python_uv_tool
  tool: my-service        # name in [project.scripts]
  cwd: my-service         # working directory relative to repo root
  env:
    MY_SERVICE_DATA_DIR: /data/castle/my-service
    MY_SERVICE_PORT: "9001"

Tools invoked by castle (jobs, scheduled tasks) use command:

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

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

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=5).

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]

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:
  tool_type: python_standalone    # or "script"
  category: document              # Grouping for display
  source: tools/document/         # 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).

build — How to build it

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

Having build outputs gives the component the frontend role.

triggers — What triggers it

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/:

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

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

# Standalone tool — scaffolds at repo root
castle create my-tool --type tool --description "Does something"

# Category tool — adds to existing tools/<category>/ package
castle create my-tool --type tool --category document --description "Does something"

Manually

Add an entry to the components: section of castle.yaml:

components:
  my-tool:
    description: Does something useful
    tool:
      tool_type: python_standalone
      category: utilities
      source: my-tool/
    install:
      path:
        alias: my-tool

Lifecycle

Service lifecycle

castle create my-service --type service   # 1. Scaffold + register
cd 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 --type tool        # 1. Scaffold + register
cd my-tool && uv sync                    # 2. Install deps
# ... implement ...
castle test my-tool                      # 3. Run tests
uv tool install --editable 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):

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/<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 cli/src/castle_cli/manifest.py. Key classes:

  • ComponentManifest — top-level model, has roles computed property
  • RunSpec — discriminated union (RunPythonUvTool, RunCommand, etc.)
  • TriggerSpec — union (TriggerSchedule, TriggerManual, TriggerEvent, TriggerRequest)
  • ExposeSpec, ProxySpec, ManageSpec, InstallSpec, ToolSpec, BuildSpec
  • CaddySpec, SystemdSpec, HttpExposeSpec, HttpInternal

Config loading: cli/src/castle_cli/config.pyload_config() parses castle.yaml into CastleConfig with typed components dict.