fix(fleet): harden managed launch composition
AMD1213-C: repair stale array consumer, fail closed on foreign link provenance, validate manifests before mutation, and exercise the fleet MCP preflight call path.
This commit is contained in:
@@ -390,7 +390,7 @@ function resolveCredential(
|
||||
profile: FleetAgentLaunchProfile,
|
||||
userHome: string,
|
||||
seatHome: string,
|
||||
managedLinks: ManagedLinkState,
|
||||
_managedLinks: ManagedLinkState,
|
||||
): Pick<FleetLaunchComposition, 'bundle' | 'credential'> {
|
||||
assertRealDirectory(userHome, 'user Mosaic root');
|
||||
const realUserHome = realpathSync(userHome);
|
||||
@@ -442,16 +442,6 @@ 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',
|
||||
@@ -571,7 +561,11 @@ function resolveManagedLinks(
|
||||
return { installs, prune };
|
||||
}
|
||||
|
||||
function readManagedLinkState(seatHome: string): ManagedLinkState {
|
||||
function readManagedLinkState(
|
||||
seatHome: string,
|
||||
profile: FleetAgentLaunchProfile,
|
||||
userHome: string,
|
||||
): ManagedLinkState {
|
||||
const path = join(seatHome, '.mosaic-managed-links.json');
|
||||
const info = lstatIfPresent(path);
|
||||
if (!info) return { path, links: new Map(), existed: false };
|
||||
@@ -605,6 +599,24 @@ function readManagedLinkState(seatHome: string): ManagedLinkState {
|
||||
`managed link manifest has invalid entry: ${path}`,
|
||||
);
|
||||
}
|
||||
const credential = join(seatHome, CREDENTIAL_FILES[profile.harness]);
|
||||
const pluginRoot = join(seatHome, 'plugins');
|
||||
const skillRoot = join(seatHome, 'skills');
|
||||
const authRoot = join(userHome, 'auth', profile.harness);
|
||||
const inRoot = (root: string, candidate: string): boolean => {
|
||||
const rel = relative(resolve(root), resolve(candidate));
|
||||
return rel !== '..' && !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
|
||||
};
|
||||
const valid =
|
||||
(link === credential && inRoot(authRoot, target)) ||
|
||||
(inRoot(pluginRoot, link) && inRoot(join(userHome, 'plugins'), target)) ||
|
||||
(inRoot(skillRoot, link) && inRoot(join(userHome, 'skills'), target));
|
||||
if (!valid) {
|
||||
throw new FleetLaunchError(
|
||||
'COMPOSITION_FAILED',
|
||||
`managed link manifest entry escapes an approved seat/store root: ${path}`,
|
||||
);
|
||||
}
|
||||
links.set(link, target);
|
||||
}
|
||||
return { path, links, existed: true };
|
||||
@@ -720,7 +732,7 @@ export function resolveFleetLaunchComposition(
|
||||
: readSettingsLayer('agent', overlayPath, false),
|
||||
];
|
||||
const merged = deepMergeSettings(...layers.map((layer) => layer.value));
|
||||
const managedLinks = readManagedLinkState(seatHome);
|
||||
const managedLinks = readManagedLinkState(seatHome, profile, roots.userHome);
|
||||
const credential = resolveCredential(profile, roots.userHome, seatHome, managedLinks);
|
||||
const plugins = resolveManagedLinks(
|
||||
'plugin',
|
||||
@@ -773,10 +785,6 @@ function ensureSymlink(link: string, target: string, managedLinks: ManagedLinkSt
|
||||
const info = lstatIfPresent(link);
|
||||
if (info?.isSymbolicLink()) {
|
||||
const current = currentLinkTarget(link);
|
||||
if (current === target) {
|
||||
managedLinks.links.set(link, target);
|
||||
return;
|
||||
}
|
||||
if (managedLinks.links.get(link) !== current) {
|
||||
throw new FleetLaunchError(
|
||||
'COMPOSITION_FAILED',
|
||||
@@ -809,7 +817,6 @@ function assertManagedLinkMutationAllowed(
|
||||
);
|
||||
}
|
||||
const current = currentLinkTarget(link);
|
||||
if (target !== undefined && current === target) return;
|
||||
if (managedLinks.links.get(link) !== current) {
|
||||
throw new FleetLaunchError(
|
||||
'COMPOSITION_FAILED',
|
||||
@@ -830,22 +837,20 @@ function canonicalJson(value: unknown): unknown {
|
||||
|
||||
/** Apply a previously resolved plan. No caller should apply a dry-run plan. */
|
||||
export function applyFleetLaunchComposition(plan: FleetLaunchComposition): void {
|
||||
// All link-state checks must complete before the first filesystem mutation.
|
||||
// This makes a late foreign/retargeted link refusal leave the seat untouched.
|
||||
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);
|
||||
}
|
||||
|
||||
mkdirSync(plan.seatHome, { recursive: true });
|
||||
const preparedManifest = prepareManagedLinkManifest(plan.managedLinks);
|
||||
let descriptorOpen = true;
|
||||
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 });
|
||||
@@ -874,11 +879,12 @@ export function applyFleetLaunchComposition(plan: FleetLaunchComposition): void
|
||||
}
|
||||
writeManagedLinkState(plan.managedLinks, preparedManifest);
|
||||
closeSync(preparedManifest.descriptor);
|
||||
descriptorOpen = false;
|
||||
renameSync(preparedManifest.path, plan.managedLinks.path);
|
||||
committedManifest = true;
|
||||
} finally {
|
||||
if (!committedManifest) {
|
||||
closeSync(preparedManifest.descriptor);
|
||||
if (descriptorOpen) closeSync(preparedManifest.descriptor);
|
||||
rmSync(preparedManifest.path, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user