From 5b1717ac572e1cb3907f21557d0a0cc556d748b8 Mon Sep 17 00:00:00 2001 From: be-coder-07 Date: Wed, 5 Aug 2026 14:50:45 -0500 Subject: [PATCH] fix(mosaic): enforce private brain checkout --- .../1051-mosaic-brain-installer.md | 4 +- .../mosaic/src/commands/brain-doctor-check.ts | 3 +- .../src/commands/brain-provision.spec.ts | 13 ++++- .../mosaic/src/commands/brain-provision.ts | 20 +++++++ .../src/commands/brain-store-runtime.spec.ts | 53 +++++++++++++++++- .../src/commands/brain-store-runtime.ts | 10 ++++ .../mosaic/src/commands/brain-store.spec.ts | 24 +++++++++ packages/mosaic/src/commands/brain-store.ts | 54 +++++++++++++++---- 8 files changed, 167 insertions(+), 14 deletions(-) diff --git a/docs/scratchpads/1051-mosaic-brain-installer.md b/docs/scratchpads/1051-mosaic-brain-installer.md index d6c92df1..09d86a34 100644 --- a/docs/scratchpads/1051-mosaic-brain-installer.md +++ b/docs/scratchpads/1051-mosaic-brain-installer.md @@ -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: 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] 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] 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. diff --git a/packages/mosaic/src/commands/brain-doctor-check.ts b/packages/mosaic/src/commands/brain-doctor-check.ts index c05501a8..69350766 100644 --- a/packages/mosaic/src/commands/brain-doctor-check.ts +++ b/packages/mosaic/src/commands/brain-doctor-check.ts @@ -50,7 +50,8 @@ function renderReport(report: DoctorRuntimeReport): InstalledDoctorResult { (finding): boolean => finding.code.endsWith('-error') || 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'] = findings.length === 0 ? 'ok' : hard ? 'error' : 'warn'; diff --git a/packages/mosaic/src/commands/brain-provision.spec.ts b/packages/mosaic/src/commands/brain-provision.spec.ts index a99dd930..fd4d0aee 100644 --- a/packages/mosaic/src/commands/brain-provision.spec.ts +++ b/packages/mosaic/src/commands/brain-provision.spec.ts @@ -1,5 +1,13 @@ 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 { join } from 'node:path'; @@ -359,6 +367,7 @@ describe('P7 brain provisioning orchestration', (): void => { const requests: CommandRequest[] = []; let commitOrdinal = 0; let approvedPaths: string[] = []; + let privateAtClone = false; const runner: CommandRunner = (request): CommandResult => { requests.push(request); if (request.program === 'mosaic') { @@ -380,6 +389,7 @@ describe('P7 brain provisioning orchestration', (): void => { }; } if (request.args[0] === 'clone') { + privateAtClone = existsSync(input.root) && (lstatSync(input.root).mode & 0o077) === 0; mkdirSync(join(input.root, '.git'), { recursive: true }); return { status: 0, stdout: '', stderr: '' }; } @@ -455,6 +465,7 @@ describe('P7 brain provisioning orchestration', (): void => { owner: { verdict: 'resolved', reasonCode: 'owner-verified' }, migration: { status: 'reported' }, }); + expect(privateAtClone).toBe(true); expect(existsSync(source)).toBe(true); const imported = result.migration?.reported ?? []; expect(imported).toEqual( diff --git a/packages/mosaic/src/commands/brain-provision.ts b/packages/mosaic/src/commands/brain-provision.ts index 8889a153..07790d46 100644 --- a/packages/mosaic/src/commands/brain-provision.ts +++ b/packages/mosaic/src/commands/brain-provision.ts @@ -1,8 +1,10 @@ +import { existsSync } from 'node:fs'; import { parseRequestedOwner, resolveProviderDurableOwner } from './brain-owner-resolver.js'; import { createBrainSkeleton, deriveBrainTarget, discoverBrainMigration, + ensureBrainRootPrivate, migrateBrainState, type MigrationResult, type MigrationOwnerResolution, @@ -94,6 +96,13 @@ export async function provisionBrain( const brainNamespace = parseRequestedOwner(input.requestedOwner); if (brainNamespace === null) return blocked('owner-name-invalid', []); 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 = { registrySource: input.estateRegistrySource, targetGitUrl: input.targetGitUrl, @@ -159,6 +168,11 @@ export async function provisionBrain( } 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 = { program: 'git', args: ['clone', '--branch', 'main', '--single-branch', target.cloneUrl, input.root], @@ -166,6 +180,11 @@ export async function provisionBrain( }; const cloneResult: CommandResult = dependencies.run(clone); 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); } @@ -177,6 +196,7 @@ export async function provisionBrain( finding.code === 'brain-branch-mismatch' || finding.code === 'brain-uncommitted-state' || finding.code === 'brain-git-state-indeterminate' || + finding.code === 'brain-root-permissions-unsafe' || finding.code.startsWith('brain-write-access-'), ); if (blockingCloneFindings.length > 0) { diff --git a/packages/mosaic/src/commands/brain-store-runtime.spec.ts b/packages/mosaic/src/commands/brain-store-runtime.spec.ts index 2b658fb6..e26e2f00 100644 --- a/packages/mosaic/src/commands/brain-store-runtime.spec.ts +++ b/packages/mosaic/src/commands/brain-store-runtime.spec.ts @@ -1,5 +1,5 @@ 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 { tmpdir } from 'node:os'; 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 => { + 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)( 'reports %s measurement failure as indeterminate rather than healthy or mismatched', async (failedMeasurement): Promise => { diff --git a/packages/mosaic/src/commands/brain-store-runtime.ts b/packages/mosaic/src/commands/brain-store-runtime.ts index 6bfa29b3..39d44e98 100644 --- a/packages/mosaic/src/commands/brain-store-runtime.ts +++ b/packages/mosaic/src/commands/brain-store-runtime.ts @@ -4,7 +4,9 @@ import { tmpdir } from 'node:os'; import { isAbsolute, join, relative, resolve, sep } from 'node:path'; import { assessCredentialResult, + brainRootIsPrivate, deriveBrainTarget, + ensureBrainRootPrivate, evaluateBrainDoctor, planBrainDoctorFix, type BrainDoctorFinding, @@ -174,6 +176,7 @@ export function collectBrainDoctorReport( const observation: BrainDoctorObservation = { rootExists, + rootPrivate: rootExists && brainRootIsPrivate(input.root), gitRepository, remote, branch, @@ -300,6 +303,13 @@ export function repairBrainDoctor( if (report.access.outcome !== 'ok') return report; continue; } + if (action.findingCode === 'brain-clone-missing') { + try { + ensureBrainRootPrivate(input.root); + } catch { + return collectBrainDoctorReport(input, run); + } + } const result = runGit(run, input.identity, action.args); if (result.status !== 0) return collectBrainDoctorReport(input, run); } diff --git a/packages/mosaic/src/commands/brain-store.spec.ts b/packages/mosaic/src/commands/brain-store.spec.ts index 9590f403..35ec2538 100644 --- a/packages/mosaic/src/commands/brain-store.spec.ts +++ b/packages/mosaic/src/commands/brain-store.spec.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it } from 'vitest'; import { existsSync, + lstatSync, mkdtempSync, mkdirSync, readFileSync, @@ -90,6 +91,7 @@ interface MigrationResult { interface BrainDoctorObservation { readonly rootExists: boolean; + readonly rootPrivate: boolean; readonly gitRepository: boolean; readonly remote: 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 => { + 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 => { const sut = await loadSut('MB-REQ-06 unapproved gitignore content'); const root = tempRoot(); @@ -998,6 +1020,7 @@ describe('R8 — doctor diagnoses defects and fixes only through approved seams' const missing = sut.evaluateBrainDoctor( { rootExists: false, + rootPrivate: false, gitRepository: false, remote: null, branch: null, @@ -1013,6 +1036,7 @@ describe('R8 — doctor diagnoses defects and fixes only through approved seams' const defects = sut.evaluateBrainDoctor( { rootExists: true, + rootPrivate: true, gitRepository: true, remote: 'https://git.uscllc.com/usc/mosaic-brain.git', branch: 'main', diff --git a/packages/mosaic/src/commands/brain-store.ts b/packages/mosaic/src/commands/brain-store.ts index 72389625..b85b088c 100644 --- a/packages/mosaic/src/commands/brain-store.ts +++ b/packages/mosaic/src/commands/brain-store.ts @@ -1,4 +1,5 @@ import { + chmodSync, closeSync, constants as fsConstants, existsSync, @@ -19,6 +20,7 @@ import { createHash, randomUUID } from 'node:crypto'; import { platform } from 'node:os'; import { z } from 'zod'; 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 COMMIT = /^[0-9a-f]{40}$/; @@ -174,6 +176,7 @@ export interface MigrationResult { export interface BrainDoctorObservation { readonly rootExists: boolean; + readonly rootPrivate: boolean; readonly gitRepository: boolean; readonly remote: 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 { const descriptor = openSync(path, 'r'); try { @@ -358,27 +391,22 @@ export function createBrainSkeleton(root: string): { } { const created: string[] = []; const publicationEntries: MigrationPublishEntry[] = []; - if (existsSync(root)) { - const rootStatus = lstatSync(root); - if (!rootStatus.isDirectory() || rootStatus.isSymbolicLink()) { - throw new Error('brain-layout-root-unsafe'); - } - } + ensureBrainRootPrivate(root); for (const directory of BRAIN_DIRECTORIES) { const path = join(root, directory); if (existsSync(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'); } + chmodSync(path, 0o700); } } - mkdirSync(root, { recursive: true }); for (const directory of BRAIN_DIRECTORIES) { const path = join(root, directory); if (!existsSync(path)) { - mkdirSync(path, { recursive: true }); + mkdirSync(path, { recursive: true, mode: 0o700 }); created.push(path); } const placeholder = join(path, '.gitkeep'); @@ -1075,8 +1103,16 @@ export function evaluateBrainDoctor( ]; } const findings: BrainDoctorFinding[] = []; + if (!observation.rootPrivate) { + findings.push({ + code: 'brain-root-permissions-unsafe', + repairable: false, + reasonCode: 'owner-only-root-required', + }); + } if (!observation.gitRepository) { findings.push({ code: 'brain-not-git-repository', repairable: true, reasonCode: null }); + if (access !== null) findings.push(access); return findings; } if (observation.remote === null) {