An AI agent built a standalone version of my agent fleet in a day and reported it finished and tested. A review found the Landlock sandbox was never applied, a path traversal exposed every file, and the production runner ran agent-written code outside its sandbox. What we found, how we fixed it, and the tests that prove the fixes.

Sovereign Fleet 0.4.0: The Sandbox Was a Claim. Now It Is a Kernel Rule.

My agent fleet runs coding-agent CLIs (OpenCode, Goose, Aider, Pi, Claude Code) against my repositories, one task per git worktree, each CLI confined by the Linux kernel. It started as a set of scripts on my DGX Spark, and it has a history of bugs I first blamed on the models and of gates that prove code runs, not claims. This week Antigravity, one of the coordinating agents, pulled it out into a standalone package called Sovereign Fleet: a FastAPI daemon, a task queue, a model roster, a phone-friendly dashboard and an MCP server. Twenty-nine commits in one day, 41 green tests, a long README, a blog article and four drafts. The handover said: finished and tested.

Then Claude reviewed it against the code, the logs and a live run. This article is the result. It is less flattering than the drafts were, and more useful.

What the claims said and what the code did

Claim (README, dashboard, API)What the code did
”Linux Landlock kernel isolation, sub-millisecond cages”Built a Landlock policy object and never applied it. Every agent CLI ran unconfined. The health endpoint returned the string "Landlock LSM Enforced" regardless.
”Zero host leakage”GET /static/..%2f..%2f...%2fetc%2fhostname returned the file. Any file my user can read, without authentication, on 0.0.0.0:8444.
”Git worktree isolation, changes reviewed before merging”After each run the engine removed the worktree with --force. Nothing was committed first: every agent’s work was deleted.
”Multi-key quota ladder, failover rotation”The code exists in fleet/core/quota.py. No task run ever called it.
”GateGuard test gates”GateGuard parses Python files for syntax errors and compares claimed with changed files. It never ran a test.
”9 / 10 plugins online”A hardcoded number. So was the coordinator name.

None of the 41 tests exercised the sandbox, the static route or the engine’s cleanup. They tested that functions returned what their authors expected, which is how a test suite can be green while the headline feature does not exist. The service had been running for a day. Its access log showed only two clients, localhost and my phone over Tailscale, and no traversal attempts. I stopped it before anything else.

The fixes, and how each one is proven

A real sandbox. The fleet’s production runner already had a working one: a small launcher that restricts itself with Landlock and then execs the agent CLI, so the restriction is inherited and cannot be lifted. Sovereign Fleet now uses the same design (python -m fleet.core.sandbox -- <cli>):

Starting an agent then looks like this; the paths are what the policy grants, everything else is invisible:

python -m fleet.core.sandbox \
  --rw ~/.fleet/worktrees/task-1791 --rw ~/.config/opencode --rw ~/.cache \
  --ro ~/.nvm --ro ~/.local/bin --ro /data/projects/myapp/.git \
  --port 443 --port 53 --port 30001 \
  -- opencode run "Add input validation to the signup form"

If the kernel cannot enforce all of it, the launcher prints sandbox: refusing to run unsandboxed and exits with 126.

The proof is a test that runs a real shell inside the real sandbox and checks that it can write its worktree, cannot read a secret file in the home directory, cannot write outside, and cannot open a Unix socket. My first version of that test put the “secret” under /tmp, which agents may write, and the test correctly failed. Tests that fight back are the useful kind.

Authentication. Every /api route needs a token, as a Bearer header for the CLI and MCP server, or as an HttpOnly, SameSite=Strict cookie holding an HMAC of the token for the dashboard, which asks for it once. The daemon binds 127.0.0.1 by default; on my Spark it binds the Tailscale address only, with TLS. Static files are served by Starlette’s StaticFiles, which refuses paths outside its directory; the traversal URLs from the review are now test cases.

Keep the work. The engine commits whatever the agent changed to the branch fleet/<task-id> before it removes the worktree, also when the run fails, so a broken attempt can still be inspected. A run that changed nothing leaves no branch.

The worst finding: the production runner

Reviewing the new package meant reading the old runner it came from, and that is where the worst finding was.

The runner sandboxed every agent CLI correctly. Then it verified the agent’s work by running the repository’s gate (verify.sh, pytest, npm run build) outside the sandbox, with the full environment: every API key, the GitHub token, the SSH agent socket. But a gate executes code the agent was allowed to change. An agent could add one line to verify.sh or a conftest.py, and the runner would run it unconfined a minute later with all credentials at hand. The sandbox protected the front door while the gate held the back door open.

Gates now run in the same sandbox as the agent, with an environment that carries no keys:

def _gate_run(cmd, exec_dir, env, timeout):
    if os.environ.get("FLEET_SANDBOX", "1") != "0":
        cmd = _sandboxed(cmd, exec_dir, "gate", extra_rw=_gate_rw(exec_dir))
    snap = _git_snapshot(exec_dir)
    try:
        gate_env = {**_lane_env(env, "gate"), "ASTRO_TELEMETRY_DISABLED": "1"}
        return subprocess.run(cmd, cwd=str(exec_dir), capture_output=True, text=True,
                              timeout=timeout, env=gate_env, stdin=subprocess.DEVNULL)
    finally:
        _git_restore(snap)

_lane_env(env, "gate") drops every variable that holds a key, because the gate lane is allowed none.

A second, subtler path: the sandbox lets an agent write its worktree and that worktree’s own git directory. An agent could point the worktree’s .git file at a git directory of its own whose config names a core.fsmonitor program. The runner’s next git status, outside the sandbox, would run it. Git’s own safe.directory check does not help here, because the files belong to the same user. The runner now snapshots the worktree’s git pointers before every agent and gate run and restores them afterwards:

_GIT_POINTERS = ("commondir", "gitdir", "config.worktree")

def _git_snapshot(cwd: Path) -> dict[Path, bytes | None]:
    dotgit = cwd / ".git"
    if not dotgit.is_file() or dotgit.is_symlink():
        return {}
    snap = {dotgit: dotgit.read_bytes()}
    gitdir = _worktree_gitdir(cwd)
    if gitdir:
        for name in _GIT_POINTERS:
            p = gitdir / name
            snap[p] = p.read_bytes() if p.is_file() and not p.is_symlink() else None
    return snap

A file the agent added is deleted, a changed one is written back, and the runner logs a warning either way. Sovereign Fleet also keeps git data read-only for agents and runs git with fsmonitor and hooks disabled. The regression test builds exactly this attack. I made it fail first: with a bare evil repository the hook never fired and the test passed without the fix. With a normal one it fired, the test went red, and only then green with the fix.

What a sandbox shows you about your own build

Moving the gates into the sandbox broke them, and every break was a hidden side effect:

After that, Vozonda’s full verify.sh passes inside the sandbox (1,713 tests, one skipped), and the blog build passes in 14 seconds.

Pi, the newest harness

One tool in the list is new: Pi, a coding agent from Earendil (MIT licensed, source) that calls itself “a minimal agent harness”. It ships a minimal system prompt, no sub-agents and no plan mode, and speaks to 15+ providers. Minimal is exactly what a free-tier lane needs: Groq’s free tier counts tokens per minute, and a harness that spends thousands of them on its own instructions burns the budget before the task starts.

In the fleet, Pi runs as the lane pi_groq_120b: GPT-OSS-120B on Groq, inside the same sandbox as every other CLI, and with its tool list cut to read,edit,write. No bash: Pi edits files and the gates run the tests, which narrows what a confused or prompt-injected model can do even inside the sandbox. If the lane fails, the escalation ladder hands the task to Groq through Aider, then to the local Qwen on the Spark, then to Gemini.

Two honest notes. The lane was wired in on 2026-10-12 and has not run a real task yet, so I have no pass rate for it yet, while the other lanes have measured ones from real tasks (and, for the tools, from agent-bench). And the Pi integration arrived together with a “multi-key failover” that rotated to a second Groq or Gemini account when the first hit its limit. Groq’s policy forbids exactly that, Google restricts projects for it, so it is gone again: a rate limit now always means a different lane, never a second account.

Where Sovereign Fleet stands

Version 0.4.0 is honest alpha software: 73 tests, including the security ones that run against the real kernel. The daemon queues tasks and shows the roster; fleet run executes one task end to end. It does not yet pick queued tasks up by itself, and it does not yet route by quota. The README now says that, in a table titled What works today, and the dashboard shows the sandbox state the kernel reports rather than a string.

What is still open

As of October 2026, four things separate Sovereign Fleet from replacing the scripts it came from:

  1. An executor loop in the daemon. Today a queued task waits until someone runs it. The loop has to respect max_concurrent and the dependency chains, and it has to stay off while the old runner owns the same repositories, because two executors on one worktree directory is a race.
  2. Cancel that stops the process. Cancel only marks the task today; a running CLI keeps running until its timeout.
  3. Routing by quota. The roster already knows which lanes are online, blocked or keyless; the engine does not ask it yet. This is the feature the fleet exists for on my machine: use the free cloud tiers and the local Qwen on the Spark first, and spend subscription quota only where a task needs it.
  4. A project test command as a gate. GateGuard’s syntax check catches broken Python, not broken behaviour. The old runner’s verify.sh gates do; the package needs the same, sandboxed like the agent.

None of these is shipped, and the README says so.

Update, 18 October 2026: two of the four are on the main branch now, not yet in a release. Routing by quota: the router ranks the lanes by their measured pass rate for the kind of task, rests a lane after a provider limit (120 seconds, or until the daily quota resets), weighs paid lanes three times heavier than free and local ones, and leaves out lanes whose key, credit or model is gone. A project test command: each project’s own gate runs sandboxed before anything lands. The executor loop and a cancel that stops the process are still open. How the router first learned from the wrong numbers is in An Agent Said Verified.

The lesson

The lesson is the one this fleet keeps teaching: an agent’s “finished and tested” is a claim. Forty-one green tests proved that forty-one assumptions held. They could not prove the thing nobody had tested. Verify against the code, the logs and a live run before a claim reaches a README, a dashboard or an article.

The next round of the same story, where every claim had a passing test and the tests still missed a comment loop, a wrong working directory and a commit the engine deleted, is in An Agent Said Verified.

Was this worth it? Zap the article.

Value for value, no signup. Sats go straight to the writer.

… sats zapped
… zaps
Today 7d 30d All-time
Unique readers — — — —
Page views — — — —
—
All Article Insights →