fix(mosaic): reject unapproved brain skeleton state

This commit is contained in:
2026-08-05 17:19:51 -05:00
parent bad53564ed
commit 22cedbb506
5 changed files with 86 additions and 17 deletions
@@ -83,10 +83,10 @@ No explicit token ceiling was supplied. Working cap: 55K tokens for implementati
- [x] C1 P5→P7 seam receipt read; no brain implementation is in C1.
- [x] RED acceptance set committed at `cf11c6c86abae073d8b02b4014cd5447ba67f12a`; author and committer read back as `be-coder-07` and branch reachability was independently verified by `tl-mosaic`.
- [x] Moving-contract REDs observed independently for v1.4 mismatch, R8 prerequisite ordering, owner resolver seam/allowlist, tracked skeleton/no-follow behavior, runtime observation/publication, and provider owner resolution.
- [x] Focused implementation includes secure migration, v1.5 write-differential/subject binding, production Git+API refusal parity, provider-backed durable owner resolution that ignores non-admin `active`, required GLPI standing-process policy, P7 provision orchestration, an internal installer command, and installed `mosaic doctor` wiring. Latest focused result: 93/93 (secure config 4, store 43, runtime 18, owner resolver 16, provision 4, provision command 3, installed doctor 5).
- [x] Focused implementation includes secure migration, v1.5 write-differential/subject binding, production Git+API refusal parity, provider-backed durable owner resolution that ignores non-admin `active`, required GLPI standing-process policy, P7 provision orchestration, an internal installer command, and installed `mosaic doctor` wiring. Latest focused result: 95/95 (secure config 4, store 44, runtime 18, owner resolver 16, provision 5, provision command 3, installed doctor 5).
- [x] MC-CRED added the required canonical reverse registry seam `ParsedCredentialEstateRegistry.resolveByHost()` at dependency head `6ca8758f`; current local copies are temporary until dependency integration and the 32-line permissive shim has been removed.
- [x] Identity gotcha measured: inline `MOSAIC_GIT_IDENTITY=be-coder-07` controls credential resolution but does not override `user.name`/`user.email` inherited from the linked worktree common-dir config (`coder-mos1`). The first local P7 RED commit was immediately amended before push with command-scoped `GIT_AUTHOR_*` + `GIT_COMMITTER_*`; resulting author and committer both read back as `be-coder-07`. Every subsequent authoring command must carry both identity sets and be verified.
- [x] R6 migration reports filename- or content-secret-shaped files without copying them; arbitrary legacy content requires an approved scanner bound to the exact source snapshot, and production currently retains/reports when no approved scanner is configured. Symlinked `.gitignore`, layout directories, and nested migration destinations fail closed.
- [x] R6 migration reports filename- or content-secret-shaped files without copying them; arbitrary legacy content requires an approved scanner bound to the exact source snapshot, and production currently retains/reports when no approved scanner is configured. `.gitignore` is canonical allowlisted content only: an existing noncanonical regular file fails closed and is never merged into publication. Symlinked `.gitignore`, layout directories, and nested migration destinations fail closed; a dirty checkout blocks provisioning before skeleton publication.
- [x] Provider owner lookup uses manual redirect handling, a five-second abort signal, strict JSON content type/shape, and an incrementally enforced 256 KiB response ceiling.
- [x] Security-critical owner policy/registry reads have direct controls for principal UID ownership, file/ancestor permissions, and descriptor-safe regular-file reads.
- [x] Automatic source deletion is parked per the shared-Git-identity governance ruling; remotely reachable snapshots still leave and report every source.
@@ -292,6 +292,63 @@ describe('P7 brain provisioning orchestration', (): void => {
expect(requests.some((request) => request.args.includes('grant'))).toBe(false);
});
it('blocks before skeleton publication when the existing checkout is dirty', async (): Promise<void> => {
const provisioner = await loadProvisioner('MB-REQ-06 dirty checkout publication gate');
const root = tempRoot();
const input = baseInput(root);
mkdirSync(join(input.root, '.git'), { recursive: true });
const requests: CommandRequest[] = [];
const result = await provisioner.provisionBrain(input, {
run: (request): CommandResult => {
requests.push(request);
if (request.program === 'mosaic') {
return request.args[2] === 'outside-seat'
? {
status: 10,
stdout: validateResult('refused', 'no-token-for-identity', 'outside-seat'),
stderr: 'refused reason=no-token-for-identity',
}
: { status: 0, stdout: validateResult('ok', 'validation-verified'), stderr: '' };
}
const command = request.args.join(' ');
if (command.includes('ls-remote')) {
return {
status: 128,
stdout: '',
stderr: 'credential helper refused reason=no-token-for-identity',
};
}
if (command.includes('rev-parse --is-inside-work-tree')) {
return { status: 0, stdout: 'true\n', stderr: '' };
}
if (command.includes('remote get-url origin')) {
return {
status: 0,
stdout: 'https://git.example.invalid/durable-owner/mosaic-brain.git\n',
stderr: '',
};
}
if (command.includes('branch --show-current')) {
return { status: 0, stdout: 'main\n', stderr: '' };
}
if (command.includes('status --porcelain')) {
return { status: 0, stdout: '?? .gitignore\n', stderr: '' };
}
return { status: 99, stdout: '', stderr: 'unexpected publication command' };
},
fetch: ownerFetch(),
absentControlName: (): string => 'generated-absent-control',
});
expect(result).toMatchObject({
status: 'blocked',
reasonCode: 'brain-postcondition-failed',
});
expect(requests.some((request) => request.args.includes('commit'))).toBe(false);
expect(requests.some((request) => request.args.includes('push'))).toBe(false);
});
it('clones, seeds, resolves owner, migrates, pushes on each write, and archives source only after reachability', async (): Promise<void> => {
const provisioner = await loadProvisioner('MB-REQ-07 complete migration transaction');
const root = tempRoot();
@@ -175,6 +175,8 @@ export async function provisionBrain(
finding.code === 'brain-not-git-repository' ||
finding.code === 'brain-remote-mismatch' ||
finding.code === 'brain-branch-mismatch' ||
finding.code === 'brain-uncommitted-state' ||
finding.code === 'brain-git-state-indeterminate' ||
finding.code.startsWith('brain-write-access-'),
);
if (blockingCloneFindings.length > 0) {
@@ -359,6 +359,16 @@ describe('R6 — brain layout refuses secret material', (): void => {
);
});
it('refuses existing noncanonical gitignore content instead of publishing it', async (): Promise<void> => {
const sut = await loadSut('MB-REQ-06 unapproved gitignore content');
const root = tempRoot();
const ignore = join(root, '.gitignore');
writeFileSync(ignore, '# token=fixture-value\n*.token\n');
expect(() => sut.createBrainSkeleton(root)).toThrow(/brain-layout-ignore-content-unsafe/);
expect(readFileSync(ignore, 'utf8')).toBe('# token=fixture-value\n*.token\n');
});
it('refuses a symlinked layout directory without writing a tracked placeholder outside the brain', async (): Promise<void> => {
const sut = await loadSut('MB-REQ-06 no-follow brain layout');
const root = tempRoot();
+15 -15
View File
@@ -10,7 +10,6 @@ import {
openSync,
readdirSync,
readFileSync,
renameSync,
rmSync,
unlinkSync,
writeFileSync,
@@ -400,21 +399,22 @@ export function createBrainSkeleton(root: string): {
throw new Error('brain-layout-ignore-unsafe');
}
}
const existing = existsSync(ignorePath)
? readFileSync(ignorePath, 'utf8')
.split(/\r?\n/)
.filter((line: string): boolean => line.length > 0)
: [];
const merged = [...existing];
for (const rule of GITIGNORE_RULES) {
if (!merged.includes(rule)) merged.push(rule);
}
const content = `${merged.join('\n')}\n`;
if (!existsSync(ignorePath) || readFileSync(ignorePath, 'utf8') !== content) {
const content = `${GITIGNORE_RULES.join('\n')}\n`;
if (existsSync(ignorePath)) {
const snapshot = stableSourceSnapshot(ignorePath);
if (!snapshot.content.equals(Buffer.from(content, 'utf8'))) {
throw new Error('brain-layout-ignore-content-unsafe');
}
} else {
const temporary = `${ignorePath}.tmp-${process.pid}-${randomUUID()}`;
writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o644, flag: 'wx' });
syncFile(temporary);
renameSync(temporary, ignorePath);
try {
writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o644, flag: 'wx' });
syncFile(temporary);
linkSync(temporary, ignorePath);
syncFile(ignorePath);
} finally {
rmSync(temporary, { force: true });
}
created.push(ignorePath);
publicationEntries.push({ path: ignorePath, content: new TextEncoder().encode(content) });
}