From 9f8e38ac4e6d69927ffc28103cef126f0341a0a8 Mon Sep 17 00:00:00 2001 From: Richardson Gunde Date: Wed, 22 Jul 2026 13:31:12 +0530 Subject: [PATCH 1/3] feat(#452): readiness-based runtime detection + opt-in bounded auto-start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - detectContainerRuntime now probes `docker/podman info` (daemon REACHABLE), not `--version` (binary installed). A present binary with a stopped daemon used to make runInSandbox spawn `docker run`, hit a connection error, and mis-record it as a test FAILURE — now it correctly degrades to 'unverified'. - detectInstalledRuntime keeps the `--version` check so auto-start can tell 'installed but stopped' (startable) from 'not installed' (nothing to start). - runtimeStartCommand: platform-aware engine boot (open -a Docker / Docker Desktop / podman machine start; null on linux system daemons). - startContainerRuntime: OPT-IN, bounded auto-start — launches the engine then polls readiness with a HARD timeout so a gate never hangs on a booting GUI. Fully injectable. Co-Authored-By: Claude Opus 4.8 --- src/core/harness/sandbox.js | 91 +++++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 4 deletions(-) diff --git a/src/core/harness/sandbox.js b/src/core/harness/sandbox.js index d0d7e861..97ecd7c9 100644 --- a/src/core/harness/sandbox.js +++ b/src/core/harness/sandbox.js @@ -29,16 +29,41 @@ export const MAX_TIMEOUT_MS = 600_000; const OUTPUT_TAIL_BYTES = 8 * 1024; // bounded like goal-check's maxBuffer precedent const RUNTIMES = ['docker', 'podman']; -// Detect an available container runtime. Injectable probe so tests never shell -// out. Returns 'docker' | 'podman' | null. -export function detectContainerRuntime({ probe = defaultProbe } = {}) { +// Detect a USABLE container runtime — one whose engine daemon is actually +// reachable, not merely installed. `docker info` / `podman info` exit 0 only +// when the daemon is up; a present binary with a STOPPED daemon must NOT count, +// or runInSandbox would spawn `docker run`, hit "Cannot connect to the Docker +// daemon", and mis-record that connection error as a test FAILURE instead of an +// honest 'unverified' degrade. Injectable probe so tests never shell out. +// Returns 'docker' | 'podman' | null. +export function detectContainerRuntime({ probe = defaultReadyProbe } = {}) { for (const runtime of RUNTIMES) { if (probe(runtime)) return runtime; } return null; } -function defaultProbe(runtime) { +function defaultReadyProbe(runtime) { + try { + const res = spawnSync(runtime, ['info'], { stdio: 'ignore', timeout: 10_000 }); + return res.status === 0; + } catch { + return false; + } +} + +// Detect an INSTALLED runtime binary (daemon may or may not be running). Used by +// the auto-start path to tell "installed but stopped" (startable) apart from +// "not installed at all" (nothing to start). `--version` succeeds even when the +// daemon is down. +export function detectInstalledRuntime({ probe = defaultInstalledProbe } = {}) { + for (const runtime of RUNTIMES) { + if (probe(runtime)) return runtime; + } + return null; +} + +function defaultInstalledProbe(runtime) { try { const res = spawnSync(runtime, ['--version'], { stdio: 'ignore', timeout: 5_000 }); return res.status === 0; @@ -47,6 +72,64 @@ function defaultProbe(runtime) { } } +// The command that boots each runtime's engine, per platform. Docker Desktop is +// a GUI app on mac/Windows (`open -a Docker` / launch "Docker Desktop"); its +// daemon then takes ~10-30s to become ready. Podman is headless +// (`podman machine start`). On Linux the docker daemon is a system service we +// will not `sudo systemctl` for the user — returns null there (manual start). +export function runtimeStartCommand(runtime, platform = process.platform) { + if (runtime === 'podman') return { cmd: 'podman', args: ['machine', 'start'] }; + if (runtime === 'docker') { + if (platform === 'darwin') return { cmd: 'open', args: ['-a', 'Docker'] }; + if (platform === 'win32') return { cmd: 'cmd', args: ['/c', 'start', '', 'Docker Desktop'] }; + return null; // linux: `sudo systemctl start docker` — not ours to run + } + return null; +} + +// OPT-IN, bounded auto-start. NEVER runs unless the caller explicitly asks +// (doctor --start-runtime or RSTACK_SANDBOX_AUTOSTART=1) — a gate must never +// silently launch a heavyweight GUI mid-validation. Launches the engine, then +// polls readiness with a HARD timeout so nothing blocks indefinitely on a +// booting daemon. Fully injectable for tests. +export async function startContainerRuntime({ + installedProbe = defaultInstalledProbe, + readyProbe = defaultReadyProbe, + spawnImpl = spawn, + platform = process.platform, + timeoutMs = 60_000, + pollMs = 2_000, + now = Date.now, + sleep = (ms) => new Promise((resolveSleep) => setTimeout(resolveSleep, ms)), +} = {}) { + const runtime = detectInstalledRuntime({ probe: installedProbe }); + if (!runtime) { + return { runtime: null, started: false, ready: false, message: 'no docker/podman binary on PATH — install one first' }; + } + if (readyProbe(runtime)) { + return { runtime, started: false, ready: true, message: `${runtime} engine already running` }; + } + const startCmd = runtimeStartCommand(runtime, platform); + if (!startCmd) { + return { runtime, started: false, ready: false, message: `${runtime} engine is not running and RStack cannot auto-start it on ${platform} — start it manually` }; + } + try { + const child = spawnImpl(startCmd.cmd, startCmd.args, { stdio: 'ignore' }); + child?.on?.('error', () => {}); + child?.unref?.(); + } catch (err) { + return { runtime, started: false, ready: false, message: `failed to launch ${runtime}: ${err?.message ?? err}` }; + } + const start = now(); + while (now() - start < timeoutMs) { + await sleep(pollMs); + if (readyProbe(runtime)) { + return { runtime, started: true, ready: true, message: `${runtime} engine started and is ready` }; + } + } + return { runtime, started: true, ready: false, message: `${runtime} launch initiated but the engine was not ready within ${timeoutMs}ms — try again shortly` }; +} + // Build the locked-down `docker/podman run` argv for an untrusted command. // Exported so the security flags are testable without a daemon. export function buildSandboxArgv(runtime, { runDir, command, network = false, image = 'alpine:3.20', limits = {}, containerName }) { From e12b1da33ffb3c0e06d7bc310dcd4e660e8adc29 Mon Sep 17 00:00:00 2001 From: Richardson Gunde Date: Wed, 22 Jul 2026 13:31:21 +0530 Subject: [PATCH 2/3] feat(#452): doctor reports the active sandbox tier + --start-runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sandboxTierCheck (pure, exported) answers 'are my test results real or self-reported?': container-verified (PASS) when a runtime is ready and a command is configured; WARN for ready-but-no-command, installed-but-stopped (with the exact start hint), no-runtime, or disabled — never a false green. - checkSandboxTier wires it into runDoctor after the config checks. - doctor --start-runtime (or RSTACK_SANDBOX_AUTOSTART=1) opts into the bounded auto-start; never launches an engine otherwise. Co-Authored-By: Claude Opus 4.8 --- bin/rstack-agents.js | 4 ++- src/commands/doctor.js | 64 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/bin/rstack-agents.js b/bin/rstack-agents.js index 03d23f5b..28e56d24 100755 --- a/bin/rstack-agents.js +++ b/bin/rstack-agents.js @@ -611,13 +611,15 @@ program .option('-f, --framework ', `host framework to check wiring for: ${DOCTOR_FRAMEWORKS.join(' | ')} (auto-detected if omitted)`) .option('-p, --project ', 'project root (defaults to current directory)') .option('--json', 'print the structured report as JSON for CI') + .option('--start-runtime', 'opt-in: if the sandbox container engine (docker/podman) is installed but stopped, launch it and wait (bounded) for it to become ready — never launched otherwise') .action(async (opts) => { try { if (opts.framework && !DOCTOR_FRAMEWORKS.includes(opts.framework)) { log.error(`Unknown framework "${opts.framework}". Expected one of: ${DOCTOR_FRAMEWORKS.join(', ')}`); process.exit(1); } - const report = await runDoctor({ framework: opts.framework, project: opts.project }); + const autostart = opts.startRuntime === true || process.env.RSTACK_SANDBOX_AUTOSTART === '1'; + const report = await runDoctor({ framework: opts.framework, project: opts.project, autostart }); if (opts.json) { process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); } else { diff --git a/src/commands/doctor.js b/src/commands/doctor.js index 69f2f9c4..097f2d3e 100644 --- a/src/commands/doctor.js +++ b/src/commands/doctor.js @@ -29,6 +29,8 @@ import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { validateProjectConfigs } from '../core/harness/config-validation.js'; +// #452 PR3: report the active sandbox execution tier + opt-in bounded auto-start. +import { detectContainerRuntime, detectInstalledRuntime, loadSandboxConfig, startContainerRuntime } from '../core/harness/sandbox.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); const PACKAGE_ROOT = resolve(__dirname, '..', '..'); @@ -190,6 +192,61 @@ function checkEmailNotifications(projectRoot, env = process.env) { 'Set RSTACK_ACS_CONNECTION_STRING in the environment and add channels.email.sender + recipients/routing to .rstack/notifications.json (see docs/mintlify/reference/approvals.mdx)'); } +// --- sandbox execution tier (#452 PR3) -------------------------------------- + +// Pure decision: given what's detected + the config, what tier is active and +// what does the operator need to know? Separated from the I/O below so the +// verdict logic is unit-testable without a container daemon. `autostart` is the +// startContainerRuntime result (or null when not requested). +export function sandboxTierCheck({ readyRuntime, installedRuntime, config, autostart }) { + const note = autostart?.message ? ` (auto-start: ${autostart.message})` : ''; + if (config && config.enabled === false) { + return check('sandbox execution tier', WARN, + `sandbox execution is DISABLED in config — sdlc_validate uses the self-reported tests_run only, never container-verified${note}`, + 'Set sandbox.enabled=true in .rstack/rstack.config.json to run tests in a container'); + } + if (readyRuntime) { + const hasCommand = Boolean(config?.command) || Object.keys(config?.perStage ?? {}).length > 0; + if (hasCommand) { + return check('sandbox execution tier', PASS, + `container-verified (${readyRuntime}) — sdlc_validate runs the authoritative command in a locked-down ${readyRuntime} container and authors execution evidence from the REAL exit code${note}`); + } + return check('sandbox execution tier', WARN, + `${readyRuntime} engine is ready, but NO authoritative test command is configured (sandbox.command / sandbox.per_stage) — execution stays UNVERIFIED until one is set or a task carries test_command${note}`, + 'Add sandbox.command (e.g. "npm test") to .rstack/rstack.config.json to turn on container-verified execution'); + } + if (installedRuntime) { + const fix = installedRuntime === 'podman' + ? 'Start the engine: podman machine start (or: rstack-agents doctor --start-runtime)' + : process.platform === 'darwin' + ? 'Start Docker Desktop (open -a Docker), or: rstack-agents doctor --start-runtime' + : `Start the ${installedRuntime} engine, or: rstack-agents doctor --start-runtime`; + return check('sandbox execution tier', WARN, + `${installedRuntime} is installed but its engine is not running — execution degrades to UNVERIFIED (contract validation only), never a false green${note}`, + fix); + } + return check('sandbox execution tier', WARN, + `no container runtime (docker/podman) available — execution is UNVERIFIED: sdlc_validate falls back to contract validation + self-reported tests_run, never a false green${note}`, + 'Install Docker or Podman for container-verified execution (https://docs.docker.com/get-docker/ or https://podman.io)'); +} + +async function checkSandboxTier(projectRoot, { autostart = false } = {}) { + let config; + try { + config = await loadSandboxConfig(projectRoot); + } catch (error) { + return check('sandbox execution tier', WARN, `could not read sandbox config: ${error.message}`, null); + } + let readyRuntime = detectContainerRuntime({}); + const installedRuntime = detectInstalledRuntime({}); + let autostartResult = null; + if (autostart && !readyRuntime) { + autostartResult = await startContainerRuntime({}); + if (autostartResult.ready) readyRuntime = autostartResult.runtime; + } + return sandboxTierCheck({ readyRuntime, installedRuntime, config, autostart: autostartResult }); +} + // --- framework wiring ------------------------------------------------------- async function detectFrameworkLocal(projectRoot) { @@ -737,7 +794,7 @@ async function checkSelfDependency(cwd) { // --- orchestration ---------------------------------------------------------- -export async function runDoctor({ framework, project, cwd = process.cwd() } = {}) { +export async function runDoctor({ framework, project, cwd = process.cwd(), autostart = false } = {}) { const projectRoot = resolve(project ?? cwd); const checks = []; @@ -761,6 +818,11 @@ export async function runDoctor({ framework, project, cwd = process.cwd() } = {} const emailCheck = checkEmailNotifications(projectRoot); if (emailCheck) checks.push(emailCheck); + // Sandbox execution tier (#452): container-verified vs. unverified — the honest + // answer to "are my test results real or self-reported?". With autostart, tries + // an opt-in bounded engine start first (never a hidden launch otherwise). + checks.push(await checkSandboxTier(projectRoot, { autostart })); + // Framework wiring (explicit --framework, else auto-detect; else all-generic) const detected = framework ?? await detectFrameworkLocal(projectRoot); const effectiveFramework = framework ?? detected ?? 'custom'; From 18f597f612531190180d9099281b9230bf656d37 Mon Sep 17 00:00:00 2001 From: Richardson Gunde Date: Wed, 22 Jul 2026 13:31:26 +0530 Subject: [PATCH 3/3] test(#452): PR3 doctor tier + auto-start coverage sandboxTierCheck verdicts (PASS/WARN across ready/no-command/stopped/absent/ disabled + auto-start note), detectInstalledRuntime, platform-aware runtimeStartCommand, and the injectable bounded startContainerRuntime (not-installed / already-running / becomes-ready / times-out / no-start-path). Co-Authored-By: Claude Opus 4.8 --- tests/sandbox-doctor-452.test.js | 136 +++++++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/sandbox-doctor-452.test.js diff --git a/tests/sandbox-doctor-452.test.js b/tests/sandbox-doctor-452.test.js new file mode 100644 index 00000000..7fef271f --- /dev/null +++ b/tests/sandbox-doctor-452.test.js @@ -0,0 +1,136 @@ +/** + * Transient Sandbox — "The Scientist" (#452), PR 3: doctor tier report + + * opt-in bounded runtime auto-start. + * + * Covers the pure tier verdict (sandboxTierCheck), readiness-vs-installed + * detection, per-platform start commands, and the injectable bounded auto-start + * (already-running / not-installed / becomes-ready / times-out) — all without a + * real container daemon. + * + * owner: RStack developed by Richardson Gunde + */ + +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + detectInstalledRuntime, + runtimeStartCommand, + startContainerRuntime, + resolveSandboxConfig, +} from '../src/core/harness/sandbox.js'; +import { sandboxTierCheck } from '../src/commands/doctor.js'; + +// --- sandboxTierCheck (pure verdict) --------------------------------------- + +test('sandboxTierCheck: ready runtime + configured command → PASS container-verified', () => { + const c = sandboxTierCheck({ readyRuntime: 'docker', installedRuntime: 'docker', config: resolveSandboxConfig({ command: 'npm test' }) }); + assert.equal(c.status, 'PASS'); + assert.match(c.detail, /container-verified \(docker\)/); +}); + +test('sandboxTierCheck: ready runtime but NO command → WARN (still unverified)', () => { + const c = sandboxTierCheck({ readyRuntime: 'docker', installedRuntime: 'docker', config: resolveSandboxConfig() }); + assert.equal(c.status, 'WARN'); + assert.match(c.detail, /NO authoritative test command/); +}); + +test('sandboxTierCheck: installed but engine stopped → WARN with start hint', () => { + const c = sandboxTierCheck({ readyRuntime: null, installedRuntime: 'podman', config: resolveSandboxConfig({ command: 'npm test' }) }); + assert.equal(c.status, 'WARN'); + assert.match(c.detail, /engine is not running/); + assert.match(c.fix, /podman machine start|--start-runtime/); +}); + +test('sandboxTierCheck: no runtime at all → WARN unverified, never a false green', () => { + const c = sandboxTierCheck({ readyRuntime: null, installedRuntime: null, config: resolveSandboxConfig({ command: 'npm test' }) }); + assert.equal(c.status, 'WARN'); + assert.match(c.detail, /no container runtime/); + assert.match(c.detail, /never a false green/); +}); + +test('sandboxTierCheck: disabled config → WARN, self-report only', () => { + const c = sandboxTierCheck({ readyRuntime: 'docker', installedRuntime: 'docker', config: resolveSandboxConfig({ enabled: false }) }); + assert.equal(c.status, 'WARN'); + assert.match(c.detail, /DISABLED/); +}); + +test('sandboxTierCheck: surfaces the auto-start outcome note', () => { + const c = sandboxTierCheck({ readyRuntime: 'docker', installedRuntime: 'docker', config: resolveSandboxConfig({ command: 'npm test' }), autostart: { message: 'docker engine started and is ready' } }); + assert.match(c.detail, /auto-start: docker engine started and is ready/); +}); + +// --- detection + start command --------------------------------------------- + +test('detectInstalledRuntime prefers docker, falls back to podman, else null', () => { + assert.equal(detectInstalledRuntime({ probe: (r) => r === 'docker' }), 'docker'); + assert.equal(detectInstalledRuntime({ probe: (r) => r === 'podman' }), 'podman'); + assert.equal(detectInstalledRuntime({ probe: () => false }), null); +}); + +test('runtimeStartCommand is platform-aware', () => { + assert.deepEqual(runtimeStartCommand('podman'), { cmd: 'podman', args: ['machine', 'start'] }); + assert.deepEqual(runtimeStartCommand('docker', 'darwin'), { cmd: 'open', args: ['-a', 'Docker'] }); + assert.equal(runtimeStartCommand('docker', 'linux'), null, 'no auto-start of a linux system daemon'); + assert.equal(runtimeStartCommand('nope', 'darwin'), null); +}); + +// --- startContainerRuntime (bounded, injectable) ---------------------------- + +test('startContainerRuntime: not installed → runtime null, nothing launched', async () => { + let launched = false; + const res = await startContainerRuntime({ installedProbe: () => false, spawnImpl: () => { launched = true; } }); + assert.equal(res.runtime, null); + assert.equal(res.ready, false); + assert.equal(launched, false); +}); + +test('startContainerRuntime: already running → no launch, ready', async () => { + let launched = false; + const res = await startContainerRuntime({ installedProbe: (r) => r === 'docker', readyProbe: () => true, spawnImpl: () => { launched = true; } }); + assert.equal(res.started, false); + assert.equal(res.ready, true); + assert.equal(launched, false, 'never relaunch an already-running engine'); +}); + +test('startContainerRuntime: installed+stopped → launches, polls, becomes ready', async () => { + let ready = false; + const res = await startContainerRuntime({ + installedProbe: (r) => r === 'docker', + readyProbe: () => ready, + spawnImpl: () => { ready = true; return { on() {}, unref() {} }; }, + platform: 'darwin', + sleep: async () => {}, + now: (() => { let t = 0; return () => (t += 1000); })(), + pollMs: 1, timeoutMs: 100000, + }); + assert.equal(res.started, true); + assert.equal(res.ready, true); + assert.equal(res.runtime, 'docker'); +}); + +test('startContainerRuntime: never ready → bounded timeout, ready false', async () => { + const res = await startContainerRuntime({ + installedProbe: (r) => r === 'docker', + readyProbe: () => false, + spawnImpl: () => ({ on() {}, unref() {} }), + platform: 'darwin', + sleep: async () => {}, + now: (() => { let t = 0; return () => (t += 50000); })(), + pollMs: 1, timeoutMs: 100000, + }); + assert.equal(res.started, true); + assert.equal(res.ready, false); + assert.match(res.message, /not ready within/); +}); + +test('startContainerRuntime: installed but no platform start path → honest message', async () => { + const res = await startContainerRuntime({ + installedProbe: (r) => r === 'docker', + readyProbe: () => false, + platform: 'linux', // docker on linux is a system daemon we won't sudo-start + spawnImpl: () => { throw new Error('should not spawn'); }, + }); + assert.equal(res.ready, false); + assert.match(res.message, /cannot auto-start/); +});