fix(mosaic): enforce private brain checkout
ci/woodpecker/pr/ci Pipeline failed

This commit is contained in:
2026-08-05 17:19:51 -05:00
parent 22cedbb506
commit a50b5a6b4d
8 changed files with 167 additions and 14 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] 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: 95/95 (secure config 4, store 44, runtime 18, owner resolver 16, provision 5, 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: 97/97 (secure config 4, store 45, runtime 19, 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. `.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] 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. The brain root is principal-owned mode `0700` before clone and after clone, all memory-bearing layout directories are mode `0700` even under umask `0022`, and doctor reports owner-accessible roots as hard unsafe findings.
- [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.
@@ -50,7 +50,8 @@ function renderReport(report: DoctorRuntimeReport): InstalledDoctorResult {
(finding): boolean => (finding): boolean =>
finding.code.endsWith('-error') || finding.code.endsWith('-error') ||
finding.code.endsWith('-indeterminate') || finding.code.endsWith('-indeterminate') ||
finding.code === 'brain-not-git-repository', finding.code === 'brain-not-git-repository' ||
finding.code === 'brain-root-permissions-unsafe',
); );
const status: InstalledDoctorResult['status'] = const status: InstalledDoctorResult['status'] =
findings.length === 0 ? 'ok' : hard ? 'error' : 'warn'; findings.length === 0 ? 'ok' : hard ? 'error' : 'warn';
@@ -1,5 +1,13 @@
import { afterEach, describe, expect, it } from 'vitest'; import { afterEach, describe, expect, it } from 'vitest';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import {
existsSync,
lstatSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
@@ -359,6 +367,7 @@ describe('P7 brain provisioning orchestration', (): void => {
const requests: CommandRequest[] = []; const requests: CommandRequest[] = [];
let commitOrdinal = 0; let commitOrdinal = 0;
let approvedPaths: string[] = []; let approvedPaths: string[] = [];
let privateAtClone = false;
const runner: CommandRunner = (request): CommandResult => { const runner: CommandRunner = (request): CommandResult => {
requests.push(request); requests.push(request);
if (request.program === 'mosaic') { if (request.program === 'mosaic') {
@@ -380,6 +389,7 @@ describe('P7 brain provisioning orchestration', (): void => {
}; };
} }
if (request.args[0] === 'clone') { if (request.args[0] === 'clone') {
privateAtClone = existsSync(input.root) && (lstatSync(input.root).mode & 0o077) === 0;
mkdirSync(join(input.root, '.git'), { recursive: true }); mkdirSync(join(input.root, '.git'), { recursive: true });
return { status: 0, stdout: '', stderr: '' }; return { status: 0, stdout: '', stderr: '' };
} }
@@ -455,6 +465,7 @@ describe('P7 brain provisioning orchestration', (): void => {
owner: { verdict: 'resolved', reasonCode: 'owner-verified' }, owner: { verdict: 'resolved', reasonCode: 'owner-verified' },
migration: { status: 'reported' }, migration: { status: 'reported' },
}); });
expect(privateAtClone).toBe(true);
expect(existsSync(source)).toBe(true); expect(existsSync(source)).toBe(true);
const imported = result.migration?.reported ?? []; const imported = result.migration?.reported ?? [];
expect(imported).toEqual( expect(imported).toEqual(
@@ -1,8 +1,10 @@
import { existsSync } from 'node:fs';
import { parseRequestedOwner, resolveProviderDurableOwner } from './brain-owner-resolver.js'; import { parseRequestedOwner, resolveProviderDurableOwner } from './brain-owner-resolver.js';
import { import {
createBrainSkeleton, createBrainSkeleton,
deriveBrainTarget, deriveBrainTarget,
discoverBrainMigration, discoverBrainMigration,
ensureBrainRootPrivate,
migrateBrainState, migrateBrainState,
type MigrationResult, type MigrationResult,
type MigrationOwnerResolution, type MigrationOwnerResolution,
@@ -94,6 +96,13 @@ export async function provisionBrain(
const brainNamespace = parseRequestedOwner(input.requestedOwner); const brainNamespace = parseRequestedOwner(input.requestedOwner);
if (brainNamespace === null) return blocked('owner-name-invalid', []); if (brainNamespace === null) return blocked('owner-name-invalid', []);
const target = deriveBrainTarget(input.estateRegistrySource, input.targetGitUrl, brainNamespace); const target = deriveBrainTarget(input.estateRegistrySource, input.targetGitUrl, brainNamespace);
if (existsSync(input.root)) {
try {
ensureBrainRootPrivate(input.root);
} catch {
return blocked('brain-root-permissions-unsafe', []);
}
}
const doctorInput = { const doctorInput = {
registrySource: input.estateRegistrySource, registrySource: input.estateRegistrySource,
targetGitUrl: input.targetGitUrl, targetGitUrl: input.targetGitUrl,
@@ -159,6 +168,11 @@ export async function provisionBrain(
} }
if (report.findings.some((finding): boolean => finding.code === 'brain-clone-missing')) { if (report.findings.some((finding): boolean => finding.code === 'brain-clone-missing')) {
try {
ensureBrainRootPrivate(input.root);
} catch {
return failed('brain-root-permissions-unsafe', report.findings);
}
const clone: CommandRequest = { const clone: CommandRequest = {
program: 'git', program: 'git',
args: ['clone', '--branch', 'main', '--single-branch', target.cloneUrl, input.root], args: ['clone', '--branch', 'main', '--single-branch', target.cloneUrl, input.root],
@@ -166,6 +180,11 @@ export async function provisionBrain(
}; };
const cloneResult: CommandResult = dependencies.run(clone); const cloneResult: CommandResult = dependencies.run(clone);
if (cloneResult.status !== 0) return failed('brain-clone-failed', report.findings); if (cloneResult.status !== 0) return failed('brain-clone-failed', report.findings);
try {
ensureBrainRootPrivate(input.root);
} catch {
return failed('brain-root-permissions-unsafe', report.findings);
}
report = collectBrainDoctorReport(doctorInput, dependencies.run); report = collectBrainDoctorReport(doctorInput, dependencies.run);
} }
@@ -177,6 +196,7 @@ export async function provisionBrain(
finding.code === 'brain-branch-mismatch' || finding.code === 'brain-branch-mismatch' ||
finding.code === 'brain-uncommitted-state' || finding.code === 'brain-uncommitted-state' ||
finding.code === 'brain-git-state-indeterminate' || finding.code === 'brain-git-state-indeterminate' ||
finding.code === 'brain-root-permissions-unsafe' ||
finding.code.startsWith('brain-write-access-'), finding.code.startsWith('brain-write-access-'),
); );
if (blockingCloneFindings.length > 0) { if (blockingCloneFindings.length > 0) {
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, it } from 'vitest'; import { afterEach, describe, expect, it } from 'vitest';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process'; import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
@@ -361,6 +361,57 @@ describe('doctor runtime observation', (): void => {
} }
}); });
it('reports an owner-accessible brain root as unsafe instead of ready', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-06 owner-only doctor measurement');
const root = join(tempRoot(), 'brain');
mkdirSync(join(root, '.git'), { recursive: true });
chmodSync(root, 0o755);
const report = runtime.collectBrainDoctorReport(
{
registrySource: registry(),
targetGitUrl: 'https://git.mosaicstack.dev/mosaicstack/stack.git',
brainNamespace: 'mosaicstack',
identity: 'seat-a',
root,
},
(request): CommandResult => {
if (request.program === 'mosaic') {
return {
status: 0,
stdout: validateResult('ok', 'validation-verified').replaceAll(
'synthetic-no-token',
'seat-a',
),
stderr: '',
};
}
const command = request.args.join(' ');
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.mosaicstack.dev/mosaicstack/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: '', stderr: '' };
}
return { status: 99, stdout: '', stderr: 'unexpected command' };
},
);
expect(report.findings).toEqual(
expect.arrayContaining([expect.objectContaining({ code: 'brain-root-permissions-unsafe' })]),
);
});
it.each(['remote', 'branch', 'status'] as const)( it.each(['remote', 'branch', 'status'] as const)(
'reports %s measurement failure as indeterminate rather than healthy or mismatched', 'reports %s measurement failure as indeterminate rather than healthy or mismatched',
async (failedMeasurement): Promise<void> => { async (failedMeasurement): Promise<void> => {
@@ -4,7 +4,9 @@ import { tmpdir } from 'node:os';
import { isAbsolute, join, relative, resolve, sep } from 'node:path'; import { isAbsolute, join, relative, resolve, sep } from 'node:path';
import { import {
assessCredentialResult, assessCredentialResult,
brainRootIsPrivate,
deriveBrainTarget, deriveBrainTarget,
ensureBrainRootPrivate,
evaluateBrainDoctor, evaluateBrainDoctor,
planBrainDoctorFix, planBrainDoctorFix,
type BrainDoctorFinding, type BrainDoctorFinding,
@@ -174,6 +176,7 @@ export function collectBrainDoctorReport(
const observation: BrainDoctorObservation = { const observation: BrainDoctorObservation = {
rootExists, rootExists,
rootPrivate: rootExists && brainRootIsPrivate(input.root),
gitRepository, gitRepository,
remote, remote,
branch, branch,
@@ -300,6 +303,13 @@ export function repairBrainDoctor(
if (report.access.outcome !== 'ok') return report; if (report.access.outcome !== 'ok') return report;
continue; continue;
} }
if (action.findingCode === 'brain-clone-missing') {
try {
ensureBrainRootPrivate(input.root);
} catch {
return collectBrainDoctorReport(input, run);
}
}
const result = runGit(run, input.identity, action.args); const result = runGit(run, input.identity, action.args);
if (result.status !== 0) return collectBrainDoctorReport(input, run); if (result.status !== 0) return collectBrainDoctorReport(input, run);
} }
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it } from 'vitest'; import { afterEach, describe, expect, it } from 'vitest';
import { import {
existsSync, existsSync,
lstatSync,
mkdtempSync, mkdtempSync,
mkdirSync, mkdirSync,
readFileSync, readFileSync,
@@ -90,6 +91,7 @@ interface MigrationResult {
interface BrainDoctorObservation { interface BrainDoctorObservation {
readonly rootExists: boolean; readonly rootExists: boolean;
readonly rootPrivate: boolean;
readonly gitRepository: boolean; readonly gitRepository: boolean;
readonly remote: string | null; readonly remote: string | null;
readonly branch: string | null; readonly branch: string | null;
@@ -359,6 +361,26 @@ describe('R6 — brain layout refuses secret material', (): void => {
); );
}); });
it('creates the brain root and memory directories owner-only under a permissive umask', async (): Promise<void> => {
const sut = await loadSut('MB-REQ-06 owner-only brain checkout');
const brain = join(tempRoot(), 'brain');
const previousUmask = process.umask(0o022);
try {
sut.createBrainSkeleton(brain);
} finally {
process.umask(previousUmask);
}
for (const path of [
brain,
...['agents', 'lanes', 'board', 'specs', 'methods', 'archives'].map((name) =>
join(brain, name),
),
]) {
expect(lstatSync(path).mode & 0o077, path).toBe(0);
}
});
it('refuses existing noncanonical gitignore content instead of publishing it', async (): Promise<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 sut = await loadSut('MB-REQ-06 unapproved gitignore content');
const root = tempRoot(); const root = tempRoot();
@@ -998,6 +1020,7 @@ describe('R8 — doctor diagnoses defects and fixes only through approved seams'
const missing = sut.evaluateBrainDoctor( const missing = sut.evaluateBrainDoctor(
{ {
rootExists: false, rootExists: false,
rootPrivate: false,
gitRepository: false, gitRepository: false,
remote: null, remote: null,
branch: null, branch: null,
@@ -1013,6 +1036,7 @@ describe('R8 — doctor diagnoses defects and fixes only through approved seams'
const defects = sut.evaluateBrainDoctor( const defects = sut.evaluateBrainDoctor(
{ {
rootExists: true, rootExists: true,
rootPrivate: true,
gitRepository: true, gitRepository: true,
remote: 'https://git.uscllc.com/usc/mosaic-brain.git', remote: 'https://git.uscllc.com/usc/mosaic-brain.git',
branch: 'main', branch: 'main',
+45 -9
View File
@@ -1,4 +1,5 @@
import { import {
chmodSync,
closeSync, closeSync,
constants as fsConstants, constants as fsConstants,
existsSync, existsSync,
@@ -19,6 +20,7 @@ import { createHash, randomUUID } from 'node:crypto';
import { platform } from 'node:os'; import { platform } from 'node:os';
import { z } from 'zod'; import { z } from 'zod';
import { parseCredentialEstateRegistry } from '../credentials/estate-registry.js'; import { parseCredentialEstateRegistry } from '../credentials/estate-registry.js';
import { assertNoSymlinkAncestors } from '../fleet/secure-file.js';
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/; const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
const COMMIT = /^[0-9a-f]{40}$/; const COMMIT = /^[0-9a-f]{40}$/;
@@ -174,6 +176,7 @@ export interface MigrationResult {
export interface BrainDoctorObservation { export interface BrainDoctorObservation {
readonly rootExists: boolean; readonly rootExists: boolean;
readonly rootPrivate: boolean;
readonly gitRepository: boolean; readonly gitRepository: boolean;
readonly remote: string | null; readonly remote: string | null;
readonly branch: string | null; readonly branch: string | null;
@@ -343,6 +346,36 @@ export function deriveBrainTarget(
}; };
} }
function processUid(): number {
if (typeof process.getuid !== 'function') throw new Error('brain-owner-check-unsupported');
return process.getuid();
}
export function brainRootIsPrivate(root: string): boolean {
try {
const status = lstatSync(root);
return (
status.isDirectory() &&
!status.isSymbolicLink() &&
status.uid === processUid() &&
(status.mode & 0o077) === 0
);
} catch {
return false;
}
}
export function ensureBrainRootPrivate(root: string): void {
assertNoSymlinkAncestors(root);
if (!existsSync(root)) mkdirSync(root, { recursive: true, mode: 0o700 });
const status = lstatSync(root);
if (!status.isDirectory() || status.isSymbolicLink() || status.uid !== processUid()) {
throw new Error('brain-layout-root-unsafe');
}
chmodSync(root, 0o700);
if (!brainRootIsPrivate(root)) throw new Error('brain-layout-root-permissions-unsafe');
}
function syncFile(path: string): void { function syncFile(path: string): void {
const descriptor = openSync(path, 'r'); const descriptor = openSync(path, 'r');
try { try {
@@ -358,27 +391,22 @@ export function createBrainSkeleton(root: string): {
} { } {
const created: string[] = []; const created: string[] = [];
const publicationEntries: MigrationPublishEntry[] = []; const publicationEntries: MigrationPublishEntry[] = [];
if (existsSync(root)) { ensureBrainRootPrivate(root);
const rootStatus = lstatSync(root);
if (!rootStatus.isDirectory() || rootStatus.isSymbolicLink()) {
throw new Error('brain-layout-root-unsafe');
}
}
for (const directory of BRAIN_DIRECTORIES) { for (const directory of BRAIN_DIRECTORIES) {
const path = join(root, directory); const path = join(root, directory);
if (existsSync(path)) { if (existsSync(path)) {
const status = lstatSync(path); const status = lstatSync(path);
if (!status.isDirectory() || status.isSymbolicLink()) { if (!status.isDirectory() || status.isSymbolicLink() || status.uid !== processUid()) {
throw new Error('brain-layout-directory-unsafe'); throw new Error('brain-layout-directory-unsafe');
} }
chmodSync(path, 0o700);
} }
} }
mkdirSync(root, { recursive: true });
for (const directory of BRAIN_DIRECTORIES) { for (const directory of BRAIN_DIRECTORIES) {
const path = join(root, directory); const path = join(root, directory);
if (!existsSync(path)) { if (!existsSync(path)) {
mkdirSync(path, { recursive: true }); mkdirSync(path, { recursive: true, mode: 0o700 });
created.push(path); created.push(path);
} }
const placeholder = join(path, '.gitkeep'); const placeholder = join(path, '.gitkeep');
@@ -1075,8 +1103,16 @@ export function evaluateBrainDoctor(
]; ];
} }
const findings: BrainDoctorFinding[] = []; const findings: BrainDoctorFinding[] = [];
if (!observation.rootPrivate) {
findings.push({
code: 'brain-root-permissions-unsafe',
repairable: false,
reasonCode: 'owner-only-root-required',
});
}
if (!observation.gitRepository) { if (!observation.gitRepository) {
findings.push({ code: 'brain-not-git-repository', repairable: true, reasonCode: null }); findings.push({ code: 'brain-not-git-repository', repairable: true, reasonCode: null });
if (access !== null) findings.push(access);
return findings; return findings;
} }
if (observation.remote === null) { if (observation.remote === null) {