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:
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
chmodSync,
|
||||
existsSync,
|
||||
lstatSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
@@ -401,6 +402,29 @@ describe('managed plugin and skill links', () => {
|
||||
expect(readlinkSync(join(pluginHome, 'foreign'))).toBe(foreign);
|
||||
});
|
||||
|
||||
it('performs no writes when a late foreign install link is refused', () => {
|
||||
const fx = fixture({ schema: 1, harness: 'claude', plugins: ['keep'] });
|
||||
const target = join(fx.userHome, 'plugins', 'keep');
|
||||
const link = join(fx.agentDir, '.claude', 'plugins', 'keep');
|
||||
mkdirSync(target, { recursive: true });
|
||||
mkdirSync(join(link, '..'), { recursive: true });
|
||||
writeFileSync(join(fx.agentDir, '.claude', '.mosaic-managed-links.json'), '{"links":{}}\n');
|
||||
symlinkSync(target, link, 'dir');
|
||||
const plan = resolveFleetLaunchComposition('fred', {
|
||||
systemHome: fx.systemHome,
|
||||
userHome: fx.userHome,
|
||||
});
|
||||
const snapshot = join(fx.agentDir, 'settings.generated.json');
|
||||
const temp = join(fx.agentDir, '.claude', '.mosaic-managed-links.json.tmp');
|
||||
|
||||
expect(() => applyFleetLaunchComposition(plan)).toThrowError(
|
||||
/unrecorded or retargeted symlink/,
|
||||
);
|
||||
expect(existsSync(snapshot)).toBe(false);
|
||||
expect(existsSync(temp)).toBe(false);
|
||||
expect(readlinkSync(link)).toBe(target);
|
||||
});
|
||||
|
||||
it('prunes a recorded matching stale symlink', () => {
|
||||
const fx = fixture({ schema: 1, harness: 'claude', plugins: ['old'] });
|
||||
mkdirSync(join(fx.userHome, 'plugins', 'old'), { recursive: true });
|
||||
@@ -455,6 +479,23 @@ describe('managed plugin and skill links', () => {
|
||||
expect(readlinkSync(link)).toBe(foreignTarget);
|
||||
});
|
||||
|
||||
it('refuses a tampered manifest entry outside this seat and leaves it intact', () => {
|
||||
const fx = fixture({ schema: 1, harness: 'claude' });
|
||||
const seatHome = join(fx.agentDir, '.claude');
|
||||
mkdirSync(seatHome, { recursive: true });
|
||||
const manifest = join(seatHome, '.mosaic-managed-links.json');
|
||||
const crossSeat = join(fx.userHome, 'fleet', 'agents', 'other', '.claude', 'plugins', 'keep');
|
||||
writeFileSync(
|
||||
manifest,
|
||||
JSON.stringify({ links: { [crossSeat]: join(fx.userHome, 'plugins', 'keep') } }),
|
||||
);
|
||||
|
||||
expect(() =>
|
||||
resolveFleetLaunchComposition('fred', { systemHome: fx.systemHome, userHome: fx.userHome }),
|
||||
).toThrowError(/escapes an approved seat\/store root/);
|
||||
expect(readFileSync(manifest, 'utf8')).toContain(crossSeat);
|
||||
});
|
||||
|
||||
it('refuses a symlinked manifest temporary path without modifying its target', () => {
|
||||
const fx = fixture({ schema: 1, harness: 'claude', plugins: ['keep'] });
|
||||
const target = join(fx.userHome, 'plugins', 'keep');
|
||||
@@ -472,33 +513,45 @@ describe('managed plugin and skill links', () => {
|
||||
expect(readFileSync(sentinel, 'utf8')).toBe('unchanged\n');
|
||||
});
|
||||
|
||||
it('adopts a pre-manifest credential link inside auth root before retargeting it', () => {
|
||||
const fx = fixture({ schema: 1, harness: 'claude', bundle: 'next' });
|
||||
const previousBundle = join(fx.userHome, 'auth', 'claude', 'previous');
|
||||
const nextBundle = join(fx.userHome, 'auth', 'claude', 'next');
|
||||
it('refuses an exact-target unrecorded credential symlink', () => {
|
||||
const fx = fixture();
|
||||
const seatHome = join(fx.agentDir, '.claude');
|
||||
mkdirSync(previousBundle, { recursive: true });
|
||||
mkdirSync(nextBundle, { recursive: true });
|
||||
writeFileSync(join(previousBundle, '.credentials.json'), '{}\n', { mode: 0o600 });
|
||||
writeFileSync(join(nextBundle, '.credentials.json'), '{}\n', { mode: 0o600 });
|
||||
const link = join(seatHome, '.credentials.json');
|
||||
mkdirSync(seatHome, { recursive: true });
|
||||
symlinkSync(
|
||||
join(previousBundle, '.credentials.json'),
|
||||
join(seatHome, '.credentials.json'),
|
||||
'file',
|
||||
);
|
||||
|
||||
symlinkSync(join(fx.namedBundleDir, '.credentials.json'), link, 'file');
|
||||
const plan = resolveFleetLaunchComposition('fred', {
|
||||
systemHome: fx.systemHome,
|
||||
userHome: fx.userHome,
|
||||
});
|
||||
applyFleetLaunchComposition(plan);
|
||||
|
||||
expect(readlinkSync(join(seatHome, '.credentials.json'))).toBe(
|
||||
join(nextBundle, '.credentials.json'),
|
||||
expect(() => applyFleetLaunchComposition(plan)).toThrowError(
|
||||
/unrecorded or retargeted symlink/,
|
||||
);
|
||||
expect(readlinkSync(link)).toBe(join(fx.namedBundleDir, '.credentials.json'));
|
||||
});
|
||||
|
||||
it.each(['plugins', 'skills'] as const)(
|
||||
'refuses an exact-target unrecorded %s symlink',
|
||||
(kind) => {
|
||||
const fx = fixture({ schema: 1, harness: 'claude', [kind]: ['keep'] });
|
||||
const target = join(fx.userHome, kind, 'keep');
|
||||
const link = join(fx.agentDir, '.claude', kind, 'keep');
|
||||
mkdirSync(target, { recursive: true });
|
||||
mkdirSync(join(link, '..'), { recursive: true });
|
||||
writeFileSync(join(fx.agentDir, '.claude', '.mosaic-managed-links.json'), '{"links":{}}\n');
|
||||
symlinkSync(target, link, 'dir');
|
||||
const plan = resolveFleetLaunchComposition('fred', {
|
||||
systemHome: fx.systemHome,
|
||||
userHome: fx.userHome,
|
||||
});
|
||||
|
||||
expect(() => applyFleetLaunchComposition(plan)).toThrowError(
|
||||
/unrecorded or retargeted symlink/,
|
||||
);
|
||||
expect(readlinkSync(link)).toBe(target);
|
||||
},
|
||||
);
|
||||
|
||||
it('refuses an unrecorded mismatched credential symlink', () => {
|
||||
const fx = fixture();
|
||||
const seatHome = join(fx.agentDir, '.claude');
|
||||
|
||||
Reference in New Issue
Block a user