OpenCharly
charly is a command-line tool that orchestrates one declarative description of a working
environment onto any of five substrates: a container, a VM guest, a Kubernetes cluster, a host, or
an Android device. You write a list of candies — each one an installable concern — and charly
realises the same list everywhere, for you and for the agents you run. Every substrate consumes the
same intermediate form, the install plan, so moving a deploy from a container to a VM is a
keyword change rather than a rewrite.
Everything and nothing
Section titled “Everything and nothing”charly brings everything and nothing. Everything you might want can be added as a plugin — a candy — and nothing is built in that you cannot trim out. If you would rather use a different tool for a particular job, remove the candy that provides it and wire yours in; the orchestrator does not care what provides a concern, only that the install plan is satisfied.
# everything — compose the kitchen sinkdev-box: candy: base: fedora candy: - '@github.com/opencharly/layer-ripgrep:v2026.235.1653' - '@github.com/opencharly/pod-sshd:v2026.239.1637' - '@github.com/opencharly/layer-charly:v2026.241.1407'
# nothing — the same box, trimmed to one concern; bring your own tool for the restminimal-box: candy: base: fedora candy: - '@github.com/opencharly/pod-sshd:v2026.239.1637'Every word charly understands is itself a plugin — every word is a plugin
— so the trim is not a special case: the core is word-blind, and a candy list is the whole
configuration.
Bring the pain forward
Section titled “Bring the pain forward”DevOps’ oldest trick is to bring the pain forward: take the hard, risky part of shipping — the
deployment, the integration, the teardown — and do it early and often, on a system built to be
destroyed, so it stops being hard by the time it matters. charly is built around that mantra.
Every candy carries a plan: — an acceptance spec that proves what it installs, baked into the
image as an OCI label. Every deploy marked disposable: true is a check bed: a test system
that exists to be destroyed. charly check run <bed> chains build, deploy, probe, destroy,
rebuild, and probe again in one command — the whole pain of shipping, brought forward to the test
system, on every change, before anything reaches a real target.
The same install plan the bed just proved is the one a production deploy realises — so the pain you bring forward is the pain you would otherwise meet in production, and by the time it gets there, it is routine.
Heritage. The design borrows deliberately from LLVM: one intermediate representation, many backends — applied to infrastructure deployment instead of code generation, and driven by agents as readily as by people. That spine is what makes the orchestration target-neutral.
A candy is the atomic unit of configuration; a box is a candy that composes others into a
buildable machine. The names are literal — candy: is a real keyword and candy/ is a real
directory.
A box is an artifact; a candybox is a process. Nothing about a box’s contents makes it safe — a
box is deliberately generous, full shell and package manager included. The isolation is a property
of the running container or VM — the candybox. That is why it is safe to hand an agent
everything inside one, and why disposable: true is a statement about a running thing rather than
about a file.
One box can be deployed many times, onto several substrates, on several machines; each of those is a deploy, and the deploys on one machine are that machine’s deploy.
Full documentation: opencharly.ai.
Install
Section titled “Install”Install charly once, then use it from anywhere. charly runs on Linux; container deploys
need Podman or Docker, VM deploys need libvirt. Everything else here — and every page on
opencharly.ai — assumes a machine with charly installed.
Once charly is on your $PATH, --repo reads any published project without cloning it:
charly --repo opencharly/charly box list boxesThe first lines of its output:
agentteams [testing]agentteams-manager [testing]agentteams-worker [testing]alpine-repo-box [testing]arch.arch [testing]…Clone the repository only if you are working on charly itself:
git clone --recurse-submodules https://github.com/opencharly/charly.gitcd charlytask build:binary # builds ./bin/charly, stamped with a CalVer (date-based) version./bin/charly box buildEvery invocation against that checkout uses ./bin/charly; each checkout or worktree gets its own.
A real box, end to end
Section titled “A real box, end to end”This is box/tutorial-shell/charly.yml, from the opencharly/distro-fedora project:
tutorial-shell: candy: description: |- The teaching box behind opencharly.ai's quickstart — a minimal, real dev shell ... base: fedora candy: - '@github.com/opencharly/layer-supervisord:v2026.240.0121' - '@github.com/opencharly/layer-ripgrep:v2026.235.1653' - '@github.com/opencharly/pod-sshd:v2026.239.1637' plan: - check: composing the service candy next to the init candy wired sshd into the assembled supervisord config — a program block neither candy produces on its own id: tutorial-shell-service-wired-into-init file: file: /etc/supervisord.conf contains: - contains: "[program:sshd]"base: points at another box defined next door; it can equally be a registry ref. The inner
candy: is the list of candies the box composes.
Note what is listed: the init candy. sshd declares a service, so charly resolves whichever init
the destination needs and composes that init’s candy — supervisord when this box is built as a
container image, the systemd init instead when the same candies land on a systemd machine. The box
names supervisord explicitly so the assembled /etc/supervisord.conf is deterministic: without
the acting init’s candy in the scanned set charly warns and injects nothing.
The plan: does not check that ripgrep and sshd are present — each candy’s own plan proves
that, and those plans run against this same image. It checks what the composition produced: that
sshd became a supervisord program. A check belongs on the candy that provides the behaviour, and
belongs on the composing box only when the claim is about the composition itself.
charly --repo opencharly/distro-fedora box validate # the schema gate — nothing runs until it passescharly --repo opencharly/distro-fedora box build tutorial-shell # → multi-stage Containerfile → imagecharly --repo opencharly/distro-fedora shell tutorial-shell # → you are inside the candyboxcharly --repo opencharly/distro-fedora check run check-tutorial-shell # → build, deploy, probe, fresh rebuild, tear downThe four stages
Section titled “The four stages”| Stage | What you write | What you run |
|---|---|---|
| Build | a candy: with base: and a candy list |
charly box build <box> |
| Run | nothing more | charly shell <box> |
| Deploy | a substrate keyword — pod: vm: kubernetes: local: android: |
charly deploy add, charly start |
| Evaluate | a plan: on each candy |
charly check box, charly check live, charly check run |
A deploy marked disposable: true is a check bed, and charly check run <bed> chains build,
deploy and evaluate into one command. It builds the image and checks it, deploys it, and waits for
steady state. Then it checks the running candybox live, destroys and rebuilds it from scratch,
checks it again, and tears everything down.
Change the substrate, keep the shape
Section titled “Change the substrate, keep the shape”Both stanzas below are real entries in box/fedora/charly.yml: this page’s example as a container
bed, and its VM twin.
# a CONTAINERcheck-tutorial-shell: pod: image: tutorial-shell disposable: true
# a VM GUESTcheck-fedora-vm: vm: from: fedora-vm disposable: true add_candy: - '@github.com/opencharly/layer-charly:v2026.241.1407'The payloads differ — the pod runs the built tutorial-shell image; the VM boots the fedora-vm
template (from: inherits its settings) and overlays the charly candy. The grammar does not:
the substrate is the keyword, pod: versus vm:, and each candy a deploy names is realised
through the same install plan on either side — the VM stanza reaches its guest over SSH and
installs packages there, the pod stanza runs an image. kubernetes:, local: and android:
take the same shape —
how that is wired →
The bigger boxes
Section titled “The bigger boxes”The teaching box above has a small candy list. The kitchen-sink dev boxes are fedora-coder,
arch-coder, debian-coder and ubuntu-coder. Each carries the AI coding CLIs (claude-code,
codex, gemini, forgecode), language runtimes, DevOps tooling, and nested rootless containers
and VMs — all at uid 1000 with no --privileged. Same format, same commands. Because those
containers and VMs are nested, a candybox can build and deploy candyboxes — charly runs inside
charly.
How it works
Section titled “How it works”One keyword, two shapes
Section titled “One keyword, two shapes”A candy is declared with one entity keyword, candy:, and one filename, charly.yml. But a candy
resolves to one of two shapes, and base:/from: is the switch:
| The candy carries | It is | And it may also carry |
|---|---|---|
neither base: nor from: |
a layer — one installable concern | package: service: plan:, and a plugin: block |
base: or from: |
a box — a candy that composes other candies; box build turns it into a container image |
a candy: list of layers, plan: |
The two shapes are mutually exclusive, and the schema enforces it — a candy that carries both
base: and package: is rejected. A box does not install packages directly; it composes layers
that do.
Every word is a plugin
Section titled “Every word is a plugin”Every provider word in this document is registered by a plugin candy. Many of them are
ordinary layers as well — every one carrying a plan:, so each is a real layer and an extension
of charly at the same time. charly is not a program with built-in support for containers, VMs
and Kubernetes that also happens to accept plugins. Its core is word-blind: it loads plugins and
routes each word to whichever one claims it. It does not know what pod: means — there is no pod
case in a switch statement in the core.
The provider index is the live census — every word and its owning plugin candy, regenerated on every docs build.
| Role | Examples |
|---|---|
substrate — deploy destinations (the census registers these as the kind and deploy classes) |
pod vm kubernetes local android |
| kind — the entity keywords themselves | candy distro group agent |
verb — probes a plan: can call |
file http cdp vnc adb kube |
command — charly subcommands |
deploy check clean marketplace |
| step — install operations | service-custom reboot |
| builder — multi-stage build patterns | pixi npm cargo aur |
candy: itself is a plugin-provided kind, registered by candy/plugin-candy-kind.
You extend charly by writing candies — in this project or any other, referenced by git URL —
and a substrate you invent is the same kind of thing as pod:. This is the everything-and-nothing
strategy in action: the whole surface is pluggable, and the core ships nothing you cannot replace.
The architecture, in one pass
Section titled “The architecture, in one pass”1. Everything starts as one resolved project. charly reads every charly.yml it discovers,
follows @github.com/... references to other repositories, and resolves each entity to a pinned
CalVer version. The result is a single in-memory project — your candies plus everyone else’s,
flattened, with no notion yet of building or deploying. A candy reference is a git URL and a
version, so a box can compose someone else’s candy without vendoring it; charly box reconcile
keeps the pins aligned.
2. A plugin is reached the same way wherever it lives. Some plugin candies are compiled into
the binary; the rest are loaded from a project’s candy/ directory and run as separate processes
over gRPC. Both implement one Provider contract, so where a plugin runs is an operational
choice — startup cost against isolation — not an API difference. All five substrates — pod:,
vm:, local:, kubernetes:, android: — are out-of-process.
3. Building and deploying share one input. Building renders a multi-stage Containerfile from
your candy list. Deploying reduces the same list to the install plan, which each substrate
backend realises its own way: an SSH session against a VM guest, a Kustomize tree, packages on a
host, an APK install — and, on pod:, the very image the build path produced.
4. The artifact carries its own description. What a box provides and how to prove it — its
declared capabilities and its acceptance plan — are written into the image as ai.opencharly.*
labels, so a pulled image can be inspected and tested by a machine that has never seen the source.
5. The schema is upstream of the code. The base grammar of charly.yml is
CUE, a typed configuration language, in the spec module
(github.com/opencharly/spec); each plugin ships its own CUE schema for the words it registers. The Go wire types are generated from that base schema, and load-time validation runs
against the same embedded schema — a grammar change cannot reach the code without going through the
module both sides consume.
Nesting: where a deploy runs
Section titled “Nesting: where a deploy runs”Candies compose a box, at authoring time. Nesting places a deploy, at run time. A candy is never “inside” another candy; a deploy can be inside another deploy.
There is no nested: field you write. Nesting is position in the file — indent one deploy
under another and the inner one runs inside the outer one’s candybox:
check-group: vm: from: eval-vm disposable: true ... check-group-member: local: from: check-group-appThat is a real entry in this repository’s charly.yml, abridged — charly migrate unrolled the
former targetless group: node into the vm: primary above (the group scalars move onto it). The
inner local: carries no host: field, and that is the point: it runs inside the parent’s candybox
rather than naming a machine of its own.
A top-level local: deploy installs packages and systemd units onto the machine charly is
running on. The same deploy, nested under a disposable vm:, installs them into a throwaway
guest instead. Either way it is reversible: a local: deploy records each step it applies in an
install ledger, and charly deploy del <name> tears it back down.
Where a running candybox actually lives
Section titled “Where a running candybox actually lives”Podman and Docker are both first-class, for building and for running. charly auto-detects
what is installed; either can be pinned per host with CHARLY_BUILD_ENGINE, CHARLY_RUN_ENGINE
or the runtime config.
On a host with Podman and systemd, a pod: deploy runs as user-level systemd quadlets. charly config generates one charly-<name>.container quadlet per deploy, carrying its ports, volumes,
devices and security settings; systemd turns it into charly-<name>.service and starts it at
boot.
Where Podman and systemd are not both present, the same deploy runs directly against the engine.
Vocabulary
Section titled “Vocabulary”Full teaching glossary: the words.
| Term | What it is |
|---|---|
| candy | the atomic unit of configuration in charly — the entity everything else is made of: boxes compose candies, deploys apply them, substrates realise them |
| layer | a candy that installs one concern — carrying neither base: nor from: |
| base | a field on a candy that names a starting image — another box or a registry ref. Carrying base: makes a candy a box |
| from | a field with two uses. On a candy, from: builder:<word> selects a multi-stage build pattern and makes the candy a box. On a deploy, from: <name> inherits another same-kind deploy’s settings — this sense does not make anything a box |
| box | a candy that composes other candies — the composite unit. A box names a starting point via base or from and stacks layers into a single entity that charly builds into an image or deploys onto a substrate |
| image | the built artifact a box produces — the generic word for what box build yields, stored in an image store or registry |
| container image (= OCI image) | an image in the OCI container format — the artifact a pod: or kubernetes: deploy runs as a container, named after the Open Container Initiative spec that defines it |
| candybox | the running, isolated form of a box — a container or a VM guest. This is the security boundary |
| container | the running OCI process — the candybox’s form on a pod: deploy. Not the image, not the box |
| substrate | the destination kind a deploy lands on — pod: vm: kubernetes: local: android:. A substrate is a place, not an artifact: the image is the payload, the substrate is where it runs |
| pod | the container substrate — a pod: deploy runs a box’s image as a container. Not a Kubernetes Pod (the kubernetes: backend emits real ones) |
| vm | the guest substrate — a vm: deploy boots a vm image as a rootless libvirt guest, reached over SSH |
| vm image | the bootable disk a vm: deploy boots — a cloud_image qcow2 or a bootc image. A different artifact from a container image |
| kubernetes | the Kubernetes substrate — a kubernetes: deploy emits a Kustomize overlay |
| local | the host substrate — a local: deploy installs onto the machine charly runs on, or onto a remote machine when it carries host |
| android | the device substrate — an android: deploy installs APKs onto a device or emulator |
| host | a field on a local: deploy naming the machine to install onto — host: local (or absent) is the machine charly runs on, host: <user@machine> is an SSH target |
| deploy | a named placement of a box on a substrate. When running, its candybox is the live thing |
| deploy | the set of deploys charly manages on this machine — the boxes deployed together, the way docker compose brings up a set of services. charly deploy add puts a deploy in it; charly deploy del reverses it |
| plugin | a candy that teaches charly a new word — it carries a plugin: block registering the words it provides, each of which is a provider. A plugin lives in the layer shape, but its role is extending charly |
| provider | a word a plugin registers, which routes to that plugin when charly sees it — a kind, verb, command, step, builder, or substrate |
| kind | the class of a top-level name in a charly.yml — the entity keywords |
| group | a kind that holds a set of deploys started and torn down together |
| verb | a probe a plan: step can call — the check vocabulary |
| command | a charly subcommand — the CLI vocabulary |
| step | an install operation in a plan: |
| builder | a multi-stage build pattern a box can select |
| plan | the ordered acceptance spec a candy carries, baked into its image as an OCI label — distinct from the install plan |
| install plan | the target-neutral form of what a deploy installs — produced once from a box’s candy list, then every substrate realises it its own way |
| check bed | a deploy marked disposable: true — one whose candybox exists to be destroyed and rebuilt, which is what authorises charly to run an unattended full test cycle on it |
Agents drive the same surface
Section titled “Agents drive the same surface”charly serves its whole command tree over MCP — the Model Context Protocol, the open
standard for exposing tools to an AI agent. It serves on either Streamable HTTP or stdio; an MCP
client sees charly’s commands as ordinary tools.
mcp is itself an out-of-process command plugin, discovered from a project’s candy/plugin-mcp
rather than compiled into the binary — so point charly at a project that supplies it:
charly --repo opencharly/plugin-mcp mcp serveAGENTS.md is the complete, harness-neutral rulebook; CLAUDE.md is one adapter of
it, not a separate source of truth. Supporting a new harness means adding an adapter, never
porting the project.
opencharly/marketplace ships one skill — a
packaged instruction set an agent loads — for every candy, box, command and contributor
subsystem. It also ships reusable agents: executors that drive the charly check beds and return
verbatim proof; enforcers that gate claims. It installs through the plugin manager of each
supported harness — Claude Code, Codex CLI, Kimi Code and pi. The exact commands are in
that repository’s README, which doubles as the full skill index.
Documentation
Section titled “Documentation”Everything factual about a candy, box, plugin or verb is generated from the sources in this repository and published at opencharly.ai, so it cannot drift from the code the way a hand-maintained copy in this file would.
| You want | Go to |
|---|---|
| to build your first thing | Quickstart → Authoring a candy |
| the vocabulary | The words |
| the ideas, in order, with runnable examples | The concepts tour — twelve short pages |
| every command and flag | CLI reference + The charly CLI |
| every candy and box | Candy reference · Box reference |
“what implements cdp:?” |
Provider index |
| something is broken | Troubleshooting |
| why the project looks like this | The vision · What it is reacting to |
| dated history | each repo’s CHANGELOG/, one file per CalVer version |
License
Section titled “License”MIT