kage new demo-a provisions a complete sandbox identity on the local k3s
cluster: a Kata VM, default-deny networking, SSH access, credentials for
the shared Nix store, and a deliberately scoped Forgejo organization and
agent user.
One command establishes the compute, network, storage, and authority
boundaries the cage will operate inside.
The cage is a Kata VM with its own guest kernel, so passwordless root is
safe inside the boundary. /home/cage persists, while the rest of the
filesystem returns to the shared base image on restart. Storey’s
/nix/store is mounted read-only.
The guide fetched here lets an agent discover how to use the surrounding
platform itself. The human-facing experience is unfinished; my workflow
today is to create a cage, start Claude, and get to hacking. More tooling
is planned so the same environment feels natural for humans too.
An ordinary flake, pinning nixpkgs and renaming GNU hello to hello-kage.
Nothing about it is platform-specific. The build input is normal Nix.
The cage evaluates the flake locally, then submits the build to Storey over
mounted-ssh-ng://. This first build is cold, so nixpkgs is fetched and
hello is compiled on the store server: 43.6 seconds.
The resulting path appears in the cage’s read-only store without the cage
ever receiving write access to it.
The result is rooted inside the cage and on the store server. The
server-side root is created under gcroots/per-remote/demo-a through the
patched Nix daemon’s AddDirectRoot operation.
Stock Nix gives an untrusted remote client no safe way to request this. The
protocol extension lets each cage retain its own builds without controlling
garbage collection for anyone else.
cage-repo new asks the forge controller to create the repository. The
controller authenticates the cage using its Kubernetes ServiceAccount token
and derives its Forgejo organization from the namespace attested by the
cluster.
The cage cannot ask to create a repository in somebody else’s organization.
The controller also installs branch protections and prepares the matching
production namespace.
Two rejections in a row. The amended commit cannot replace history already
pushed, and main refuses direct pushes.
These are ordinary Forgejo branch protections. Pushing becomes a checkpoint:
once work reaches the server, nothing the cage does next can erase it.
The cage opens the pull request using its scoped Forgejo token. It can
publish work and ask for review, but it cannot merge or write to main.
Opening the request is the last step it can perform by itself.
A second cage, provisioned by the same command, shows which resources are
shared and which remain isolated.
demo-b-agent belongs only to the demo-b Forgejo organization, so Forgejo
denies access to demo-a.
This is authorization enforced by the forge, not a network trick or naming
convention.
The same flake produces the same derivation, and Storey already has its
output. The second cage reaches the same immutable store path in 2.13
seconds instead of rebuilding it in 43.6.
The cages share the physical Nix store without sharing repositories,
credentials, home directories, or writable state.
Approval and merge use the operator’s Forgejo token from outside every cage.
Agenix delivers the token to the desktop; it is never mounted into a
sandbox.
In practice, review, merge, and the /deploy comment all happen in the
Forgejo web UI. The recording drives them through the API only to keep the
demo in one terminal.
No cage has a credential capable of merging to main, making the human gate
structural rather than procedural.
A /deploy comment reaches a network-restricted Tekton EventListener
through a Forgejo webhook. The trigger accepts it only for a merged pull
request and from a commenter with repository administrator permission.
Tekton clones the approved revision and runs cf push. Korifi then hands
the source to kpack inside g8s, where the production cluster builds its own
image with the Paketo nginx buildpack.
The route declared in manifest.yml now serves the page written inside the
cage. Contour and Envoy handle the route, with a certificate issued by
cert-manager using Let’s Encrypt DNS validation.
The production prerequisites were created with the repository, so the
application is valid on its first request. Every step from agent-authored
code to this response crossed a merge performed by a human.
kage rm deletes the cage’s Kubernetes namespace, including its VM,
persistent home volume, secrets, and network policies, after requiring the
operator to type its name.
The Forgejo organization and repositories live outside the namespace and
survive by construction.
Removing the namespace does not discard the cage’s claims on the shared
store. storey-reap removes the departed client’s per-remote roots, then
runs an ordinary Nix garbage collection.
With those roots released, 181 MiB can be collected safely.
demo-a’s build survives because its server-side root is still held through
AddDirectRoot. The deployed application is unaffected because production
owns its own source revision, image, and runtime state.
The page deployed in this recording is still up at
web.demo-a.pylae.dev.