Repository navigation
fix(sandbox): E2B fails fast on RALPH_DIR at the workspace root or outside it (#383) - #387
Conversation
…tside it (#383) validate_e2b_sandbox_config now rejects a RALPH_DIR that is the workspace root (., a/.., $PWD, the symlink-resolved path) or outside the workspace (absolute, ../x) before any sandbox is created. At the root the control-dir guards cannot tell control state from project files and the upload came back empty or leaked state; outside it the control files got wrong tar member paths.
Cross-family review (codex, pre-PR — diff unchanged since review, HEAD
|
|
Claude finished @frankbria's task in 9m 15s —— View job Precision bug-hunt review — complete✅ GLM review: no defects found.
What was verifiedGuard logic (
Tests — the bats setup Disclosed limitations confirmed as described (not counted as findings — both are in the PR body):
One degenerate pre-existing (not in this diff) note: if the CWD were deleted mid-run, No files were modified and nothing was pushed. |
Demo verification (Showboat, real
|
| Criterion | Action | Outcome evidence | Status |
|---|---|---|---|
Workspace-root RALPH_DIR fails fast with a clear message |
RALPH_DIR=., sub/.., <project abs path> with ralph --sandbox e2b |
Each: Error: RALPH_DIR '…' is the workspace root — --sandbox e2b needs it in a subdirectory (e.g. .ralph), exit 1, E2B helper calls: none |
VERIFIED |
Outside-workspace RALPH_DIR fails fast for E2B |
RALPH_DIR=../outside-ralph and its absolute path (valid control files there) |
Each: Error: RALPH_DIR '…' is outside the workspace — --sandbox e2b needs it inside the project (e.g. .ralph), exit 1, E2B helper calls: none |
VERIFIED |
Tests for both shapes in tests/unit/test_sandbox_e2b.bats |
bats tests/unit/test_sandbox_e2b.bats -f 383 |
4 of 4 pass: root rejected, outside rejected, in-workspace accepted, init_e2b_sandbox fails before any helper call |
VERIFIED |
Control: default .ralph unaffected |
RALPH_DIR=.ralph ralph --sandbox e2b |
No RALPH_DIR error; ralph proceeds to the helper (E2B helper calls: check), where the mock fails by design |
VERIFIED |
The demo also shows the hazard being closed: with RALPH_DIR=. the upload list for a project containing app.py shrinks from 9 entries to just .ralphrc.
Full Showboat transcript
Issue #383: E2B fails fast on RALPH_DIR at the workspace root or outside it
2026-10-10T06:40:54Z by Showboat 0.6.1
Setup: a throwaway git project with control files in three places (project root, .ralph/, and a sibling outside-ralph/). run.sh <RALPH_DIR> runs the real ralph_loop.sh --sandbox e2b from PR #387 in that project. The E2B python interpreter is a mock that records every helper call and then fails, so "E2B helper calls: none" proves ralph stopped before contacting E2B.
cat <tmp>/run.sh | sed -n '8,19p'T=<tmp>
cd "$T/demo/proj" || exit 2
rm -f "$T/helper_calls.log"
E2B_API_KEY=demo-key SANDBOX_E2B_PYTHON="$T/mock-python" RALPH_DIR="$1" \
timeout 30 bash "$R/ralph_loop.sh" --sandbox e2b 2>&1 \
| sed 's/\x1b\[[0-9;]*m//g' | grep -E "RALPH_DIR|E2B" | sed "s|$T|<tmp>|g"
echo "ralph exit code: ${PIPESTATUS[0]}"
if [[ -f "$T/helper_calls.log" ]]; then
echo "E2B helper calls: $(tr '\n' ' ' < "$T/helper_calls.log")"
else
echo "E2B helper calls: none"
fi
The hazard (the upload-list builder is the same on main and on this branch — the PR only adds the validator). With the default RALPH_DIR=.ralph, the project file app.py and the control files are listed for upload. With RALPH_DIR=. the whole project listing disappears: app.py, the prompt and the plan are all gone and only .ralphrc is left. Before this PR nothing stopped ralph from uploading that.
<tmp>/uploadlist.sh .ralph; echo; <tmp>/uploadlist.sh ..ralph/AGENT.md
.ralph/PROMPT.md
.ralph/fix_plan.md
.ralphrc
.ralphrc
AGENT.md
PROMPT.md
app.py
fix_plan.md
(9 entries for RALPH_DIR=.ralph)
.ralphrc
(1 entries for RALPH_DIR=.)
Criterion 1 — a workspace-root RALPH_DIR fails fast with a clear message. Three spellings of the root: ., sub/.. (the project has a sub/ directory) and the absolute project path. Each exits 1 with the "is the workspace root" error and makes no E2B helper call.
<tmp>/run.sh .; echo; <tmp>/run.sh sub/..; echo; <tmp>/run.sh <tmp>/demo/proj[2026-10-09 23:41:36] [INFO] ☁️ E2B sandbox mode enabled (template: base, session timeout: 3600s)
Error: RALPH_DIR '.' is the workspace root — --sandbox e2b needs it in a subdirectory (e.g. .ralph)
[2026-10-09 23:41:36] [ERROR] E2B sandbox setup failed — refusing to fall back to host execution
ralph exit code: 1
E2B helper calls: none
[2026-10-09 23:41:36] [INFO] ☁️ E2B sandbox mode enabled (template: base, session timeout: 3600s)
Error: RALPH_DIR 'sub/..' is the workspace root — --sandbox e2b needs it in a subdirectory (e.g. .ralph)
[2026-10-09 23:41:36] [ERROR] E2B sandbox setup failed — refusing to fall back to host execution
ralph exit code: 1
E2B helper calls: none
[2026-10-09 23:41:36] [INFO] ☁️ E2B sandbox mode enabled (template: base, session timeout: 3600s)
Error: RALPH_DIR '<tmp>/demo/proj' is the workspace root — --sandbox e2b needs it in a subdirectory (e.g. .ralph)
[2026-10-09 23:41:36] [ERROR] E2B sandbox setup failed — refusing to fall back to host execution
ralph exit code: 1
E2B helper calls: none
Criterion 2 — an outside-workspace RALPH_DIR fails fast for E2B. A relative ../outside-ralph and the same directory as an absolute path (both hold valid control files, so ralph gets past its own integrity check). Each exits 1 with the "is outside the workspace" error and makes no E2B helper call.
<tmp>/run.sh ../outside-ralph; echo; <tmp>/run.sh <tmp>/demo/outside-ralph[2026-10-09 23:41:36] [INFO] ☁️ E2B sandbox mode enabled (template: base, session timeout: 3600s)
Error: RALPH_DIR '../outside-ralph' is outside the workspace — --sandbox e2b needs it inside the project (e.g. .ralph)
[2026-10-09 23:41:36] [ERROR] E2B sandbox setup failed — refusing to fall back to host execution
ralph exit code: 1
E2B helper calls: none
[2026-10-09 23:41:36] [INFO] ☁️ E2B sandbox mode enabled (template: base, session timeout: 3600s)
Error: RALPH_DIR '<tmp>/demo/outside-ralph' is outside the workspace — --sandbox e2b needs it inside the project (e.g. .ralph)
[2026-10-09 23:41:36] [ERROR] E2B sandbox setup failed — refusing to fall back to host execution
ralph exit code: 1
E2B helper calls: none
Control — the default RALPH_DIR=.ralph is not affected: validation passes and ralph goes on to call the E2B helper (check), which is where the mock interpreter deliberately fails.
<tmp>/run.sh .ralph[2026-10-09 23:41:36] [INFO] ☁️ E2B sandbox mode enabled (template: base, session timeout: 3600s)
[2026-10-09 23:41:36] [ERROR] E2B SDK unavailable: helper check failed. Install it with: pip install e2b
[2026-10-09 23:41:36] [ERROR] E2B sandbox setup failed — refusing to fall back to host execution
ralph exit code: 1
E2B helper calls: check
Criterion 3 — tests for both shapes live in tests/unit/test_sandbox_e2b.bats.
cd /home/frankbria/projects/ralph-claude-code && npx bats tests/unit/test_sandbox_e2b.bats -f 383 2>&1 | grep -E '^(1\.\.|ok|not ok)'1..4
ok 1 validate_e2b_sandbox_config: rejects RALPH_DIR at the workspace root (#383)
ok 2 validate_e2b_sandbox_config: rejects RALPH_DIR outside the workspace (#383)
ok 3 validate_e2b_sandbox_config: accepts in-workspace RALPH_DIR spellings (#383)
ok 4 init_e2b_sandbox: workspace-root RALPH_DIR fails before any helper call (#383)
Review — PR 387: E2B fail-fast on bad RALPH_DIRThe approach is correct: adding the guard to Logic: why the explicit PWD comparisons are load-bearingThe root check is: rb=$(_e2b_control_dir)
if [[ -z "$rb" || "$rb" == "$(_e2b_collapse_path "$PWD")" || "$rb" == "$(_e2b_collapse_path "$(pwd -P)")" ]]; thenThe The existing comment explains the why (what breaks if RALPH_DIR is at the root), but a brief note that the explicit comparisons handle the absolute-exact-match case that # _e2b_control_dir only strips an absolute RALPH_DIR when it has a subdir
# suffix; an exact match ($RALPH_DIR == $PWD) comes back as the full path,
# so we compare explicitly.Not a blocker, but would prevent confusion during a future refactor. Test diagnostics: acceptance loop has no per-iteration failure messageThe rejection loops correctly identify which value triggered the failure: [[ "$output" == *"workspace root"* ]] || fail "no root message for RALPH_DIR=$d: $output"The acceptance loop does not: for d in .ralph state/ralph "$TEST_DIR/state/ralph/"; do
export RALPH_DIR="$d"
run validate_e2b_sandbox_config
assert_success # ← if this fails, the output doesn't say which $d
doneSince Trailing note on
|
…; note why the $PWD comparisons are needed
Review triage (after the first push)
No Critical or Major findings. The follow-up commit changes one code comment and two test assertions only; e2b unit file still 104/104. |
|
Claude finished @frankbria's task in 4m 20s —— View job Precision bug-hunt review — complete (follow-up commit
|
| Severity | File:line | Finding |
|---|---|---|
| — | — | No confirmed defects in the diff |
- Fetch full diff (
origin/main...HEAD) and the follow-up commit's changes - Check existing comments for already-posted findings (prior GLM pass: clean; claude-review suggestions applied in
becae89) - Read
lib/sandbox_e2b.sh— validator,_e2b_control_dir,_e2b_collapse_path,init_e2b_sandboxand callers - Verify the new test assertions and comment are correct (no logic regressions)
- Post summary
Delta since the last review
becae89 touches exactly two things, neither behavioral:
- Comment in
validate_e2b_sandbox_config(lib/sandbox_e2b.sh:186-188) — verified accurate against the code:_e2b_control_dir's stripping loop uses"$base"/*, which requires a subdirectory suffix, so an absoluteRALPH_DIRexactly equal to the workspace survives as a full absolute path and is caught only by the two explicit$PWD/pwd -Pcomparisons. The comment states this correctly. - Two test assertions (
tests/unit/test_sandbox_e2b.bats:219, 228) —assert_success→[[ "$status" -eq 0 ]] || fail "rejected valid RALPH_DIR=$d: …"and the bare pattern check →… || fail "expected workspace root message: …". Both are the correct bats idiom ($statusis always numeric fromrun;failinterpolates$d/$outputfor per-case diagnostics). No assertion semantics changed — the same conditions fail on the same inputs.
Re-verification of the guard (unchanged code, re-checked)
- Root check ordering is load-bearing and correct: the root branch runs before the outside branch, so an absolute
RALPH_DIR == $PWDgets the accurate "workspace root" message rather than a misleading "outside"; an absolute path genuinely outside matches/*only after failing both base-equality comparisons. Both comparisons mirror exactly the two bases_e2b_control_diritself tries ($PWD,pwd -P, both collapsed), so a symlinked CWD is handled consistently. - Outside patterns don't over-match:
..hidden(a literal dirname) correctly fails none of/*,..,../*—../*requires a/after... Escape spellings (..,../out,sub/../../out) all collapse via_e2b_collapse_path(dense-arrayunset-of-tail verified) into a form one of the three patterns catches. - Placement:
validate_e2b_sandbox_configremains the first statement ofinit_e2b_sandbox, ahead of the transport probe, credentials, state writes, and sandbox creation;ralph_loop.sh:2773is its only production caller, failure is fatal with cleanup and no host fallback. - No injection/quoting hazards:
$rbis the quoted subject (never the pattern) in every[[ == ]]; the unquoted/*/../*are the intended patterns;$RALPH_DIRreaches only stderr messages. - Tests: the bats setup
cd "$TEST_DIR"makes$PWD == $TEST_DIR, so the root-spelling assertions exercise the real production bases; exports don't leak across bats test subshells; the accept-list includes the absolute-with-trailing-slash spelling ($TEST_DIR/state/ralph/) that collapses and strips tostate/ralph.
Previously disclosed limitations (cwd / false rejection, symlinked RALPH_DIR) are unchanged and remain documented in the PR body — not counted as findings.
No inline findings — nothing met the defect bar.
Follow-up review (after the polish commit)All three suggestions from the prior review have been applied: 1. Explanatory comment in The added comment correctly explains both why the guard lives in the validator (not in 2. Acceptance loop diagnostic — test file, acceptance test [[ "$status" -eq 0 ]] || fail "rejected valid RALPH_DIR=$d: $output"Now consistent with the rejection loops — a failure will identify which path triggered it. 3. [[ "$output" == *"workspace root"* ]] || fail "expected workspace root message: $output"Message check is now airtight: a false No new issues found. The implementation is correct, the comment explains the non-obvious invariant, and the test suite covers root spellings, outside spellings, in-workspace spellings, and the ordering guarantee (validator fires before any helper call). Good to merge. |
|
Post-merge disposition:
|
Summary
ralph --sandbox e2bnow fails fast whenRALPH_DIRis the workspace root or outside the workspace, instead of uploading an empty or state-leaking workspace.validate_e2b_sandbox_config(first step ofinit_e2b_sandbox, before any sandbox is created) derives the control dir via_e2b_control_dirand rejects:.,./,a/..,$PWD,$PWD/, the symlink-resolved path →RALPH_DIR '…' is the workspace root..,../x,sub/../../x→RALPH_DIR '…' is outside the workspace_e2b_control_diritself is unchanged.Acceptance Criteria
RALPH_DIRfails fast with a clear messageRALPH_DIRfails fast for E2B (the issue allowed fail-fast or a fixed in-sandbox mapping; this takes fail-fast)tests/unit/test_sandbox_e2b.batsTest Plan
.ralph, nested, absolute-inside) still accepted,init_e2b_sandboxfails before any helper callnpm test: 1368 passed, 0 failed (was 1364)RALPH_DIR=. | $PWD | <outside dir> ralph --sandbox e2bexits 1 with the new message before the helper runs;RALPH_DIR=.ralphpasses the checkReview
Pre-PR cross-family review:
codex review --base main— no findings. opencode/GLM (the primary reviewer) stalled twice with no output, so codex was used as the fallback.Known Limitations / Intentionally Deferred
RALPH_DIRis rejected for E2B rather than mapped to a fixed in-sandbox path. E2B with external control state is unsupported until someone needs it.RALPH_DIRthat is a symlink to an outside directory is not detected./as the working directory, an in-workspaceRALPH_DIRsuch as/.ralphis reported as outside (_e2b_control_dirdoes not strip a bare/base).Implementation Notes
The root check lives in the validator, not in
_e2b_control_dir: an existing #376 test relies on$PWD/staying absolute there so the upload pathspec stays valid.Closes #383