feat(fleet): harden v1 migration preview
Some checks failed
ci/woodpecker/pr/ci Pipeline failed

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Jarvis
2026-07-16 00:57:33 -05:00
parent 0e814324ca
commit 04891422b3
14 changed files with 477 additions and 82 deletions

View File

@@ -1,3 +1,5 @@
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import { Command } from 'commander';
import { registerBrainCommand } from '@mosaicstack/brain';
@@ -16,6 +18,8 @@ import { registerConfigCommand } from './commands/config.js';
// without throwing. This is the "mosaic <cmd> --help exits 0" gate that
// guards the sub-package CLI surface (CU-05-01..08) from silent breakage.
const CLI_PATH = fileURLToPath(new URL('../dist/cli.js', import.meta.url));
const REGISTRARS: Array<[string, (program: Command) => void]> = [
['auth', registerAuthCommand],
['brain', registerBrainCommand],
@@ -46,6 +50,40 @@ describe('sub-package CLI smoke (CU-05-10)', () => {
});
}
it.each(['source', 'decisions', 'observations'] as const)(
'production CLI emits one blocked JSON object for bare --%s',
(bareOption) => {
const args = [
CLI_PATH,
'fleet',
'migrate-v1',
'preview',
'--source=source',
'--decisions=decisions',
'--observations=observations',
];
args[args.findIndex((argument) => argument.startsWith(`--${bareOption}=`))] =
`--${bareOption}`;
const result = spawnSync(process.execPath, args, { encoding: 'utf8' });
const outputLines = result.stdout.trim().split('\n');
expect(result.status).toBe(1);
expect(result.stderr).toBe('');
expect(outputLines).toHaveLength(1);
expect(JSON.parse(outputLines[0]!)).toEqual({
status: 'blocked',
blockers: [
{
code: 'missing-migration-preview-option-value',
path: `request.${bareOption}`,
detail: 'Required migration preview option value is missing.',
},
],
});
},
);
it('all nine sub-package commands coexist on a single program', () => {
const program = new Command();
for (const [, register] of REGISTRARS) register(program);

View File

@@ -116,6 +116,55 @@ describe('fleet migrate-v1 preview command', (): void => {
expect(setExitCode).toHaveBeenCalledWith(1);
});
it.each(['source', 'decisions', 'observations'] as const)(
'emits one stable blocked JSON result when bare --%s has no value',
async (bareOption) => {
const printJson = vi.fn();
const setExitCode = vi.fn();
const readText = vi.fn();
const writeErr = vi.fn();
const program = new Command().exitOverride();
program.configureOutput({ writeErr, writeOut: vi.fn() });
const fleet = program.command('fleet').option('--mosaic-home <path>', '', '/unused');
registerFleetMigrationCommand(fleet, {
mosaicHome: '/unused',
readText,
printJson,
setExitCode,
});
const args = [
'node',
'test',
'fleet',
'migrate-v1',
'preview',
'--source=source',
'--decisions=decisions',
'--observations=observations',
];
args[args.findIndex((argument) => argument.startsWith(`--${bareOption}=`))] =
`--${bareOption}`;
await expect(program.parseAsync(args)).resolves.toBe(program);
expect(printJson).toHaveBeenCalledTimes(1);
expect(printJson).toHaveBeenCalledWith({
status: 'blocked',
blockers: [
{
code: 'missing-migration-preview-option-value',
path: `request.${bareOption}`,
detail: 'Required migration preview option value is missing.',
},
],
});
expect(setExitCode).toHaveBeenCalledTimes(1);
expect(setExitCode).toHaveBeenCalledWith(1);
expect(readText).not.toHaveBeenCalled();
expect(writeErr).not.toHaveBeenCalled();
},
);
it.each(['source', 'decisions', 'observations'] as const)(
'emits one stable blocked JSON result when --%s is present-empty',
async (emptyOption) => {

View File

@@ -17,9 +17,9 @@ export interface FleetMigrationCommandDeps {
}
interface PreviewOptions {
readonly source?: string;
readonly decisions?: string;
readonly observations?: string;
readonly source?: string | boolean;
readonly decisions?: string | boolean;
readonly observations?: string | boolean;
}
/** Registers preview-only v1 migration. This command has no mutation verbs or runners. */
@@ -32,9 +32,9 @@ export function registerFleetMigrationCommand(
.description('Preview a field-complete v1-to-v2 roster migration')
.command('preview')
.description('Compile migration evidence without writing files or changing runtimes')
.option('--source <path>', 'v1 roster YAML or JSON')
.option('--decisions <path>', 'explicit migration decisions JSON')
.option('--observations <path>', 'reviewed lifecycle observations JSON')
.option('--source [path]', 'v1 roster YAML or JSON')
.option('--decisions [path]', 'explicit migration decisions JSON')
.option('--observations [path]', 'reviewed lifecycle observations JSON')
.action(async (options: PreviewOptions): Promise<void> => {
const readText = deps.readText ?? ((path: string): Promise<string> => readFile(path, 'utf8'));
const printJson =
@@ -58,7 +58,24 @@ export function registerFleetMigrationCommand(
setExitCode(1);
return;
}
const emptyOption = requiredOptionNames.find((name) => options[name]?.trim() === '');
const missingValueOption = requiredOptionNames.find((name) => options[name] === true);
if (missingValueOption !== undefined) {
printJson({
status: 'blocked',
blockers: [
{
code: 'missing-migration-preview-option-value',
path: `request.${missingValueOption}`,
detail: 'Required migration preview option value is missing.',
},
],
});
setExitCode(1);
return;
}
const emptyOption = requiredOptionNames.find(
(name) => typeof options[name] === 'string' && options[name].trim() === '',
);
if (emptyOption !== undefined) {
printJson({
status: 'blocked',
@@ -77,9 +94,9 @@ export function registerFleetMigrationCommand(
const decisionsPath = options.decisions;
const observationsPath = options.observations;
if (
sourcePath === undefined ||
decisionsPath === undefined ||
observationsPath === undefined
typeof sourcePath !== 'string' ||
typeof decisionsPath !== 'string' ||
typeof observationsPath !== 'string'
) {
throw new Error('Validated migration preview options became unavailable.');
}

View File

@@ -198,6 +198,26 @@ describe('fleet roster parsing', () => {
expect(getRosterAgent(roster, 'canary-pi').runtime).toBe('pi');
});
it('uses /clear for an explicitly declared empty pi runtime config', async () => {
cleanup = await tempDir();
const rosterPath = join(cleanup, 'roster.yaml');
await writeFile(
rosterPath,
[
'version: 1',
'transport: tmux',
'runtimes:',
' pi: {}',
'agents:',
' - name: canary-pi',
' runtime: pi',
].join('\n'),
);
const loaded = await loadFleetRoster(rosterPath);
expect(loaded.runtimes.pi?.resetCommand).toBe('/clear');
});
it('accepts optional agent alias and provider metadata without requiring them', async () => {
cleanup = await tempDir();
const rosterPath = join(cleanup, 'roster.yaml');
@@ -271,6 +291,32 @@ describe('fleet roster parsing', () => {
expect(env).toContain('MOSAIC_TMUX_SOCKET=\n');
});
it('preserves home-relative traversal until the shared environment boundary rejects it', async () => {
for (const workingDirectory of ['~/../escape', '~/src/../../escape']) {
cleanup = await tempDir();
const rosterPath = join(cleanup, 'roster.json');
await writeFile(
rosterPath,
JSON.stringify({
version: 1,
transport: 'tmux',
defaults: { working_directory: workingDirectory },
agents: [{ name: 'coder0', runtime: 'pi' }],
}),
);
const roster = await loadFleetRoster(rosterPath);
expect(() => generateAgentEnv(roster, getRosterAgent(roster, 'coder0'))).toThrow(
expect.objectContaining({
diagnostic: expect.objectContaining({
code: 'unsafe-path',
key: 'MOSAIC_AGENT_WORKDIR',
}),
}),
);
}
});
it('generates deterministic per-agent EnvironmentFile content', async () => {
cleanup = await tempDir();
const rosterPath = join(cleanup, 'roster.json');

View File

@@ -561,7 +561,7 @@ function generateAgentEnvValues(
MOSAIC_AGENT_MODEL: agent.modelHint ?? '',
MOSAIC_AGENT_REASONING: agent.reasoningLevel ?? '',
MOSAIC_AGENT_TOOL_POLICY: agent.toolPolicy ?? '',
MOSAIC_AGENT_WORKDIR: expandHome(workingDirectory),
MOSAIC_AGENT_WORKDIR: workingDirectory,
MOSAIC_TMUX_SOCKET: roster.tmux.socketName,
};
}
@@ -2742,10 +2742,6 @@ function optionalBooleanOrString(value: unknown, label = 'Value'): boolean | str
return value;
}
function expandHome(path: string): string {
return path === '~' || path.startsWith('~/') ? join(homedir(), path.slice(2)) : path;
}
async function stopFleetBestEffort(runner: CommandRunner, agentNames: string[]): Promise<void> {
const failures: string[] = [];
for (const agentName of agentNames) {

View File

@@ -274,17 +274,64 @@ describe('fleet roster-owned reconciler', (): void => {
]);
});
it.each(['start', 'apply'] as const)(
'fails closed before %s can start fixed named-socket services for a default-server roster',
it.each(['plan', 'status', 'doctor', 'verify'] as const)(
'keeps default-server %s observational and free of lifecycle effects',
async (command) => {
const calls: string[][] = [];
const defaultServerRoster: FleetRosterV2 = {
...roster,
tmux: { ...roster.tmux, socketName: '' },
};
await expect(
executeFleetReconcile({
roster: defaultServerRoster,
command,
deps: deps({
runner: async (executable, args) => {
calls.push([executable, ...args]);
if (executable === 'tmux' && args.includes('list-sessions')) {
return { stdout: '_holder\n', stderr: '', exitCode: 0 };
}
if (executable === 'tmux' && args.includes('show-environment')) {
return {
stdout:
'HOME=/home/mosaic\nMOSAIC_FLEET_OWNER=11111111-1111-4111-8111-111111111111\nMOSAIC_TMUX_HOLDER=_holder\nMOSAIC_TMUX_SOCKET=\nPATH=/usr/bin:/bin\nPWD=/home/mosaic\n',
stderr: '',
exitCode: 0,
};
}
return { stdout: 'ActiveState=inactive\n', stderr: '', exitCode: 0 };
},
}),
}),
).resolves.toMatchObject({ applied: false, lifecycle: 'not-applied' });
expect(
calls.some(
([executable, , action]): boolean =>
executable === 'systemctl' &&
(action === 'start' || action === 'stop' || action === 'restart'),
),
).toBe(false);
},
);
it.each(['start', 'stop', 'restart', 'apply', 'reconcile'] as const)(
'fails closed before %s can route fixed named-socket services for a default-server roster',
async (command) => {
const calls: string[][] = [];
let projectionPrepares = 0;
let projectionApplies = 0;
const defaultServerRoster: FleetRosterV2 = {
...roster,
tmux: { ...roster.tmux, socketName: '' },
agents: [
{
...roster.agents[0]!,
lifecycle: { enabled: true, desiredState: 'running' },
lifecycle: {
enabled: true,
desiredState: command === 'start' || command === 'restart' ? 'running' : 'stopped',
},
},
],
};
@@ -296,28 +343,23 @@ describe('fleet roster-owned reconciler', (): void => {
expectedGeneration: 7,
deps: deps({
readRoster: async () => defaultServerRoster,
prepareProjections: async () => {
projectionPrepares += 1;
return [{ agentName: 'coder0' }];
},
applyProjection: async () => {
projectionApplies += 1;
},
runner: async (executable, args) => {
calls.push([executable, ...args]);
if (executable === 'tmux' && args.includes('list-sessions')) {
return { stdout: '', stderr: '', exitCode: 1 };
}
return { stdout: '', stderr: '', exitCode: 0 };
},
}),
}),
).rejects.toMatchObject({ code: 'lifecycle-precondition-failed' });
expect(calls).not.toContainEqual([
'systemctl',
'--user',
'start',
'mosaic-tmux-holder.service',
]);
expect(calls).not.toContainEqual([
'systemctl',
'--user',
'start',
'mosaic-agent@coder0.service',
]);
expect(projectionPrepares).toBe(0);
expect(projectionApplies).toBe(0);
expect(calls).toEqual([]);
},
);

View File

@@ -479,18 +479,14 @@ function assertVerificationSafe(plan: FleetReconcilePlan): void {
}
function assertLifecycleSocketAuthority(request: FleetReconcileRequest): void {
const mayStart =
request.command === 'start' ||
request.command === 'restart' ||
((request.command === 'apply' || request.command === 'reconcile') &&
request.roster.agents.some(
(agent: FleetRosterV2Agent): boolean =>
agent.lifecycle.enabled && agent.lifecycle.desiredState === 'running',
));
if (mayStart && request.roster.tmux.socketName === '') {
const usesFixedLifecycleUnits =
request.command === 'apply' ||
request.command === 'reconcile' ||
isLifecycleCommand(request.command);
if (usesFixedLifecycleUnits && request.roster.tmux.socketName === '') {
throw new FleetReconcileError(
'lifecycle-precondition-failed',
'Default-server lifecycle start is unsupported by the fixed named-socket systemd units.',
'Default-server lifecycle mutation is unsupported by the fixed named-socket systemd units.',
);
}
}

View File

@@ -9,7 +9,7 @@ import {
symlink,
writeFile,
} from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { homedir, tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, describe, expect, it } from 'vitest';
import {
@@ -87,6 +87,42 @@ describe('generated fleet agent environment boundary', (): void => {
}).toThrow(AgentEnvBoundaryError);
});
it('rejects traversal in home-relative workdirs before expansion', (): void => {
for (const workingDirectory of ['~/../escape', '~/src/../../escape']) {
expect((): void => {
renderGeneratedAgentEnvironment({
...generatedValues,
MOSAIC_AGENT_WORKDIR: workingDirectory,
});
}).toThrow(
expect.objectContaining({
diagnostic: expect.objectContaining({
code: 'unsafe-path',
key: 'MOSAIC_AGENT_WORKDIR',
}),
}),
);
}
});
it('expands home-relative workdirs before preserving absolute-path validation', (): void => {
expect(
renderGeneratedAgentEnvironment({
...generatedValues,
MOSAIC_AGENT_WORKDIR: '~/src',
}),
).toContain(`MOSAIC_AGENT_WORKDIR=${join(homedir(), 'src')}\n`);
for (const workingDirectory of ['relative/path', '../outside']) {
expect((): void => {
renderGeneratedAgentEnvironment({
...generatedValues,
MOSAIC_AGENT_WORKDIR: workingDirectory,
});
}).toThrow(AgentEnvBoundaryError);
}
});
it('previews legacy relocation and quarantine without exposing content or mutating files', async (): Promise<void> => {
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
const mosaicHome = join(cleanup, 'mosaic');

View File

@@ -1,5 +1,6 @@
import { createHash, randomUUID } from 'node:crypto';
import { chmod, lstat, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { compareCodePoints } from './deterministic-order.js';
@@ -318,7 +319,7 @@ function normalizeGeneratedValues(
for (const key of GENERATED_AGENT_ENV_KEYS) {
const value = values[key];
if (value === undefined) throw new AgentEnvBoundaryError('missing-key', key, '');
normalized[key] = value;
normalized[key] = key === 'MOSAIC_AGENT_WORKDIR' ? expandHomeDirectory(value) : value;
}
for (const [key, value] of Object.entries(values)) {
if (!GENERATED_KEY_SET.has(key)) throw new AgentEnvBoundaryError('unknown-key', key, value);
@@ -327,6 +328,12 @@ function normalizeGeneratedValues(
return Object.freeze(normalized);
}
function expandHomeDirectory(path: string): string {
if (path === '~') return homedir();
if (!path.startsWith('~/') || path.split('/').includes('..')) return path;
return join(homedir(), path.slice(2));
}
function renderLocalAgentEnvironment(values: Readonly<Record<string, string>>): string {
if (Object.keys(values).length === 0) return '';
const parsed = parseAgentEnvironment(

View File

@@ -4,6 +4,8 @@ import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import YAML from 'yaml';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { loadFleetRoster } from '../commands/fleet.js';
import { executeFleetReconcile } from './fleet-reconciler.js';
import { compareCodePoints } from './deterministic-order.js';
import { SHIPPED_FLEET_ARTIFACT_DISPOSITIONS } from './example-profile-dispositions.js';
import {
@@ -11,6 +13,7 @@ import {
previewV1ToV2Migration,
validateShippedFleetMigrationDispositions,
type V1ToV2MigrationDecisions,
type V1ToV2MigrationPreview,
} from './v1-v2-migration.js';
const temporaryDirectories: string[] = [];
@@ -529,6 +532,122 @@ describe('v1-to-v2 migration preview', (): void => {
});
});
it('matches production v1 reset defaults when a declared runtime config is empty', async () => {
const source = v1Source.replace(' pi:\n reset_command: /new', ' pi: {}');
const productionSource = source
.replace(/ - name: remote0[\s\S]*?connector:/, 'connector:')
.replace(/connector:[\s\S]*$/, '');
const rosterPath = join(await mkdtemp(join(tmpdir(), 'v1-runtime-parity-')), 'roster.yaml');
temporaryDirectories.push(dirname(rosterPath));
await writeFile(rosterPath, productionSource);
const productionRoster = await loadFleetRoster(rosterPath);
const preview = await previewV1ToV2Migration({
source,
decisions: decisions(),
observations: observations(),
personaDirs: await semanticDirs(),
});
expect(preview.status).toBe('ready');
expect(productionRoster.runtimes.pi?.resetCommand).toBe('/clear');
expect(preview.canonicalRoster?.runtimes.pi?.resetCommand).toBe(
productionRoster.runtimes.pi?.resetCommand,
);
});
it('emits byte-identical preview evidence when equivalent local agents are reordered', async () => {
const coder1 = ` - name: coder1
alias: Coder One
provider: openai
runtime: pi
class: reviewer
working_directory: /srv/coder1
reasoning_level: high
tool_policy: reviewer
`;
const sourceA = v1Source.replace(' - name: remote0', `${coder1} - name: remote0`);
const sourceB = sourceA.replace(
/( - name: coder0[\s\S]*?)( - name: coder1[\s\S]*?)( - name: remote0)/,
'$2$1$3',
);
const migrationDecisions = decisions();
migrationDecisions.agents.coder1 = {
provider: 'openai',
model: 'gpt-5.6-sol',
reasoning: 'high',
enabled: true,
launchYolo: false,
};
const lifecycle = {
...observations(),
coder1: { systemd: 'active' as const, tmux: 'present' as const },
};
const dirs = await semanticDirs();
const previews = await Promise.all(
[sourceA, sourceB].map((source) =>
previewV1ToV2Migration({
source,
decisions: migrationDecisions,
observations: lifecycle,
personaDirs: dirs,
}),
),
);
const previewA = previews[0]!;
const previewB = previews[1]!;
expect(previewA.status).toBe('ready');
expect(previewB.status).toBe('ready');
expect(previewB.evidence.observations).toEqual(previewA.evidence.observations);
expect({
...previewB.evidence,
source: { ...previewB.evidence.source, sha256: '<source-specific>' },
}).toEqual({
...previewA.evidence,
source: { ...previewA.evidence.source, sha256: '<source-specific>' },
});
expect(previewB.canonicalYaml).toBe(previewA.canonicalYaml);
});
it('emits byte-identical preview evidence when equivalent remote agents are reordered', async () => {
const remote1 = ` - name: remote1
runtime: pi
class: reviewer
host: review.example.test
ssh: operator@review.example.test
socket: review-fleet
`;
const sourceA = v1Source.replace('connector:', `${remote1}connector:`);
const sourceB = sourceA.replace(
/( - name: remote0[\s\S]*?)( - name: remote1[\s\S]*?)(connector:)/,
'$2$1$3',
);
const dirs = await semanticDirs();
const previews = await Promise.all(
[sourceA, sourceB].map((source) =>
previewV1ToV2Migration({
source,
decisions: decisions(),
observations: observations(),
personaDirs: dirs,
}),
),
);
const previewA = previews[0]!;
const previewB = previews[1]!;
const semanticOutput = (preview: V1ToV2MigrationPreview): object => ({
...preview,
evidence: {
...preview.evidence,
source: { ...preview.evidence.source, sha256: '<source-specific>' },
},
});
expect(previewA.status).toBe('ready');
expect(previewB.status).toBe('ready');
expect(semanticOutput(previewB)).toEqual(semanticOutput(previewA));
});
it('emits byte-identical canonical evidence regardless of locale collation', async () => {
expect(['😀', 'z', '', 'ä'].sort(compareCodePoints)).toEqual(['z', 'ä', '', '😀']);
const dirs = await semanticDirs();
@@ -797,6 +916,48 @@ describe('v1-to-v2 migration preview', (): void => {
).toContain(forbiddenValue);
});
it('feeds a ready home-relative candidate through the production projection boundary', async () => {
const source = v1Source.replace('working_directory: /srv/coder0', 'working_directory: ~/src');
const preview = await previewV1ToV2Migration({
source,
decisions: decisions(),
observations: observations(),
personaDirs: await semanticDirs(),
});
expect(preview.status).toBe('ready');
expect(preview.canonicalRoster?.agents[0]?.workingDirectory).toBe('~/src');
expect(preview.canonicalYaml).toContain('working_directory: ~/src');
const canonicalRoster = preview.canonicalRoster!;
const mosaicHome = await mkdtemp(join(tmpdir(), 'v1-v2-production-projection-'));
temporaryDirectories.push(mosaicHome);
const fleetDir = join(mosaicHome, 'fleet');
const agentEnvDir = join(fleetDir, 'agents');
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
await chmod(mosaicHome, 0o700);
await chmod(fleetDir, 0o700);
await chmod(agentEnvDir, 0o700);
const projectionDirs = await semanticDirs();
await expect(
executeFleetReconcile({
roster: canonicalRoster,
command: 'plan',
deps: {
mosaicHome,
rolesDir: projectionDirs.rolesDir,
overrideDir: projectionDirs.overrideDir,
runner: async (command, args) => {
if (command === 'tmux' && args.includes('list-sessions')) {
return { stdout: '', stderr: '', exitCode: 1 };
}
return { stdout: 'ActiveState=inactive\n', stderr: '', exitCode: 0 };
},
},
}),
).resolves.toMatchObject({ applied: false, projections: 'not-applied' });
});
it('blocks undeclared v1 fields, synonym collisions, and duplicate names', async () => {
const baseOptions = {
decisions: decisions(),

View File

@@ -1,6 +1,5 @@
import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import { join } from 'node:path';
import YAML from 'yaml';
import { canonicalizeRoleClass, type PersonaDirs } from '../commands/fleet-personas.js';
@@ -646,11 +645,14 @@ export async function previewV1ToV2Migration(
}
}
const canonicalInventoryOnlyAgents = [...inventoryOnlyAgents].sort((left, right): number =>
compareCodePoints(left.name, right.name),
);
const status = blockers.length === 0 ? 'ready' : 'blocked';
return {
status,
inventory,
inventoryOnlyAgents,
inventoryOnlyAgents: canonicalInventoryOnlyAgents,
blockers,
...(status === 'ready' && canonicalRoster && canonicalYaml
? { canonicalRoster, canonicalYaml }
@@ -664,10 +666,12 @@ export async function previewV1ToV2Migration(
...(status === 'ready' && canonicalYaml
? { candidate: { sha256: sha256(canonicalYaml), generation: decisions.generation } }
: {}),
observations,
observations: [...observations].sort((left, right): number =>
compareCodePoints(left.agent, right.agent),
),
excludedInventory: [
...(raw.connector === undefined ? [] : ['connector']),
...inventoryOnlyAgents.map(({ name }): string => `agents.${name}`),
...canonicalInventoryOnlyAgents.map(({ name }): string => `agents.${name}`),
],
recovery: {
executable: false,
@@ -1161,9 +1165,7 @@ function normalizeRuntimes(
}
const runtime = record(value);
const resetCommand =
stringValue(runtime.reset_command) ||
stringValue(runtime.resetCommand) ||
V1_RUNTIME_RESETS[name];
stringValue(runtime.reset_command) || stringValue(runtime.resetCommand) || '/clear';
result[name] = { reset_command: resetCommand };
}
return result;
@@ -1294,15 +1296,11 @@ function generatedValues(
MOSAIC_AGENT_MODEL: agent.model,
MOSAIC_AGENT_REASONING: agent.reasoning,
MOSAIC_AGENT_TOOL_POLICY: agent.toolPolicy,
MOSAIC_AGENT_WORKDIR: expandHome(agent.workingDirectory),
MOSAIC_AGENT_WORKDIR: agent.workingDirectory,
MOSAIC_TMUX_SOCKET: roster.tmux.socketName,
};
}
function expandHome(path: string): string {
return path === '~' ? homedir() : path.startsWith('~/') ? join(homedir(), path.slice(2)) : path;
}
function safeEnvironmentDiagnostic(error: unknown): string {
if (
typeof error === 'object' &&