Compare commits

...
Author SHA1 Message Date
fred 1a6c8fe0cb wip(fleet): brain-home drop-in for [email protected] (#1310)
UNVERIFIED. Committed locally to survive a session boundary; not pushed,
not typechecked past the eslint pass, tests not run, B-mutation not done.

`fleet install` now writes 10-brain-home.conf under [email protected],
clearing the inherited ConditionPathExists and re-pointing it at the resolved
brain home. On a split install the shipped condition names the config home
while the reconciler writes the brain home, so every seat is skipped and
`fleet start` still reports rc=0.

The spec's existing coverage could not see this: its fixture never creates
<HOME>/.mosaic/fleet/agents, so resolveBrainHome() only ever reaches legacy
mode. Replaced with three arms - legacy, split (fails without this change),
and stale-override removal with a present-first control.
2026-08-18 14:24:07 -05:00
2 changed files with 183 additions and 3 deletions
@@ -244,12 +244,115 @@ describe('[email protected]', (): void => {
* absent env — either of which turns the condition into decoration while the
* literal-string assertion above still passes.
*/
/**
* The condition an installed unit actually presents to systemd: the shipped
* unit's values with drop-ins applied in lexical order, honouring the list
* reset an empty assignment performs.
*
* Reading the *installed* tree rather than the template is the point. The
* template alone cannot show what `install` produced, and #1310 lived in that
* gap: the template was correct for the install it was written for, and wrong
* for the one the operator had.
*/
async function effectiveConditionPaths(systemdUserDir: string): Promise<string[]> {
const dropInDir = join(systemdUserDir, '[email protected]');
const dropIns = (
await readdir(dropInDir).catch((error: NodeJS.ErrnoException): string[] => {
if (error.code === 'ENOENT') return [];
throw error;
})
)
.filter((name): boolean => name.endsWith('.conf'))
.sort();
const sources = [
join(systemdUserDir, '[email protected]'),
...dropIns.map((name): string => join(dropInDir, name)),
];
const conditions: string[] = [];
for (const source of sources) {
for (const line of (await readFile(source, 'utf8')).split('\n')) {
const match = line.match(/^ConditionPathExists=(.*)$/);
if (!match) continue;
const value = match[1]!.trim();
if (value === '') conditions.length = 0;
else conditions.push(value);
}
}
return conditions;
}
/** Makes `~/.mosaic` adoptable — the shape that puts a real box in split mode. */
async function adoptableBrainHome(): Promise<string> {
const brainHome = join(tempHome!, '.mosaic');
await mkdir(join(brainHome, 'fleet', 'agents'), { recursive: true, mode: 0o700 });
return brainHome;
}
it('guards exactly the file the fleet writes, so the two cannot drift apart', async (): Promise<void> => {
const mosaicHome = await v2Home();
const rendered = (await conditionPath()).replace('%h', tempHome!).replace('%i', 'coder0');
const systemdUserDir = join(tempHome!, '.config', 'systemd', 'user');
await program().parseAsync(['node', 'mosaic', 'fleet', 'install', '--no-enable']);
const rendered = (await effectiveConditionPaths(systemdUserDir)).map((value): string =>
value.replace('%h', tempHome!).replace('%i', 'coder0'),
);
// The path an installed fleet actually places for this agent.
expect(rendered).toBe(join(mosaicHome, 'fleet', 'agents', 'coder0.env.generated'));
expect(rendered).toEqual([join(mosaicHome, 'fleet', 'agents', 'coder0.env.generated')]);
});
/**
* #1310. The test above passed throughout the bug, because its fixture never
* creates `~/.mosaic/fleet/agents` and so can only ever reach legacy mode,
* where brain home and config home are the same directory and the drift it
* names is unobservable. A guard that cannot reach the state it guards is
* decoration; this is the arm that reaches it.
*
* Measured consequence on a real box (mosaic-sbx-dev 1124, 0.0.50-next.2507):
* the config-home condition can never be met, systemd reports every seat
* `ConditionResult=no` with `Result=success` and `ExecMainStatus=0`, and
* `fleet start` exits 0 having started nothing.
*/
it('follows the brain home when one is active, not the config home', async (): Promise<void> => {
const mosaicHome = await v2Home();
const brainHome = await adoptableBrainHome();
const systemdUserDir = join(tempHome!, '.config', 'systemd', 'user');
await program().parseAsync(['node', 'mosaic', 'fleet', 'install', '--no-enable']);
const rendered = (await effectiveConditionPaths(systemdUserDir)).map((value): string =>
value.replace('%h', tempHome!).replace('%i', 'coder0'),
);
// Exactly one condition: the drop-in must reset the shipped value, not join
// it. Two conditions would both have to pass and the seat would stay skipped.
expect(rendered).toEqual([join(brainHome, 'fleet', 'agents', 'coder0.env.generated')]);
expect(rendered[0]).not.toContain(mosaicHome);
});
/**
* The other direction, which fails the same silent way: a box that had a
* brain home and no longer does would keep a condition pointing at a path the
* reconciler has stopped writing.
*/
it('drops a stale brain-home override when the brain home goes away', async (): Promise<void> => {
const mosaicHome = await v2Home();
const systemdUserDir = join(tempHome!, '.config', 'systemd', 'user');
await adoptableBrainHome();
await program().parseAsync(['node', 'mosaic', 'fleet', 'install', '--no-enable']);
// Control: the override is present before the brain home is removed, so a
// passing assertion below cannot come from it never having been written.
expect(await effectiveConditionPaths(systemdUserDir)).toHaveLength(1);
expect((await effectiveConditionPaths(systemdUserDir))[0]).toContain('.mosaic/fleet/agents');
await rm(join(tempHome!, '.mosaic'), { recursive: true, force: true });
await program().parseAsync(['node', 'mosaic', 'fleet', 'install', '--no-enable']);
const rendered = (await effectiveConditionPaths(systemdUserDir)).map((value): string =>
value.replace('%h', tempHome!).replace('%i', 'coder0'),
);
expect(rendered).toEqual([join(mosaicHome, 'fleet', 'agents', 'coder0.env.generated')]);
});
it('guards a real failure — the launcher rejects an absent generated env', async (): Promise<void> => {
+78 -1
View File
@@ -13,7 +13,7 @@ import {
import { randomUUID } from 'node:crypto';
import { homedir, hostname, userInfo } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fleetAgentEnvDir } from '../fleet/brain-home.js';
import { brainHomeIsActive, fleetAgentEnvDir } from '../fleet/brain-home.js';
import { fileURLToPath } from 'node:url';
import { spawn } from 'node:child_process';
import * as readline from 'node:readline';
@@ -2350,6 +2350,80 @@ export function registerFleetAgentCommands(
});
}
/** Directory holding the install-managed drop-ins for `[email protected]`. */
export function agentUnitDropInDir(systemdUserDir: string): string {
return join(systemdUserDir, '[email protected]');
}
/** The one drop-in `mosaic fleet install` owns: the brain-home env condition. */
export function agentUnitBrainHomeDropInPath(systemdUserDir: string): string {
return join(agentUnitDropInDir(systemdUserDir), '10-brain-home.conf');
}
/**
* Drop-in re-pointing the agent unit's env condition at the resolved brain home.
*
* The shipped unit gates on `%h/.config/mosaic/fleet/agents/%i.env.generated`,
* which is right for a legacy single-tree install. When a brain home is active
* (canon `docs/STRUCTURE-CANON.md` §2) the reconciler writes seat env under the
* brain home instead, so that condition can never be met — and an unmet
* condition is reported as success (`ConditionResult=no`, `Result=success`,
* `ExecMainStatus=0`). `fleet start` therefore returns 0, prints nothing, and
* starts no seat, with every status surface agreeing the fleet is healthy
* (#1310, measured on mosaic-sbx-dev 1124 against 0.0.50-next.2507).
*
* An empty `ConditionPathExists=` resets the inherited list before the correct
* path is added, so the shipped condition is replaced rather than joined — two
* conditions would both have to pass and the seat would stay skipped.
*
* The boot-window behaviour the shipped unit documents is deliberately kept:
* this still gates on the generated env existing, only at the path that
* actually receives it. The launcher already resolves the brain home itself,
* so nothing downstream of the condition needs to change.
*/
export function renderAgentUnitBrainHomeDropIn(agentEnvDir: string): string {
return [
'# Managed by `mosaic fleet install`. Do not edit; re-run install to update.',
'#',
'# Re-points the seat env condition at the active brain home. Without this the',
'# shipped config-home condition can never be met on a split install and every',
'# seat is silently skipped while `fleet start` reports success (#1310).',
'[Unit]',
'ConditionPathExists=',
`ConditionPathExists=${join(agentEnvDir, '%i.env.generated')}`,
'',
].join('\n');
}
/**
* Write the brain-home drop-in when a brain is active, and remove a stale one
* when it is not.
*
* The removal arm is load-bearing, not tidiness: a host that had a brain home
* and no longer does would otherwise keep a condition pointing at a path the
* reconciler has stopped writing, which fails in exactly the same silent
* direction as the bug this fixes.
*/
export async function syncAgentUnitBrainHomeDropIn(
systemdUserDir: string,
mosaicHome: string,
): Promise<void> {
const dropInPath = agentUnitBrainHomeDropInPath(systemdUserDir);
if (!brainHomeIsActive(mosaicHome)) {
try {
await unlink(dropInPath);
} catch (error) {
// Absent is the expected state on a legacy install; anything else is real.
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
throw error;
}
}
return;
}
await mkdir(agentUnitDropInDir(systemdUserDir), { recursive: true });
await writeFile(dropInPath, renderAgentUnitBrainHomeDropIn(fleetAgentEnvDir(mosaicHome)), 'utf8');
}
async function installFleet(cmd: Command, frameworkRoot: string): Promise<void> {
const activePaths = resolveFleetPaths(cmd.opts<{ mosaicHome: string }>().mosaicHome);
assertDefaultMosaicHomeForSystemd(activePaths.mosaicHome);
@@ -2410,6 +2484,9 @@ async function installFleet(cmd: Command, frameworkRoot: string): Promise<void>
join(frameworkRoot, 'systemd', 'user', '[email protected]'),
join(activePaths.systemdUserDir, '[email protected]'),
);
// Must follow the copy: the drop-in overrides the unit just written, and on a
// split install the unit alone gates every seat on a path nothing writes.
await syncAgentUnitBrainHomeDropIn(activePaths.systemdUserDir, activePaths.mosaicHome);
await copyFile(
join(frameworkRoot, 'systemd', 'user', '[email protected]'),
join(activePaths.systemdUserDir, '[email protected]'),