# AGENTS.md — Wild PC You are working in **Wild PC**, a personal software platform. Wild PC is a monorepo of independent programs managed by the `wildpc` CLI. From this directory you can **manage all the software on this box from source** — create programs, deploy them as services, jobs, tools, or static frontends, route them through the gateway, expose them over TLS or a public tunnel, and coordinate across nodes. This file is the canonical, agent-agnostic guide. For exhaustive detail every section links to a doc under `docs/`. Read those before non-trivial changes. --- ## 1. Mental model — two layers Wild PC splits every piece of software into **what it is** and **how it runs here**: - **`programs/.yaml`** — the software *catalog*: source, stack, build, system dependencies. "What software exists." - **`deployments/.yaml`** — how a program is *realized on this node*. Discriminated on **`manager`**: `systemd` | `caddy` | `path` | `none`. The human-facing **kind** is **derived** from the manager (+ `schedule`), never stored: | manager | + schedule? | derived **kind** | what it is | |---------|-------------|------------------|------------| | `systemd` | no | **service** | a long-running daemon | | `systemd` | yes | **job** | a scheduled task (`.timer`) | | `path` | — | **tool** | a CLI installed on your `PATH` | | `caddy` | — | **static** | a built frontend served by the gateway | | `none` | — | **reference** | an external service on another node | A program may have **no** deployment (just source you develop), or one/more deployments. Global settings live in **`wildpc.yaml`** (`gateway`, `repo`, `agents`). Config root defaults to `~/.wildpc/`. **Prime directive:** regular programs must **never depend on wildpc**. They take standard config (data dir, port, URLs) via **env vars**; only wildpc's own programs (CLI, gateway, api) know wildpc internals. When you scaffold or adopt a program, wire it with env vars — not wildpc imports. → Full reference: **`docs/registry.md`** (wildpc.yaml structure, every field, manifest models, lifecycle). Architecture rationale: **`docs/design.md`**. --- ## 2. The `wildpc` CLI Resource-first: operations live under the resource they act on. Names can collide across resource types, so the resource is explicit. ```bash # Programs — the software catalog wildpc program list [--kind service] [--stack python-cli] [--json] wildpc program info [--json] wildpc program create [--stack ...] [--description ...] # scaffold NEW code wildpc program add [--name ...] # adopt EXISTING repo wildpc program clone [name] # provision repo: source wildpc program delete [--source] [-y] wildpc program run [args...] # declared run command wildpc program build|test|lint|format|type-check|check [name] # dev verbs # Services — daemons (manager: systemd, no schedule) wildpc service list|info [--json] wildpc service create [--program P] [--port N] [--health ...] [--launcher ...] wildpc service restart # imperative bounce wildpc service logs [-f] [-n 50] # Jobs — scheduled tasks (manager: systemd + schedule). create takes --schedule wildpc job create [--program P] --schedule "0 2 * * *" [--launcher ...] wildpc job ... # Tools — CLIs on PATH (manager: path) wildpc tool list [--json] # each tool's executable + description + install state wildpc tool info [--json] # Secrets — read/write the ACTIVE backend (file or openbao); never hand-write the store wildpc secret list # names in the active backend wildpc secret set [VALUE] # VALUE omitted → hidden prompt / stdin wildpc secret get # print a value wildpc secret rm [-y] # delete # Platform-wide wildpc apply [name] [--plan] # converge runtime to config — the workhorse wildpc list [--kind ...] [--stack ...] [--json] # all deployments wildpc status # unified health/status wildpc doctor # diagnose setup + runtime, with fix hints wildpc restart [name] # imperative bounce (one or all) wildpc gateway # gateway status + route table (inspection) ``` `wildpc service`/`job`/`tool` are **views** over the one deployment set, filtered by derived kind. Lifecycle is **convergence**: edit config, then **`wildpc apply`** renders units + the Caddyfile and reconciles the runtime (activate what's enabled, restart what changed, deactivate what's disabled). To durably turn a deployment off, set `enabled: false` and apply — there is no separate start/stop/enable. `wildpc restart` is the one imperative bounce. **Dev verbs resolve per-program:** a declared `commands:` entry (or `build:`) overrides the program's stack default, else the stack handler, else the verb is unavailable — so a wired-in repo with **no stack** works if it declares its commands. All projects use **uv** (Python) / **pnpm** (frontends). --- ## 3. Recipes ### Create a new service (HTTP daemon) ```bash wildpc program create my-service --stack python-fastapi --description "Does X" cd /data/repos/my-service && uv sync # implement it wildpc program test my-service wildpc service create my-service --program my-service --port 9001 wildpc apply my-service # renders the unit + gateway route and starts it ``` The service reads its port/data dir from env vars that `deployments/my-service.yaml` maps via placeholders (see §6). Stack guide: **`docs/stacks/python-fastapi.md`**. ### Create a CLI tool ```bash wildpc program create my-tool --stack python-cli --description "Does Y" cd /data/repos/my-tool && uv sync wildpc apply my-tool # installs the path deployment on PATH ``` `wildpc tool list --json` is the machine-readable tool catalog (each tool's real **executable**, which may differ from the program name). Stack: **`docs/stacks/python-cli.md`**. ### Create a scheduled job A job is a `manager: systemd` deployment with a `schedule` (cron). Generates a `.service` (Type=oneshot) + a `.timer`. ```bash wildpc job create nightly --program my-tool --schedule "0 2 * * *" --launcher command wildpc apply nightly ``` ### Create a static frontend ```bash # scaffold a Vite/React app under /data/repos/my-frontend (see docs/stacks/react-vite.md) wildpc program build my-frontend # produces dist/ # deployments/my-frontend.yaml → manager: caddy, root: dist wildpc apply # served at my-frontend. ``` The gateway serves the build **in place** from `/` — no copy, no Node process. Stack: **`docs/stacks/react-vite.md`**. Content-driven static sites built by Hugo: **`docs/stacks/hugo.md`**. Database-backed apps on the shared Supabase substrate: **`docs/stacks/supabase.md`**. ### Adopt an existing repo (no stack needed) ```bash wildpc program add ~/projects/some-rust-tool # local path wildpc program add https://github.com/me/widget.git --name widget ``` Wild PC detects dev-verb commands (pyproject→uv/ruff/pytest, Cargo.toml→cargo, …) or you declare them under `commands:` in `programs/.yaml`. --- ## 4. The gateway — routing & exposure The **Caddy gateway** (port 9000) is the single ingress. It's both a reverse proxy (to local services) and a static file server (for built frontends). It maps a public **address** (always a subdomain, `.`) to a **target**: | target kind | is | declared by | |-------------|----|-------------| | **proxy** | a local service on a port | a service's `proxy: true` | | **static** | a built frontend's `dist/` | a `manager: caddy` deployment's `root:` | | **remote** | a service on another node | mesh discovery | Exposure is a **checkbox** on a service: ```yaml proxy: true # expose at . public: true # ALSO expose to the internet via Cloudflare tunnel (requires proxy) ``` - `proxy: false`/omitted → reachable only at its own `host:port`. - The subdomain is always the **service name** (rename the service to change it). - There are **no path-prefix routes** — a whole subdomain maps to the backend root, so root-relative assets and `window.location` WebSocket URLs just work. Inspect routes: `wildpc gateway` / `GET /gateway`. Regenerate routes + reload the gateway: `wildpc apply` (converge). → Field-level detail: **`docs/registry.md`**. --- ## 5. DNS & TLS — making names resolve and be trusted Two orthogonal questions for `https://foo./` to work from a LAN browser: **resolve** (DNS) and **trust** (TLS). Wild PC doesn't run DNS — you add one **wildcard** record on the LAN's DNS server (usually the router): `address=//` (dnsmasq) pinned with a DHCP reservation. `gateway.tls` in `wildpc.yaml` picks the trust mode: | `gateway.tls` | serves | client setup | when | |---------------|--------|--------------|------| | `off` *(default)* | plain HTTP on `:9000` | none | no HTTPS needed / no domain | | `acme` | **real Let's Encrypt wildcard** `*.` via DNS-01 | **nothing** | you own a domain; any device | `acme` gets a publicly-trusted cert with **no CA to install**, while services stay **internal-only** (DNS-01 writes a TXT to the public zone; only LAN DNS resolves the names — the public zone has no A records). HTTPS also unlocks **secure context** (`crypto.subtle`, service workers), which plain-HTTP LAN hosts lack. `acme` operational prerequisites (wildpc can't do these for you): - **DNS-plugin Caddy** at `/usr/local/bin/caddy` — `./install.sh --with-dns-plugin=cloudflare`. - **Provider token** stored as a secret and mapped into the gateway service env (`CLOUDFLARE_API_TOKEN`); `wildpc apply` warns if missing. - **Bind :443/:80** — lower the floor once: `net.ipv4.ip_unprivileged_port_start=80` in `/etc/sysctl.d/` (beats `setcap`, which `NoNewPrivileges` would void). - **Stage first**: `WILDPC_ACME_STAGING=1` at deploy, verify issuance, then unset and redeploy for a production cert. → Full conceptual + step-by-step guide: **`docs/dns-and-tls.md`**. Read the actual values for *this* node in `~/.wildpc/wildpc.yaml` (`gateway.domain`, `tls`, etc.). --- ## 6. Environment, secrets, data, placeholders `defaults.env` in a deployment is the **single explicit source** of the env a service/job runs with — wildpc injects nothing implicitly. Map the vars your program reads to wildpc's computed values with placeholders: ```yaml expose: { http: { internal: { port: 9001 }, health_path: /health } } defaults: env: MY_SERVICE_PORT: ${port} # = expose.http.internal.port MY_SERVICE_DATA_DIR: ${data_dir} # = $WILDPC_DATA_DIR/ PUBLIC_URL: ${public_url} # gateway origin (CORS/allowlists) API_KEY: ${secret:MY_API_KEY} # reads ~/.wildpc/secrets/MY_API_KEY ``` | placeholder | expands to | |-------------|-----------| | `${port}` | the service's `expose.http.internal.port` | | `${data_dir}` | `$WILDPC_DATA_DIR/` (default `/data/wildpc/`) | | `${name}` | the deployment name | | `${public_url}` | `https://.` under acme, else `http://localhost:` | | `${secret:NAME}` | the secret `NAME` from the active backend | **Secret backend.** `${secret:NAME}` (and `read_secret()` for direct-read code) resolve through a backend selected by the **`secrets:` block in `wildpc.yaml`** (`core/secret_backends.py`): ```yaml secrets: backend: openbao # or `file` (default) addr: https://wildpc-openbao.:8200 mount: wildpc # KV-v2 mount token_secret: OPENBAO_ROOT_TOKEN # vault token, read from a file node_prefix: nodes/ # optional: per-node overrides ``` - **file** (default) → `~/.wildpc/secrets/NAME`. - **openbao** → vault KV-v2 `mount/NAME`, no file fallback. The vault **token** is still a file (`token_secret`, the bootstrap root of trust — it can't live in the vault it opens). With `node_prefix`, a read tries `mount//NAME` then shared `mount/NAME`, so one vault serves shared secrets + per-node overrides (e.g. each node's postgres password). Env vars (`WILDPC_SECRET_BACKEND`, `WILDPC_OPENBAO_*`) override the block (tests/CI force `file`). - **This fleet runs `openbao`**: civil is the authority (root token), primer reads via a least-privilege `wildpc-read` token. Only the bootstrap tier stays as files (`OPENBAO_ROOT_TOKEN`/`OPENBAO_UNSEAL_KEY`, the `cloudflared` creds dir). **Write secrets with `wildpc secret set NAME`, never by hand.** It targets the *active* backend, so a value can't land in a store the resolver never reads — the failure mode where a file-written secret is silently ignored on an openbao fleet and `${secret:NAME}` degrades to a literal `` that a service then uses as its credential. `wildpc apply` **refuses to converge** (exits non-zero, writes nothing) any deployment whose `${secret:…}` refs don't resolve in the active backend, naming the fix. **Never** put secret *values* in `wildpc.yaml` or project dirs — use `${secret:…}`. Roots: **`WILDPC_HOME`** (config/code/artifacts/secrets, default `~/.wildpc`, env-only — it *contains* wildpc.yaml) and **program data** (base of `${data_dir}`, default `/data/wildpc`) + **repos** (default `/data/repos`). The latter two resolve **env > `wildpc.yaml` > default** — set `data_dir:` / `repos_dir:` in `wildpc.yaml` (the single source of truth both the CLI and the api read), not a per-shell env var that only one of them sees. → **`docs/registry.md`** (wildpc.yaml globals). --- ## 7. Public exposure — Cloudflare tunnel `public: true` (requires `proxy: true`) projects a service to the internet at `.` (a **separate** zone, so internal subdomain names stay out of public DNS). `wildpc apply` generates the cloudflared ingress from the set of public services. Needs `gateway.public_domain` + `gateway.tunnel_id` set and the `wildpc-tunnel` service running. → One-time setup: **`docs/tunnel-setup.md`**. Routing only moves bytes — it does **not** supply a backend's own auth. Do not make a service public unless it authenticates or is meant to be open. --- ## 8. Mesh — multi-node coordination (opt-in) Runs on **NATS JetStream** (`wildpc-nats`, TLS + token). Enable via env on `wildpc-api`: `WILDPC_API_NATS_ENABLED=true`, `WILDPC_API_NATS_URL=tls://wildpc-nats.:4222`, `WILDPC_API_NATS_TOKEN=${secret:NATS_TOKEN}`. Each node publishes its (secret-stripped) registry to a JetStream **KV** bucket, renews a **presence** key, and watches for peers; remote deployments surface as `manager: none` **reference** kinds. A static **`role`** (`authority`|`follower`, in `wildpc.yaml`) gates who may write the shared-config bucket. A consumed cross-node service (`requires: - ref: X` satisfied by a peer) is routed by the gateway with a presence-gated circuit-breaker. Inspect + drive from the CLI: **`wildpc mesh status`** / **`wildpc mesh nodes`** / **`wildpc mesh config list|get|set`** (or `GET /mesh/status`, `/nodes`, `/mesh/config`). Modules: `wildpc_api.nats_client`, `.mesh`, `.mesh_gateway`, `.mdns`; secrets via `core` `secret_backends` (file default, OpenBao opt-in). → Full history + operations: **`docs/fleet-mesh-plan.md`**. --- ## 9. Where to read more | Topic | Doc | |-------|-----| | Registry model, `wildpc.yaml`, every field, lifecycle | **`docs/registry.md`** | | Why wildpc is shaped this way | **`docs/design.md`** | | DNS resolution + the two TLS modes, acme recipe | **`docs/dns-and-tls.md`** | | Public exposure (cloudflared) one-time setup | **`docs/tunnel-setup.md`** | | Writing FastAPI services | **`docs/stacks/python-fastapi.md`** | | Writing CLI tools | **`docs/stacks/python-cli.md`** | | Writing React/Vite frontends | **`docs/stacks/react-vite.md`** | | Writing Hugo static sites | **`docs/stacks/hugo.md`** | | Database-backed apps (shared Supabase) | **`docs/stacks/supabase.md`** | | **Developing Wild PC itself** (CLI/core/api/app, key files, endpoints) | **`docs/developing-wildpc.md`** | Wild PC's own programs live in this repo (`source: repo:` → cli, core, wildpc-api, app). Your programs live under `/data/repos//` with an absolute `source:`. When in doubt about *this* node's actual config, read `~/.wildpc/wildpc.yaml` and `wildpc status`.