fix(fleet): preserve managed link provenance

AMD1213-B3: record Mosaic-owned links and refuse foreign or retargeted symlink mutations. Out-of-scope review follow-up: settings output/snapshot apply-time TOCTOU remains reported, not patched.
This commit is contained in:
terra
2026-08-13 14:38:25 -05:00
parent 4fde3f622d
commit fe2cf19461
2 changed files with 350 additions and 35 deletions
@@ -1,13 +1,17 @@
import {
closeSync,
lstatSync,
mkdirSync,
openSync,
readFileSync,
readlinkSync,
readdirSync,
realpathSync,
renameSync,
rmSync,
symlinkSync,
writeFileSync,
writeSync,
type Stats,
} from 'node:fs';
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
@@ -91,6 +95,16 @@ interface PlannedLink {
readonly target: string;
}
interface ManagedLinkManifest {
readonly links: Record<string, string>;
}
interface ManagedLinkState {
readonly path: string;
readonly links: Map<string, string>;
readonly existed: boolean;
}
export interface FleetLaunchComposition {
readonly name: string;
readonly profilePath: string;
@@ -113,6 +127,7 @@ export interface FleetLaunchComposition {
readonly link: string;
readonly target: string;
};
readonly managedLinks: ManagedLinkState;
readonly installs: readonly PlannedLink[];
readonly prune: readonly string[];
readonly env: Readonly<Record<string, string>>;
@@ -374,6 +389,7 @@ function resolveCredential(
profile: FleetAgentLaunchProfile,
userHome: string,
seatHome: string,
managedLinks: ManagedLinkState,
): Pick<FleetLaunchComposition, 'bundle' | 'credential'> {
assertRealDirectory(userHome, 'user Mosaic root');
const realUserHome = realpathSync(userHome);
@@ -425,6 +441,16 @@ function resolveCredential(
const credentialLink = join(seatHome, CREDENTIAL_FILES[profile.harness]);
const seatInfo = lstatIfPresent(credentialLink);
if (seatInfo?.isSymbolicLink() && !managedLinks.existed) {
const existingTarget = currentLinkTarget(credentialLink);
try {
assertContained(resolvedAuthRoot, realpathSync(existingTarget), 'credential migration');
// A pre-manifest seat may adopt only a credential link inside the central auth root.
managedLinks.links.set(credentialLink, existingTarget);
} catch {
// Foreign links remain unrecorded and are refused by apply before any writes.
}
}
if (seatInfo && !seatInfo.isSymbolicLink()) {
throw new FleetLaunchError(
'FIRST_AUTH_REFUSAL',
@@ -449,11 +475,16 @@ function resolveCredential(
};
}
function currentLinkTarget(link: string): string {
return resolve(dirname(link), readlinkSync(link));
}
function resolveManagedLinks(
kind: PlannedLink['kind'],
names: readonly string[],
userHome: string,
seatHome: string,
managedLinks: ManagedLinkState,
): { installs: PlannedLink[]; prune: string[] } {
const plural = kind === 'plugin' ? 'plugins' : 'skills';
const storeRoot = join(userHome, plural);
@@ -494,7 +525,6 @@ function resolveManagedLinks(
if (installRootInfo?.isDirectory()) {
for (const entry of readdirSync(installRoot, { withFileTypes: true })) {
const path = join(installRoot, entry.name);
if (desired.has(entry.name)) continue;
if (!entry.isSymbolicLink()) {
// The harness writes its own metadata files (e.g. installed_plugins.json)
// beside the managed links; only a real directory is an unmanaged entry
@@ -507,12 +537,107 @@ function resolveManagedLinks(
}
continue;
}
prune.push(path);
const currentTarget = currentLinkTarget(path);
const recordedTarget = managedLinks.links.get(path);
if (recordedTarget === undefined) {
if (
!managedLinks.existed &&
(() => {
try {
assertContained(
realpathSync(storeRoot),
realpathSync(currentTarget),
`${kind} migration`,
);
return true;
} catch {
return false;
}
})()
) {
// A pre-manifest seat may adopt only links to the central Mosaic store; foreign links refuse.
managedLinks.links.set(path, currentTarget);
}
} else if (recordedTarget !== currentTarget) {
throw new FleetLaunchError(
'COMPOSITION_FAILED',
`managed ${kind} symlink target changed since composition: ${path}`,
);
}
if (!desired.has(entry.name)) prune.push(path);
}
}
return { installs, prune };
}
function readManagedLinkState(seatHome: string): ManagedLinkState {
const path = join(seatHome, '.mosaic-managed-links.json');
const info = lstatIfPresent(path);
if (!info) return { path, links: new Map(), existed: false };
if (!info.isFile() || info.isSymbolicLink()) {
throw new FleetLaunchError(
'COMPOSITION_FAILED',
`managed link manifest must be a real, non-symlink JSON file: ${path}`,
);
}
let parsed: unknown;
try {
parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown;
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
throw new FleetLaunchError(
'COMPOSITION_FAILED',
`managed link manifest is invalid JSON: ${detail}`,
);
}
if (!isPlainObject(parsed) || !isPlainObject(parsed['links'])) {
throw new FleetLaunchError(
'COMPOSITION_FAILED',
`managed link manifest has invalid shape: ${path}`,
);
}
const links = new Map<string, string>();
for (const [link, target] of Object.entries(parsed['links'])) {
if (typeof target !== 'string' || !isAbsolute(link) || !isAbsolute(target)) {
throw new FleetLaunchError(
'COMPOSITION_FAILED',
`managed link manifest has invalid entry: ${path}`,
);
}
links.set(link, target);
}
return { path, links, existed: true };
}
interface PreparedManagedLinkManifest {
readonly path: string;
readonly descriptor: number;
}
function prepareManagedLinkManifest(managedLinks: ManagedLinkState): PreparedManagedLinkManifest {
const path = `${managedLinks.path}.tmp`;
try {
return { path, descriptor: openSync(path, 'wx', 0o600) };
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
throw new FleetLaunchError(
'COMPOSITION_FAILED',
`managed link manifest temporary file cannot be created exclusively: ${detail}`,
);
}
}
function writeManagedLinkState(
managedLinks: ManagedLinkState,
prepared: PreparedManagedLinkManifest,
): void {
const entries = [...managedLinks.links.entries()].sort(([left], [right]) =>
left.localeCompare(right),
);
const manifest: ManagedLinkManifest = { links: Object.fromEntries(entries) };
writeSync(prepared.descriptor, `${JSON.stringify(manifest, null, 2)}\n`);
}
function buildArgv(
profile: FleetAgentLaunchProfile,
seatHome: string,
@@ -594,9 +719,22 @@ export function resolveFleetLaunchComposition(
: readSettingsLayer('agent', overlayPath, false),
];
const merged = deepMergeSettings(...layers.map((layer) => layer.value));
const credential = resolveCredential(profile, roots.userHome, seatHome);
const plugins = resolveManagedLinks('plugin', profile.plugins, roots.userHome, seatHome);
const skills = resolveManagedLinks('skill', profile.skills, roots.userHome, seatHome);
const managedLinks = readManagedLinkState(seatHome);
const credential = resolveCredential(profile, roots.userHome, seatHome, managedLinks);
const plugins = resolveManagedLinks(
'plugin',
profile.plugins,
roots.userHome,
seatHome,
managedLinks,
);
const skills = resolveManagedLinks(
'skill',
profile.skills,
roots.userHome,
seatHome,
managedLinks,
);
const homeEnvName: Record<RuntimeName, string> = {
claude: 'CLAUDE_CONFIG_DIR',
pi: 'PI_CODING_AGENT_DIR',
@@ -621,6 +759,7 @@ export function resolveFleetLaunchComposition(
snapshot: settingsSnapshot,
},
...credential,
managedLinks,
installs: [...plugins.installs, ...skills.installs],
prune: [...plugins.prune, ...skills.prune],
env,
@@ -628,11 +767,20 @@ export function resolveFleetLaunchComposition(
};
}
function ensureSymlink(link: string, target: string): void {
function ensureSymlink(link: string, target: string, managedLinks: ManagedLinkState): void {
const info = lstatIfPresent(link);
if (info?.isSymbolicLink()) {
const current = resolve(dirname(link), readlinkSync(link));
if (current === target) return;
const current = currentLinkTarget(link);
if (current === target) {
managedLinks.links.set(link, target);
return;
}
if (managedLinks.links.get(link) !== current) {
throw new FleetLaunchError(
'COMPOSITION_FAILED',
`unrecorded or retargeted symlink occupies managed path: ${link}`,
);
}
rmSync(link);
} else if (info) {
throw new FleetLaunchError(
@@ -642,6 +790,30 @@ function ensureSymlink(link: string, target: string): void {
}
mkdirSync(dirname(link), { recursive: true });
symlinkSync(target, link, 'file');
managedLinks.links.set(link, target);
}
function assertManagedLinkMutationAllowed(
link: string,
target: string | undefined,
managedLinks: ManagedLinkState,
): void {
const info = lstatIfPresent(link);
if (!info) return;
if (!info.isSymbolicLink()) {
throw new FleetLaunchError(
'COMPOSITION_FAILED',
`real object occupies managed symlink path ${link}; refusing to delete it.`,
);
}
const current = currentLinkTarget(link);
if (target !== undefined && current === target) return;
if (managedLinks.links.get(link) !== current) {
throw new FleetLaunchError(
'COMPOSITION_FAILED',
`unrecorded or retargeted symlink occupies managed path: ${link}`,
);
}
}
function canonicalJson(value: unknown): unknown {
@@ -657,21 +829,57 @@ function canonicalJson(value: unknown): unknown {
/** Apply a previously resolved plan. No caller should apply a dry-run plan. */
export function applyFleetLaunchComposition(plan: FleetLaunchComposition): void {
mkdirSync(plan.seatHome, { recursive: true });
const settings = `${JSON.stringify(canonicalJson(plan.settings.merged), null, 2)}\n`;
writeFileSync(plan.settings.output, settings, { mode: 0o600 });
writeFileSync(plan.settings.snapshot, settings, { mode: 0o600 });
ensureSymlink(plan.credential.link, plan.credential.target);
for (const path of plan.prune) {
const info = lstatIfPresent(path);
if (info?.isSymbolicLink()) rmSync(path);
else if (info) {
throw new FleetLaunchError(
'COMPOSITION_FAILED',
`real object replaced managed symlink before prune: ${path}`,
);
const preparedManifest = prepareManagedLinkManifest(plan.managedLinks);
let committedManifest = false;
try {
assertManagedLinkMutationAllowed(
plan.credential.link,
plan.credential.target,
plan.managedLinks,
);
for (const path of plan.prune) {
assertManagedLinkMutationAllowed(path, undefined, plan.managedLinks);
}
for (const install of plan.installs) {
assertManagedLinkMutationAllowed(install.link, install.target, plan.managedLinks);
}
const settings = `${JSON.stringify(canonicalJson(plan.settings.merged), null, 2)}\n`;
writeFileSync(plan.settings.output, settings, { mode: 0o600 });
writeFileSync(plan.settings.snapshot, settings, { mode: 0o600 });
ensureSymlink(plan.credential.link, plan.credential.target, plan.managedLinks);
for (const path of plan.prune) {
const info = lstatIfPresent(path);
if (info?.isSymbolicLink()) {
const current = currentLinkTarget(path);
if (plan.managedLinks.links.get(path) !== current) {
throw new FleetLaunchError(
'COMPOSITION_FAILED',
`unrecorded or retargeted symlink cannot be pruned: ${path}`,
);
}
rmSync(path);
plan.managedLinks.links.delete(path);
} else if (info) {
throw new FleetLaunchError(
'COMPOSITION_FAILED',
`real object replaced managed symlink before prune: ${path}`,
);
}
}
for (const install of plan.installs) {
ensureSymlink(install.link, install.target, plan.managedLinks);
}
writeManagedLinkState(plan.managedLinks, preparedManifest);
closeSync(preparedManifest.descriptor);
renameSync(preparedManifest.path, plan.managedLinks.path);
committedManifest = true;
} finally {
if (!committedManifest) {
closeSync(preparedManifest.descriptor);
rmSync(preparedManifest.path, { force: true });
}
}
for (const install of plan.installs) ensureSymlink(install.link, install.target);
}
/** Stable, auditable text representation used by --dry-run and snapshot tests. */