From dabd02dc76263ae1928c772a4c9c210b16098b59 Mon Sep 17 00:00:00 2001 From: Chris Phillipson Date: Wed, 12 Aug 2026 16:00:56 -0400 Subject: [PATCH] fix: anchor provider-cli pick spawns at the sandbox project, add a real-repo tripwire MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit akPick() destructured { cwd, ... } but pickSandbox() returns { project, ... }, so every call site spawned `ak x host pick --yes` with cwd undefined — inherited from the test process: the real repository root. HOME/XDG/APPDATA were correctly sandboxed, but pick's PROJECT-scoped writes anchor at repoRoot(cwd), so each full test run rewrote the developer's real .claude/settings.local.json (ENABLE_CODEX=false) and .agentic-qe/llm-config.json (fixture openai chain, empty overrides). Idempotent rewrites kept the corruption invisible on an already-hit repo, and CI's throwaway checkout never surfaced it. The spawn now anchors at cwd ?? project, and an after() tripwire records the real cwd's two project files at module load and asserts them byte-identical when the suite ends — a future harness regression fails loudly instead of corrupting machines silently. Verified: from a healthy repo state the tripwire fired before the anchor fix, and a full pnpm run check now leaves both files hash-identical. Fixes #137 --- tests/kit/provider-cli.test.mjs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/tests/kit/provider-cli.test.mjs b/tests/kit/provider-cli.test.mjs index e43269b..f77619f 100644 --- a/tests/kit/provider-cli.test.mjs +++ b/tests/kit/provider-cli.test.mjs @@ -5,7 +5,7 @@ // real machine's kit.json or ~/.claude — src/lib/paths.mjs's configBase() // reads APPDATA (not XDG_CONFIG_HOME) on win32, so both must be set or the // sandbox is silently bypassed there. -import { test } from 'node:test'; +import { test, after } from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; @@ -14,6 +14,22 @@ import os from 'node:os'; import path from 'node:path'; import { DUAL_ROLE_TIP, JUDGE_BIAS_TIP } from '../../src/lib/providers.mjs'; +// Tripwire (#137): a spawned `ak x host pick` whose cwd falls back to the test +// process's cwd writes PROJECT-scoped config (.claude/settings.local.json, +// .agentic-qe/llm-config.json) into the REAL repository the suite runs from — +// HOME sandboxing cannot catch that class of leak. Record the real cwd's state +// at module load and prove it byte-identical when the suite ends. +const REAL_PROJECT_FILES = ['.claude/settings.local.json', '.agentic-qe/llm-config.json'] + .map((rel) => path.resolve(process.cwd(), rel)); +const readOrNull = (f) => { try { return fs.readFileSync(f, 'utf8'); } catch { return null; } }; +const realProjectBefore = REAL_PROJECT_FILES.map(readOrNull); +after(() => { + REAL_PROJECT_FILES.forEach((f, i) => { + assert.equal(readOrNull(f), realProjectBefore[i], + `${f} was modified by this suite — a spawned ak command leaked out of the sandbox (cwd not anchored to the sandbox project?)`); + }); +}); + const BIN = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../bin/agentic-kit.mjs'); function sandbox({ hosts }) { @@ -163,10 +179,13 @@ function pickSandbox({ return { home, project, binDir, catalog }; } -function akPick(args, { cwd, home, binDir, catalog }, { input, env = {} } = {}) { +function akPick(args, { cwd, project, home, binDir, catalog }, { input, env = {} } = {}) { return spawnSync(process.execPath, [BIN, ...args], { encoding: 'utf8', - cwd, + // Anchor at the sandbox project (#137): an undefined cwd inherits the test + // process's cwd — the real repository — and pick's PROJECT-scoped writes + // (settings.local.json, llm-config.json) would land there. + cwd: cwd ?? project, input, env: { ...process.env,