feat: process sandbox via firejail (closes #17) - #19
Merged
Conversation
Adds an OS-level guarantee that the executor role cannot write outside
its assigned git worktree. The kernel still relies on the existing
worktree + scope check for protecting evolution/accepted, but it can now
also prevent a misbehaving executor from leaking writes to /tmp, ~/.ssh
or anywhere else during an experiment.
Design
- New `evolution_kernel/sandbox.py`: pure-stdlib `SandboxConfig` plus a
single `wrap_argv()` function that prepends a firejail launcher
(`--quiet --noprofile --read-only=/ --read-write=<worktree>
--read-write=<run_dir>` and any user `extra_args`) before the role's
argv. Disabled is bit-for-bit identical to v0.3.
- `evolution_kernel/config.py`: new `sandbox` YAML block (default off).
- `evolution_kernel/governor.py`: `Governor` accepts a `sandbox` kwarg
and only the **executor** invocation is wrapped. Planner and
evaluator are read-mostly and remain unsandboxed.
- `evolution_kernel/cli.py`: forwards `cfg.sandbox` into the governor.
Tests
- `tests/test_pr7a.py` (16 new tests, all green):
- config parsing (defaults, validation errors)
- `wrap_argv` unit tests (disabled, enabled, extra_writable dedup,
extra_args placement, unsupported backend)
- end-to-end with a real firejail subprocess: an executor fixture
that tries to write both inside the worktree and outside it.
Sandbox ON → outside write fails with OSError, escape file
absent on disk. Sandbox OFF → outside write succeeds (sanity).
- Whole suite: 83 passed (was 67).
Example
`examples/sandbox_demo/` ships a runnable demo:
`bash examples/sandbox_demo/setup.sh` initializes a target repo, then
`evolution-kernel --config examples/sandbox_demo/evolution.yml ...`
runs a single round that writes EVOLUTION_MARKER.txt inside the
worktree and proves firejail blocks the planted /tmp escape attempt.
Single-dependency rule preserved (still only PyYAML).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Without it, TestSandboxBlocksEscape skips via the @skipUnless guard and CI gives a false sense of coverage. Installing firejail from the Ubuntu universe is a one-line apt step. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Adds an OS-level process sandbox (via firejail) for the executor role so an experiment cannot write outside its assigned worktree, complementing the existing post-hoc scope check. The new wrapping is opt-in (sandbox.enabled: false by default) to preserve v0.3 byte-compatibility.
Changes:
- New
evolution_kernel/sandbox.pyprovidingSandboxConfig+ a purewrap_argv()(currently onlyfirejailbackend). - Config parsing + Governor plumbing so the executor invocation (in both serial
run_onceand per-branch parallel path) is wrapped withfirejail --read-only=/plus read-write on the worktree and the run's ledger dir; planner/evaluator unchanged. - E2E demo under
examples/sandbox_demo/plus a newtests/test_pr7a.pycovering config parsing,wrap_argvunits, and a real-firejail end-to-end test (with sandbox-off sanity check).
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| evolution_kernel/sandbox.py | New module; SandboxConfig dataclass and wrap_argv firejail wrapper. |
| evolution_kernel/config.py | Adds sandbox field to EvolutionConfig and _parse_sandbox validation. |
| evolution_kernel/governor.py | Stores sandbox config and applies it to the executor invocation in both run_once and _run_single_branch via _run_role(sandbox=...). |
| evolution_kernel/cli.py | Wires cfg.sandbox into the constructed Governor. |
| tests/test_pr7a.py | New unit + E2E tests for config parsing, wrap_argv, and firejail-backed escape blocking. |
| tests/fixtures/executor_escape_attempt.py | Executor fixture that attempts both an in-worktree and an out-of-worktree write. |
| examples/sandbox_demo/{setup.sh,evolution.yml,target/bots/*.py} | Runnable demo of the sandbox feature with planner/executor/evaluator role scripts. |
| README.md, README.zh.md | Documents the new sandbox block, marks the sandbox roadmap item complete, and updates Qwen3-7B→8B references. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+597
to
+606
| if sandbox is not None and sandbox.enabled: | ||
| # Allow writes to the run's ledger directory so the role can persist | ||
| # its --output JSON, plus any role-declared stdout/stderr capture | ||
| # next to it. Everything else stays read-only under the sandbox. | ||
| argv = sandbox_wrap_argv( | ||
| argv, | ||
| worktree=worktree, | ||
| writable=[output_path.parent], | ||
| config=sandbox, | ||
| ) |
Comment on lines
+21
to
+25
| sandbox: | ||
| enabled: true | ||
| backend: firejail | ||
| # extra_args is for advanced operators; defaults are fine for this demo. | ||
| extra_args: [] |
Comment on lines
+359
to
+363
| if not isinstance(entry, str) or not entry.strip(): | ||
| raise ConfigError( | ||
| f"`sandbox.extra_args[{index}]` must be a non-empty string" | ||
| ) | ||
| extras.append(entry.strip()) |
Comment on lines
+90
to
+97
| seen = {worktree_abs} | ||
| for w in writable: | ||
| abs_path = str(Path(w).resolve()) | ||
| if abs_path in seen: | ||
| continue | ||
| seen.add(abs_path) | ||
| prefix.append(f"--read-write={abs_path}") | ||
| prefix.extend(extra_args) |
Comment on lines
+28
to
+36
| planner: | ||
| - "/usr/bin/python3" | ||
| - "bots/planner.py" | ||
| executor: | ||
| - "/usr/bin/python3" | ||
| - "bots/executor.py" | ||
| evaluator: | ||
| - "/usr/bin/python3" | ||
| - "bots/evaluator.py" |
| | Goal evaluator — stops when mission is "won" | ✅ | | ||
| | k-branch parallel exploration (FunSearch / AlphaEvolve style) | ✅ | | ||
| | Process sandbox (firejail / bwrap) for production safety | 🔧 PR #7 | | ||
| | Process sandbox via firejail — executor cannot write outside its worktree | ✅ | |
Comment on lines
+213
to
+222
| self.escape.parent.mkdir(parents=True, exist_ok=True) | ||
| self._old_env = os.environ.get("ESCAPE_TARGET") | ||
| os.environ["ESCAPE_TARGET"] = str(self.escape) | ||
|
|
||
| def tearDown(self): | ||
| if self._old_env is None: | ||
| os.environ.pop("ESCAPE_TARGET", None) | ||
| else: | ||
| os.environ["ESCAPE_TARGET"] = self._old_env | ||
| self._tmp.cleanup() |
Protocol-zero-0
added a commit
that referenced
this pull request
May 14, 2026
Bumps version 0.3.0 → 1.0.0 and updates the status badge to v1.0 following the Phase 4 work (PR #19 firejail sandbox, PR #20 HTTP evidence source). The kernel now has: - A process-level sandbox that stops the executor from writing outside its assigned worktree (firejail backed by OS-level read-only mount, verified end-to-end on CI). - An HTTP evidence source so the observer can pull live state from deployed services into observation.json alongside file and shell sources. Together with the v0.2 multi-round LLM loop, k-branch parallel exploration, goal evaluator, and full ledger audit chain, this crosses the bar for "灵魂插件": a kernel you can point at any git repository and trust to evolve it unattended. 99 tests · CI green on Python 3.10 + 3.12 · single dependency (PyYAML) preserved. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
evolution_kernel/sandbox.py(SandboxConfig+ purewrap_argv()); only the executor invocation is wrapped — planner and evaluator are read-mostly and unaffected.sandbox.enabled / backend / extra_args), default disabled so v0.3 byte-compatibility is preserved.examples/sandbox_demo/and 16 new tests (real firejail subprocess + sanity check that confirms the OFF path still leaks).Closes #17.
Test plan
python3 -m pytest tests/ -q— 83 passed (was 67; +16 new).bash examples/sandbox_demo/setup.shpython3 -m evolution_kernel.cli --config examples/sandbox_demo/evolution.yml --repo /tmp/sandbox-demo-target --ledger /tmp/sandbox-demo-ledgerexecutor_output.jsonshowsoutside_write_ok: falsewithOSError: [Errno 30] Read-only file system./tmp/sandbox-leak-0001.txtdoes not exist on disk.🤖 Generated with Claude Code