feat(smolmachines): PRD 0022 sandbox-escape suite green under smolmachines (PRD 0023 chunk 5)
test / unit (pull_request) Successful in 26s
test / integration (pull_request) Successful in 41s

Final PRD 0023 chunk. The PRD 0022 attack suite was already
backend-agnostic — it goes through get_bottle_backend(), so the
right dispatch happens based on CLAUDE_BOTTLE_BACKEND. Two
cleanups to make it actually run cleanly under
CLAUDE_BOTTLE_BACKEND=smolmachines:

- setUpClass raises unittest.SkipTest with a useful message when
  CLAUDE_BOTTLE_BACKEND=smolmachines but smolvm isn't on PATH, or
  when the host isn't macOS (libkrun + TSI single-IP allowlist is
  macOS-only in v1). Without this, the test would die deep inside
  backend.prepare's smolmachines_preflight rather than skipping.

- test_5_readme_push_blocked switches from a hardcoded
  `git://git-gate/...` remote URL (only resolvable on docker via
  the bundle's short alias) to the bottle's declared upstream URL
  (`ssh://git@unreachable.invalid:22/throwaway.git`). The agent's
  ~/.gitconfig insteadOf rewrite — set up by provision_git on both
  backends — transparently redirects to the gate, so the same test
  exercises docker's `git://git-gate/...` and smolmachines's
  `git://<bundle_ip>:9418/...` URLs without branching on backend.

README gets a "Backend selection" subsection under Quickstart
documenting CLAUDE_BOTTLE_BACKEND, the macOS-only v1 scope for
smolmachines, and the `curl -sSL .../install.sh | sh` install
prerequisite — per PRD 0023's acceptance criteria.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 14:31:27 -04:00
parent ac8c7ba696
commit 78345b5343
2 changed files with 49 additions and 3 deletions
+21
View File
@@ -190,6 +190,27 @@ The container is removed automatically when the session ends. If the script
is killed with SIGKILL the exit trap won't fire and the container may be is killed with SIGKILL the exit trap won't fire and the container may be
left running; remove it with `docker rm -f <container-name>`. left running; remove it with `docker rm -f <container-name>`.
### Backend selection
The default backend uses Docker for both the agent and the sidecar
bundle. An experimental smolmachines backend runs the agent in a
[smolvm](https://smolmachines.com) micro-VM (libkrun on macOS) and
keeps the sidecar bundle in Docker:
```sh
CLAUDE_BOTTLE_BACKEND=smolmachines ./cli.py start <agent>
```
The smolmachines backend is **macOS-only in v1** (libkrun + TSI
single-IP allowlisting) and requires `smolvm` on PATH:
```sh
curl -sSL https://smolmachines.com/install.sh | sh
```
The integration tests run against whichever backend the env var
selects and skip cleanly when its prerequisites are missing.
## Manifest ## Manifest
Bottles and agents live as Markdown files with YAML frontmatter under Bottles and agents live as Markdown files with YAML frontmatter under
+28 -3
View File
@@ -23,6 +23,7 @@ from __future__ import annotations
import os import os
import shutil import shutil
import sys
import tempfile import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
@@ -71,6 +72,25 @@ class TestSandboxEscape(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls) -> None: def setUpClass(cls) -> None:
# Per-backend prerequisites. Docker is always required (both
# backends use it — docker for the agent + sidecars, smolmachines
# for the sidecar bundle); the class-level @skip_unless_docker
# already covers that. Smolmachines additionally needs smolvm on
# PATH and is macOS-only in v1 (libkrun/TSI). Skip cleanly when
# those are missing rather than die-ing inside backend.prepare.
backend_name = os.environ.get("CLAUDE_BOTTLE_BACKEND", "docker")
if backend_name == "smolmachines":
if sys.platform != "darwin":
raise unittest.SkipTest(
"CLAUDE_BOTTLE_BACKEND=smolmachines is macOS-only in "
"v1 (libkrun TSI)"
)
if shutil.which("smolvm") is None:
raise unittest.SkipTest(
"CLAUDE_BOTTLE_BACKEND=smolmachines requires `smolvm` "
"on PATH: curl -sSL https://smolmachines.com/install.sh | sh"
)
# Throwaway "identity file" so the manifest's _validate_git_entries # Throwaway "identity file" so the manifest's _validate_git_entries
# passes (it only checks `os.path.isfile`, not that the content is # passes (it only checks `os.path.isfile`, not that the content is
# a real SSH key). Test 5 reaches gitleaks before any SSH attempt # a real SSH key). Test 5 reaches gitleaks before any SSH attempt
@@ -402,7 +422,13 @@ class TestSandboxEscape(unittest.TestCase):
("aws", "TEST_SECRET_AWS"), ("aws", "TEST_SECRET_AWS"),
("generic", "TEST_SECRET_GENERIC"), ("generic", "TEST_SECRET_GENERIC"),
] ]
gate_host = "git-gate" # Use the bottle's declared upstream URL; the agent's
# ~/.gitconfig insteadOf rewrite (set up by provision_git)
# redirects to the gate. This makes the test backend-
# agnostic: docker resolves the gate via the short `git-gate`
# alias, smolmachines via `<bundle_ip>:9418` — both
# transparent to the test through insteadOf.
upstream_url = "ssh://git@unreachable.invalid:22/throwaway.git"
for name, var in shapes: for name, var in shapes:
with self.subTest(secret=name): with self.subTest(secret=name):
@@ -420,8 +446,7 @@ class TestSandboxEscape(unittest.TestCase):
'> README.md\n' '> README.md\n'
'git add README.md\n' 'git add README.md\n'
'git commit -m "leak" >/dev/null\n' 'git commit -m "leak" >/dev/null\n'
'git remote add origin ' f'git remote add origin {upstream_url}\n'
f'git://{gate_host}/throwaway.git\n'
'git push origin HEAD:refs/heads/master 2>&1\n' 'git push origin HEAD:refs/heads/master 2>&1\n'
) )
r = self._bottle.exec(script) r = self._bottle.exec(script)