280 lines
11 KiB
TypeScript
280 lines
11 KiB
TypeScript
import {
|
|
chmod,
|
|
mkdir,
|
|
mkdtemp,
|
|
readFile,
|
|
rename,
|
|
rm,
|
|
stat,
|
|
symlink,
|
|
writeFile,
|
|
} from 'node:fs/promises';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
import { afterEach, describe, expect, it } from 'vitest';
|
|
import {
|
|
AgentEnvBoundaryError,
|
|
parseAgentEnvironment,
|
|
renderGeneratedAgentEnvironment,
|
|
writeAgentEnvironmentProjection,
|
|
} from './generated-env-boundary.js';
|
|
|
|
const generatedValues = {
|
|
MOSAIC_AGENT_NAME: 'coder0',
|
|
MOSAIC_AGENT_CLASS: 'code',
|
|
MOSAIC_AGENT_RUNTIME: 'pi',
|
|
MOSAIC_AGENT_MODEL: 'openai-codex/gpt-5.6-sol',
|
|
MOSAIC_AGENT_REASONING: 'high',
|
|
MOSAIC_AGENT_TOOL_POLICY: 'code',
|
|
MOSAIC_AGENT_WORKDIR: '/srv/mosaic',
|
|
MOSAIC_TMUX_SOCKET: 'mosaic-fleet',
|
|
};
|
|
|
|
describe('generated fleet agent environment boundary', (): void => {
|
|
let cleanup: string | undefined;
|
|
|
|
afterEach(async (): Promise<void> => {
|
|
if (cleanup) {
|
|
await rm(cleanup, { recursive: true, force: true });
|
|
cleanup = undefined;
|
|
}
|
|
});
|
|
|
|
it('renders a deterministic complete generated projection', (): void => {
|
|
expect(renderGeneratedAgentEnvironment(generatedValues)).toBe(
|
|
[
|
|
'MOSAIC_AGENT_NAME=coder0',
|
|
'MOSAIC_AGENT_CLASS=code',
|
|
'MOSAIC_AGENT_RUNTIME=pi',
|
|
'MOSAIC_AGENT_MODEL=openai-codex/gpt-5.6-sol',
|
|
'MOSAIC_AGENT_REASONING=high',
|
|
'MOSAIC_AGENT_TOOL_POLICY=code',
|
|
'MOSAIC_AGENT_WORKDIR=/srv/mosaic',
|
|
'MOSAIC_TMUX_SOCKET=mosaic-fleet',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
['generated-key shadowing', 'MOSAIC_AGENT_RUNTIME=codex\n'],
|
|
['unknown key', 'UNRELATED_SETTING=value\n'],
|
|
['arbitrary command', 'MOSAIC_AGENT_COMMAND=mosaic yolo codex\n'],
|
|
['sensitive key', 'MOSAIC_AGENT_TOKEN=do-not-log-me\n'],
|
|
['malformed syntax', 'export MOSAIC_RUNTIME_BIN=/opt/bin\n'],
|
|
['duplicate keys', 'MOSAIC_RUNTIME_BIN=/opt/bin\nMOSAIC_RUNTIME_BIN=/other/bin\n'],
|
|
])('rejects local %s without echoing values', (_name: string, source: string): void => {
|
|
let error: unknown;
|
|
try {
|
|
parseAgentEnvironment(source, 'local');
|
|
} catch (caught: unknown) {
|
|
error = caught;
|
|
}
|
|
|
|
expect(error).toBeInstanceOf(AgentEnvBoundaryError);
|
|
expect(String(error)).not.toContain('mosaic yolo codex');
|
|
expect(String(error)).not.toContain('do-not-log-me');
|
|
expect(String(error)).toMatch(/key=.*sha256=/);
|
|
});
|
|
|
|
it('rejects unsafe generated paths before any launch consumer can use them', (): void => {
|
|
expect((): void => {
|
|
renderGeneratedAgentEnvironment({
|
|
...generatedValues,
|
|
MOSAIC_AGENT_WORKDIR: '../outside',
|
|
});
|
|
}).toThrow(AgentEnvBoundaryError);
|
|
});
|
|
|
|
it('creates missing managed directories privately before writing a projection', async (): Promise<void> => {
|
|
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
|
|
const mosaicHome = join(cleanup, 'mosaic');
|
|
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
|
|
|
|
const result = await writeAgentEnvironmentProjection({
|
|
mosaicHome,
|
|
agentEnvDir,
|
|
agentName: 'coder0',
|
|
generated: generatedValues,
|
|
});
|
|
|
|
for (const directory of [mosaicHome, join(mosaicHome, 'fleet'), agentEnvDir]) {
|
|
expect((await stat(directory)).mode & 0o777).toBe(0o700);
|
|
}
|
|
expect((await stat(result.generatedPath)).mode & 0o777).toBe(0o600);
|
|
});
|
|
|
|
it('regenerates desired keys, relocates safe legacy local data, and quarantines forbidden legacy input', async (): Promise<void> => {
|
|
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
|
|
const mosaicHome = join(cleanup, 'mosaic');
|
|
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
|
|
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
|
|
await writeFile(
|
|
join(agentEnvDir, 'coder0.env'),
|
|
[
|
|
'MOSAIC_AGENT_NAME=stale-name',
|
|
'MOSAIC_AGENT_RUNTIME=codex',
|
|
'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin',
|
|
'MOSAIC_AGENT_COMMAND=mosaic yolo codex --dangerous',
|
|
'',
|
|
].join('\n'),
|
|
{ mode: 0o600 },
|
|
);
|
|
|
|
const result = await writeAgentEnvironmentProjection({
|
|
mosaicHome,
|
|
agentEnvDir,
|
|
agentName: 'coder0',
|
|
generated: generatedValues,
|
|
});
|
|
|
|
expect(await readFile(result.generatedPath, 'utf8')).toBe(
|
|
renderGeneratedAgentEnvironment(generatedValues),
|
|
);
|
|
expect(await readFile(result.localPath, 'utf8')).toBe('MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n');
|
|
const quarantine = await readFile(result.quarantinePath!, 'utf8');
|
|
expect(quarantine).toContain('MOSAIC_AGENT_COMMAND=mosaic yolo codex --dangerous');
|
|
await expect(readFile(join(agentEnvDir, 'coder0.env'), 'utf8')).rejects.toThrow();
|
|
expect((await stat(result.generatedPath)).mode & 0o077).toBe(0);
|
|
expect((await stat(result.localPath)).mode & 0o077).toBe(0);
|
|
expect(result.diagnostics).toEqual([expect.objectContaining({ key: 'MOSAIC_AGENT_COMMAND' })]);
|
|
expect(JSON.stringify(result.diagnostics)).not.toContain('mosaic yolo codex --dangerous');
|
|
});
|
|
|
|
it('fails closed on an existing local override with unsafe permissions', async (): Promise<void> => {
|
|
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
|
|
const mosaicHome = join(cleanup, 'mosaic');
|
|
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
|
|
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
|
|
const localPath = join(agentEnvDir, 'coder0.env.local');
|
|
await writeFile(localPath, 'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n', { mode: 0o600 });
|
|
await chmod(localPath, 0o644);
|
|
|
|
await expect(
|
|
writeAgentEnvironmentProjection({
|
|
mosaicHome,
|
|
agentEnvDir,
|
|
agentName: 'coder0',
|
|
generated: generatedValues,
|
|
}),
|
|
).rejects.toThrow(/permissions/i);
|
|
});
|
|
|
|
it('rejects an existing group/world-accessible projection directory before reading it', async (): Promise<void> => {
|
|
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
|
|
const mosaicHome = join(cleanup, 'mosaic');
|
|
const agentEnvDir = join(mosaicHome, 'fleet', 'agents');
|
|
const localPath = join(agentEnvDir, 'coder0.env.local');
|
|
const generatedPath = join(agentEnvDir, 'coder0.env.generated');
|
|
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
|
|
await writeFile(localPath, 'MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n', { mode: 0o600 });
|
|
await chmod(agentEnvDir, 0o777);
|
|
|
|
await expect(
|
|
writeAgentEnvironmentProjection({
|
|
mosaicHome,
|
|
agentEnvDir,
|
|
agentName: 'coder0',
|
|
generated: generatedValues,
|
|
}),
|
|
).rejects.toThrow(/unsafe-permissions/i);
|
|
|
|
await expect(readFile(localPath, 'utf8')).resolves.toBe('MOSAIC_RUNTIME_BIN=/opt/mosaic/bin\n');
|
|
await expect(readFile(generatedPath, 'utf8')).rejects.toThrow();
|
|
});
|
|
|
|
it.each([
|
|
['MOSAIC_HOME', 'symlink'],
|
|
['MOSAIC_HOME/fleet', 'symlink'],
|
|
['MOSAIC_HOME/fleet/agents', 'symlink'],
|
|
['MOSAIC_HOME', 'group/world-writable'],
|
|
['MOSAIC_HOME/fleet', 'group/world-writable'],
|
|
['MOSAIC_HOME/fleet/agents', 'group/world-writable'],
|
|
])(
|
|
'rejects a %s %s managed ancestor before any projection mutation',
|
|
async (ancestor: string, hazard: string): Promise<void> => {
|
|
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
|
|
const mosaicHome = join(cleanup, 'mosaic');
|
|
const fleetDir = join(mosaicHome, 'fleet');
|
|
const agentEnvDir = join(fleetDir, 'agents');
|
|
const generatedPath = join(agentEnvDir, 'coder0.env.generated');
|
|
const localPath = join(agentEnvDir, 'coder0.env.local');
|
|
const legacyPath = join(agentEnvDir, 'coder0.env');
|
|
const quarantinePath = join(agentEnvDir, 'coder0.env.quarantine');
|
|
const managedPaths: Record<string, string> = {
|
|
MOSAIC_HOME: mosaicHome,
|
|
'MOSAIC_HOME/fleet': fleetDir,
|
|
'MOSAIC_HOME/fleet/agents': agentEnvDir,
|
|
};
|
|
const unsafePath = managedPaths[ancestor];
|
|
if (unsafePath === undefined) throw new Error(`Unknown managed ancestor: ${ancestor}`);
|
|
|
|
await mkdir(agentEnvDir, { recursive: true, mode: 0o700 });
|
|
await chmod(mosaicHome, 0o700);
|
|
await chmod(fleetDir, 0o700);
|
|
await chmod(agentEnvDir, 0o700);
|
|
await writeFile(generatedPath, 'generated-before\n', { mode: 0o600 });
|
|
await writeFile(localPath, 'MOSAIC_RUNTIME_BIN=/safe/runtime\n', { mode: 0o600 });
|
|
await writeFile(legacyPath, 'MOSAIC_AGENT_COMMAND=legacy-command\n', { mode: 0o600 });
|
|
|
|
if (hazard === 'symlink') {
|
|
const targetPath = `${unsafePath}-target`;
|
|
await rename(unsafePath, targetPath);
|
|
await symlink(targetPath, unsafePath, 'dir');
|
|
} else {
|
|
await chmod(unsafePath, 0o777);
|
|
}
|
|
|
|
await expect(
|
|
writeAgentEnvironmentProjection({
|
|
mosaicHome,
|
|
agentEnvDir,
|
|
agentName: 'coder0',
|
|
generated: generatedValues,
|
|
}),
|
|
).rejects.toThrow(/unsafe-(directory|permissions)/i);
|
|
|
|
await expect(readFile(generatedPath, 'utf8')).resolves.toBe('generated-before\n');
|
|
await expect(readFile(localPath, 'utf8')).resolves.toBe('MOSAIC_RUNTIME_BIN=/safe/runtime\n');
|
|
await expect(readFile(legacyPath, 'utf8')).resolves.toBe(
|
|
'MOSAIC_AGENT_COMMAND=legacy-command\n',
|
|
);
|
|
await expect(readFile(quarantinePath, 'utf8')).rejects.toThrow();
|
|
},
|
|
);
|
|
|
|
it('rejects a symlinked projection directory without mutating its target', async (): Promise<void> => {
|
|
cleanup = await mkdtemp(join(tmpdir(), 'mosaic-generated-env-'));
|
|
const mosaicHome = join(cleanup, 'mosaic');
|
|
const targetDirectory = join(cleanup, 'target');
|
|
const linkedDirectory = join(mosaicHome, 'fleet', 'agents');
|
|
const legacyPath = join(targetDirectory, 'coder0.env');
|
|
const generatedPath = join(targetDirectory, 'coder0.env.generated');
|
|
const localPath = join(targetDirectory, 'coder0.env.local');
|
|
const quarantinePath = join(targetDirectory, 'coder0.env.quarantine');
|
|
await mkdir(join(mosaicHome, 'fleet'), { recursive: true, mode: 0o700 });
|
|
await mkdir(targetDirectory, { mode: 0o700 });
|
|
await writeFile(legacyPath, 'MOSAIC_AGENT_COMMAND=legacy-command\n', { mode: 0o600 });
|
|
await writeFile(generatedPath, 'generated-before\n', { mode: 0o600 });
|
|
await writeFile(localPath, 'local-before\n', { mode: 0o600 });
|
|
await writeFile(quarantinePath, 'quarantine-before\n', { mode: 0o600 });
|
|
await symlink(targetDirectory, linkedDirectory, 'dir');
|
|
|
|
await expect(
|
|
writeAgentEnvironmentProjection({
|
|
mosaicHome,
|
|
agentEnvDir: linkedDirectory,
|
|
agentName: 'coder0',
|
|
generated: generatedValues,
|
|
}),
|
|
).rejects.toThrow(/unsafe-directory/i);
|
|
|
|
await expect(readFile(legacyPath, 'utf8')).resolves.toBe(
|
|
'MOSAIC_AGENT_COMMAND=legacy-command\n',
|
|
);
|
|
await expect(readFile(generatedPath, 'utf8')).resolves.toBe('generated-before\n');
|
|
await expect(readFile(localPath, 'utf8')).resolves.toBe('local-before\n');
|
|
await expect(readFile(quarantinePath, 'utf8')).resolves.toBe('quarantine-before\n');
|
|
});
|
|
});
|