Summoning Daemons
Software has a long history of magic metaphors:
As in certain cults it is possible to kill a process if you know its true name. – Ken Thompson and Dennis M. Ritchie
Today is no different. Let’s talk about how I use a summoning circle (container) to constrain my daemonic minions (LLM agents) in cloche
On the Importance of Properly Binding a Minion
Any time we bind (execute) a minion (any software) to a task, we assume it will not break the contract we’ve set for it. e.g. we trust that our database will continue acting like a database and won’t suddenly decide to become a word processor.
…And as anyone in computer security can tell you, most hacking involves tricking some piece of software into breaking its contract to briefly behave like a shell.
While securing software often includes fixing bugs, we also set summoning circles around our minions. We inscribe the runes of IAM and mutter the incantations of least-access-principle and zero-trust in order to restrict their access to (presumably) safe boundaries. Should a rogue stats web service suddenly decide to follow its dreams of being a remote shell server, best practice is to ensure that its shell-form cannot access your financial systems!
LLM coding agents are an especially risky minion for the very reasons they’re so potent. Because their capabilities are so unconstrained (assuming you grant them tool-use permissions) an agent can quickly get into Sorcerer’s Apprentice territory!
Summoning Circles
When designing Cloche I decided one of the design principles must be isolation from the rest of the system. If one of my summoned minions goes rogue and tries to erase all the code I don’t want to lose anything but maybe the task it was actively working on.
To that end, I rely upon the common summoning circle of our industry: Docker containers.
In Cloche, the default is to execute agents within a containerized environment. You don’t even bind mount your project code. Instead, every container gets its own copy of the codebase to act on. Barring a container Jailbreak this ensures that the contained agent can only access or destroy what we’ve put inside the container itself. The rest of everything on the host system is on the outside of the circle.
A Three-Headed Daemon
Of course, if everything stayed inside the container, no useful work could be done.
Cloche divides its responsibilities between four applications, filling three roles:
- The Orchestrator:
clochedis the host-side daemon that manages system state, launches containers and ensures the workflows are followed - The Agent:
cloche-agentthe container-side wrapper that executes workflow commands sent from cloched - and sends back status updates. - The CLI Tools:
clocheandclo- CLI tooling for interacting with the Cloche system.
clocheis host-side and user-facing for interacting with the Cloche system generally.clois a stripped downclochefor use inside the container. It lacks commands for managing the environment, but keeps the commands for passing data between host/container… which is pretty much why it exists.
Generally speaking, cloched is the brains of the operation, with everything else either commanding or supporting it.
Users interact with cloched by making use of the thin-client cloche/clo CLI tools. Since these tools provide
access to the cloched system while keeping no control logic of their own, a single cloched instance can manage
multiple Cloche projects at once, while the CLI tooling provides users with a project-oriented tool as their interface.
The divide between cloche and clo is a little more confusing - why not just use cloche inside containers?
It comes down to the separation of concerns and the least access principle. On the host, the User needs to command
cloched directly - to launch tasks, start/stop the orchestration loop, etc. Inside a container though, we don’t
need (or want!) to expose those same powers. If the container-side environment can send directly executable commands
to the host, it defeats the whole purpose of containizing things in the first place. Instead, we provide a stripped
down version of the CLI tool. (clo is cloche but smaller, get it?)
clo provides only a small subset of the commands that cloche provides, such as clo get <key> to access values
stored in cloched’s key-value store. Thus, both scripts on both sides have access to a simple method for sharing
data between the host and container during a task run - but using an explicitly limited channel.
Breaking the Trifecta
This division of responsibilities allows cloche to strike a balance between functionality (Just Do Things!) and safety (Wait, Don’t Do That). I wanted to be able to turn my agents loose and let them run without asking permission non-stop but I also didn’t feel comfortable exposing my machine to the Lethal Trifecta.
By isolating prompts in containers, I greatly reduce the general exposure to the “Private Data” portion of the Lethal Trifecta (e.g. if my agent gets injected and starts leaking ENV vars, it can only leak what I’ve put inside the container. That’s not nothing, but it’s far less than getting everything on my machine!). Further, docker (and cloche) provides an allow-list for network access, meaning that I can greatly limit Exposure to Untrusted Content and Ability to Externally Communicate. If I put, say, docs.python.org in my allow-list (alongside my agent’s required domains of course!), I can ensure my agents have access to the documentation they need for my python projects, while preventing them from accessing the wild-and-wooly corners of the web.
At the same time, running everything inside a docker environment comes with its own challenges and pain points. Thus, I moved the management of my docker containers into cloche, and converted my main Get Things Done interface into the act of cutting tickets.
Next time, I’ll discuss my current cloche workflows and how I go about automating my work and side project development.