Move all instance state into ~/.castle/ with clear separation: - code/ — user project source (was components/) - artifacts/ — generated specs, built content (was generated/, static/) - data/ — service runtime data (was /data/castle/) - secrets/ — credentials castle.yaml moves to ~/.castle/castle.yaml as the canonical location. Source paths use repo: prefix for git repo programs (castle-api, app) and relative paths for user projects (code/central-context). Add idempotent install.sh for bootstrapping infrastructure (Docker, Caddy, MQTT, Postgres, Neo4j) with interactive service detection. Remove components/ from repo (now in ~/.castle/code/), deinit submodules, remove .gitmodules.
10 KiB
Yes — that changes the design a lot, in a good way.
If castle is not a general platform, but a curated machine for you + an AI assistant, then the ontology should be optimized for:
- few concepts
- strong defaults
- predictable placement
- limited extension paths
- easy generation from natural language
That means castle should not try to model all possible machine semantics. Ubuntu already does that. Castle should define a small opinionated overlay on top of Ubuntu.
So I’d stop trying to find the perfect universal ontology and instead design a usefully incomplete one.
The design principle
Castle should answer:
What are the few things I commonly want the machine to have or do?
From your examples, those are roughly:
- a tool
- a site
- a service
- a worker/daemon
- a job
- a store/broker
- maybe a library/project
That suggests the core user-facing abstraction should not be “artifact” or “namespace” or even “runtime.”
It should be something more like:
Castle’s main abstraction: unit
A unit is a thing the castle machine knows how to host.
That thing may be:
- a Python tool
- a static site
- a web service
- a daemon
- a scheduled job
- a database
- a broker
Then beneath that, castle can have a smaller set of internal dimensions.
A much simpler castle model
I think the practical model is:
unit
kind
stack
uses
Where:
unit
The named thing on the machine.
Examples:
reddit-tooldocs-siteapitransform-workermorning-searchpostgresmqtt
kind
The machine role.
Examples:
toolsiteserviceworkerjobstorebroker
stack
How it is built/run.
Examples:
pythonrustreact-staticfastapishell
This is where language/build/runtime conventions collapse into one curated choice.
uses
What castle subsystems or other units it depends on.
Examples:
postgresneo4jmqttcaddyredditweb
This is much closer to how you actually want to talk to the AI.
Why this is better
You don’t want to say:
create a deployment of kind service using runtime python with artifact class program and lifecycle systemd
You want to say:
add a Python service that does X and uses Postgres
So castle should be shaped around intentional unit kinds, not abstract machine theory.
The machine theory should stay underneath as implementation.
What castle can standardize
Because castle is curated, you can hardcode a lot:
Always true on castle
- Ubuntu
- systemd
- Caddy
- standard directory layout
- standard logging
- standard service supervision
- standard way to expose commands on PATH
- standard way to run Python via
uv - standard way to build JS via
pnpm - standard way to define dependencies between units
That means users and AI do not need to specify those every time.
This is the key simplification move.
Recommended fixed subsystems
I’d make these built-in castle subsystems:
- python via
uv - rust via
cargo - node/react via
pnpm - java via one standard choice
- caddy
- systemd
- postgres
- mqtt
- maybe neo4j
- maybe web-search ingestion as a castle-native capability
Then the schema does not need to model arbitrary runtimes. It only needs to reference these known capabilities.
Strong defaults for placement
You said castle can always use well-known spots. Good. Do that aggressively.
For example:
/castle/units/<name>/src
/castle/units/<name>/build
/castle/units/<name>/env
/castle/units/<name>/state
/castle/units/<name>/config
/castle/units/<name>/logs
And for exposure:
/castle/bin/<tool>
/castle/sites/<site>
/castle/data/<store>
Then castle itself can generate:
- symlinks into
/usr/local/bin - systemd units
- Caddy config
- env files
- state dirs
So the schema does not need to talk much about placement, because placement is mostly implied.
The actual user-facing kinds
I’d recommend just these:
1. tool
A command you can run from anywhere.
Examples:
- Python CLI
- Rust CLI
- shell helper
Defaults:
- exposed on PATH
- no long-running process
- build/install conventions by stack
Example:
units:
summarize-reddit:
kind: tool
stack: python
2. site
A human-facing HTTP site served by Caddy.
Examples:
- React static site
- docs site
- maybe proxied app frontend later
Defaults:
- served by Caddy
- standard root dir
- standard local hostname or port
Example:
units:
castle-web:
kind: site
stack: react-static
port: 8080
3. service
A long-running HTTP or TCP process supervised by systemd.
Examples:
- FastAPI app
- Rust API
- Java backend
Defaults:
- systemd-managed
- logs to journal
- optional Caddy exposure if HTTP
- standard environment/config paths
Example:
units:
reddit-api:
kind: service
stack: fastapi
port: 9090
uses: [postgres, neo4j, reddit]
4. worker
A long-running non-HTTP daemon, often connected to broker/store inputs.
Examples:
- MQTT consumer
- pubsub transformer
- sync loop
- queue processor
Defaults:
- systemd-managed
- usually not publicly exposed
- can subscribe to broker/topic or poll/store
Example:
units:
topic-transformer:
kind: worker
stack: python
uses: [mqtt]
subscribe: channel-b
publish: channel-y
This kind seems especially important for your goals.
5. job
A scheduled or one-shot task.
Examples:
- morning search
- report generation
- backup
- fetch and write JSON
Defaults:
- systemd timer
- writes to known output locations
- can use web-search capability
Example:
units:
morning-z-search:
kind: job
stack: python
schedule: daily@08:00
uses: [web]
outputs:
- /castle/shared/search/z/
6. store
A built-in stateful service.
Examples:
- Postgres
- Neo4j
Defaults:
- castle manages lifecycle and placement
- other units reference by name
Example:
units:
postgres:
kind: store
engine: postgres
neo4j:
kind: store
engine: neo4j
You may even decide these are so built-in they don’t need to be declared unless customized.
7. broker
A built-in messaging service.
Examples:
- MQTT
Example:
units:
mqtt:
kind: broker
engine: mqtt
Again, maybe built-in by default.
The schema can now be tiny
Instead of a universal schema, use a compact one with per-kind defaults.
Example:
units:
reddit-tool:
kind: tool
stack: python
description: CLI for querying reddit summaries
castle-web:
kind: site
stack: react-static
port: 8080
reddit-api:
kind: service
stack: fastapi
port: 9090
uses: [postgres, neo4j, reddit]
topic-transformer:
kind: worker
stack: python
uses: [mqtt]
subscribe: channel-b
publish: channel-y
morning-z-search:
kind: job
stack: python
schedule: "daily@08:00"
uses: [web]
output_dir: /castle/shared/search/z
That is probably closer to the right level.
What stack should mean now
Since castle is curated, stack can absorb a lot:
Instead of:
- build system
- runtime
- conventions
- entrypoint rules
just use a known stack name.
Examples:
pythonfastapirustreact-staticjava-serviceshell
Each stack implies:
- where source goes
- how to build
- how to run
- what files are expected
- how dependencies are installed
This is much simpler than separately modeling build/runtime/deployment for every case.
So for castle:
stack = curated implementation pattern
That’s probably the right definition.
What uses should mean
This is another very valuable simplification.
Instead of asking the user to define bindings and interfaces, let them say what the unit uses.
Examples:
postgresneo4jmqttcaddywebreddit- another unit name
Then castle knows how to wire it.
Example:
uses: [postgres, neo4j, reddit]
could imply:
- env vars for local connection strings
- service ordering in systemd
- generated client config
- AI awareness that those capabilities are available
This is much more useful than formal dependency graphs.
Under the hood
Internally, castle can still map to:
- artifacts
- runtimes
- systemd
- Caddy
- mount namespaces
- env files
- directories
But that should be hidden.
The AI assistant should primarily think in terms of:
- What kind of unit is this?
- What stack should it use?
- What does it use?
- What should it be named?
That’s enough to generate the machine config.
So the real castle ontology may be
User-facing
unit
kind
stack
uses
Machine-facing
source
artifact
runtime
deployment
placement
This split is important.
You were trying to find one ontology for both humans and machines. You probably want two:
- a simple command ontology for you + AI
- a lower-level implementation ontology for castle internals
That resolves a lot of the tension.
Example natural language mapping
“Add a python tool that does X”
becomes:
name: x-tool
kind: tool
stack: python
“Make a web service that does Y”
becomes:
name: y-service
kind: service
stack: fastapi
“Make a daemon that watches pubsub channel B and transforms the data and puts it on Y”
becomes:
name: b-to-y
kind: worker
stack: python
uses: [mqtt]
subscribe: B
publish: Y
“Every morning, do a web search for Z and put the results in PATH as json files”
becomes:
name: z-search
kind: job
stack: python
uses: [web]
schedule: daily@08:00
output_dir: /castle/shared/search/z
format: json
This is very manageable.
Recommendation
I would make castle intentionally opinionated around:
- 7 unit kinds max
- 5–8 stacks max
- a handful of built-in capabilities in
uses - fixed placement conventions
- automatic generation of systemd/Caddy/config
That seems like the right balance of simplicity and extensibility for what you want.
The next useful step is to draft the minimal v1 castle schema with only those user-facing fields and strong defaults.