Rename Castle -> Wild PC across the repo

Repo-side rename only (Phases 1-3 of the migration plan); the live box
(~/.castle, systemd units, /data/castle, domains) is a separate cutover.

- Slug `castle` -> `wildpc`: CLI command, module names (wildpc_core/cli/api),
  dist names, entry point `wildpc = wildpc_cli.main:main`.
- Identifiers: CastleConfig/NATSClient/DirError/MDNS -> Wildpc*.
- Env/constants: CASTLE_* -> WILDPC_*; ~/.castle -> ~/.wildpc, castle.yaml ->
  wildpc.yaml, /data/castle -> /data/wildpc.
- Systemd UNIT_PREFIX castle- -> wildpc-; own programs castle-api/gateway/etc.
- Display prose "Castle" -> "Wild PC" in docs, agent-guide files, README, frontend.
- Package dirs and bootstrap yaml renamed via git mv; lockfiles regenerated;
  redundant nested uv.lock files dropped (workspace root lock is authoritative).

Tests: core 273, cli 47, wildpc-api 120 all pass. Frontend type-checks + builds.
Fixed a stale test fixture (secret_env_path kind arg) broken pre-rename.
This commit is contained in:
2026-07-18 22:55:08 -07:00
parent 25d7a522cc
commit 05b28cb584
190 changed files with 2428 additions and 3337 deletions

View File

@@ -1,12 +1,12 @@
# Castle Design
# Wild PC Design
Castle is a personal software platform. It manages independent services,
Wild PC is a personal software platform. It manages independent services,
tools, and frontends on a Linux machine using standard Unix primitives —
systemd for process supervision, Caddy for HTTP routing, the filesystem
for storage, and env vars for configuration. The `castle` CLI and API
for storage, and env vars for configuration. The `wildpc` CLI and API
provide a registry and coordination layer on top.
The long-term goal: multiple Castle nodes (machines) that discover each
The long-term goal: multiple Wild PC nodes (machines) that discover each
other and coordinate, forming a personal infrastructure mesh. Each node
is self-sufficient. The mesh is optional.
@@ -15,9 +15,9 @@ is self-sufficient. The mesh is optional.
1. **Unix-native.** Use the OS. systemd, journald, filesystem, signals,
env vars, DNS. Don't reimplement what Linux already provides.
2. **Independence.** Components never depend on Castle. They accept
2. **Independence.** Components never depend on Wild PC. They accept
standard configuration (ports, data dirs, URLs) via env vars. A
Castle service is just a well-behaved Unix daemon that happens to
Wild PC service is just a well-behaved Unix daemon that happens to
be registered in a manifest.
3. **Stack and kind.** Each program has an optional *stack* (development
@@ -29,12 +29,12 @@ is self-sufficient. The mesh is optional.
4. **Language-agnostic above the build line.** Below the build line,
every language is different (uv, pnpm, cargo, go). Above it,
everything is just processes, ports, files, and signals. Castle
everything is just processes, ports, files, and signals. Wild PC
operates above the line.
5. **Separate source from runtime.** The repo is for development. The
runtime lives in standard Unix locations (`$CASTLE_HOME`, default
`~/.castle/`, plus systemd units). Nothing running should point into the
runtime lives in standard Unix locations (`$WILDPC_HOME`, default
`~/.wildpc/`, plus systemd units). Nothing running should point into the
source tree.
6. **AI-manageable.** The CLI and API exist so that AI assistants can
@@ -70,7 +70,7 @@ responds to SIGTERM.
### Build Layer
Transforms source code into runnable artifacts. Castle does not abstract
Transforms source code into runnable artifacts. Wild PC does not abstract
over language toolchains — it just records the build commands and their
outputs.
@@ -81,11 +81,11 @@ outputs.
| Rust | cargo | Binary |
| Go | go build | Binary |
Castle's `build` spec is intentionally minimal: a list of shell commands
and a list of output paths. This works for any language without Castle
Wild PC's `build` spec is intentionally minimal: a list of shell commands
and a list of output paths. This works for any language without Wild PC
needing to understand the toolchain.
For interpreted languages (Python, Node), Castle also needs to know the
For interpreted languages (Python, Node), Wild PC also needs to know the
runtime wrapper — how to invoke the artifact. For a `manager: systemd`
deployment this is the nested `run:` block's **launcher** variants:
@@ -99,7 +99,7 @@ deployment this is the nested `run:` block's **launcher** variants:
`manager: path` installs a CLI, `manager: none` is an external reference.)
Compiled languages (Rust, Go) use the `command` launcher — once built, they're
just binaries. No Castle-specific launcher needed.
just binaries. No Wild PC-specific launcher needed.
### Runtime Layer
@@ -120,12 +120,12 @@ Manages running processes using standard Linux infrastructure.
- TLS termination
**Filesystem** handles storage:
- Service data: `$CASTLE_DATA_DIR/<name>/` (default `/data/castle/`, on a
- Service data: `$WILDPC_DATA_DIR/<name>/` (default `/data/wildpc/`, on a
dedicated volume)
- Secrets: `$CASTLE_HOME/secrets/` (default `~/.castle/secrets/`)
- Generated config: `$CASTLE_HOME/artifacts/specs/` (Caddyfile, registry.yaml)
- Secrets: `$WILDPC_HOME/secrets/` (default `~/.wildpc/secrets/`)
- Generated config: `$WILDPC_HOME/artifacts/specs/` (Caddyfile, registry.yaml)
Castle generates systemd unit files and Caddyfile entries from the
Wild PC generates systemd unit files and Caddyfile entries from the
registry. It doesn't run a daemon itself — it configures OS-level
infrastructure and gets out of the way.
@@ -136,7 +136,7 @@ than stage a copy, Caddy serves their built assets **in place** from the repo
### Registry Layer
The registry is the central concept in Castle. It tracks what programs
The registry is the central concept in Wild PC. It tracks what programs
exist, what they can do, and how they're configured. But it's not a
single thing — it's three distinct concepts:
@@ -146,7 +146,7 @@ information, version-controlled in the repo. It answers: "what programs
could exist?"
**2. Node config** — what's *deployed on this machine*, with what concrete
ports, data paths, and env vars. This is per-machine. Two Castle nodes
ports, data paths, and env vars. This is per-machine. Two Wild PC nodes
might run different subsets of programs with different parameters. It
answers: "what's running here, and how?"
@@ -156,7 +156,7 @@ answers: "is it working?"
#### Source vs. runtime split
These map to the config root (`castle.yaml` globals plus per-resource files
These map to the config root (`wildpc.yaml` globals plus per-resource files
under `programs/` and `deployments/`), version-controlled in the repo:
```yaml
@@ -198,19 +198,19 @@ reference) captures whether it's an always-on daemon, a scheduled task, a CLI on
PATH, a served frontend, or an external reference.
A deployment can reference a program via `program:` for description fallthrough
and source code linking. It can also exist independently (e.g., `castle-gateway`
and source code linking. It can also exist independently (e.g., `wildpc-gateway`
runs Caddy — not our software).
A service's env is exactly its `defaults.env`castle injects nothing
A service's env is exactly its `defaults.env`wildpc injects nothing
implicitly. Values may use `${port}`/`${data_dir}`/`${name}`/`${secret:…}`
placeholders, which deploy resolves into the registry's flat `env`.
**`$CASTLE_HOME/artifacts/specs/registry.yaml`** (per-node, not in the repo, generated by `castle apply`) — Node config:
**`$WILDPC_HOME/artifacts/specs/registry.yaml`** (per-node, not in the repo, generated by `wildpc apply`) — Node config:
```yaml
node:
hostname: tower
castle_root: /data/repos/castle
wildpc_root: /data/repos/wildpc
gateway_port: 9000
deployed:
central-context:
@@ -218,7 +218,7 @@ deployed:
launcher: python
run_cmd: [/home/user/.local/bin/central-context]
env:
CENTRAL_CONTEXT_DATA_DIR: /home/user/.castle/data/central-context
CENTRAL_CONTEXT_DATA_DIR: /home/user/.wildpc/data/central-context
CENTRAL_CONTEXT_PORT: "9001"
kind: service
stack: python-fastapi
@@ -229,7 +229,7 @@ deployed:
```
The node config says what's deployed *here* and with what concrete
values. `castle apply` reads the spec from the repo, resolves the
values. `wildpc apply` reads the spec from the repo, resolves the
`defaults.env` placeholders and secrets, resolves binary paths,
and writes the registry. Systemd units and Caddyfile are then generated
from the registry — never from the spec directly.
@@ -244,31 +244,31 @@ This separation means:
Three interfaces expose the registry:
- **CLI** (`castle`) — For AI agents and terminal users. Structured
- **CLI** (`wildpc`) — For AI agents and terminal users. Structured
output via `--json`. Commands for listing, inspecting, creating,
and managing programs.
- **API** (`castle-api`) — For programmatic access over HTTP. Used by
- **API** (`wildpc-api`) — For programmatic access over HTTP. Used by
the dashboard, other nodes, and remote agents.
- **Dashboard** (`castle`) — For human discoverability. Visual
- **Dashboard** (`wildpc`) — For human discoverability. Visual
overview of what's running, health status, logs.
### Coordination Layer
Coordination handles discovery and communication — both between
programs on a single node and across multiple Castle nodes.
programs on a single node and across multiple Wild PC nodes.
**Intra-node coordination:**
- Programs find each other through the gateway or direct port access via env
vars. A gateway route maps a host address (`<name>.<domain>`) to a target of one
kind: **proxy** (a local service port), **remote** (a service on another
node), or **static** (a built frontend's `dist/`, served as files). The same
computed route list drives the Caddyfile, `castle gateway status`, and the
computed route list drives the Caddyfile, `wildpc gateway status`, and the
dashboard, so they always agree.
- The registry (CLI/API) provides discoverability.
- No service mesh or message broker required for basic operation.
**Inter-node coordination:**
- Each Castle node runs the API, which exposes its program registry.
- Each Wild PC node runs the API, which exposes its program registry.
- Nodes discover each other via MQTT retained messages and mDNS/DNS-SD
(python-zeroconf) for LAN environments.
- The gateway on each node can proxy to services on other nodes via
@@ -277,21 +277,21 @@ programs on a single node and across multiple Castle nodes.
they're talking to.
- MQTT provides pub/sub messaging for events, status, and coordination
across nodes.
- All mesh features are opt-in: `CASTLE_API_MQTT_ENABLED=true` and
`CASTLE_API_MDNS_ENABLED=true`. Single-node works without them.
- All mesh features are opt-in: `WILDPC_API_MQTT_ENABLED=true` and
`WILDPC_API_MDNS_ENABLED=true`. Single-node works without them.
**MQTT topics:**
- `castle/{hostname}/registry` — retained JSON, full NodeRegistry.
Published on connect and after `castle apply`.
- `castle/{hostname}/status``"online"` (retained) / `"offline"` (LWT).
- `wildpc/{hostname}/registry` — retained JSON, full NodeRegistry.
Published on connect and after `wildpc apply`.
- `wildpc/{hostname}/status``"online"` (retained) / `"offline"` (LWT).
LWT ensures nodes are marked offline if they disconnect unexpectedly.
**MeshStateManager** (`castle_api.mesh`) holds remote NodeRegistry
**MeshStateManager** (`wildpc_api.mesh`) holds remote NodeRegistry
instances in memory, indexed by hostname. 5-minute staleness TTL.
Updated by the MQTT client on incoming messages. Read by API endpoints
to serve cross-node data.
**mDNS** (`castle_api.mdns`) advertises `_castle._tcp` and browses
**mDNS** (`wildpc_api.mdns`) advertises `_wildpc._tcp` and browses
for peers and `_mqtt._tcp` broker. Uses python-zeroconf. Properties
include hostname, gateway_port, api_port.
@@ -302,27 +302,27 @@ Local paths always take precedence.
**Why MQTT over custom gossip:**
- Standard protocol, every language has a client library.
- Retained messages give new nodes an immediate view of the network.
- Topic-based routing maps naturally to `castle/{node}/{program}`.
- Topic-based routing maps naturally to `wildpc/{node}/{program}`.
- Works across networks (not just LAN like mDNS).
- Mosquitto is a single binary, simple to run as a Castle program.
- Mosquitto is a single binary, simple to run as a Wild PC program.
**Why mDNS/DNS-SD as a complement:**
- Zero-config LAN discovery via python-zeroconf.
- Each node advertises `_castle._tcp` — standard tooling works
- Each node advertises `_wildpc._tcp` — standard tooling works
(`avahi-browse`, `dns-sd`).
- Good for bootstrapping: find the MQTT broker without hardcoding
its address.
### Dashboard
The web dashboard (`castle`) is a React SPA served by Caddy in place from
The web dashboard (`wildpc`) is a React SPA served by Caddy in place from
its repo build output (`<source>/dist/`) at the root `/`. It talks to
`castle-api` via the gateway proxy at `/api`.
`wildpc-api` via the gateway proxy at `/api`.
**Layout:**
```
Castle
Wild PC
Personal software platform
[tower] [devbox (3)] ← NodeBar (hidden in single-node)
@@ -331,7 +331,7 @@ Personal software platform
│ Gateway · tower · port 9000 · 4 routes [Reload] [Caddyfile] │
│ │
│ Path Component Port Node Health│
│ /api castle-api 9020 tower ● up │
│ /api wildpc-api 9020 tower ● up │
│ /central-context central-context 9001 tower ● up │
│ /notifications notification-bridge 9002 tower ● up │
│ /devbox-api devbox-api 9020 devbox ● up │
@@ -343,7 +343,7 @@ Personal software platform
Daemons · Long-running processes that expose ports
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
castle-api │ │ central-ctx │ │ notif-bridge │
wildpc-api │ │ central-ctx │ │ notif-bridge │
│ ● up 5ms │ │ ● up 12ms │ │ ● up 8ms │
│ :9020 │ │ :9001 │ │ :9002 │
└──────────────┘ └──────────────┘ └──────────────┘
@@ -352,7 +352,7 @@ Components · Software catalog
Name Stack Kind Schedule Status
pdf2md Python / CLI tool — installed
protonmail Python / CLI tool */5 * * * * installed
castle React / Vite static — —
wildpc React / Vite static — —
backup-collect Python / CLI job 0 2 * * * —
```
@@ -381,7 +381,7 @@ Components · Software catalog
## Component Contract
Every Castle program, regardless of language, must satisfy a minimal
Every Wild PC program, regardless of language, must satisfy a minimal
contract. This is what makes the system uniform above the build line.
### Services (long-running daemons)
@@ -394,8 +394,8 @@ contract. This is what makes the system uniform above the build line.
| Data storage | Read `*_DATA_DIR` env var, write there |
| Logging | stdout for output, stderr for errors |
| Graceful shutdown | Handle SIGTERM, exit cleanly |
| Secrets | Read from env vars (Castle resolves `${secret:NAME}`) |
| No Castle dependency | Must run standalone with just env vars set |
| Secrets | Read from env vars (Wild PC resolves `${secret:NAME}`) |
| No Wild PC dependency | Must run standalone with just env vars set |
### Tools (CLI utilities)
@@ -419,73 +419,73 @@ Same contract as tools, plus:
## Component Lifecycle
The path from source to managed process is two moves — **develop** (against the
program) and **converge** (`castle apply`):
program) and **converge** (`wildpc apply`):
```
source → [dev verbs: build/test/…] → config (programs/ + deployments/) → [castle apply] → running
source → [dev verbs: build/test/…] → config (programs/ + deployments/) → [wildpc apply] → running
```
- **Develop** — language-specific dev verbs (`build`, `test`, …) that Castle
- **Develop** — language-specific dev verbs (`build`, `test`, …) that Wild PC
records but never runs implicitly. Frontends build in place under the repo
(`<source>/<dist>/`), served from there — no copy step.
- **Converge** — `castle apply` reads the config, generates systemd units +
- **Converge** — `wildpc 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.
start step, and no per-kind verb. `wildpc apply --plan` shows the diff first.
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
something is `enabled: false` + apply. `wildpc 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, 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/<name>/` (`$CASTLE_REPOS_DIR`).
Two roots, each overridable by an env var: `$WILDPC_HOME` (config, artifacts,
secrets; default `~/.wildpc`) and `$WILDPC_DATA_DIR` (bulk program data; default
`/data/wildpc`, on a dedicated volume). Program source lives separately under
`/data/repos/<name>/` (`$WILDPC_REPOS_DIR`).
```
$CASTLE_HOME/ ← Config & artifacts (default ~/.castle)
├── castle.yaml ← Global settings (gateway, repo, agents)
$WILDPC_HOME/ ← Config & artifacts (default ~/.wildpc)
├── wildpc.yaml ← Global settings (gateway, repo, agents)
├── programs/ deployments/ ← One YAML file per program / deployment
├── infra.conf ← Infrastructure install choices
├── artifacts/specs/ ← Generated by `castle apply`
├── artifacts/specs/ ← Generated by `wildpc apply`
│ ├── Caddyfile
│ └── registry.yaml ← Node config (what's deployed here)
└── secrets/ ← Secret files (NAME → value)
/data/repos/<name>/ ← Program source (your programs; absolute source:)
$CASTLE_DATA_DIR/<name>/ ← Persistent service data (default /data/castle)
~/.config/systemd/user/ ← Systemd units + timers (castle-*.service/.timer)
$WILDPC_DATA_DIR/<name>/ ← Persistent service data (default /data/wildpc)
~/.config/systemd/user/ ← Systemd units + timers (wildpc-*.service/.timer)
```
Compiled-language tools (Rust, Go — planned) install their binaries to the
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.
`$WILDPC_HOME`, `$WILDPC_DATA_DIR`, or standard systemd paths.
## OTP as Design Guide
Castle's architecture parallels Erlang/OTP, mapped onto Unix:
Wild PC's architecture parallels Erlang/OTP, mapped onto Unix:
| OTP Concept | Castle Equivalent |
| OTP Concept | Wild PC Equivalent |
|-------------|------------------|
| Application | Component (independent, self-contained) |
| Application resource file | Component spec in `castle.yaml` |
| Release config (sys.config) | Node config in `$CASTLE_HOME/artifacts/specs/registry.yaml` |
| Release assembly | `castle apply` (spec + node config → runtime) |
| Application resource file | Component spec in `wildpc.yaml` |
| Release config (sys.config) | Node config in `$WILDPC_HOME/artifacts/specs/registry.yaml` |
| Release assembly | `wildpc apply` (spec + node config → runtime) |
| Supervisor | systemd (restart policies, ordering) |
| Process | Running service/worker/job |
| Application env | Env vars |
| Node | A machine running Castle |
| Node | A machine running Wild PC |
| epmd | mDNS / MQTT discovery |
| Distribution | Inter-node coordination via MQTT + gateway proxying |
| "Let it crash" | `restart: on-failure` in systemd |
| Global registry | Merged node registries via MQTT retained messages |
The mapping is conceptual, not literal. Castle doesn't implement OTP
The mapping is conceptual, not literal. Wild PC doesn't implement OTP
semantics — it uses OTP's *thinking* to guide which Unix primitives
to compose and how.
@@ -501,41 +501,41 @@ Key OTP ideas that apply:
can remap these across nodes.
- **Spec vs. config.** In OTP, an application defines its structure
(the `.app` file) and a release provides the deployment config
(`sys.config`). Castle mirrors this: the program spec defines
(`sys.config`). Wild PC mirrors this: the program spec defines
structure, the node config provides deployment values.
## Current State
What exists today:
- **CLI** — `castle` command, installed via `uv tool install --editable cli/`
- **Three packages** — `castle-core` (models, config, generators),
`castle-cli` (commands), `castle-api` (HTTP API)
- **Source/runtime split** — `castle.yaml` (spec) → `castle apply`
`$CASTLE_HOME/artifacts/specs/registry.yaml` (node config). Systemd units and
- **CLI** — `wildpc` command, installed via `uv tool install --editable cli/`
- **Three packages** — `wildpc-core` (models, config, generators),
`wildpc-cli` (commands), `wildpc-api` (HTTP API)
- **Source/runtime split** — `wildpc.yaml` (spec) → `wildpc apply`
`$WILDPC_HOME/artifacts/specs/registry.yaml` (node config). Systemd units and
Caddyfile generated from registry with fully resolved paths. No repo references
in runtime artifacts.
- **Explicit env with placeholders** — a deployment's env is exactly its
`defaults.env`; `castle apply` resolves `${port}`/`${data_dir}`/`${name}`/
`defaults.env`; `wildpc apply` resolves `${port}`/`${data_dir}`/`${name}`/
`${secret:…}` into concrete values. No hidden convention injection.
- **Gateway** — Caddy on port 9000, Caddyfile generated from registry
- **API** — `castle-api` on port 9020, reads from registry (optional
castle.yaml fallback for non-deployed programs)
- **Dashboard** — `castle` React/Vite frontend, static assets
- **API** — `wildpc-api` on port 9020, reads from registry (optional
wildpc.yaml fallback for non-deployed programs)
- **Dashboard** — `wildpc` React/Vite frontend, static assets
served in place from its repo build output (`<source>/dist/`)
- **Services** — central-context (content storage), notification-bridge
(desktop notification forwarder)
- **Jobs** — protonmail (email sync every 5 min), backup-collect (nightly),
backup-data (nightly restic backup)
- **Tools** — ~15 CLI utilities (pdf2md, docx2md, search, gpt, etc.)
- **Manifest** — `castle.yaml` with typed Pydantic models
- **Manifest** — `wildpc.yaml` with typed Pydantic models
- **Mesh infrastructure** — MQTT client (paho-mqtt), mDNS discovery
(python-zeroconf), MeshStateManager, all wired into API lifespan.
Opt-in via `CASTLE_API_MQTT_ENABLED` / `CASTLE_API_MDNS_ENABLED`.
- **MQTT broker** — Mosquitto running as `castle-mqtt` Docker container
Opt-in via `WILDPC_API_MQTT_ENABLED` / `WILDPC_API_MDNS_ENABLED`.
- **MQTT broker** — Mosquitto running as `wildpc-mqtt` Docker container
on port 1883, managed by systemd. Config and data in
`$CASTLE_DATA_DIR/castle-mqtt/`.
`$WILDPC_DATA_DIR/wildpc-mqtt/`.
- **Node API** — `GET /mesh/status`, `GET /nodes`, `GET /nodes/{hostname}`.
`GET /deployments?include_remote=true` for cross-node program listing.
- **Gateway panel** — Dedicated UI showing route table, health per route,
@@ -551,10 +551,10 @@ What doesn't exist yet:
- **Multi-language support** — Rust and Go programs (the abstractions
support them via the `command` launcher, but no examples exist yet)
- **Build automation** — Castle records build specs but doesn't
- **Build automation** — Wild PC records build specs but doesn't
orchestrate builds (each project builds independently)
- **Multi-machine testing** — Mesh infrastructure is built and running
on one node, but not yet tested with a second Castle node
on one node, but not yet tested with a second Wild PC node
## Technology Map
@@ -562,18 +562,18 @@ What doesn't exist yet:
|---------|-----------|--------|
| Process supervision | systemd (user units) | Active |
| HTTP routing | Caddy (port 9000) | Active |
| Component specs | castle.yaml + Pydantic models | Active |
| Node config | `$CASTLE_HOME/artifacts/specs/registry.yaml` | Active |
| CLI | castle (Python, uv) | Active |
| API | castle-api (FastAPI) | Active |
| Dashboard | castle (React, Vite, shadcn/ui) | Active |
| Component specs | wildpc.yaml + Pydantic models | Active |
| Node config | `$WILDPC_HOME/artifacts/specs/registry.yaml` | Active |
| CLI | wildpc (Python, uv) | Active |
| API | wildpc-api (FastAPI) | Active |
| Dashboard | wildpc (React, Vite, shadcn/ui) | Active |
| Python packaging | uv | Active |
| Node packaging | pnpm | Active |
| Linting | ruff (Python), ESLint (TS) | Active |
| Type checking | pyright (Python), tsc (TS) | Active |
| Testing | pytest (Python), Vitest (TS) | Active |
| Secrets | `~/.castle/secrets/` file-based | Active |
| Data storage | Filesystem (`$CASTLE_DATA_DIR/`, default `/data/castle/`) | Active |
| Secrets | `~/.wildpc/secrets/` file-based | Active |
| Data storage | Filesystem (`$WILDPC_DATA_DIR/`, default `/data/wildpc/`) | Active |
| Messaging | MQTT (paho-mqtt client, Mosquitto broker) | Active (opt-in) |
| Node discovery | mDNS (python-zeroconf) + MQTT | Active (opt-in) |
| Rust packaging | cargo | Planned |