diff --git a/apps/worker/src/run-task/__tests__/run-task.test.ts b/apps/worker/src/run-task/__tests__/run-task.test.ts index dc7082df5..427331e56 100644 --- a/apps/worker/src/run-task/__tests__/run-task.test.ts +++ b/apps/worker/src/run-task/__tests__/run-task.test.ts @@ -235,6 +235,7 @@ vi.mock('../agent-home', () => ({ activateSkillsFolder: activateSkillsFolderMock, readConfiguredSkillsFolder: vi.fn(() => undefined), resolvePackagedSkillsFolder: resolvePackagedSkillsFolderMock, + seedRuntimeHomeMiseGlobalConfig: vi.fn(() => false), })); vi.mock('../create-harness', () => ({ diff --git a/apps/worker/src/run-task/__tests__/zero-integration-gating.test.ts b/apps/worker/src/run-task/__tests__/zero-integration-gating.test.ts index e0daca167..7e5638596 100644 --- a/apps/worker/src/run-task/__tests__/zero-integration-gating.test.ts +++ b/apps/worker/src/run-task/__tests__/zero-integration-gating.test.ts @@ -172,6 +172,7 @@ vi.mock('../agent-home', () => ({ activateSkillsFolder: activateSkillsFolderMock, readConfiguredSkillsFolder: vi.fn(() => undefined), resolvePackagedSkillsFolder: resolvePackagedSkillsFolderMock, + seedRuntimeHomeMiseGlobalConfig: vi.fn(() => false), })); vi.mock('../create-harness', () => ({ diff --git a/apps/worker/src/run-task/agent-home.test.ts b/apps/worker/src/run-task/agent-home.test.ts index a1ca2df84..03814c1ce 100644 --- a/apps/worker/src/run-task/agent-home.test.ts +++ b/apps/worker/src/run-task/agent-home.test.ts @@ -13,6 +13,7 @@ import { join } from 'node:path'; import { generateOpenCodeConfig, rematerializeOpenCodeCredentialFiles, + seedRuntimeHomeMiseGlobalConfig, } from './agent-home'; describe('generateOpenCodeConfig provider support', () => { @@ -784,3 +785,75 @@ describe('rematerializeOpenCodeCredentialFiles', () => { expect(logger.warn).toHaveBeenCalledTimes(1); }); }); + +describe('seedRuntimeHomeMiseGlobalConfig', () => { + const tempDirs: string[] = []; + + afterEach(() => { + for (const tempDir of tempDirs.splice(0)) { + rmSync(tempDir, { recursive: true, force: true }); + } + }); + + function createDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; + } + + function writeMiseConfig(homeDir: string, content: string): string { + const configDir = join(homeDir, '.config', 'mise'); + mkdirSync(configDir, { recursive: true }); + const configPath = join(configDir, 'config.toml'); + writeFileSync(configPath, content, 'utf8'); + return configPath; + } + + it('copies the worker home global config into the runtime home', () => { + const sourceHomeDir = createDir('roomote-mise-source-'); + const homeDir = createDir('roomote-mise-runtime-'); + writeMiseConfig(sourceHomeDir, '[tools]\nnodejs = "22"\n'); + + expect(seedRuntimeHomeMiseGlobalConfig({ homeDir, sourceHomeDir })).toBe( + true, + ); + expect( + readFileSync(join(homeDir, '.config', 'mise', 'config.toml'), 'utf8'), + ).toBe('[tools]\nnodejs = "22"\n'); + }); + + it('leaves an existing runtime home config untouched', () => { + const sourceHomeDir = createDir('roomote-mise-source-'); + const homeDir = createDir('roomote-mise-runtime-'); + writeMiseConfig(sourceHomeDir, '[tools]\nnodejs = "22"\n'); + writeMiseConfig(homeDir, '[tools]\nnodejs = "20"\n'); + + expect(seedRuntimeHomeMiseGlobalConfig({ homeDir, sourceHomeDir })).toBe( + false, + ); + expect( + readFileSync(join(homeDir, '.config', 'mise', 'config.toml'), 'utf8'), + ).toBe('[tools]\nnodejs = "20"\n'); + }); + + it('does nothing when the worker home has no global config', () => { + const sourceHomeDir = createDir('roomote-mise-source-'); + const homeDir = createDir('roomote-mise-runtime-'); + + expect(seedRuntimeHomeMiseGlobalConfig({ homeDir, sourceHomeDir })).toBe( + false, + ); + expect(existsSync(join(homeDir, '.config', 'mise', 'config.toml'))).toBe( + false, + ); + }); + + it('does nothing when the runtime home is the worker home', () => { + const homeDir = createDir('roomote-mise-source-'); + writeMiseConfig(homeDir, '[tools]\nnodejs = "22"\n'); + + expect( + seedRuntimeHomeMiseGlobalConfig({ homeDir, sourceHomeDir: homeDir }), + ).toBe(false); + }); +}); diff --git a/apps/worker/src/run-task/agent-home.ts b/apps/worker/src/run-task/agent-home.ts index 2064a9262..1d691bafb 100644 --- a/apps/worker/src/run-task/agent-home.ts +++ b/apps/worker/src/run-task/agent-home.ts @@ -594,6 +594,55 @@ export function activateSkillsFolder({ return true; } +const MISE_GLOBAL_CONFIG_RELATIVE_PATH = ['.config', 'mise', 'config.toml']; + +/** + * Seeds the task runtime HOME with the worker home's global mise config. + * + * Mise shims (node, npx, ...) resolve tool versions from the global config at + * `$HOME/.config/mise/config.toml` when no repo-level mise config applies to + * the process cwd. The task runtime HOME starts empty, so without this seed + * any process spawned with the runtime HOME outside a version-pinned + * repository — including environment-defined stdio MCP servers + * (`npx -y ...`) — fails with "mise ERROR No version is set for shim: node". + * + * An existing runtime-home config (restored from a snapshot, or written by + * the agent via `mise use -g`) is left untouched. Best-effort: returns + * whether the config was seeded. + */ +export function seedRuntimeHomeMiseGlobalConfig({ + homeDir, + sourceHomeDir, +}: { + homeDir: string; + sourceHomeDir: string; +}): boolean { + if (!homeDir || !sourceHomeDir || homeDir === sourceHomeDir) { + return false; + } + + const sourceConfigPath = path.join( + sourceHomeDir, + ...MISE_GLOBAL_CONFIG_RELATIVE_PATH, + ); + const targetConfigPath = path.join( + homeDir, + ...MISE_GLOBAL_CONFIG_RELATIVE_PATH, + ); + + try { + if (!fs.existsSync(sourceConfigPath) || fs.existsSync(targetConfigPath)) { + return false; + } + + fs.mkdirSync(path.dirname(targetConfigPath), { recursive: true }); + fs.copyFileSync(sourceConfigPath, targetConfigPath); + return true; + } catch { + return false; + } +} + interface GenerateOpenCodeConfigOptions { homeDir: string; runtimeEnv: Record; diff --git a/apps/worker/src/run-task/run-task.ts b/apps/worker/src/run-task/run-task.ts index 8b69c989c..9f7fa7a0e 100644 --- a/apps/worker/src/run-task/run-task.ts +++ b/apps/worker/src/run-task/run-task.ts @@ -69,6 +69,7 @@ import { TaskCancellationController } from './task-cancellation-controller'; import { activateSkillsFolder, resolvePackagedSkillsFolder, + seedRuntimeHomeMiseGlobalConfig, } from './agent-home'; import { installZeroCli } from '../commands/setup/agent-clis'; @@ -766,6 +767,17 @@ export const runTask = async ({ if (workerHomeDir) { runtimeEnv.HOME = resolveTaskRuntimeHomeDir(workspacePath); + + if ( + seedRuntimeHomeMiseGlobalConfig({ + homeDir: runtimeEnv.HOME, + sourceHomeDir: workerHomeDir, + }) + ) { + logger.info( + `[runTask] Seeded runtime home mise global config from ${workerHomeDir}`, + ); + } } delete runtimeEnv.ROOMOTE_TASK_TERMINAL;