From ace777dd563863bf1d8b71bb881f8bc7ab8d2558 Mon Sep 17 00:00:00 2001 From: sim Date: Sat, 15 Aug 2026 03:35:10 -0400 Subject: [PATCH] fix(#3534): hermetic child env for fixture home; contain agent read to agents dir --- src/commands.cts | 9 ++++++++- tests/effort-surface-axis.test.cjs | 27 ++++++++++----------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/commands.cts b/src/commands.cts index cc54fa41c..41e4bf13c 100644 --- a/src/commands.cts +++ b/src/commands.cts @@ -635,7 +635,14 @@ function cmdResolveExecution(cwd: string, agentType: string | undefined, raw: bo try { // eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/unbound-method const { getGlobalConfigDir } = require('./runtime-homes.cjs') as { getGlobalConfigDir(runtime: string, explicitDir?: string | null): string }; - const agentPath = path.join(getGlobalConfigDir(runtime), 'agents', `${agentType}.md`); + const agentsDirEff = path.join(getGlobalConfigDir(runtime), 'agents'); + const agentPath = path.join(agentsDirEff, `${agentType}.md`); + // agentType is an unvalidated CLI positional: keep the read inside the + // agents dir so `../../x` cannot point it elsewhere (defense in depth — + // the reflected surface is only a frontmatter effort line). + if (!path.resolve(agentPath).startsWith(path.resolve(agentsDirEff) + path.sep)) { + throw new Error('agent path escapes the agents directory'); + } const agentContent = fs.readFileSync(agentPath, 'utf8'); // eslint-disable-next-line local/no-unbounded-quantifier -- same lazy `*?` bounded by the `^---$/m` closing anchor as the sibling frontmatter regexes in this file const fmMatchEff = /^---\r?\n([\s\S]*?)^---\r?$/m.exec(agentContent); diff --git a/tests/effort-surface-axis.test.cjs b/tests/effort-surface-axis.test.cjs index 3903531ae..938dfbb5f 100644 --- a/tests/effort-surface-axis.test.cjs +++ b/tests/effort-surface-axis.test.cjs @@ -102,9 +102,9 @@ describe('#3534 resolve-execution reports resolved AND effective effort', () => return home; } - function resolveExecution(dir, agent = 'gsd-executor', extra = []) { + function resolveExecution(dir, agent = 'gsd-executor', extra = [], env = {}) { return JSON.parse( - runGsdTools(`query resolve-execution ${agent} ${extra.join(' ')}`, dir).output, + runGsdTools(`query resolve-execution ${agent} ${extra.join(' ')}`, dir, env).output, ); } @@ -112,7 +112,10 @@ describe('#3534 resolve-execution reports resolved AND effective effort', () => const dir = projectWithEffort('high'); t.after(() => cleanup(dir)); const home = agentHome(t, '---\nname: gsd-executor\neffort: low\ndescription: x\n---\nBody.\n'); - const out = resolveExecutionWithClaudeHome(dir, home); + // #3534: pass the fixture home as the CHILD env argument — testEnvBase() + // blanks CLAUDE_CONFIG_DIR after the process.env spread, so a process.env + // mutation never reaches the child (and a dev's real ~/.claude would). + const out = resolveExecution(dir, 'gsd-executor', [], { CLAUDE_CONFIG_DIR: home }); assert.equal(out.effort, 'high', 'resolved cascade value unchanged'); assert.equal(out.effort_effective, 'low', 'the installed frontmatter value'); assert.equal(out.effort_effective_source, 'frontmatter'); @@ -126,7 +129,7 @@ describe('#3534 resolve-execution reports resolved AND effective effort', () => const dir = projectWithEffort('high'); t.after(() => cleanup(dir)); const home = agentHome(t, '---\nname: gsd-executor\ndescription: x\n---\nBody.\n'); - const out = resolveExecutionWithClaudeHome(dir, home); + const out = resolveExecution(dir, 'gsd-executor', [], { CLAUDE_CONFIG_DIR: home }); assert.equal(out.effort, 'high'); assert.equal(out.effort_effective, 'inherit', 'absent key = follows the session'); assert.equal(out.effort_effective_source, 'frontmatter-absent'); @@ -136,7 +139,7 @@ describe('#3534 resolve-execution reports resolved AND effective effort', () => const dir = projectWithEffort('high'); t.after(() => cleanup(dir)); const home = agentHome(t, null); - const out = resolveExecutionWithClaudeHome(dir, home); + const out = resolveExecution(dir, 'gsd-executor', [], { CLAUDE_CONFIG_DIR: home }); assert.equal(out.effort_effective, out.effort); assert.equal(out.effort_effective_source, 'resolved'); }); @@ -158,7 +161,7 @@ describe('#3534 resolve-execution reports resolved AND effective effort', () => const dir = projectWithEffort('high'); t.after(() => cleanup(dir)); const home = agentHome(t, ['---', 'name: gsd-executor', 'effort: xhigh', 'description: x', '---', 'Body.', ''].join('\r\n')); - const out = resolveExecutionWithClaudeHome(dir, home); + const out = resolveExecution(dir, 'gsd-executor', [], { CLAUDE_CONFIG_DIR: home }); assert.equal(out.effort_effective, 'xhigh'); assert.equal(out.effort_effective_source, 'frontmatter'); }); @@ -167,21 +170,11 @@ describe('#3534 resolve-execution reports resolved AND effective effort', () => const dir = projectWithEffort('high'); t.after(() => cleanup(dir)); const home = agentHome(t, 'No frontmatter here at all.\n'); - const out = resolveExecutionWithClaudeHome(dir, home); + const out = resolveExecution(dir, 'gsd-executor', [], { CLAUDE_CONFIG_DIR: home }); assert.equal(out.effort_effective, out.effort); assert.equal(out.effort_effective_source, 'resolved'); }); - function resolveExecutionWithClaudeHome(dir, home) { - const prev = process.env.CLAUDE_CONFIG_DIR; - process.env.CLAUDE_CONFIG_DIR = home; - try { - return resolveExecution(dir); - } finally { - if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR; - else process.env.CLAUDE_CONFIG_DIR = prev; - } - } }); describe('#2481 effortSurface — closed vocabulary', () => {