Skip to content

feat(#452): Transient Sandbox core — container-only execution + evidence (PR 1/3)#461

Merged
richard-devbot merged 2 commits into
mainfrom
feat/452-sandbox-scientist
Jul 22, 2026
Merged

feat(#452): Transient Sandbox core — container-only execution + evidence (PR 1/3)#461
richard-devbot merged 2 commits into
mainfrom
feat/452-sandbox-scientist

Conversation

@richard-devbot

@richard-devbot richard-devbot commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Part of #452 (PR 1 of 3 — does not close the issue). Spec + threat model on the issue; posture signed off as container-only.

What this delivers (the core "Scientist")

runInSandbox(runDir, { taskId, command, network, timeoutMs }) → a harness-authored execution evidence record from the real container exit code, so validation can stop trusting self-reported tests_run.

Container-only, locked down (per sign-off):

  • Runs only inside docker/podman. No unconfined child-process tier. No runtime → execution: unverified (status observed, exit_code: null) → gate degrades to contract validation, never unconfined, never false-green.
  • Every invocation: --network none (default; opt-in bridge), no host env, --cap-drop ALL + no-new-privileges, non-root user, --read-only root + 64m tmpfs, memory/pids/cpu caps, code mounted read-only, hard wall-clock timeout → SIGKILL the tree.
  • Evidence authored from the child's exit code — never from anything the executed code writes.

Verification

Injectable runtime probe + spawn, so CI proves it without a daemon: argv lockdown (no-net/caps/non-root/RO-mount/limits), docker→podman→null detection, exit-0 PASS / non-zero FAIL with captured stdout/stderr, timeout→kill→FAIL, and the no-runtime→unverified degrade. Full suite 1565/0 · typecheck + lint + security clean.

Next

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added isolated container execution for untrusted commands with restricted networking, resources, filesystem access, and privileges.
    • Added automatic Docker/Podman runtime detection.
    • Added execution results with status, exit code, duration, and captured output evidence.
    • Added timeout handling that terminates stalled executions.
    • Safely reports executions as unverified when no container runtime is available or startup fails.
  • Tests

    • Added coverage for runtime detection, sandbox security settings, execution outcomes, output capture, and timeout behavior.

…nce (PR 1/3)

"The Scientist": execute untrusted builder commands and capture the REAL exit
code/output as harness-authored evidence, replacing self-reported tests_run.

Posture (Richardson's call): CONTAINER-ONLY. No unconfined child-process tier
ever. runInSandbox runs the command only inside docker/podman; with no runtime
it returns an `execution: unverified` record (status 'observed', exit null) so
the gate degrades to contract validation — never unconfined, never a false
green. Every invocation is locked down: --network none (default; opt-in bridge),
no host env, --cap-drop ALL + no-new-privileges, non-root, --read-only root +
64m tmpfs, memory/pids/cpu caps, code mounted READ-ONLY, hard wall-clock
timeout that SIGKILLs the tree. Evidence (kind:'execution' with exit_code,
duration_ms, tier, bounded stdout/stderr tails, truncated) is authored from the
child's exit code — never read from anything the executed code writes.

Injectable runtime probe + spawn so CI verifies the argv lockdown, exit-code
mapping, timeout kill, and the no-runtime degrade with no daemon.

Next: PR 2 wires this into sdlc_validate (execution evidence replaces the
self-report at the gate; failing stderr → the #451 critique). PR 3 adds a
doctor tier report. 1565/0, typecheck + lint + security clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@richard-devbot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 51 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e4405f85-8b6d-4e16-ba03-1ba9003c6f7f

📥 Commits

Reviewing files that changed from the base of the PR and between a17cc13 and 7af05a2.

📒 Files selected for processing (1)
  • src/core/harness/sandbox.js
📝 Walkthrough

Walkthrough

Adds a container-only execution harness with runtime detection, hardened Docker/Podman arguments, bounded output capture, timeout termination, structured status mapping, and tests covering unavailable runtimes, isolation flags, exit outcomes, and timeouts.

Changes

Transient sandbox execution

Layer / File(s) Summary
Sandbox contract and invocation
src/core/harness/sandbox.js, tests/sandbox-execution-452.test.js
Defines runtime detection and constructs Docker/Podman arguments with isolation, resource limits, read-only mounting, and optional networking; tests validate these behaviors.
Sandbox execution and evidence
src/core/harness/sandbox.js, tests/sandbox-execution-452.test.js
Executes commands with injectable dependencies, captures bounded stdout/stderr, handles unavailable runtimes and errors, terminates timed-out processes, and maps results to structured execution records.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

  • Issue 452 — Directly covers the transient sandbox execution capability implemented here.

Poem

A rabbit hops where containers gleam,
Docker and Podman guard the stream.
No network sneaks through the midnight gate,
Timeouts thump when processes wait.
PASS or FAIL, the clues are spun—
Safe little burrow, evidence won!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: core transient sandbox container-only execution with evidence capture.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/452-sandbox-scientist

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/sandbox-execution-452.test.js (1)

20-33: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Consider an opt-in real-runtime smoke test alongside the mocked unit tests.

All tests here inject spawn/probe/runtime, which is great for deterministic CI, but none exercise an actual docker/podman daemon. The fakeChild.kill (line 24) always "succeeds" synchronously, which would never have surfaced the real-world gap where killing the CLI client doesn't necessarily stop the container (see sandbox.js comment). Consider adding one integration test — skipped cleanly via detectContainerRuntime() when no runtime is present (mirroring the existing browser-test skip pattern) — that runs a real hung command and asserts the container itself is actually gone afterward (docker ps/podman ps).

As per coding guidelines, "tests must exercise the real enforcement path rather than only mocks."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/sandbox-execution-452.test.js` around lines 20 - 33, Add an opt-in
integration smoke test alongside the mocked tests that uses
detectContainerRuntime() to skip cleanly when Docker or Podman is unavailable,
then runs a genuinely hung command through the real sandbox enforcement path.
After timeout or termination, verify via the runtime’s ps command that the
container itself no longer exists, covering the behavior not represented by
fakeChild.kill.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/core/harness/sandbox.js`:
- Around line 116-154: Update the timeout enforcement around the child process
and finish handler so the container started by the docker/podman client is given
a unique name or identifier and is explicitly stopped or killed on timeout,
rather than relying only on child.kill('SIGKILL'). Preserve the existing timeout
result while ensuring cleanup targets the actual container, and record any
failure from the container-kill operation instead of silently swallowing it.

In `@tests/sandbox-execution-452.test.js`:
- Line 26: Declare setImmediate as a recognized global for tests/**/*.js in
eslint.config.js, or enable the appropriate Node test environment, so the
setImmediate call in the sandbox execution test passes no-undef validation.

---

Nitpick comments:
In `@tests/sandbox-execution-452.test.js`:
- Around line 20-33: Add an opt-in integration smoke test alongside the mocked
tests that uses detectContainerRuntime() to skip cleanly when Docker or Podman
is unavailable, then runs a genuinely hung command through the real sandbox
enforcement path. After timeout or termination, verify via the runtime’s ps
command that the container itself no longer exists, covering the behavior not
represented by fakeChild.kill.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9b097ce3-9e4f-4f4b-a94e-9a6cc6d97c8f

📥 Commits

Reviewing files that changed from the base of the PR and between a1303ba and a17cc13.

📒 Files selected for processing (2)
  • src/core/harness/sandbox.js
  • tests/sandbox-execution-452.test.js

Comment on lines +116 to +154
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
try { child.kill('SIGKILL'); } catch { /* already gone */ }
}, bounded);
timer.unref?.();

const finish = (exitCode) => {
if (settled) return;
settled = true;
clearTimeout(timer);
const stdout = tail(Buffer.concat(out.map((c) => (Buffer.isBuffer(c) ? c : Buffer.from(c)))));
const stderr = tail(Buffer.concat(errBuf.map((c) => (Buffer.isBuffer(c) ? c : Buffer.from(c)))));
const duration = (deps.now ? deps.now() : Date.now()) - start;
const passed = !timedOut && exitCode === 0;
resolvePromise({
task_id: taskId, kind: 'execution',
status: passed ? 'PASS' : 'FAIL',
tier: runtime,
evidence: timedOut
? `execution timed out after ${bounded}ms (killed)`
: `exit ${exitCode} in ${duration}ms`,
command, exit_code: timedOut ? null : exitCode, duration_ms: duration,
network, stdout_tail: stdout.text, stderr_tail: stderr.text,
truncated: stdout.truncated || stderr.truncated,
});
};

child.on('error', (err) => {
if (settled) return;
settled = true;
clearTimeout(timer);
resolvePromise({
task_id: taskId, kind: 'execution', status: 'observed', tier: 'unverified',
evidence: `container execution error: ${err?.message ?? err}`,
command, exit_code: null, duration_ms: 0, network, truncated: false,
});
});
child.on('close', (code) => finish(code));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Timeout only kills the local CLI client, not the container — "kills the whole process tree" is not actually enforced.

child.kill('SIGKILL') (line 119) terminates the spawned docker run/podman run client process, but that process is essentially a thin API client to the daemon (dockerd/podman). Killing the client does not reliably stop the running container — this is a well-documented Docker/Podman gotcha (orphaned containers surviving client termination). So:

  • The header's promise ("A hard wall-clock timeout kills the whole process tree", lines 13-18/16) is not backed by runtime enforcement, violating the guideline that every documented guardrail must be enforced in code, not just comments.
  • A runaway or malicious command — especially with network: true — can keep executing (and potentially exfiltrating) after the harness has already reported FAIL/timed-out, silently defeating the sandbox's core security guarantee.
  • Line 119's catch { /* already gone */ } also silently swallows any kill failure with no record, compounding the gap (guideline: don't silently swallow errors on enforcement paths).
🔒️ Suggested fix — name the container and kill it by name/id, not just the client
 export function buildSandboxArgv(runtime, { runDir, command, network = false, image = 'alpine:3.20', limits = {} }) {
   const mem = limits.memory ?? '512m';
   const pids = String(limits.pids ?? 256);
   const cpus = String(limits.cpus ?? 1);
+  const name = limits.name ?? `rstack-sandbox-${Date.now()}-${Math.random().toString(36).slice(2)}`;
   return [
     runtime, 'run', '--rm',
+    '--name', name,
     '--network', network ? 'bridge' : 'none',
     const timer = setTimeout(() => {
       timedOut = true;
-      try { child.kill('SIGKILL'); } catch { /* already gone */ }
+      try { child.kill('SIGKILL'); } catch (e) { /* record e via structured log */ }
+      try { spawnSync(runtime, ['kill', containerName], { stdio: 'ignore', timeout: 5_000 }); } catch (e) { /* record e */ }
     }, bounded);

As per coding guidelines, "Every documented guardrail, approval gate, and contract promise must be enforced at runtime in src/core/harness/, never by prompt text alone" and "Do not silently swallow errors in governance, audit, persistence, or enforcement paths."

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 116-119: Avoid using the initial state variable in setState
Context: setTimeout(() => {
timedOut = true;
try { child.kill('SIGKILL'); } catch { /* already gone */ }
}, bounded)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/core/harness/sandbox.js` around lines 116 - 154, Update the timeout
enforcement around the child process and finish handler so the container started
by the docker/podman client is given a unique name or identifier and is
explicitly stopped or killed on timeout, rather than relying only on
child.kill('SIGKILL'). Preserve the existing timeout result while ensuring
cleanup targets the actual container, and record any failure from the
container-kill operation instead of silently swallowing it.

Source: Coding guidelines

child.stderr = new EventEmitter();
child.kill = () => { child.killed = true; child.emit('close', null); };
if (!hang) {
setImmediate(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether ESLint config declares node/commonjs env or globals for the tests directory
fd -e json -e js -e cjs -e mjs eslintrc . 
rg -n "env" -A5 $(fd -i eslintrc.json -o -i eslintrc.js 2>/dev/null | head -1) 2>/dev/null
rg -n "\"env\"|env:" package.json .eslintrc* eslint.config.* 2>/dev/null

Repository: richard-devbot/SDLC-rstack

Length of output: 239


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate ESLint/package files =="
fd -a -H -t f '(^|/)(eslint\.config\.[cm]?js|\.eslintrc(\..+)?|package\.json)$' .

echo
echo "== package.json (relevant lint bits) =="
if [ -f package.json ]; then
  rg -n '"eslint|lint|test"|"type"|\"env\"|globals' package.json || true
fi

echo
echo "== eslint config contents (first 250 lines each) =="
for f in $(fd -a -H -t f '(^|/)(eslint\.config\.[cm]?js|\.eslintrc(\..+)?|package\.json)$' .); do
  echo "--- $f ---"
  sed -n '1,250p' "$f"
done

echo
echo "== test file around flagged line =="
sed -n '1,80p' tests/sandbox-execution-452.test.js

Repository: richard-devbot/SDLC-rstack

Length of output: 8422


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "FILES:"
git ls-files 'package.json' '.eslintrc*' 'eslint.config.*' 'tests/sandbox-execution-452.test.js'
echo
for f in package.json .eslintrc* eslint.config.* tests/sandbox-execution-452.test.js; do
  [ -f "$f" ] || continue
  echo "===== $f ====="
  sed -n '1,120p' "$f"
done

Repository: richard-devbot/SDLC-rstack

Length of output: 8359


Declare setImmediate for test files. eslint.config.js covers tests/**/*.js, but it omits setImmediate, so tests/sandbox-execution-452.test.js:26 can still trip no-undef. Add it to the test globals or use a Node test env.

🧰 Tools
🪛 ESLint

[error] 26-26: 'setImmediate' is not defined.

(no-undef)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/sandbox-execution-452.test.js` at line 26, Declare setImmediate as a
recognized global for tests/**/*.js in eslint.config.js, or enable the
appropriate Node test environment, so the setImmediate call in the sandbox
execution test passes no-undef validation.

Source: Linters/SAST tools

…out test)

CI (cleaner event loop than local) surfaced 'Promise resolution is still
pending but the event loop has already resolved' on the hung-command test: the
timeout timer was unref'd, so with no other loop activity it never fired and
the promise hung. A kill-timeout for a runaway container MUST fire — removed
the unref (it is always cleared on close, so it never over-holds the loop).
Verified stable x3 in isolation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@richard-devbot
richard-devbot merged commit 53039ee into main Jul 22, 2026
9 checks passed
@richard-devbot
richard-devbot deleted the feat/452-sandbox-scientist branch July 22, 2026 05:21
richard-devbot added a commit that referenced this pull request Jul 22, 2026
fix(#452): reap the container on timeout (CodeRabbit follow-up to #461)
richard-devbot pushed a commit that referenced this pull request Jul 22, 2026
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants