fix(mosaic): reject unapproved brain skeleton state
This commit is contained in:
@@ -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] 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] 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] 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] 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] 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] 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] 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.
|
- [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);
|
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> => {
|
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 provisioner = await loadProvisioner('MB-REQ-07 complete migration transaction');
|
||||||
const root = tempRoot();
|
const root = tempRoot();
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ export async function provisionBrain(
|
|||||||
finding.code === 'brain-not-git-repository' ||
|
finding.code === 'brain-not-git-repository' ||
|
||||||
finding.code === 'brain-remote-mismatch' ||
|
finding.code === 'brain-remote-mismatch' ||
|
||||||
finding.code === 'brain-branch-mismatch' ||
|
finding.code === 'brain-branch-mismatch' ||
|
||||||
|
finding.code === 'brain-uncommitted-state' ||
|
||||||
|
finding.code === 'brain-git-state-indeterminate' ||
|
||||||
finding.code.startsWith('brain-write-access-'),
|
finding.code.startsWith('brain-write-access-'),
|
||||||
);
|
);
|
||||||
if (blockingCloneFindings.length > 0) {
|
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> => {
|
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 sut = await loadSut('MB-REQ-06 no-follow brain layout');
|
||||||
const root = tempRoot();
|
const root = tempRoot();
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
openSync,
|
openSync,
|
||||||
readdirSync,
|
readdirSync,
|
||||||
readFileSync,
|
readFileSync,
|
||||||
renameSync,
|
|
||||||
rmSync,
|
rmSync,
|
||||||
unlinkSync,
|
unlinkSync,
|
||||||
writeFileSync,
|
writeFileSync,
|
||||||
@@ -400,21 +399,22 @@ export function createBrainSkeleton(root: string): {
|
|||||||
throw new Error('brain-layout-ignore-unsafe');
|
throw new Error('brain-layout-ignore-unsafe');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const existing = existsSync(ignorePath)
|
const content = `${GITIGNORE_RULES.join('\n')}\n`;
|
||||||
? readFileSync(ignorePath, 'utf8')
|
if (existsSync(ignorePath)) {
|
||||||
.split(/\r?\n/)
|
const snapshot = stableSourceSnapshot(ignorePath);
|
||||||
.filter((line: string): boolean => line.length > 0)
|
if (!snapshot.content.equals(Buffer.from(content, 'utf8'))) {
|
||||||
: [];
|
throw new Error('brain-layout-ignore-content-unsafe');
|
||||||
const merged = [...existing];
|
}
|
||||||
for (const rule of GITIGNORE_RULES) {
|
} else {
|
||||||
if (!merged.includes(rule)) merged.push(rule);
|
|
||||||
}
|
|
||||||
const content = `${merged.join('\n')}\n`;
|
|
||||||
if (!existsSync(ignorePath) || readFileSync(ignorePath, 'utf8') !== content) {
|
|
||||||
const temporary = `${ignorePath}.tmp-${process.pid}-${randomUUID()}`;
|
const temporary = `${ignorePath}.tmp-${process.pid}-${randomUUID()}`;
|
||||||
writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o644, flag: 'wx' });
|
try {
|
||||||
syncFile(temporary);
|
writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o644, flag: 'wx' });
|
||||||
renameSync(temporary, ignorePath);
|
syncFile(temporary);
|
||||||
|
linkSync(temporary, ignorePath);
|
||||||
|
syncFile(ignorePath);
|
||||||
|
} finally {
|
||||||
|
rmSync(temporary, { force: true });
|
||||||
|
}
|
||||||
created.push(ignorePath);
|
created.push(ignorePath);
|
||||||
publicationEntries.push({ path: ignorePath, content: new TextEncoder().encode(content) });
|
publicationEntries.push({ path: ignorePath, content: new TextEncoder().encode(content) });
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user