feat(smolmachines): PRD 0022 sandbox-escape suite green under smolmachines (PRD 0023 chunk 5)
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:
@@ -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
|
||||
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
|
||||
|
||||
Bottles and agents live as Markdown files with YAML frontmatter under
|
||||
|
||||
@@ -23,6 +23,7 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
@@ -71,6 +72,25 @@ class TestSandboxEscape(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
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
|
||||
# passes (it only checks `os.path.isfile`, not that the content is
|
||||
# a real SSH key). Test 5 reaches gitleaks before any SSH attempt
|
||||
@@ -402,7 +422,13 @@ class TestSandboxEscape(unittest.TestCase):
|
||||
("aws", "TEST_SECRET_AWS"),
|
||||
("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:
|
||||
with self.subTest(secret=name):
|
||||
@@ -420,8 +446,7 @@ class TestSandboxEscape(unittest.TestCase):
|
||||
'> README.md\n'
|
||||
'git add README.md\n'
|
||||
'git commit -m "leak" >/dev/null\n'
|
||||
'git remote add origin '
|
||||
f'git://{gate_host}/throwaway.git\n'
|
||||
f'git remote add origin {upstream_url}\n'
|
||||
'git push origin HEAD:refs/heads/master 2>&1\n'
|
||||
)
|
||||
r = self._bottle.exec(script)
|
||||
|
||||
Reference in New Issue
Block a user