fix(mosaic): bound brain migration and owner lookups

This commit is contained in:
2026-08-05 17:19:50 -05:00
parent 656fa9ceb7
commit 836aab1ab5
4 changed files with 77 additions and 6 deletions
@@ -164,6 +164,38 @@ describe('provider-backed durable owner resolver', (): void => {
expect(calls.every((call) => call.authorization === null)).toBe(true);
});
it('refuses provider redirects and configures a bounded no-redirect request', async (): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 owner lookup SSRF boundary');
const requests: RequestInit[] = [];
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: 'user:durable-owner',
},
{
fetch: async (_input, init): Promise<Response> => {
requests.push(init ?? {});
return new Response(JSON.stringify({ message: 'redirect' }), {
status: 302,
headers: {
'content-type': 'application/json',
location: 'http://127.0.0.1/internal',
},
});
},
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({ verdict: 'not-measured', reasonCode: 'owner-control-invalid' });
expect(requests).toHaveLength(1);
expect(requests[0]?.redirect).toBe('manual');
expect(requests[0]?.signal).toBeInstanceOf(AbortSignal);
});
it('requires the GLPI standing remediation queue in the local estate policy', async (): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 standing process policy');
const raw = JSON.parse(ownerPolicy()) as { estates: Array<Record<string, unknown>> };
@@ -109,6 +109,8 @@ async function readPublicIdentity(
Accept: 'application/json',
'User-Agent': 'mosaic-brain-owner/1',
},
redirect: 'manual',
signal: AbortSignal.timeout(5_000),
});
} catch {
throw new Error('owner-provider-unavailable');
@@ -672,6 +672,11 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v
mkdirSync(join(laneRoot, path, '..'), { recursive: true });
writeFileSync(join(laneRoot, path), 'NESTED-SECRET-MARKER\n');
}
const embeddedSecret = join(laneRoot, 'benign-looking.md');
writeFileSync(
embeddedSecret,
'Authorization: Bearer NESTED-CONTENT-SECRET-MARKER-1234567890\n',
);
const plan = sut.discoverBrainMigration(
{ sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true },
@@ -680,9 +685,9 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v
expect(plan.status).toBe('ready');
expect(plan.candidates).toHaveLength(0);
expect(plan.reported).toHaveLength(secretNames.length + nestedSecretPaths.length);
expect(plan.reported).toHaveLength(secretNames.length + nestedSecretPaths.length + 1);
expect(plan.reported.every((entry) => /secret/i.test(entry.reason))).toBe(true);
expect(JSON.stringify(plan)).not.toContain('NESTED-SECRET-MARKER');
expect(JSON.stringify(plan)).not.toMatch(/NESTED-(?:CONTENT-)?SECRET-MARKER/);
expect(existsSync(brainRoot)).toBe(false);
});
+36 -4
View File
@@ -46,6 +46,7 @@ const GITIGNORE_RULES = [
'id_ed25519',
] as const;
const BRAIN_DIRECTORIES = ['agents', 'lanes', 'board', 'specs', 'methods', 'archives'] as const;
const MAX_MIGRATION_FILE_BYTES = 1024 * 1024;
const TERMINAL_EXITS = {
ok: 0,
refused: 10,
@@ -615,6 +616,7 @@ function migrationCandidate(
lane: string,
): MigrationCandidate {
const source = stableSourceSnapshot(path);
if (sourceContainsSecretMaterial(source.content)) throw new Error('migration-secret-content');
const key = relative(sourceRoot, path).split(sep).join('/');
const digest = createHash('sha256')
.update(key)
@@ -639,6 +641,7 @@ function migrationCandidate(
function secretShapedName(value: string): boolean {
const name = value.toLowerCase();
return (
['.npmrc', '.netrc', 'auth.json', '.dockerconfigjson'].includes(name) ||
name === '.env' ||
name.startsWith('.env.') ||
name === 'credentials.json' ||
@@ -658,6 +661,23 @@ function secretShapedPath(sourceRoot: string, path: string): boolean {
return relative(sourceRoot, path).split(sep).filter(Boolean).some(secretShapedName);
}
function sourceContainsSecretMaterial(content: Buffer): boolean {
if (content.byteLength > MAX_MIGRATION_FILE_BYTES || content.includes(0)) return true;
let source: string;
try {
source = new TextDecoder('utf-8', { fatal: true }).decode(content);
} catch {
return true;
}
return [
/-----BEGIN [^-\r\n]*PRIVATE KEY-----/i,
/\bauthorization\s*:\s*(?:bearer|basic)\s+\S+/i,
/\b(?:password|passwd|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret)\s*[:=]\s*\S+/i,
/https?:\/\/[^/\s:@]+:[^/\s@]+@/i,
/\b[A-Za-z0-9+/_=-]{40,}\b/,
].some((pattern): boolean => pattern.test(source));
}
function reportAll(paths: readonly string[], reason: string): MigrationReport[] {
return paths.map((path: string): MigrationReport => ({ path, reason }));
}
@@ -729,8 +749,14 @@ export function discoverBrainMigration(
candidates.push(
migrationCandidate(input.sourceRoot, input.brainRoot, path, 'lane', seat, lane),
);
} catch {
reported.push({ path, reason: 'Lane state was not a regular file; retained and reported.' });
} catch (error: unknown) {
reported.push({
path,
reason:
error instanceof Error && error.message === 'migration-secret-content'
? 'Content may contain secret material; retained and reported.'
: 'Lane state was not a regular file; retained and reported.',
});
}
}
for (const path of seatFiles) {
@@ -745,8 +771,14 @@ export function discoverBrainMigration(
candidates.push(
migrationCandidate(input.sourceRoot, input.brainRoot, path, 'seat', seat, lane),
);
} catch {
reported.push({ path, reason: 'Seat state was not a regular file; retained and reported.' });
} catch (error: unknown) {
reported.push({
path,
reason:
error instanceof Error && error.message === 'migration-secret-content'
? 'Content may contain secret material; retained and reported.'
: 'Seat state was not a regular file; retained and reported.',
});
}
}
return { status: 'ready', candidates, reported, owner: ownerResolution.principal };