diff --git a/docs/design.md b/docs/design.md index 244629d..544add1 100644 --- a/docs/design.md +++ b/docs/design.md @@ -416,71 +416,53 @@ Same contract as tools, plus: ## Component Lifecycle -The path from source to managed process: +The path from source to managed process is two moves — **develop** (against the +program) and **converge** (`castle apply`): ``` -source → [build] → artifact → [install] → available → [deploy] → managed +source → [dev verbs: build/test/…] → config (programs/ + deployments/) → [castle apply] → running ``` -Each step is distinct: +- **Develop** — language-specific dev verbs (`build`, `test`, …) that Castle + records but never runs implicitly. Frontends build in place under the repo + (`//`), served from there — no copy step. +- **Converge** — `castle apply` reads the config, generates systemd units + + Caddyfile entries, then reconciles reality to the desired state: activate what's + enabled, restart what changed, deactivate what's disabled. Activation is + *polymorphic over the manager* — systemd `enable --now`, `uv tool install` for a + tool on PATH, a gateway route for a static — so there is no separate install or + start step, and no per-kind verb. `castle apply --plan` shows the diff first. -1. **Build** — Language-specific. Produces an artifact (binary, venv - entry point, static bundle). Castle records the commands but doesn't - execute them implicitly. - -2. **Install** — Makes the artifact available on the system. For tools: - `uv tool install` or compiled binary placed in `~/.local/bin/`. For - services: same — the binary or entry point is on PATH or in a known - location. For frontends: assets built in place under the repo - (`//`), served from there — no copy. - -3. **Deploy** — Materializes the runtime configuration. Reads the - program spec, merges with node config, generates systemd units - and Caddyfile entries that reference *installed* artifacts — never - the source tree. Enables and starts services. - -For compiled languages (Rust, Go), build produces a standalone binary -and install is just placing it in `~/.local/bin/`. For interpreted -languages (Python, Node), the runtime wrapper (uv, node) handles -finding the installed artifact. +Desired on/off is `enabled` on the deployment; the only way to durably stop +something is `enabled: false` + apply. `castle restart` is the one imperative +bounce that re-actualizes current state without changing it. ## Runtime Filesystem Layout -Two roots, each overridable by an env var: `$CASTLE_HOME` (config, code, -artifacts, secrets; default `~/.castle`) and `$CASTLE_DATA_DIR` (bulk program -data; default `/data/castle`, kept on a dedicated volume): +Two roots, each overridable by an env var: `$CASTLE_HOME` (config, artifacts, +secrets; default `~/.castle`) and `$CASTLE_DATA_DIR` (bulk program data; default +`/data/castle`, on a dedicated volume). Program source lives separately under +`/data/repos//` (`$CASTLE_REPOS_DIR`). ``` $CASTLE_HOME/ ← Config & artifacts (default ~/.castle) -├── castle.yaml ← Global settings; resources under programs/ + deployments/ +├── castle.yaml ← Global settings (gateway, repo, agents) +├── programs/ deployments/ ← One YAML file per program / deployment ├── infra.conf ← Infrastructure install choices -├── code/ ← Program source (your programs) -│ └── / -├── artifacts/ -│ ├── specs/ ← Generated by `castle apply` -│ │ ├── Caddyfile -│ │ └── registry.yaml ← Node config (what's deployed here) -│ └── content/ ← Built frontend assets -│ └── castle/ ← (index.html + assets, served at root) +├── artifacts/specs/ ← Generated by `castle apply` +│ ├── Caddyfile +│ └── registry.yaml ← Node config (what's deployed here) └── secrets/ ← Secret files (NAME → value) - └── PROTONMAIL_API_KEY -$CASTLE_DATA_DIR/ ← Persistent service data (default /data/castle) -└── / - -~/.config/systemd/user/ ← Systemd units (standard location) -├── castle-central-context.service -├── castle-protonmail.service -├── castle-protonmail.timer -└── ... +/data/repos// ← Program source (your programs; absolute source:) +$CASTLE_DATA_DIR// ← Persistent service data (default /data/castle) +~/.config/systemd/user/ ← Systemd units + timers (castle-*.service/.timer) ``` Compiled-language tools (Rust, Go — planned) install their binaries to the -standard `~/.local/bin/`, not under `$CASTLE_HOME`. - -Source (the repo) is referenced only during build and install. Everything -the runtime touches lives under `$CASTLE_HOME`, `$CASTLE_DATA_DIR`, or standard -systemd paths. +standard `~/.local/bin/`. Source is referenced only by dev verbs and while a +deployment is materialized; everything the runtime touches lives under +`$CASTLE_HOME`, `$CASTLE_DATA_DIR`, or standard systemd paths. ## OTP as Design Guide diff --git a/docs/developing-castle.md b/docs/developing-castle.md index 8eed038..c30862d 100644 --- a/docs/developing-castle.md +++ b/docs/developing-castle.md @@ -47,15 +47,17 @@ 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}`, - `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`) +- 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}/{action}`, `GET /services/{name}/unit` +- Service actions: `POST /services/{name}/restart`, `GET /services/{name}/unit` ## Infrastructure internals diff --git a/docs/registry.md b/docs/registry.md index 5752f85..e62d29d 100644 --- a/docs/registry.md +++ b/docs/registry.md @@ -225,14 +225,14 @@ declares a **`manager`** — who makes it available and supervises its lifecycle ### `manager` — Who realizes it (the discriminant) A deployment is a *managed materialization* of a program. Its **`manager`** is -the stored discriminant — the single axis lifecycle, deploy, and status all -dispatch on: +the stored discriminant — the single axis `castle apply` and status dispatch on +(it's what makes activation polymorphic: one verb, kind-specific mechanism): -| Manager | Makes available as | Launch mechanism | start/stop | Kind | -|---------|--------------------|------------------|------------|------| -| **systemd** | a running process (or a `.timer` for jobs) | nested `run: { launcher: … }` | `systemctl` | service / job | -| **caddy** | a gateway static file_server route | *(none — files on disk; `root:`)* | add/remove route + reload | static | -| **path** | an installed CLI on `PATH` | *(none — `uv tool install`)* | `uv tool install` / `uninstall` | tool | +| Manager | Makes available as | Launch mechanism | how `apply` activates | Kind | +|---------|--------------------|------------------|-----------------------|------| +| **systemd** | a running process (or a `.timer` for jobs) | nested `run: { launcher: … }` | `systemctl enable --now` | service / job | +| **caddy** | a gateway static file_server route | *(none — files on disk; `root:`)* | wire the route + reload | static | +| **path** | an installed CLI on `PATH` | *(none — `uv tool install`)* | `uv tool install` | tool | | **none** | an external reference | *(none; `base_url:`/`health_url:`)* | *(nothing — not ours)* | reference | The **kind** (service/job/tool/static/reference) is **derived** from `manager` (+ @@ -630,55 +630,34 @@ manage: ## Lifecycle -### Service lifecycle +One flow for every kind — scaffold, implement, **`castle apply`**. Activation is +polymorphic over the `manager`, so the *verb* never changes; only what apply *does* +does: ```bash -castle program create my-service --stack python-fastapi # 1. Scaffold + register -cd /data/repos/my-service && uv sync # 2. Install deps -# ... implement ... -castle program test my-service # 3. Run tests -castle apply my-service # 4. Render unit + routes and reconcile (start it) +castle program create my-thing --stack python-fastapi # scaffold source + deployment +cd /data/repos/my-thing && uv sync # implement +castle program test my-thing # dev verbs (build/test/lint/…) +castle apply my-thing # converge: render + activate ``` -`castle apply` renders the systemd unit + Caddy route and starts the service -(which then runs on boot and restarts on failure). Manage with: +| kind (manager) | what `castle apply` does | +|----------------|--------------------------| +| **service** (systemd) | render the `.service` unit + gateway route, `enable --now` | +| **job** (systemd + schedule) | render a `.service` (Type=oneshot) **and** a `.timer` | +| **tool** (path) | `uv tool install` — put the executable on PATH | +| **static** (caddy) | wire the gateway `file_server` route to `/` | + +Manage a running deployment: ```bash -castle logs my-service -f # Tail logs -castle service run my-service # Run in foreground (for debugging) -castle service restart my-service # Imperative bounce -# To stop it durably: set `enabled: false` in its deployment, then `castle apply` +castle logs my-thing -f # Tail logs +castle program run my-thing # Run in foreground (for debugging) +castle restart my-thing # Imperative bounce — re-actualize current state ``` -### Tool lifecycle - -```bash -castle program create my-tool --stack python-cli # 1. Scaffold + register -cd /data/repos/my-tool && uv sync # 2. Install deps -# ... implement ... -castle program test my-tool # 3. Run tests -castle apply my-tool # 4. Install the path deployment on PATH -``` - -### Job lifecycle - -Jobs are deployments with `manager: systemd` plus a `schedule` — a -`deployments/my-job.yaml` file with a nested `run:` launch block: - -```yaml -# deployments/my-job.yaml (manager: systemd + schedule → kind: job) -program: my-job -manager: systemd -run: - launcher: command - argv: ["my-job"] -schedule: "0 2 * * *" -manage: - systemd: {} -``` - -`castle apply my-job` generates both a `.service` (Type=oneshot) -and a `.timer` file. +To durably turn something off, set `enabled: false` in its deployment and +`castle apply` — there is no start/stop/enable/disable/install verb. ## Infrastructure paths @@ -693,8 +672,8 @@ variable (both expand `~` and resolve relative paths): | What | Where | |------|-------| | Castle home | `$CASTLE_HOME` (default `~/.castle`) | -| Registry | `$CASTLE_HOME/castle.yaml` | -| Program source (yours) | `$CASTLE_HOME/code//` | +| Config | `$CASTLE_HOME/castle.yaml` + `programs/` + `deployments/` | +| Program source (yours) | `/data/repos//` (`$CASTLE_REPOS_DIR`; absolute `source:`) | | Program source (castle's) | `/` (via `source: repo:`) | | Secrets | `$CASTLE_HOME/secrets/` | | Generated Caddyfile | `$CASTLE_HOME/artifacts/specs/Caddyfile` |