Let an AI agent run commands without a shell to misuse
An agent can read and write files in a folder of its own and run the programs you name, one command at a time, each in a sandbox that ends when the command does.
In the adminAgents
An agent's workspace is a folder of its own, kept between runs. The agent never holds a shell. It names a program it was given and the arguments, and Quilzo runs that one command in a fresh box against the workspace. Nothing it writes is ever handed to a shell to interpret, unless you name a shell as one of its programs. Its files are read and written by Quilzo itself, through a handle on the workspace that cannot be walked out of.
Declare what it may run
{
"name": "site-keeper",
"kind": "task",
"purpose": "Keep the site's repository tidy and its tests passing",
"capabilities": ["file_list", "file_read", "file_write", "shell_run"],
"autonomy": "draft",
"shell": {
"programs": ["git", "go", "make", "ls", "grep"],
"hosts": ["proxy.golang.org", "github.com"],
"ask": ["network"],
"timeout": "2m", "memory_mb": 2048, "quota_mb": 2048
},
"budget": {"steps": 40, "tool_calls": 1, "duration": "20m"}
}
| Field | What it allows |
|---|---|
programs | The only programs it may run, by name. Each is found on the box's own path and run with the arguments exactly as given: no shell, so nothing in an argument is ever a command. |
hosts | What a command that reaches the network may reach, through the run's proxy, on 443. None means no command reaches the network at all. |
ask | Which kinds of command wait for a person: network (the default) and write. A command that deletes or rewrites history always waits. |
workspace | The folder. Empty means one of its own beside the store's other folders. You may name another, as long as it is not the store, a folder holding the store, or a system folder. |
timeout, memory_mb, quota_mb | Each command's time and memory, and the workspace's size. A workspace over its quota takes no more writes until something is removed. |
What the agent can do
| Action | What it does |
|---|---|
file_list | Lists a folder in the workspace: names, sizes and kinds. |
file_read | Reads a file's text, up to 256 KB. A binary file is described, not shown. |
file_write | Writes a file whole (up to 1 MB), making the folders it needs, and says what changed. |
shell_run | Runs one program with its arguments, and gives back the exit status, the start and end of what it printed, how long it took and which files it changed. |
A file is written where its name says: never through a link, and never inside git's own folder, where a configured hook would be a program git runs later. Reading the workspace, listing it, or what a command printed, taints the run. A repository cloned or a file a page gave may hold somebody else's words, so the run is treated like one that read stored content.
What each command is
Every command is classed before it runs, from the program and its arguments, without guessing at intent. A program not known is taken as writing.
| Kind | Examples | Waits for a person |
|---|---|---|
| reads | ls, cat, grep, git status, git log, go vet | never |
| writes | git commit, go build, go test, make, sed -i, anything unknown | when ask has write |
| reaches the network | git fetch, git push, go mod download, npm install, curl | when ask has network |
| destroys | rm, git reset, git clean -f, git push --force, find -delete, and any shell, which can do anything | always |
The kind is enforced as well as asked about. Only a command classed as reaching the network is given a way out, so go test runs with no network at all. A command classed as reading gets the workspace read-only, so whatever it runs in turn (a repository's configured hook, a search tool's preprocessor) cannot change it either.
One limit to know: a program that runs other code, such as a build, a script or a test suite, is writing, and that code can delete what the command may write. "Destroys" catches the commands that say so. To approve every change, add write to ask, or keep the workspace in git, so nothing written is lost. If the run holds something private and has read somebody else's words, a command that reaches the network also carries the breaker's reason when it asks. What a run writes into the workspace after reading something private stays private to the next run on it.
How a command runs
- A fresh box for each command, the same one programs and the browser run in: its own namespaces, Landlock, the system call filter, no capabilities, and no way to reach a socket on the machine's filesystem. It may write only the workspace and the run's scratch home. It reads the system's programs and libraries, and nothing of the store. A command starts in well under a tenth of a second.
- A home made fresh for each run, and removed when the run ends, so nothing a command leaves there (a configuration, a startup file) reaches the next run. The agent's caches (Go modules and builds, npm, pip) are kept between runs, and only a command that writes may write them. A command that only reads gets a home of its own, removed after it.
- No prompts. Its input is empty and its environment says
CI=1, with no pager and no colour. A program that would ask a question gets nothing rather than waiting. Both of its output streams are captured together. - Nothing outlives it. When the command ends, everything it started ends with it. The time limit, Stop the run and the run's budget all end it the same way.
- One run at a time works on a workspace; another is told which run has it.
- The quota holds while a command runs. The workspace and the run's home are measured every few seconds, and a command that goes over is stopped. Memory is bounded per process, and each command may start at most 512, so one command can still use a good share of a small machine.
What is kept, and what a person sees
- Each step's record in the signed log names the command, and has the SHA-256 digest of everything it printed and its exit status. The output itself (up to 1 MB) is kept with the run, sealed when the store has a keyring, and pruned with the run.
- The run's page shows each command with its result, a link to the whole output, and which files it changed. A held command shows exactly what would run and why it is held: Let it go ahead or Decline, there or with
quilzo agent approve RUN STEP. - Running with less, it only reads. A read-only token, an agent held to proposing, and every evaluation get only the commands that read. Nothing is written.
quilzo agent workspace site-keeper
quilzo agent workspace site-keeper --clear
The first says where the workspace is and how big. --clear empties it, with its caches, and the log records who did.
Not yet
- Credentials in commands. A secret handed into the box would be readable by whatever runs there, so a command that has to sign in to something is not offered one yet.
- macOS and Windows hosts. The box is Linux's.
- Long-running services and interactive programs. Commands run to the end with no terminal, which is what an agent needs; a terminal a person can take over comes later.