feat(mosaic): provision per-estate durable brain

This commit is contained in:
2026-08-05 17:19:50 -05:00
parent c6329ec91e
commit 2451c2f21a
16 changed files with 4423 additions and 81 deletions
+3 -3
View File
@@ -104,18 +104,18 @@ In scope now: estate derivation, secret exclusion, non-destructive migration, do
### Acceptance criteria ### Acceptance criteria
1. `AC-MB-01`: Contract tests observe RED before implementation and then distinguish `ok/0`, `refused/10`, `error/20`, and `indeterminate/30`, preserving stable reason codes including `identity-not-found`, `credential-rejected`, and `provider-unavailable`. 1. `AC-MB-01`: Contract tests observe RED before implementation and then distinguish `ok/0`, `refused/10`, `error/20`, and `indeterminate/30`, preserving v1.5 diagnoses including refused `provider-identity-mismatch`/`credential-rejected` and indeterminate `identity-not-measured`/`provider-unavailable`. A scope-forbidden `/user` result with confirmed in-scope repository capability is never represented as a dead credential. `identity-not-found` remains reserved for a future visibility-authorized inventory operation and is not an expected `validate` result.
2. `AC-MB-02`: Estate resolution uses the configured target git host and one registry; unknown, mismatched, and host-machine-derived inputs fail closed. 2. `AC-MB-02`: Estate resolution uses the configured target git host and one registry; unknown, mismatched, and host-machine-derived inputs fail closed.
3. `AC-MB-03`: A clean fixture contains the required layout and exact secret exclusions, and seeded secret-shaped files remain ignored without their values appearing in output. 3. `AC-MB-03`: A clean fixture contains the required layout and exact secret exclusions, and seeded secret-shaped files remain ignored without their values appearing in output.
4. `AC-MB-04`: Migration moves lane-durable and seat-state content into collision-safe archive/ledger paths, preserves source on any incomplete move, never overwrites an existing finding, never deletes by age/size, and reports unresolved items explicitly. 4. `AC-MB-04`: Migration moves lane-durable and seat-state content into collision-safe archive/ledger paths, preserves source on any incomplete move, never overwrites an existing finding, never deletes by age/size, and reports unresolved items explicitly.
5. `AC-MB-05`: Doctor detects all four R8 defect classes; `--fix` repairs eligible classes through the approved P7/broker seam and leaves unresolved credential-dependent states visible. 5. `AC-MB-05`: Doctor detects all four R8 defect classes; `--fix` repairs eligible classes through the approved P7/broker seam and leaves unresolved credential-dependent states visible.
6. `AC-MB-06`: Git-axis and API-axis refusal must both be authoritative `refused` outcomes with matching stable reason codes; any disagreement yields `indeterminate`. 6. `AC-MB-06`: Git-axis and API-axis refusal must both be authoritative `refused` outcomes with matching stable reason codes; any disagreement yields `indeterminate`.
7. `AC-MB-07`: Independent code review and security review pass at the exact head, and HOMELAB Woodpecker instance `mosaic` is terminal green before integration. 7. `AC-MB-07`: Independent code review and security review pass at the exact head, and HOMELAB Woodpecker instance `mosaic` is terminal green before integration.
8. `AC-MB-08`: Integration into `next` is reported only as **believed-fixed, pending validation AND pending promotion to `main`**; issue #1051 remains open for #1037 promotion and W-jarvis validation. 8. `AC-MB-08`: After reviewed merge to `main`, report only **believed-fixed, pending jarvis validation**; issue #1051 remains open until W-jarvis validates the installed result.
### Constraints and risks ### Constraints and risks
- MC-CRED-01 contract v1.3 is the caller boundary; no independent credential/token lookup is permitted. - MC-CRED-01 contract v1.5 is the caller boundary; no independent credential/token lookup is permitted. Identity is established from governed mint-time binding and provider evidence when measurable, never a credential filename. Runtime validation does not widen a least-privilege token merely to make `/user` observable.
- C1 owns installer phase sequencing. This slice consumes P5/P7 ordering without renumbering or duplicating the phase machine. - C1 owns installer phase sequencing. This slice consumes P5/P7 ordering without renumbering or duplicating the phase machine.
- Lane content is findings, so last-writer-wins is data loss. Append-only names and explicit collision handling are mandatory. - Lane content is findings, so last-writer-wins is data loss. Append-only names and explicit collision handling are mandatory.
- A created-but-empty brain beside unbacked local doctrine fails the objective; migration is a primary acceptance gate. - A created-but-empty brain beside unbacked local doctrine fails the objective; migration is a primary acceptance gate.
+26 -10
View File
@@ -10,9 +10,9 @@ Codify estate-derived, repository-backed `~/.mosaic` support with secret exclusi
- Provider issue: HOMELAB `git.mosaicstack.dev`, `GET /api/v1/repos/mosaicstack/stack/issues/1051`, `application/json;charset=utf-8`. - Provider issue: HOMELAB `git.mosaicstack.dev`, `GET /api/v1/repos/mosaicstack/stack/issues/1051`, `application/json;charset=utf-8`.
- Issue requirements: R1R8 read directly on 2026-08-05. - Issue requirements: R1R8 read directly on 2026-08-05.
- MC-CRED caller contract: v1.3, SHA-256 `8cfa4853d2b0b0e8cc9e792fa8411310e16d7704c06e0af9d9a57155131d8086` at intake. - MC-CRED caller contract: v1.5, SHA-256 `4cecba3386b37431d4a075205c6dfe43555c7673922fed61b84f43cac1a6ae92` at the 2026-08-05 re-derivation. Earlier moving bindings were v1.5 `710d22d61a93a4b9c70fc55506a023a675a110417fa7a6e72dc051c0d9fe8237`, v1.4 `27f20158561ae8292f3bfc926b5e97f398de93db6a1cf65fcc215d08811d39af`/`d12ad4595b7aef078e392988a07ab5cb00244440775c9c733dc825746d7ac67b`, and v1.3 `8cfa4853d2b0b0e8cc9e792fa8411310e16d7704c06e0af9d9a57155131d8086`.
- Fleet doctrine: SHA-256 `026b43322e0551ef15b646a9f30d3a6aef58c662a810b732be2a03b1ecf7d36e` at intake. - Fleet doctrine: SHA-256 `026b43322e0551ef15b646a9f30d3a6aef58c662a810b732be2a03b1ecf7d36e` at intake.
- Base: HOMELAB provider `next` = `4df478cdd150fdf8d52ea109f02ade5d85017acd`; `main` = `5916aeefd6ed12bcac086c6834c7f6c4ae38e1bc`; provider branch objects matched fetched refs and `main` is reachable from `next`. - Intake base was HOMELAB provider `next` = `4df478cdd150fdf8d52ea109f02ade5d85017acd`; `main` = `5916aeefd6ed12bcac086c6834c7f6c4ae38e1bc`. On 2026-08-05 `mos-claude` ruled that L0 trunk-based gate 15 requires all three lanes to retarget to `main`; `next` remains a non-merging integration branch. Never weaken or patch `pr-merge.sh`.
## Scope ## Scope
@@ -33,6 +33,15 @@ Codify estate-derived, repository-backed `~/.mosaic` support with secret exclusi
- No phase renumbering; C1 owns the phase machine and provides the P5→P7 seam. - No phase renumbering; C1 owns the phase machine and provides the P5→P7 seam.
- No age/size reaping or deletion. - No age/size reaping or deletion.
## Owner authority ruling and resolver seam
- Binding addendum: `/home/hermes/agent-work/tl-mosaic/CHARTER-MB-BRAIN-01-ADDENDUM.md`; re-read after compaction.
- HOMELAB durable lane-archive owner and user-namespace brain owner are the human provider account selected by local estate policy (operator ruling: `jason.woltje`) with a required GLPI queue as the standing remediation process. The brain target is therefore `<policy-owner>/mosaic-brain` on the estate host, not `<installer-source-org>/mosaic-brain`. Framework source remains operator-agnostic: the actual login and queue are local policy, not hardcoded open-source context.
- Provider lookup is anonymous because the ruled owner is public. It requires exact allowlisted login plus a same-invocation public known-good control, private 404 control, and generated absent 404 control. It sends no Authorization header and never widens token scope.
- Provider `active` is deliberately ignored: non-admin reads return false for demonstrably active accounts. Resolvability + exact login + public visibility are the gate.
- Private and absent principals both return anonymous 404. The fail-closed reason is `owner-not-resolvable`, never owner-not-found.
- Caller `owner` strings and `validated=true` are ignored. Migration consumes only an injected source-of-truth resolver result. Owner grammar is NFKC-stable, ASCII allowlisted, exact-policy matched, and mission-seat class is excluded.
## Plan ## Plan
1. Pre-register acceptance tests and observe each requirement RED for its own missing behavior. 1. Pre-register acceptance tests and observe each requirement RED for its own missing behavior.
@@ -42,7 +51,7 @@ Codify estate-derived, repository-backed `~/.mosaic` support with secret exclusi
5. Integrate the helper into C1's P7 seam and `mosaic doctor` after C1 lands/rebase. 5. Integrate the helper into C1's P7 seam and `mosaic doctor` after C1 lands/rebase.
6. Run focused, package, installer, lint, typecheck, format, and situational security tests. 6. Run focused, package, installer, lint, typecheck, format, and situational security tests.
7. Run independent code and security reviews in parallel; remediate and re-review. 7. Run independent code and security reviews in parallel; remediate and re-review.
8. Push after HOMELAB queue guard, open PR to `next`, and wait for merge order C1 → MC-CRED → MB-BRAIN. 8. Push after HOMELAB queue guard, open the reviewed PR to `main`, and preserve merge order C1 → MC-CRED → MB-BRAIN. Do not modify the merge guard; `next` is non-merging integration only.
9. Re-take CI measurement at the rebased exact head; do not rework code solely because base evidence moved. 9. Re-take CI measurement at the rebased exact head; do not rework code solely because base evidence moved.
## Acceptance interpretation registered before results ## Acceptance interpretation registered before results
@@ -51,7 +60,7 @@ Codify estate-derived, repository-backed `~/.mosaic` support with secret exclusi
- `refused/10`: complete authoritative denial only. - `refused/10`: complete authoritative denial only.
- `error/20`: local contract/control failure; never reinterpret as denial. - `error/20`: local contract/control failure; never reinterpret as denial.
- `indeterminate/30`: incomplete/disagreeing evidence; fail closed, never resolve permissively. - `indeterminate/30`: incomplete/disagreeing evidence; fail closed, never resolve permissively.
- Both Git and API axes must return authoritative `refused` with the same stable reason code for R5. Any axis disagreement is `indeterminate`. - Both Git and API axes must return authoritative `refused` with the same stable reason code for R5. Any axis disagreement is `indeterminate`. A provider `/user` login mismatch is first-class `provider-identity-mismatch`; credential filenames never establish principal identity.
- Migration success requires the durable object to contain the moved item and no overwrite; incomplete moves retain the source and are reported. - Migration success requires the durable object to contain the moved item and no overwrite; incomplete moves retain the source and are reported.
- Secret exclusion is tested through both exact ignore rules and seeded secret-shaped controls; output is scanned without printing secret values. - Secret exclusion is tested through both exact ignore rules and seeded secret-shaped controls; output is scanned without printing secret values.
@@ -61,7 +70,7 @@ No explicit token ceiling was supplied. Working cap: 55K tokens for implementati
## Risks ## Risks
- C1 and MC-CRED branches have not merged into `next`; integration edits must wait for their exact interfaces or be confined to stable contract seams. - C1 and MC-CRED branches have not merged into `main`; integration edits must wait for their exact interfaces or be confined to stable contract seams.
- A broker runtime test before MC-CRED lands would either fail for an irrelevant reason or pressure a hand-rolled workaround; contract fixtures are allowed, live capability claims are not. - A broker runtime test before MC-CRED lands would either fail for an irrelevant reason or pressure a hand-rolled workaround; contract fixtures are allowed, live capability claims are not.
- Migration can lose data through overwrite, cross-device move failure, or partial copy. Implementation must stage, verify resulting bytes, and retain/report source on incomplete transfer. - Migration can lose data through overwrite, cross-device move failure, or partial copy. Implementation must stage, verify resulting bytes, and retain/report source on incomplete transfer.
- `~/.mosaic` is a git repo, while current working state may live under multiple local roots; detection must be explicit and cannot treat age/size as ownership. - `~/.mosaic` is a git repo, while current working state may live under multiple local roots; detection must be explicit and cannot treat age/size as ownership.
@@ -70,15 +79,22 @@ No explicit token ceiling was supplied. Working cap: 55K tokens for implementati
- [x] Charter receipt accepted by `tl-mosaic`. - [x] Charter receipt accepted by `tl-mosaic`.
- [x] Issue #1051 R1R8 read directly from provider. - [x] Issue #1051 R1R8 read directly from provider.
- [x] Contract re-derived at v1.3. - [x] Contract re-derived through v1.3, moving v1.4, and v1.5 before R5 integration. v1.5 separates in-scope repository capability from `/user` identity measurement: 401 is `credential-rejected`/refused, 403/404 may become `identity-not-measured` only after in-scope capability succeeds, and 200 login mismatch is refused. `identity-not-found` is not reachable from `validate`.
- [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.
- [ ] RED acceptance set committed. - [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`.
- [ ] Implementation green. - [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: 76/76 (store 38, runtime 12, owner resolver 14, provision 4, 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 now reports secret-shaped files without copying them; symlinked `.gitignore`, layout directories, and nested migration destinations fail closed.
- [x] Multi-host push-on-write retries non-fast-forward races via fetch/rebase rather than LWW, and unknown publication reachability retains both sources and local copies.
- [ ] Installer shell P7 invocation after C1 + MC-CRED integration; production command is registered but the C1 shell has not yet called it.
- [ ] Implementation green on merged dependency base.
- [ ] Independent code review. - [ ] Independent code review.
- [ ] Independent security review. - [ ] Independent security review.
- [ ] HOMELAB CI terminal green at exact head. - [ ] HOMELAB CI terminal green at exact head.
- [ ] Integrated to `next` after C1 and MC-CRED. - [ ] Reviewed PR retargeted to `main` after C1 and MC-CRED; `next` remains non-merging integration only.
## Completion language ## Completion language
Only: **believed-fixed, pending validation AND pending promotion to `main`**. Issue #1051 remains open; #1037 is the promotion vehicle and W-jarvis is the external validator. After reviewed merge to `main`, only: **believed-fixed, pending jarvis validation**. Issue #1051 remains open until W-jarvis validates the installed result.
+5
View File
@@ -23,6 +23,7 @@ import { registerSkillCommand } from './commands/skill.js';
import { registerLaunchCommands } from './commands/launch.js'; import { registerLaunchCommands } from './commands/launch.js';
import { registerLeaseCapabilityProbe } from './commands/lease-activation-probe.js'; import { registerLeaseCapabilityProbe } from './commands/lease-activation-probe.js';
import { registerInstallOrderingGuardCommand } from './commands/install-ordering-guard.js'; import { registerInstallOrderingGuardCommand } from './commands/install-ordering-guard.js';
import { registerBrainProvisionCommand } from './commands/brain-provision-command.js';
import { registerAuthCommand } from './commands/auth.js'; import { registerAuthCommand } from './commands/auth.js';
import { registerFederationCommand } from './commands/federation.js'; import { registerFederationCommand } from './commands/federation.js';
import { registerGatewayCommand } from './commands/gateway.js'; import { registerGatewayCommand } from './commands/gateway.js';
@@ -85,6 +86,10 @@ registerLeaseCapabilityProbe(program);
registerInstallOrderingGuardCommand(program); registerInstallOrderingGuardCommand(program);
// ─── durable brain P7 provisioner (hidden; #1051) ───────────────────────
registerBrainProvisionCommand(program);
// ─── login ────────────────────────────────────────────────────────────── // ─── login ──────────────────────────────────────────────────────────────
program program
@@ -0,0 +1,272 @@
import { afterEach, describe, expect, it } from 'vitest';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
interface CommandRequest {
readonly program: 'git' | 'mosaic';
readonly args: readonly string[];
readonly env: Readonly<Record<string, string>>;
}
interface CommandResult {
readonly status: number;
readonly stdout: string;
readonly stderr: string;
}
interface InstalledDoctorResult {
readonly status: 'ok' | 'warn' | 'error';
readonly findings: readonly {
readonly code: string;
readonly reasonCode: string | null;
}[];
readonly lines: readonly string[];
}
interface BrainDoctorModule {
runInstalledBrainDoctorCheck(
options: {
readonly mosaicHome: string;
readonly home: string;
readonly identity?: string;
readonly fix: boolean;
},
run: (request: CommandRequest) => CommandResult,
): InstalledDoctorResult;
}
const MODULE_PATH = './brain-doctor-check.js';
const roots: string[] = [];
async function loadDoctor(requirement: string): Promise<BrainDoctorModule> {
try {
return (await import(MODULE_PATH)) as BrainDoctorModule;
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`${requirement}: installed brain doctor check is absent (${detail})`);
}
}
function tempRoot(): string {
const root = mkdtempSync(join(tmpdir(), 'mosaic-brain-doctor-'));
roots.push(root);
return root;
}
function installConfig(root: string): { readonly home: string; readonly mosaicHome: string } {
const home = join(root, 'home');
const mosaicHome = join(home, '.config', 'mosaic');
mkdirSync(join(mosaicHome, 'cred'), { recursive: true });
mkdirSync(join(mosaicHome, 'brain'), { recursive: true });
writeFileSync(
join(mosaicHome, 'cred', 'estates.json'),
JSON.stringify({
version: 1,
estates: [
{
name: 'homelab',
readOnlyControlIdentity: 'read-control',
hosts: [
{
host: 'git.example.invalid',
provider: 'gitea',
apiBaseUrl: 'https://git.example.invalid',
tokenPrefix: 'gitea-example',
},
],
},
],
}),
{ mode: 0o600 },
);
writeFileSync(
join(mosaicHome, 'brain', 'owners.json'),
JSON.stringify({
version: 1,
estates: [
{
estate: 'homelab',
laneArchiveOwners: [{ kind: 'provider-user', login: 'durable-owner' }],
standingProcess: { kind: 'glpi-queue', queue: 'mosaic-brain-remediation' },
controls: { publicIdentity: 'public-control', privateIdentity: 'private-control' },
},
],
}),
{ mode: 0o600 },
);
writeFileSync(
join(mosaicHome, '.install-manifest.json'),
JSON.stringify({
version: 2,
status: 'committed',
sourceRepo: 'https://git.example.invalid/example/stack.git',
}),
{ mode: 0o600 },
);
return { home, mosaicHome };
}
function validateResult(outcome: 'ok' | 'refused' | 'indeterminate', reasonCode: string): string {
const exitCode = outcome === 'ok' ? 0 : outcome === 'refused' ? 10 : 30;
return JSON.stringify({
schemaVersion: 1,
operation: 'validate',
outcome,
exitCode,
retryable: false,
subject: {
identity: 'seat-a',
estate: 'homelab',
host: 'git.example.invalid',
repo: 'durable-owner/mosaic-brain',
},
mutation: 'none',
reason: { code: reasonCode, message: 'non-secret' },
evidence: {
providerIdentity:
outcome === 'ok'
? {
login: 'seat-a',
endpoint: 'GET /api/v1/user',
contentType: 'application/json',
}
: null,
repositoryPermission:
outcome === 'ok'
? {
requested: 'write',
effective: 'write',
endpoint: 'GET /api/v1/repos/durable-owner/mosaic-brain',
contentType: 'application/json',
}
: null,
writeDifferential:
outcome === 'ok'
? {
state: 'can-write',
credentialBinding: 'same-resolution',
transportPrincipal: 'seat-a',
authenticatedReceivePack: 'advertised',
readOnlyControl: {
identity: 'read-control',
providerPermission: 'read',
receivePack: 'refused',
},
unauthenticatedReceivePack: 'refused',
artifactCreated: false,
proves: 'non-secret evidence',
doesNotProve: 'branch update acceptance',
}
: null,
},
audit: { journalId: 'opaque', state: 'sealed' },
});
}
afterEach((): void => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});
describe('installed mosaic doctor brain checks', (): void => {
it('derives the target from the committed install manifest and surfaces a missing clone plus refusal', async (): Promise<void> => {
const doctor = await loadDoctor('MB-REQ-08 installed doctor missing clone');
const config = installConfig(tempRoot());
const requests: CommandRequest[] = [];
const result = doctor.runInstalledBrainDoctorCheck(
{ ...config, identity: 'seat-a', fix: false },
(request): CommandResult => {
requests.push(request);
return {
status: 10,
stdout: validateResult('refused', 'no-token-for-identity'),
stderr: 'refused reason=no-token-for-identity',
};
},
);
expect(result.status).toBe('warn');
expect(result.findings.map((finding) => finding.code)).toEqual(
expect.arrayContaining(['brain-clone-missing', 'brain-write-access-refused']),
);
expect(result.lines.join('\n')).toMatch(/brain-clone-missing/);
expect(result.lines.join('\n')).toMatch(/no-token-for-identity/);
expect(requests[0]?.args).toContain('durable-owner/mosaic-brain');
});
it('fails closed without an explicit identity and performs no command', async (): Promise<void> => {
const doctor = await loadDoctor('MB-REQ-08 explicit identity');
const config = installConfig(tempRoot());
let calls = 0;
const result = doctor.runInstalledBrainDoctorCheck(
{ ...config, fix: false },
(): CommandResult => {
calls += 1;
return { status: 0, stdout: '', stderr: '' };
},
);
expect(result).toMatchObject({
status: 'error',
findings: [{ code: 'brain-identity-required', reasonCode: 'identity-required' }],
});
expect(calls).toBe(0);
});
it('treats identity-not-measured as an error, not no-write refusal and not a repairable grant case', async (): Promise<void> => {
const doctor = await loadDoctor('MB-REQ-08 identity measurement axis');
const config = installConfig(tempRoot());
const requests: CommandRequest[] = [];
const result = doctor.runInstalledBrainDoctorCheck(
{ ...config, identity: 'seat-a', fix: true },
(request): CommandResult => {
requests.push(request);
return {
status: 30,
stdout: validateResult('indeterminate', 'identity-not-measured'),
stderr: 'identity not measured',
};
},
);
expect(result.status).toBe('error');
expect(result.findings).toEqual(
expect.arrayContaining([
expect.objectContaining({
code: 'brain-write-access-indeterminate',
reasonCode: 'identity-not-measured',
}),
]),
);
expect(requests.some((request) => request.args.includes('grant'))).toBe(false);
});
it('is wired into the top-level mosaic doctor path before the shell audit runs', (): void => {
const launch = readFileSync(join(process.cwd(), 'src', 'commands', 'launch.ts'), 'utf8');
expect(launch).toContain('runInstalledBrainDoctorCheck');
expect(launch).toContain('defaultInstalledBrainDoctorOptions');
expect(launch).toContain('systemCommandRunner');
expect(launch).toMatch(/brainCheckFailed[\s\S]*runDoctorScriptAndExit/);
});
it('reports a missing or unsafe registry/manifest as configuration error rather than defaulting estate', async (): Promise<void> => {
const doctor = await loadDoctor('MB-REQ-02 missing mapping fail-closed');
const root = tempRoot();
const home = join(root, 'home');
const mosaicHome = join(home, '.config', 'mosaic');
mkdirSync(mosaicHome, { recursive: true });
const result = doctor.runInstalledBrainDoctorCheck(
{ home, mosaicHome, identity: 'seat-a', fix: false },
(): CommandResult => ({ status: 0, stdout: '', stderr: '' }),
);
expect(result.status).toBe('error');
expect(result.findings[0]?.code).toMatch(/brain-(estate-registry|install-manifest)-/);
expect(result.lines.join('\n')).not.toMatch(/homelab|usc/);
});
});
@@ -0,0 +1,161 @@
import { homedir } from 'node:os';
import { join } from 'node:path';
import { z } from 'zod';
import { readRegularFileSecure } from '../fleet/secure-file.js';
import { resolveBrainOwnerPolicy } from './brain-owner-resolver.js';
import { deriveBrainTarget } from './brain-store.js';
import {
collectBrainDoctorReport,
repairBrainDoctor,
type CommandRunner,
type DoctorRuntimeReport,
} from './brain-store-runtime.js';
const MAX_CONFIG_BYTES = 256 * 1024;
const IDENTITY = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
const manifestSchema = z
.object({
version: z.literal(2),
status: z.literal('committed'),
sourceRepo: z.string().min(1),
})
.passthrough();
export interface InstalledDoctorFinding {
readonly code: string;
readonly reasonCode: string | null;
}
export interface InstalledDoctorResult {
readonly status: 'ok' | 'warn' | 'error';
readonly findings: readonly InstalledDoctorFinding[];
readonly lines: readonly string[];
}
function configurationError(code: string, reasonCode = code): InstalledDoctorResult {
return {
status: 'error',
findings: [{ code, reasonCode }],
lines: [`[mosaic-doctor] [ERROR] ${code}`],
};
}
function readUtf8(path: string, root: string): string {
const snapshot = readRegularFileSecure(path, { root, maxBytes: MAX_CONFIG_BYTES });
try {
return new TextDecoder('utf-8', { fatal: true }).decode(snapshot.content);
} catch {
throw new Error('config-not-utf8');
}
}
function renderReport(report: DoctorRuntimeReport): InstalledDoctorResult {
const findings = report.findings.map(
(finding): InstalledDoctorFinding => ({
code: finding.code,
reasonCode: finding.reasonCode,
}),
);
const hard = findings.some(
(finding): boolean =>
finding.code.endsWith('-error') ||
finding.code.endsWith('-indeterminate') ||
finding.code === 'brain-not-git-repository',
);
const status: InstalledDoctorResult['status'] =
findings.length === 0 ? 'ok' : hard ? 'error' : 'warn';
const severity = status === 'error' ? 'ERROR' : status === 'warn' ? 'WARN' : 'OK';
const lines =
findings.length === 0
? ['[mosaic-doctor] [OK] mosaic-brain ready']
: findings.map(
(finding): string =>
`[mosaic-doctor] [${severity}] ${finding.code}${
finding.reasonCode === null ? '' : ` reason=${finding.reasonCode}`
}`,
);
return { status, findings, lines };
}
export function runInstalledBrainDoctorCheck(
options: {
readonly mosaicHome: string;
readonly home: string;
readonly identity?: string;
readonly fix: boolean;
},
run: CommandRunner,
): InstalledDoctorResult {
if (options.identity === undefined || !IDENTITY.test(options.identity)) {
return configurationError('brain-identity-required', 'identity-required');
}
const registryPath = join(options.mosaicHome, 'cred', 'estates.json');
const manifestPath = join(options.mosaicHome, '.install-manifest.json');
const ownerPolicyPath = join(options.mosaicHome, 'brain', 'owners.json');
let registrySource: string;
try {
registrySource = readUtf8(registryPath, options.mosaicHome);
} catch {
return configurationError('brain-estate-registry-unavailable');
}
let manifestSource: string;
try {
manifestSource = readUtf8(manifestPath, options.mosaicHome);
} catch {
return configurationError('brain-install-manifest-unavailable');
}
let manifestRaw: unknown;
try {
manifestRaw = JSON.parse(manifestSource);
} catch {
return configurationError('brain-install-manifest-invalid');
}
const manifest = manifestSchema.safeParse(manifestRaw);
if (!manifest.success) return configurationError('brain-install-manifest-invalid');
let ownerPolicySource: string;
try {
ownerPolicySource = readUtf8(ownerPolicyPath, options.mosaicHome);
} catch {
return configurationError('brain-owner-policy-unavailable');
}
let preliminaryTarget: ReturnType<typeof deriveBrainTarget>;
try {
preliminaryTarget = deriveBrainTarget(registrySource, manifest.data.sourceRepo, 'policy-probe');
} catch {
return configurationError('brain-estate-registry-invalid');
}
const ownerPolicy = resolveBrainOwnerPolicy(ownerPolicySource, preliminaryTarget.estate);
if (ownerPolicy === undefined) return configurationError('brain-owner-policy-invalid');
const input = {
registrySource,
targetGitUrl: manifest.data.sourceRepo,
brainNamespace: ownerPolicy.brainNamespace,
identity: options.identity,
root: join(options.home, '.mosaic'),
};
try {
return renderReport(
options.fix ? repairBrainDoctor(input, run) : collectBrainDoctorReport(input, run),
);
} catch {
return configurationError('brain-estate-registry-invalid');
}
}
export function defaultInstalledBrainDoctorOptions(fix: boolean): {
readonly mosaicHome: string;
readonly home: string;
readonly identity?: string;
readonly fix: boolean;
} {
const home = homedir();
const identity = process.env['MOSAIC_GIT_IDENTITY'];
return {
mosaicHome: process.env['MOSAIC_HOME'] ?? join(home, '.config', 'mosaic'),
home,
...(identity === undefined ? {} : { identity }),
fix,
};
}
@@ -0,0 +1,304 @@
import { describe, expect, it } from 'vitest';
/**
* Red-first owner-authority resolver contract for #1051.
*
* Fixtures are operator-agnostic. The HOMELAB owner name belongs in the local
* estate policy, never in framework source. Anonymous lookup is intentional:
* the ruled owner class is PUBLIC and least-privilege seats may lack read:user.
*/
interface MigrationOwnerResolution {
readonly verdict: 'resolved' | 'refused' | 'not-measured';
readonly reasonCode: string;
readonly principal: {
readonly name: string;
readonly kind: 'durable-human';
} | null;
readonly authority: {
readonly system: 'gitea';
readonly endpoint: string;
readonly contentType: 'application/json';
} | null;
}
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
interface OwnerResolverModule {
resolveProviderDurableOwner(
input: {
readonly estateRegistrySource: string;
readonly ownerPolicySource: string;
readonly host: string;
readonly requestedOwner: string;
},
dependencies: {
readonly fetch: FetchLike;
readonly absentControlName: () => string;
},
): Promise<MigrationOwnerResolution>;
}
const MODULE_PATH = './brain-owner-resolver.js';
async function loadResolver(requirement: string): Promise<OwnerResolverModule> {
try {
return (await import(MODULE_PATH)) as OwnerResolverModule;
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`${requirement}: brain owner resolver is absent (${detail})`);
}
}
function estateRegistry(): string {
return JSON.stringify({
version: 1,
estates: [
{
name: 'homelab',
readOnlyControlIdentity: 'read-control',
hosts: [
{
host: 'git.example.invalid',
provider: 'gitea',
apiBaseUrl: 'https://git.example.invalid',
tokenPrefix: 'gitea-example',
},
],
},
],
});
}
function ownerPolicy(): string {
return JSON.stringify({
version: 1,
estates: [
{
estate: 'homelab',
laneArchiveOwners: [{ kind: 'provider-user', login: 'durable-owner' }],
standingProcess: { kind: 'glpi-queue', queue: 'mosaic-brain-remediation' },
controls: {
publicIdentity: 'public-control',
privateIdentity: 'private-control',
},
},
],
});
}
function jsonResponse(status: number, body: unknown): Response {
return new Response(JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json; charset=utf-8' },
});
}
function publicUser(login: string, active = false): Response {
return jsonResponse(200, {
id: 42,
login,
visibility: 'public',
active,
});
}
function identityFromUrl(input: string | URL | Request): string {
const value = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
return decodeURIComponent(new URL(value).pathname.split('/').at(-1) ?? '');
}
function controlledFetch(
overrides: Readonly<Record<string, Response>> = {},
calls: Array<{ identity: string; authorization: string | null }> = [],
): FetchLike {
return async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
const identity = identityFromUrl(input);
const headers = new Headers(init?.headers);
calls.push({ identity, authorization: headers.get('authorization') });
const override = overrides[identity];
if (override !== undefined) return override.clone();
if (identity === 'public-control') return publicUser('public-control');
if (identity === 'private-control' || identity === 'generated-absent-control') {
return jsonResponse(404, { message: 'not found' });
}
if (identity === 'durable-owner') return publicUser('durable-owner', false);
return jsonResponse(404, { message: 'not found' });
};
}
describe('provider-backed durable owner resolver', (): void => {
it('resolves an allowlisted PUBLIC owner by exact login with public/private/absent controls and ignores active=false', async (): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 provider owner resolution');
const calls: Array<{ identity: string; authorization: string | null }> = [];
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: 'user:durable-owner',
},
{
fetch: controlledFetch({}, calls),
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toEqual({
verdict: 'resolved',
reasonCode: 'owner-verified',
principal: { name: 'user:durable-owner', kind: 'durable-human' },
authority: {
system: 'gitea',
endpoint: 'GET /api/v1/users/durable-owner',
contentType: 'application/json',
},
});
expect(calls.map((call) => call.identity)).toEqual([
'public-control',
'private-control',
'generated-absent-control',
'durable-owner',
]);
expect(calls.every((call) => call.authorization === null)).toBe(true);
});
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>> };
delete raw.estates[0]?.['standingProcess'];
let fetchCalls = 0;
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: JSON.stringify(raw),
host: 'git.example.invalid',
requestedOwner: 'user:durable-owner',
},
{
fetch: async (): Promise<Response> => {
fetchCalls += 1;
return publicUser('durable-owner');
},
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({ verdict: 'refused', reasonCode: 'owner-policy-invalid' });
expect(fetchCalls).toBe(0);
});
it('rejects a provider-valid but unlisted principal before provider lookup', async (): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 provider-valid unlisted owner refusal');
const calls: Array<{ identity: string; authorization: string | null }> = [];
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: 'user:other-public-user',
},
{
fetch: controlledFetch({ 'other-public-user': publicUser('other-public-user') }, calls),
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({ verdict: 'refused', reasonCode: 'owner-not-allowlisted' });
expect(calls).toHaveLength(0);
});
it.each([
['user:durableowner', 'owner-name-invalid'],
[' user:durable-owner ', 'owner-name-invalid'],
['user:durable.owner', 'owner-not-allowlisted'],
['user:durable owner', 'owner-name-invalid'],
['user:urable-owner', 'owner-name-invalid'],
['user:be-coder-07@mission-seat', 'owner-name-invalid'],
] as const)(
'rejects non-canonical, unlisted, or transient-seat presentation %s before lookup',
async (name, reasonCode): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 owner allowlist grammar');
let fetchCalls = 0;
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: name,
},
{
fetch: async (): Promise<Response> => {
fetchCalls += 1;
return publicUser('durable-owner');
},
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({ verdict: 'refused', reasonCode });
expect(fetchCalls).toBe(0);
},
);
it('fails closed as not-resolvable rather than claiming a private-or-absent owner does not exist', async (): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 private/absent ambiguity');
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: 'user:durable-owner',
},
{
fetch: controlledFetch({ 'durable-owner': jsonResponse(404, { message: 'hidden' }) }),
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({
verdict: 'not-measured',
reasonCode: 'owner-not-resolvable',
principal: null,
});
expect(JSON.stringify(result)).not.toMatch(/owner-not-found|does-not-exist/);
});
it.each([
['public control hidden', { 'public-control': jsonResponse(404, {}) }],
['public control login mismatch', { 'public-control': publicUser('other') }],
['private control unexpectedly public', { 'private-control': publicUser('private-control') }],
[
'generated absent control unexpectedly resolves',
{ 'generated-absent-control': publicUser('generated-absent-control') },
],
] as const)(
'makes the whole result not-measured when %s',
async (_caseName, overrides): Promise<void> => {
const resolver = await loadResolver('MB-REQ-09 owner resolver controls');
const result = await resolver.resolveProviderDurableOwner(
{
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
host: 'git.example.invalid',
requestedOwner: 'user:durable-owner',
},
{
fetch: controlledFetch(overrides),
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({
verdict: 'not-measured',
reasonCode: 'owner-control-invalid',
});
},
);
});
@@ -0,0 +1,255 @@
import { z } from 'zod';
import { parseCredentialEstateRegistry } from '../credentials/estate-registry.js';
import type { MigrationOwnerResolution } from './brain-store.js';
const MAX_BODY_BYTES = 256 * 1024;
const LOGIN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)*$/;
const REQUESTED_OWNER = /^user:(.+)$/;
const ownerPolicySchema = z
.object({
version: z.literal(1),
estates: z
.array(
z
.object({
estate: z.string().min(1),
laneArchiveOwners: z
.array(
z
.object({
kind: z.literal('provider-user'),
login: z.string().min(1),
})
.strict(),
)
.min(1),
standingProcess: z
.object({
kind: z.literal('glpi-queue'),
queue: z.string().regex(/^[a-z0-9][a-z0-9-]*$/),
})
.strict(),
controls: z
.object({
publicIdentity: z.string().min(1),
privateIdentity: z.string().min(1),
})
.strict(),
})
.strict(),
)
.min(1),
})
.strict();
const providerUserSchema = z
.object({
id: z.number().int(),
login: z.string().min(1),
visibility: z.literal('public'),
})
.passthrough();
export type OwnerFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
function unresolved(reasonCode: string): MigrationOwnerResolution {
return {
verdict: 'not-measured',
reasonCode,
principal: null,
authority: null,
};
}
function refused(reasonCode: string): MigrationOwnerResolution {
return {
verdict: 'refused',
reasonCode,
principal: null,
authority: null,
};
}
function exactCanonicalLogin(value: string): boolean {
return value.normalize('NFKC') === value && LOGIN.test(value);
}
async function boundedJson(response: Response): Promise<unknown> {
const contentType = response.headers.get('content-type') ?? '';
if (!contentType.toLowerCase().startsWith('application/json')) {
throw new Error('owner-unexpected-content-type');
}
const declared = response.headers.get('content-length');
if (declared !== null) {
const size = Number.parseInt(declared, 10);
if (Number.isFinite(size) && size > MAX_BODY_BYTES) {
throw new Error('owner-unexpected-provider-shape');
}
}
const body = new Uint8Array(await response.arrayBuffer());
if (body.byteLength > MAX_BODY_BYTES) throw new Error('owner-unexpected-provider-shape');
try {
return JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(body));
} catch {
throw new Error('owner-unexpected-provider-shape');
}
}
async function readPublicIdentity(
origin: string,
identity: string,
fetchImpl: OwnerFetch,
): Promise<{ readonly status: number; readonly user: unknown }> {
let response: Response;
try {
response = await fetchImpl(`${origin}/api/v1/users/${encodeURIComponent(identity)}`, {
method: 'GET',
headers: {
Accept: 'application/json',
'User-Agent': 'mosaic-brain-owner/1',
},
});
} catch {
throw new Error('owner-provider-unavailable');
}
return { status: response.status, user: await boundedJson(response) };
}
function publicIdentityMatches(value: unknown, identity: string): boolean {
const parsed = providerUserSchema.safeParse(value);
return parsed.success && parsed.data.login === identity;
}
export interface BrainOwnerPolicyBinding {
readonly brainNamespace: string;
readonly publicControl: string;
readonly privateControl: string;
readonly standingQueue: string;
}
export function resolveBrainOwnerPolicy(
ownerPolicySource: string,
estate: string,
): BrainOwnerPolicyBinding | undefined {
let rawPolicy: unknown;
try {
rawPolicy = JSON.parse(ownerPolicySource);
} catch {
return undefined;
}
const policy = ownerPolicySchema.safeParse(rawPolicy);
if (!policy.success) return undefined;
const estatePolicies = policy.data.estates.filter(
(candidate): boolean => candidate.estate === estate,
);
if (estatePolicies.length !== 1) return undefined;
const estatePolicy = estatePolicies[0];
if (estatePolicy === undefined || estatePolicy.laneArchiveOwners.length !== 1) return undefined;
const brainNamespace = estatePolicy.laneArchiveOwners[0]?.login;
if (brainNamespace === undefined || !exactCanonicalLogin(brainNamespace)) return undefined;
return {
brainNamespace,
publicControl: estatePolicy.controls.publicIdentity,
privateControl: estatePolicy.controls.privateIdentity,
standingQueue: estatePolicy.standingProcess.queue,
};
}
export function parseRequestedOwner(requestedOwner: string): string | null {
if (requestedOwner.normalize('NFKC') !== requestedOwner) return null;
const match = REQUESTED_OWNER.exec(requestedOwner);
const login = match?.[1];
if (login === undefined || !exactCanonicalLogin(login)) return null;
return login;
}
export async function resolveProviderDurableOwner(
input: {
readonly estateRegistrySource: string;
readonly ownerPolicySource: string;
readonly host: string;
readonly requestedOwner: string;
},
dependencies: {
readonly fetch: OwnerFetch;
readonly absentControlName: () => string;
},
): Promise<MigrationOwnerResolution> {
const requestedLogin = parseRequestedOwner(input.requestedOwner);
if (requestedLogin === null) return refused('owner-name-invalid');
const target = parseCredentialEstateRegistry(input.estateRegistrySource).resolveByHost(
input.host,
);
if (target === undefined) return refused('estate-host-unmapped');
const policy = resolveBrainOwnerPolicy(input.ownerPolicySource, target.estate);
if (policy === undefined) return refused('owner-policy-invalid');
if (policy.brainNamespace !== requestedLogin) return refused('owner-not-allowlisted');
const publicControl = policy.publicControl;
const privateControl = policy.privateControl;
const absentControl = dependencies.absentControlName();
if (
!exactCanonicalLogin(publicControl) ||
!exactCanonicalLogin(privateControl) ||
!exactCanonicalLogin(absentControl) ||
new Set([publicControl, privateControl, absentControl, requestedLogin]).size !== 4
) {
return refused('owner-policy-invalid');
}
try {
const publicResult = await readPublicIdentity(
target.host.apiBaseUrl,
publicControl,
dependencies.fetch,
);
if (publicResult.status !== 200 || !publicIdentityMatches(publicResult.user, publicControl)) {
return unresolved('owner-control-invalid');
}
const privateResult = await readPublicIdentity(
target.host.apiBaseUrl,
privateControl,
dependencies.fetch,
);
if (privateResult.status !== 404) return unresolved('owner-control-invalid');
const absentResult = await readPublicIdentity(
target.host.apiBaseUrl,
absentControl,
dependencies.fetch,
);
if (absentResult.status !== 404) return unresolved('owner-control-invalid');
const ownerResult = await readPublicIdentity(
target.host.apiBaseUrl,
requestedLogin,
dependencies.fetch,
);
if (ownerResult.status === 401 || ownerResult.status === 403 || ownerResult.status === 404) {
return unresolved('owner-not-resolvable');
}
if (ownerResult.status !== 200) return unresolved('owner-provider-unavailable');
if (!publicIdentityMatches(ownerResult.user, requestedLogin)) {
return unresolved('owner-provider-identity-mismatch');
}
return {
verdict: 'resolved',
reasonCode: 'owner-verified',
principal: { name: `user:${requestedLogin}`, kind: 'durable-human' },
authority: {
system: 'gitea',
endpoint: `GET /api/v1/users/${requestedLogin}`,
contentType: 'application/json',
},
};
} catch (error: unknown) {
const reason = error instanceof Error ? error.message : 'owner-provider-unavailable';
if (reason === 'owner-unexpected-content-type') return unresolved(reason);
if (reason === 'owner-unexpected-provider-shape') return unresolved(reason);
return unresolved('owner-provider-unavailable');
}
}
@@ -0,0 +1,122 @@
import { afterEach, describe, expect, it } from 'vitest';
import { Command } from 'commander';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
interface BrainProvisionCommandModule {
readonly BRAIN_PROVISION_COMMAND: string;
registerBrainProvisionCommand(program: Command): void;
executeBrainProvisionCommand(
options: {
readonly mosaicHome: string;
readonly home: string;
readonly identity: string;
readonly refusalIdentity: string;
readonly targetUrl: string;
readonly owner: string;
readonly lane: string;
readonly sourceRoot?: string;
readonly brainRoot?: string;
readonly ownerPolicy?: string;
readonly registry?: string;
},
dependencies: {
readonly run: () => never;
readonly fetch: typeof fetch;
readonly absentControlName: () => string;
},
): Promise<{
readonly status: 'provisioned' | 'blocked' | 'failed';
readonly reasonCode: string;
}>;
}
const MODULE_PATH = './brain-provision-command.js';
const roots: string[] = [];
async function loadCommand(requirement: string): Promise<BrainProvisionCommandModule> {
try {
return (await import(MODULE_PATH)) as BrainProvisionCommandModule;
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`${requirement}: brain provision command is absent (${detail})`);
}
}
function tempRoot(): string {
const root = mkdtempSync(join(tmpdir(), 'mosaic-brain-command-'));
roots.push(root);
return root;
}
afterEach((): void => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});
describe('internal P7 brain provision command', (): void => {
it('registers only explicit non-secret contract inputs and no credential/token lookup switches', async (): Promise<void> => {
const module = await loadCommand('MB-REQ-03 broker-only provision command');
const program = new Command();
module.registerBrainProvisionCommand(program);
const command = program.commands.find(
(candidate) => candidate.name() === module.BRAIN_PROVISION_COMMAND,
);
expect(command).toBeDefined();
const flags = command?.options.map((option) => option.flags) ?? [];
expect(flags.join(' ')).toContain('--identity');
expect(flags.join(' ')).toContain('--target-url');
expect(flags.join(' ')).toContain('--refusal-identity');
expect(flags.join(' ')).toContain('--owner-policy');
expect(flags.join(' ')).toContain('--owner');
expect(flags.join(' ')).toContain('--lane');
expect(flags.join(' ')).not.toMatch(/token|password|authorization|grant-authority/i);
});
it('is registered by the shipped CLI', async (): Promise<void> => {
const module = await loadCommand('MB-REQ-10 shipped P7 command');
const cli = readFileSync(join(process.cwd(), 'src', 'cli.ts'), 'utf8');
expect(cli).toContain('registerBrainProvisionCommand');
expect(cli).toContain(`registerBrainProvisionCommand(program)`);
expect(module.BRAIN_PROVISION_COMMAND).toBe('__brain-provision');
});
it('fails closed before commands when the local owner policy is absent', async (): Promise<void> => {
const module = await loadCommand('MB-REQ-09 owner policy required');
const root = tempRoot();
const home = join(root, 'home');
const mosaicHome = join(home, '.config', 'mosaic');
mkdirSync(join(mosaicHome, 'cred'), { recursive: true });
writeFileSync(
join(mosaicHome, 'cred', 'estates.json'),
JSON.stringify({ version: 1, estates: [] }),
{ mode: 0o600 },
);
let commands = 0;
const result = await module.executeBrainProvisionCommand(
{
mosaicHome,
home,
identity: 'seat-a',
refusalIdentity: 'outside-seat',
targetUrl: 'https://git.example.invalid/example/stack.git',
owner: 'user:durable-owner',
lane: 'lane-a',
},
{
run: (): never => {
commands += 1;
throw new Error('must not run');
},
fetch,
absentControlName: (): string => 'generated-absent-control',
},
);
expect(result).toMatchObject({ status: 'failed', reasonCode: 'owner-policy-unavailable' });
expect(commands).toBe(0);
});
});
@@ -0,0 +1,139 @@
import { randomUUID } from 'node:crypto';
import { homedir } from 'node:os';
import { join } from 'node:path';
import type { Command } from 'commander';
import { readRegularFileSecure } from '../fleet/secure-file.js';
import { provisionBrain, type ProvisionResult } from './brain-provision.js';
import { systemCommandRunner, type CommandRunner } from './brain-store-runtime.js';
import type { OwnerFetch } from './brain-owner-resolver.js';
const MAX_POLICY_BYTES = 256 * 1024;
export const BRAIN_PROVISION_COMMAND = '__brain-provision';
interface BrainProvisionCommandOptions {
readonly mosaicHome: string;
readonly home: string;
readonly identity: string;
readonly refusalIdentity: string;
readonly targetUrl: string;
readonly owner: string;
readonly lane: string;
readonly sourceRoot?: string;
readonly brainRoot?: string;
readonly ownerPolicy?: string;
readonly registry?: string;
}
interface BrainProvisionCommandDependencies {
readonly run: CommandRunner;
readonly fetch: OwnerFetch;
readonly absentControlName: () => string;
}
function configFailure(reasonCode: string): ProvisionResult {
return {
status: 'failed',
reasonCode,
findings: [{ code: `brain-${reasonCode}`, reasonCode }],
owner: null,
migration: null,
};
}
function readConfig(path: string, root: string): string {
const content = readRegularFileSecure(path, { root, maxBytes: MAX_POLICY_BYTES }).content;
return new TextDecoder('utf-8', { fatal: true }).decode(content);
}
export async function executeBrainProvisionCommand(
options: BrainProvisionCommandOptions,
dependencies: BrainProvisionCommandDependencies,
): Promise<ProvisionResult> {
const registry = options.registry ?? join(options.mosaicHome, 'cred', 'estates.json');
const ownerPolicy = options.ownerPolicy ?? join(options.mosaicHome, 'brain', 'owners.json');
let estateRegistrySource: string;
try {
estateRegistrySource = readConfig(registry, options.mosaicHome);
} catch {
return configFailure('estate-registry-unavailable');
}
let ownerPolicySource: string;
try {
ownerPolicySource = readConfig(ownerPolicy, options.mosaicHome);
} catch {
return configFailure('owner-policy-unavailable');
}
try {
return await provisionBrain(
{
estateRegistrySource,
ownerPolicySource,
targetGitUrl: options.targetUrl,
requestedOwner: options.owner,
identity: options.identity,
refusalIdentity: options.refusalIdentity,
root: options.brainRoot ?? join(options.home, '.mosaic'),
sourceRoot: options.sourceRoot ?? join(options.mosaicHome, 'memory'),
seat: options.identity,
lane: options.lane,
laneActive: false,
},
dependencies,
);
} catch {
return configFailure('brain-provision-exception');
}
}
export function registerBrainProvisionCommand(program: Command): void {
program
.command(BRAIN_PROVISION_COMMAND, { hidden: true })
.description('Internal installer P7 durable-brain provisioner')
.requiredOption('--identity <name>', 'explicit fleet identity')
.requiredOption('--target-url <url>', 'configured target git URL')
.requiredOption('--refusal-identity <name>', 'explicit out-of-estate negative control')
.requiredOption('--owner <owner>', 'policy-bound durable owner candidate')
.requiredOption('--lane <name>', 'source lane to migrate')
.option('--mosaic-home <path>', 'installed Mosaic home')
.option('--home <path>', 'principal home')
.option('--source-root <path>', 'legacy memory root')
.option('--brain-root <path>', 'per-estate brain checkout root')
.option('--owner-policy <path>', 'durable-owner allowlist policy')
.option('--registry <path>', 'estate registry path')
.action(async (raw: Record<string, string | undefined>): Promise<void> => {
const home = raw['home'] ?? homedir();
const mosaicHome =
raw['mosaicHome'] ?? process.env['MOSAIC_HOME'] ?? join(home, '.config', 'mosaic');
const result = await executeBrainProvisionCommand(
{
mosaicHome,
home,
identity: raw['identity']!,
targetUrl: raw['targetUrl']!,
refusalIdentity: raw['refusalIdentity']!,
owner: raw['owner']!,
lane: raw['lane']!,
...(raw['sourceRoot'] === undefined ? {} : { sourceRoot: raw['sourceRoot'] }),
...(raw['brainRoot'] === undefined ? {} : { brainRoot: raw['brainRoot'] }),
...(raw['ownerPolicy'] === undefined ? {} : { ownerPolicy: raw['ownerPolicy'] }),
...(raw['registry'] === undefined ? {} : { registry: raw['registry'] }),
},
{
run: systemCommandRunner,
fetch,
absentControlName: (): string => `mosaic-absent-${randomUUID()}`,
},
);
process.stdout.write(
`${JSON.stringify({
status: result.status,
reasonCode: result.reasonCode,
findings: result.findings,
owner: result.owner,
migration: result.migration,
})}\n`,
);
if (result.status !== 'provisioned') process.exitCode = result.status === 'blocked' ? 30 : 20;
});
}
@@ -0,0 +1,458 @@
import { afterEach, describe, expect, it } from 'vitest';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
interface CommandRequest {
readonly program: 'git' | 'mosaic';
readonly args: readonly string[];
readonly env: Readonly<Record<string, string>>;
}
interface CommandResult {
readonly status: number;
readonly stdout: string;
readonly stderr: string;
}
type CommandRunner = (request: CommandRequest) => CommandResult;
type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
interface ProvisionResult {
readonly status: 'provisioned' | 'blocked' | 'failed';
readonly reasonCode: string;
readonly findings: readonly { code: string; reasonCode: string | null }[];
readonly owner: {
readonly verdict: 'resolved' | 'refused' | 'not-measured';
readonly reasonCode: string;
} | null;
readonly migration: {
readonly status: 'migrated' | 'reported' | 'failed';
readonly reported: readonly { path: string; reason: string }[];
} | null;
}
interface ProvisionModule {
provisionBrain(
input: {
readonly estateRegistrySource: string;
readonly ownerPolicySource: string;
readonly targetGitUrl: string;
readonly requestedOwner: string;
readonly identity: string;
readonly refusalIdentity: string;
readonly root: string;
readonly sourceRoot: string;
readonly seat: string;
readonly lane: string;
readonly laneActive: boolean;
},
dependencies: {
readonly run: CommandRunner;
readonly fetch: FetchLike;
readonly absentControlName: () => string;
},
): Promise<ProvisionResult>;
}
const MODULE_PATH = './brain-provision.js';
const roots: string[] = [];
async function loadProvisioner(requirement: string): Promise<ProvisionModule> {
try {
return (await import(MODULE_PATH)) as ProvisionModule;
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`${requirement}: brain provisioner is absent (${detail})`);
}
}
function tempRoot(): string {
const root = mkdtempSync(join(tmpdir(), 'mosaic-brain-provision-'));
roots.push(root);
return root;
}
function estateRegistry(): string {
return JSON.stringify({
version: 1,
estates: [
{
name: 'homelab',
readOnlyControlIdentity: 'read-control',
hosts: [
{
host: 'git.example.invalid',
provider: 'gitea',
apiBaseUrl: 'https://git.example.invalid',
tokenPrefix: 'gitea-example',
},
],
},
],
});
}
function ownerPolicy(): string {
return JSON.stringify({
version: 1,
estates: [
{
estate: 'homelab',
laneArchiveOwners: [{ kind: 'provider-user', login: 'durable-owner' }],
standingProcess: { kind: 'glpi-queue', queue: 'mosaic-brain-remediation' },
controls: { publicIdentity: 'public-control', privateIdentity: 'private-control' },
},
],
});
}
function validateResult(
outcome: 'ok' | 'refused' | 'indeterminate',
reasonCode: string,
identity = 'seat-a',
): string {
const exitCode = outcome === 'ok' ? 0 : outcome === 'refused' ? 10 : 30;
return JSON.stringify({
schemaVersion: 1,
operation: 'validate',
outcome,
exitCode,
retryable: false,
subject: {
identity,
estate: 'homelab',
host: 'git.example.invalid',
repo: 'durable-owner/mosaic-brain',
},
mutation: 'none',
reason: { code: reasonCode, message: 'non-secret' },
evidence: {
providerIdentity:
outcome === 'ok'
? {
login: identity,
endpoint: 'GET /api/v1/user',
contentType: 'application/json',
}
: null,
repositoryPermission:
outcome === 'ok'
? {
requested: 'write',
effective: 'write',
endpoint: 'GET /api/v1/repos/durable-owner/mosaic-brain',
contentType: 'application/json',
}
: null,
writeDifferential:
outcome === 'ok'
? {
state: 'can-write',
credentialBinding: 'same-resolution',
transportPrincipal: identity,
authenticatedReceivePack: 'advertised',
readOnlyControl: {
identity: 'read-control',
providerPermission: 'read',
receivePack: 'refused',
},
unauthenticatedReceivePack: 'refused',
artifactCreated: false,
proves: 'non-secret evidence',
doesNotProve: 'branch update acceptance',
}
: null,
},
audit: { journalId: 'opaque', state: 'sealed' },
});
}
function publicUser(login: string): Response {
return new Response(JSON.stringify({ id: 1, login, visibility: 'public', active: false }), {
status: 200,
headers: { 'content-type': 'application/json' },
});
}
function ownerFetch(ownerStatus = 200): FetchLike {
return async (input): Promise<Response> => {
const raw = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
const identity = decodeURIComponent(new URL(raw).pathname.split('/').at(-1) ?? '');
if (identity === 'public-control') return publicUser(identity);
if (identity === 'private-control' || identity === 'generated-absent-control') {
return new Response(JSON.stringify({ message: 'hidden or absent' }), {
status: 404,
headers: { 'content-type': 'application/json' },
});
}
if (identity === 'durable-owner' && ownerStatus === 200) return publicUser(identity);
return new Response(JSON.stringify({ message: 'hidden or absent' }), {
status: ownerStatus,
headers: { 'content-type': 'application/json' },
});
};
}
function baseInput(root: string): {
readonly estateRegistrySource: string;
readonly ownerPolicySource: string;
readonly targetGitUrl: string;
readonly requestedOwner: string;
readonly identity: string;
readonly refusalIdentity: string;
readonly root: string;
readonly sourceRoot: string;
readonly seat: string;
readonly lane: string;
readonly laneActive: boolean;
} {
return {
estateRegistrySource: estateRegistry(),
ownerPolicySource: ownerPolicy(),
targetGitUrl: 'https://git.example.invalid/example/stack.git',
requestedOwner: 'user:durable-owner',
identity: 'seat-a',
refusalIdentity: 'outside-seat',
root: join(root, 'brain'),
sourceRoot: join(root, 'local-memory'),
seat: 'seat-a',
lane: 'lane-a',
laneActive: false,
};
}
afterEach((): void => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});
describe('P7 brain provisioning orchestration', (): void => {
it('requires the P5 write-capability postcondition and never grants or clones on refusal', async (): Promise<void> => {
const provisioner = await loadProvisioner('MB-REQ-10 P5 before P7');
const root = tempRoot();
const requests: CommandRequest[] = [];
const result = await provisioner.provisionBrain(baseInput(root), {
run: (request): CommandResult => {
requests.push(request);
return {
status: 10,
stdout: validateResult('refused', 'no-token-for-identity'),
stderr: 'refused reason=no-token-for-identity',
};
},
fetch: ownerFetch(),
absentControlName: (): string => 'generated-absent-control',
});
expect(result).toMatchObject({
status: 'blocked',
reasonCode: 'credential-postcondition-failed',
});
expect(requests).toHaveLength(1);
expect(requests[0]?.program).toBe('mosaic');
expect(requests[0]?.args.slice(0, 3)).toEqual(['cred', 'validate', 'seat-a']);
expect(requests.some((request) => request.args.includes('grant'))).toBe(false);
expect(requests.some((request) => request.args.includes('clone'))).toBe(false);
});
it('blocks before clone when the out-of-estate Git and API axes disagree', async (): Promise<void> => {
const provisioner = await loadProvisioner('MB-REQ-05 P7 refusal control gate');
const root = tempRoot();
const requests: CommandRequest[] = [];
const result = await provisioner.provisionBrain(baseInput(root), {
run: (request): CommandResult => {
requests.push(request);
if (request.program === 'mosaic' && request.args[2] === 'outside-seat') {
return {
status: 10,
stdout: validateResult('refused', 'no-token-for-identity', 'outside-seat'),
stderr: 'refused reason=no-token-for-identity',
};
}
if (request.program === 'mosaic') {
return { status: 0, stdout: validateResult('ok', 'validation-verified'), stderr: '' };
}
if (request.args.includes('ls-remote')) {
return { status: 0, stdout: 'refs are visible', stderr: '' };
}
return { status: 99, stdout: '', stderr: 'unexpected command' };
},
fetch: ownerFetch(),
absentControlName: (): string => 'generated-absent-control',
});
expect(result).toMatchObject({
status: 'blocked',
reasonCode: 'refusal-control-failed',
});
expect(requests.some((request) => request.args.includes('clone'))).toBe(false);
expect(requests.some((request) => request.args.includes('grant'))).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();
const input = baseInput(root);
mkdirSync(join(input.sourceRoot, 'lanes', 'lane-a'), { recursive: true });
const source = join(input.sourceRoot, 'lanes', 'lane-a', 'finding.md');
writeFileSync(source, 'durable finding\n');
const requests: CommandRequest[] = [];
let commitOrdinal = 0;
const runner: CommandRunner = (request): CommandResult => {
requests.push(request);
if (request.program === 'mosaic') {
if (request.args[2] === 'outside-seat') {
return {
status: 10,
stdout: validateResult('refused', 'no-token-for-identity', 'outside-seat'),
stderr: 'refused reason=no-token-for-identity',
};
}
return { 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 (request.args[0] === 'clone') {
mkdirSync(join(input.root, '.git'), { recursive: true });
return { status: 0, stdout: '', stderr: '' };
}
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: '', stderr: '' };
}
if (command.includes('rev-parse HEAD')) {
commitOrdinal += 1;
return {
status: 0,
stdout: `${commitOrdinal === 1 ? 'a' : 'c'.repeat(1)}`.repeat(40) + '\n',
stderr: '',
};
}
if (command.includes('rev-parse origin/main')) {
const value = commitOrdinal === 1 ? 'b' : 'd';
return { status: 0, stdout: `${value.repeat(40)}\n`, stderr: '' };
}
return { status: 0, stdout: '', stderr: '' };
};
const result = await provisioner.provisionBrain(input, {
run: runner,
fetch: ownerFetch(),
absentControlName: (): string => 'generated-absent-control',
});
expect(result).toMatchObject({
status: 'provisioned',
reasonCode: 'brain-provisioned',
owner: { verdict: 'resolved', reasonCode: 'owner-verified' },
migration: { status: 'migrated' },
});
expect(existsSync(source)).toBe(false);
const imported = result.migration?.reported ?? [];
expect(imported).toEqual([]);
const laneImports = join(input.root, 'lanes', 'lane-a', 'findings', 'imports');
const archiveImports = join(input.root, 'archives', 'imports', 'lane');
expect(existsSync(laneImports)).toBe(true);
expect(existsSync(archiveImports)).toBe(true);
expect(
requests.filter((request) => request.program === 'git' && request.args.includes('push')),
).toHaveLength(2);
expect(requests.some((request) => request.args.includes('grant'))).toBe(false);
});
it('keeps every source and reports the owner ambiguity when the public owner cannot be resolved', async (): Promise<void> => {
const provisioner = await loadProvisioner('MB-REQ-07 owner-blocked detection/reporting');
const root = tempRoot();
const input = baseInput(root);
mkdirSync(input.root, { recursive: true });
mkdirSync(join(input.root, '.git'), { recursive: true });
mkdirSync(join(input.sourceRoot, 'lanes', 'lane-a'), { recursive: true });
const source = join(input.sourceRoot, 'lanes', 'lane-a', 'finding.md');
writeFileSync(source, 'retain me\n');
let commitOrdinal = 0;
const result = await provisioner.provisionBrain(input, {
run: (request): CommandResult => {
if (request.program === 'mosaic') {
if (request.args[2] === 'outside-seat') {
return {
status: 10,
stdout: validateResult('refused', 'no-token-for-identity', 'outside-seat'),
stderr: 'refused reason=no-token-for-identity',
};
}
return { 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: '', stderr: '' };
}
if (command.includes('rev-parse HEAD')) {
commitOrdinal += 1;
return { status: 0, stdout: `${'a'.repeat(40)}\n`, stderr: '' };
}
if (command.includes('rev-parse origin/main')) {
return { status: 0, stdout: `${'b'.repeat(40)}\n`, stderr: '' };
}
return { status: 0, stdout: '', stderr: '' };
},
fetch: ownerFetch(404),
absentControlName: (): string => 'generated-absent-control',
});
expect(result).toMatchObject({
status: 'blocked',
reasonCode: 'owner-not-resolvable',
owner: { verdict: 'not-measured', reasonCode: 'owner-not-resolvable' },
migration: { status: 'reported' },
});
expect(readFileSync(source, 'utf8')).toBe('retain me\n');
expect(result.migration?.reported).toEqual(
expect.arrayContaining([
expect.objectContaining({ path: source, reason: expect.stringMatching(/owner/i) }),
]),
);
expect(JSON.stringify(result)).not.toMatch(/owner-not-found|does-not-exist/);
expect(commitOrdinal).toBe(0);
});
});
@@ -0,0 +1,247 @@
import { lstatSync } from 'node:fs';
import { parseRequestedOwner, resolveProviderDurableOwner } from './brain-owner-resolver.js';
import {
createBrainSkeleton,
deriveBrainTarget,
discoverBrainMigration,
migrateBrainState,
type MigrationResult,
type MigrationOwnerResolution,
} from './brain-store.js';
import {
collectBrainDoctorReport,
collectBrainRefusalControl,
publishBrainPaths,
type CommandRequest,
type CommandResult,
type CommandRunner,
} from './brain-store-runtime.js';
import type { OwnerFetch } from './brain-owner-resolver.js';
export interface ProvisionResult {
readonly status: 'provisioned' | 'blocked' | 'failed';
readonly reasonCode: string;
readonly findings: readonly {
readonly code: string;
readonly reasonCode: string | null;
}[];
readonly owner: Pick<MigrationOwnerResolution, 'verdict' | 'reasonCode'> | null;
readonly migration: Pick<MigrationResult, 'status' | 'reported'> | null;
}
function commandEnv(identity: string): Readonly<Record<string, string>> {
return { MOSAIC_GIT_IDENTITY: identity, GIT_TERMINAL_PROMPT: '0' };
}
function findingView(
findings: readonly { readonly code: string; readonly reasonCode: string | null }[],
): readonly { readonly code: string; readonly reasonCode: string | null }[] {
return findings.map((finding): { readonly code: string; readonly reasonCode: string | null } => ({
code: finding.code,
reasonCode: finding.reasonCode,
}));
}
function blocked(
reasonCode: string,
findings: readonly { readonly code: string; readonly reasonCode: string | null }[],
owner: MigrationOwnerResolution | null = null,
migration: MigrationResult | null = null,
): ProvisionResult {
return {
status: 'blocked',
reasonCode,
findings: findingView(findings),
owner: owner === null ? null : { verdict: owner.verdict, reasonCode: owner.reasonCode },
migration:
migration === null ? null : { status: migration.status, reported: migration.reported },
};
}
function failed(
reasonCode: string,
findings: readonly { readonly code: string; readonly reasonCode: string | null }[],
owner: MigrationOwnerResolution | null = null,
migration: MigrationResult | null = null,
): ProvisionResult {
return {
...blocked(reasonCode, findings, owner, migration),
status: 'failed',
};
}
export async function provisionBrain(
input: {
readonly estateRegistrySource: string;
readonly ownerPolicySource: string;
readonly targetGitUrl: string;
readonly requestedOwner: string;
readonly identity: string;
readonly refusalIdentity: string;
readonly root: string;
readonly sourceRoot: string;
readonly seat: string;
readonly lane: string;
readonly laneActive: boolean;
},
dependencies: {
readonly run: CommandRunner;
readonly fetch: OwnerFetch;
readonly absentControlName: () => string;
},
): Promise<ProvisionResult> {
const brainNamespace = parseRequestedOwner(input.requestedOwner);
if (brainNamespace === null) return blocked('owner-name-invalid', []);
const target = deriveBrainTarget(input.estateRegistrySource, input.targetGitUrl, brainNamespace);
const doctorInput = {
registrySource: input.estateRegistrySource,
targetGitUrl: input.targetGitUrl,
brainNamespace,
identity: input.identity,
root: input.root,
};
let report = collectBrainDoctorReport(doctorInput, dependencies.run);
if (report.access.outcome !== 'ok') {
return blocked('credential-postcondition-failed', report.findings);
}
const refusalControl = collectBrainRefusalControl(
{
registrySource: input.estateRegistrySource,
targetGitUrl: input.targetGitUrl,
brainNamespace,
refusalIdentity: input.refusalIdentity,
},
dependencies.run,
);
if (!refusalControl.observed) {
return blocked('refusal-control-failed', [
...report.findings,
{
code: 'brain-refusal-control-indeterminate',
reasonCode: refusalControl.reasonCode,
},
]);
}
const owner = await resolveProviderDurableOwner(
{
estateRegistrySource: input.estateRegistrySource,
ownerPolicySource: input.ownerPolicySource,
host: target.host,
requestedOwner: input.requestedOwner,
},
{
fetch: dependencies.fetch,
absentControlName: dependencies.absentControlName,
},
);
if (owner.verdict !== 'resolved') {
const plan = discoverBrainMigration(
{
sourceRoot: input.sourceRoot,
brainRoot: input.root,
seat: input.seat,
lane: input.lane,
laneActive: input.laneActive,
},
(): MigrationOwnerResolution => owner,
);
const migration = migrateBrainState(
plan,
(): never => {
throw new Error('blocked owner cannot publish');
},
input.root,
);
return blocked(owner.reasonCode, report.findings, owner, migration);
}
if (report.findings.some((finding): boolean => finding.code === 'brain-clone-missing')) {
const clone: CommandRequest = {
program: 'git',
args: ['clone', '--branch', 'main', '--single-branch', target.cloneUrl, input.root],
env: commandEnv(input.identity),
};
const cloneResult: CommandResult = dependencies.run(clone);
if (cloneResult.status !== 0) return failed('brain-clone-failed', report.findings);
report = collectBrainDoctorReport(doctorInput, dependencies.run);
}
const blockingCloneFindings = report.findings.filter(
(finding): boolean =>
finding.code === 'brain-clone-missing' ||
finding.code === 'brain-not-git-repository' ||
finding.code === 'brain-remote-mismatch' ||
finding.code === 'brain-branch-mismatch' ||
finding.code.startsWith('brain-write-access-'),
);
if (blockingCloneFindings.length > 0) {
return blocked('brain-postcondition-failed', report.findings);
}
let skeletonPaths: readonly string[];
try {
const skeleton = createBrainSkeleton(input.root);
skeletonPaths = skeleton.created.filter((path: string): boolean => lstatSync(path).isFile());
} catch {
return failed('brain-skeleton-failed', report.findings);
}
if (skeletonPaths.length > 0) {
try {
const evidence = publishBrainPaths(
{
root: input.root,
identity: input.identity,
paths: skeletonPaths,
message: 'chore: seed durable brain layout',
},
dependencies.run,
);
if (!evidence.reachable) return failed('brain-skeleton-not-reachable', report.findings);
} catch {
return failed('brain-skeleton-publish-failed', report.findings);
}
}
const plan = discoverBrainMigration(
{
sourceRoot: input.sourceRoot,
brainRoot: input.root,
seat: input.seat,
lane: input.lane,
laneActive: input.laneActive,
},
(): MigrationOwnerResolution => owner,
);
const migration = migrateBrainState(
plan,
(brainRoot: string, paths: readonly string[]) =>
publishBrainPaths(
{
root: brainRoot,
identity: input.identity,
paths,
message: `migrate: archive ${input.lane} working memory`,
},
dependencies.run,
),
input.root,
);
if (migration.status === 'failed') {
return failed('brain-migration-publish-failed', report.findings, owner, migration);
}
report = collectBrainDoctorReport(doctorInput, dependencies.run);
if (report.findings.length > 0) {
return blocked('brain-final-postcondition-failed', report.findings, owner, migration);
}
return {
status: 'provisioned',
reasonCode: 'brain-provisioned',
findings: [],
owner: { verdict: owner.verdict, reasonCode: owner.reasonCode },
migration: { status: migration.status, reported: migration.reported },
};
}
@@ -0,0 +1,715 @@
import { afterEach, describe, expect, it } from 'vitest';
import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
/**
* Red-first integration seam for #1051.
*
* Live broker grants and remote writes are deliberately not exercised here.
* The injected runner records exact commands and returns contract objects; the
* production path must call `mosaic cred`, never resolve a token itself.
*/
interface CommandRequest {
readonly program: 'git' | 'mosaic';
readonly args: readonly string[];
readonly cwd?: string;
readonly env: Readonly<Record<string, string>>;
}
interface CommandResult {
readonly status: number;
readonly stdout: string;
readonly stderr: string;
}
type CommandRunner = (request: CommandRequest) => CommandResult;
interface DoctorRuntimeReport {
readonly findings: readonly { code: string; repairable: boolean; reasonCode: string | null }[];
readonly access: {
readonly outcome: 'ok' | 'refused' | 'error' | 'indeterminate';
readonly exitCode: 0 | 10 | 20 | 30;
readonly reasonCode: string;
};
readonly refusalControl: {
readonly observed: boolean;
readonly reasonCode: string | null;
};
}
interface PublishEvidence {
readonly commit: string;
readonly remoteHead: string;
readonly reachable: boolean;
}
interface BrainRuntimeModule {
collectBrainDoctorReport(
input: {
readonly registrySource: string;
readonly targetGitUrl: string;
readonly brainNamespace: string;
readonly identity: string;
readonly root: string;
},
run: CommandRunner,
): DoctorRuntimeReport;
repairBrainDoctor(
input: {
readonly registrySource: string;
readonly targetGitUrl: string;
readonly brainNamespace: string;
readonly identity: string;
readonly root: string;
},
run: CommandRunner,
): DoctorRuntimeReport;
collectBrainRefusalControl(
input: {
readonly registrySource: string;
readonly targetGitUrl: string;
readonly brainNamespace: string;
readonly refusalIdentity: string;
},
run: CommandRunner,
): {
readonly observed: boolean;
readonly reasonCode: string | null;
readonly gitReasonCode: string;
readonly apiReasonCode: string;
};
publishBrainPaths(
input: {
readonly root: string;
readonly identity: string;
readonly paths: readonly string[];
readonly message: string;
},
run: CommandRunner,
): PublishEvidence;
}
const MODULE_PATH = './brain-store-runtime.js';
const roots: string[] = [];
async function loadRuntime(requirement: string): Promise<BrainRuntimeModule> {
try {
return (await import(MODULE_PATH)) as BrainRuntimeModule;
} catch (error: unknown) {
const detail = error instanceof Error ? error.message : String(error);
throw new Error(`${requirement}: brain-store runtime is absent (${detail})`);
}
}
function tempRoot(): string {
const root = mkdtempSync(join(tmpdir(), 'mosaic-brain-runtime-'));
roots.push(root);
return root;
}
function registry(): string {
return JSON.stringify({
version: 1,
estates: [
{
name: 'homelab',
readOnlyControlIdentity: 'read-control',
hosts: [
{
host: 'git.mosaicstack.dev',
provider: 'gitea',
apiBaseUrl: 'https://git.mosaicstack.dev',
tokenPrefix: 'gitea-mosaicstack',
},
],
},
],
});
}
function validateResult(
outcome: 'ok' | 'refused' | 'error' | 'indeterminate',
reasonCode: string,
): string {
const exits = { ok: 0, refused: 10, error: 20, indeterminate: 30 } as const;
return JSON.stringify({
schemaVersion: 1,
operation: 'validate',
outcome,
exitCode: exits[outcome],
retryable: false,
subject: {
identity: 'synthetic-no-token',
estate: 'homelab',
host: 'git.mosaicstack.dev',
repo: 'mosaicstack/mosaic-brain',
},
mutation: 'none',
reason: { code: reasonCode, message: 'non-secret' },
evidence: {
providerIdentity:
outcome === 'ok'
? {
login: 'synthetic-no-token',
endpoint: 'GET /api/v1/user',
contentType: 'application/json',
}
: null,
repositoryPermission:
outcome === 'ok'
? {
requested: 'write',
effective: 'write',
endpoint: 'GET /api/v1/repos/mosaicstack/mosaic-brain',
contentType: 'application/json',
}
: null,
writeDifferential:
outcome === 'ok'
? {
state: 'can-write',
credentialBinding: 'same-resolution',
transportPrincipal: 'synthetic-no-token',
authenticatedReceivePack: 'advertised',
readOnlyControl: {
identity: 'read-control',
providerPermission: 'read',
receivePack: 'refused',
},
unauthenticatedReceivePack: 'refused',
artifactCreated: false,
proves: 'non-secret evidence',
doesNotProve: 'branch update acceptance',
}
: null,
},
audit: { journalId: 'opaque', state: 'sealed' },
});
}
function requestHasSecretShape(request: CommandRequest): boolean {
return JSON.stringify(request).match(/authorization|password|\.token|token-dir/i) !== null;
}
afterEach((): void => {
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true });
});
describe('doctor runtime observation', (): void => {
it('runs a synthetic no-token positive control through mosaic cred even when the clone is missing', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-05 synthetic refusal control');
const root = join(tempRoot(), 'missing-brain');
const requests: CommandRequest[] = [];
const runner: CommandRunner = (request): CommandResult => {
requests.push(request);
return {
status: 10,
stdout: validateResult('refused', 'no-token-for-identity'),
stderr: 'refused reason=no-token-for-identity',
};
};
const report = runtime.collectBrainDoctorReport(
{
registrySource: registry(),
targetGitUrl: 'https://git.mosaicstack.dev/mosaicstack/stack.git',
brainNamespace: 'mosaicstack',
identity: 'synthetic-no-token',
root,
},
runner,
);
expect(requests).toHaveLength(1);
expect(requests[0]).toMatchObject({
program: 'mosaic',
args: [
'cred',
'validate',
'synthetic-no-token',
'--estate',
'homelab',
'--host',
'git.mosaicstack.dev',
'--repo',
'mosaicstack/mosaic-brain',
'--require',
'write',
'--json',
],
});
expect(requests.some(requestHasSecretShape)).toBe(false);
expect(report.refusalControl).toEqual({
observed: true,
reasonCode: 'no-token-for-identity',
});
expect(report.findings.map((finding) => finding.code)).toEqual(
expect.arrayContaining(['brain-clone-missing', 'brain-write-access-refused']),
);
});
it('treats process/object terminal-class disagreement as indeterminate', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-08 process/object disagreement');
const root = join(tempRoot(), 'missing-brain');
const report = runtime.collectBrainDoctorReport(
{
registrySource: registry(),
targetGitUrl: 'https://git.mosaicstack.dev/mosaicstack/stack.git',
brainNamespace: 'mosaicstack',
identity: 'synthetic-no-token',
root,
},
(): CommandResult => ({
status: 0,
stdout: validateResult('refused', 'no-token-for-identity'),
stderr: '',
}),
);
expect(report.access).toMatchObject({
outcome: 'indeterminate',
exitCode: 30,
reasonCode: 'unexpected-provider-shape',
});
expect(report.refusalControl.observed).toBe(false);
});
it('rejects a self-consistent broker object for a different declared subject', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-04 caller subject binding');
const root = join(tempRoot(), 'missing-brain');
const report = runtime.collectBrainDoctorReport(
{
registrySource: registry(),
targetGitUrl: 'https://git.mosaicstack.dev/mosaicstack/stack.git',
brainNamespace: 'mosaicstack',
identity: 'seat-a',
root,
},
(): CommandResult => ({
status: 0,
stdout: validateResult('ok', 'validation-verified'),
stderr: '',
}),
);
expect(report.access).toMatchObject({
outcome: 'indeterminate',
exitCode: 30,
reasonCode: 'unexpected-provider-shape',
});
});
it('reads wrong remote and dirty state from git while preserving credential identity binding', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-08 real git observation seam');
const root = join(tempRoot(), 'brain');
mkdirSync(join(root, '.git'), { recursive: true });
const requests: CommandRequest[] = [];
const runner: CommandRunner = (request): CommandResult => {
requests.push(request);
if (request.program === 'mosaic') {
const source = validateResult('ok', 'validation-verified').replaceAll(
'synthetic-no-token',
'seat-a',
);
return { status: 0, stdout: source, 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.uscllc.com/usc/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: '?? uncommitted.md\n', stderr: '' };
}
return { status: 99, stdout: '', stderr: 'unexpected command' };
};
const report = runtime.collectBrainDoctorReport(
{
registrySource: registry(),
targetGitUrl: 'https://git.mosaicstack.dev/mosaicstack/stack.git',
brainNamespace: 'mosaicstack',
identity: 'seat-a',
root,
},
runner,
);
expect(report.findings.map((finding) => finding.code)).toEqual(
expect.arrayContaining(['brain-remote-mismatch', 'brain-uncommitted-state']),
);
expect(requests.filter((request) => request.program === 'git')).toHaveLength(4);
for (const request of requests) {
expect(request.env['MOSAIC_GIT_IDENTITY']).toBe('seat-a');
expect(request.env['GIT_TERMINAL_PROMPT']).toBe('0');
}
});
it('repairs write refusal through mosaic cred, revalidates, then clones and verifies the resulting object', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-08 broker-only doctor repair');
const root = join(tempRoot(), 'brain');
const requests: CommandRequest[] = [];
let validationCount = 0;
const runner: CommandRunner = (request): CommandResult => {
requests.push(request);
if (request.program === 'mosaic' && request.args[1] === 'validate') {
validationCount += 1;
if (validationCount === 1) {
return {
status: 10,
stdout: validateResult('refused', 'no-token-for-identity').replaceAll(
'synthetic-no-token',
'seat-a',
),
stderr: 'refused reason=no-token-for-identity',
};
}
const source = validateResult('ok', 'validation-verified').replaceAll(
'synthetic-no-token',
'seat-a',
);
return { status: 0, stdout: source, stderr: '' };
}
if (request.program === 'mosaic' && request.args[1] === 'grant') {
return { status: 0, stdout: '{"outcome":"ok"}\n', stderr: '' };
}
if (request.program === 'git' && request.args[0] === 'clone') {
mkdirSync(join(root, '.git'), { recursive: true });
return { status: 0, stdout: '', 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' };
};
const report = runtime.repairBrainDoctor(
{
registrySource: registry(),
targetGitUrl: 'https://git.mosaicstack.dev/mosaicstack/stack.git',
brainNamespace: 'mosaicstack',
identity: 'seat-a',
root,
},
runner,
);
expect(report.findings).toEqual([]);
const sequence = requests.map(
(request) => `${request.program}:${request.args[1] ?? request.args[0]}`,
);
expect(sequence.slice(0, 4)).toEqual([
'mosaic:validate',
'mosaic:grant',
'mosaic:validate',
'git:--branch',
]);
expect(requests.some(requestHasSecretShape)).toBe(false);
});
it('does not clone when broker revalidation remains refused after a grant attempt', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-08 failed grant remains visible');
const root = join(tempRoot(), 'brain');
const requests: CommandRequest[] = [];
const report = runtime.repairBrainDoctor(
{
registrySource: registry(),
targetGitUrl: 'https://git.mosaicstack.dev/mosaicstack/stack.git',
brainNamespace: 'mosaicstack',
identity: 'synthetic-no-token',
root,
},
(request): CommandResult => {
requests.push(request);
if (request.program === 'mosaic' && request.args[1] === 'grant') {
return { status: 10, stdout: '{"outcome":"refused"}\n', stderr: 'refused' };
}
return {
status: 10,
stdout: validateResult('refused', 'no-token-for-identity'),
stderr: 'refused reason=no-token-for-identity',
};
},
);
expect(report.findings.map((finding) => finding.code)).toEqual(
expect.arrayContaining(['brain-clone-missing', 'brain-write-access-refused']),
);
expect(requests.some((request) => request.program === 'git')).toBe(false);
});
});
describe('R5 production refusal control', (): void => {
it('requires matching refusal on both Git transport and broker API axes', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-05 production both-axis refusal');
const requests: CommandRequest[] = [];
const result = runtime.collectBrainRefusalControl(
{
registrySource: registry(),
targetGitUrl: 'https://git.mosaicstack.dev/mosaicstack/stack.git',
brainNamespace: 'mosaicstack',
refusalIdentity: 'synthetic-no-token',
},
(request): CommandResult => {
requests.push(request);
if (request.program === 'mosaic') {
return {
status: 10,
stdout: validateResult('refused', 'no-token-for-identity'),
stderr: 'refused reason=no-token-for-identity',
};
}
return {
status: 128,
stdout: '',
stderr: 'credential helper refused reason=no-token-for-identity',
};
},
);
expect(result).toEqual({
observed: true,
reasonCode: 'no-token-for-identity',
gitReasonCode: 'no-token-for-identity',
apiReasonCode: 'no-token-for-identity',
});
expect(requests.map((request) => request.program)).toEqual(['mosaic', 'git']);
expect(requests[1]?.args).toEqual([
'ls-remote',
'https://git.mosaicstack.dev/mosaicstack/mosaic-brain.git',
'HEAD',
]);
expect(
requests.every((request) => request.env['MOSAIC_GIT_IDENTITY'] === 'synthetic-no-token'),
).toBe(true);
expect(requests.some(requestHasSecretShape)).toBe(false);
});
it('fails when the API refuses but Git transport accepts the out-of-estate identity', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-05 production axis disagreement');
const result = runtime.collectBrainRefusalControl(
{
registrySource: registry(),
targetGitUrl: 'https://git.mosaicstack.dev/mosaicstack/stack.git',
brainNamespace: 'mosaicstack',
refusalIdentity: 'synthetic-no-token',
},
(request): CommandResult =>
request.program === 'mosaic'
? {
status: 10,
stdout: validateResult('refused', 'no-token-for-identity'),
stderr: 'refused reason=no-token-for-identity',
}
: { status: 0, stdout: 'refs are visible', stderr: '' },
);
expect(result).toMatchObject({
observed: false,
reasonCode: 'permission-evidence-disagrees',
gitReasonCode: 'transport-accepted',
apiReasonCode: 'no-token-for-identity',
});
});
});
describe('push-on-write publication', (): void => {
it('commits with command-scoped identity, pushes immediately, and proves reachability from origin/main', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-07 publish-on-write reachability');
const root = tempRoot();
const requests: CommandRequest[] = [];
const commit = 'a'.repeat(40);
const remoteHead = 'b'.repeat(40);
const runner: CommandRunner = (request): CommandResult => {
requests.push(request);
const command = request.args.join(' ');
if (command.includes('rev-parse HEAD'))
return { status: 0, stdout: `${commit}\n`, stderr: '' };
if (command.includes('rev-parse origin/main')) {
return { status: 0, stdout: `${remoteHead}\n`, stderr: '' };
}
return { status: 0, stdout: '', stderr: '' };
};
const evidence = runtime.publishBrainPaths(
{
root,
identity: 'seat-a',
paths: [join(root, '.gitignore'), join(root, 'lanes', 'lane-a', '.gitkeep')],
message: 'migrate lane-a state',
},
runner,
);
expect(evidence).toEqual({ commit, remoteHead, reachable: true });
const rendered = requests.map((request) => `${request.program} ${request.args.join(' ')}`);
expect(rendered).toEqual(
expect.arrayContaining([
expect.stringMatching(/git -C .* add -- \.gitignore lanes\/lane-a\/\.gitkeep/),
expect.stringMatching(
/git -C .* -c user\.name=seat-a -c user\.email=seat-a@fleet\.mosaicstack\.dev commit/,
),
expect.stringMatching(/git -C .* push origin HEAD:main/),
expect.stringMatching(/git -C .* fetch origin main/),
expect.stringMatching(/git -C .* merge-base --is-ancestor/),
]),
);
const pushIndex = rendered.findIndex((command) => command.includes(' push origin HEAD:main'));
const fetchIndex = rendered.findIndex((command) => command.includes(' fetch origin main'));
expect(pushIndex).toBeGreaterThan(-1);
expect(fetchIndex).toBeGreaterThan(pushIndex);
expect(rendered.join('\n')).not.toMatch(/timer|cron|interval/);
expect(requests.some(requestHasSecretShape)).toBe(false);
});
it('rebases and retries a rejected concurrent append-only push instead of choosing last-writer-wins', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-07 append-only multi-host conflict');
const root = tempRoot();
const requests: CommandRequest[] = [];
const firstCommit = 'a'.repeat(40);
const rebasedCommit = 'c'.repeat(40);
const remoteHead = 'd'.repeat(40);
let pushes = 0;
let rebased = false;
const evidence = runtime.publishBrainPaths(
{
root,
identity: 'seat-a',
paths: [join(root, 'lanes', 'lane-a', 'findings', 'host-a.md')],
message: 'append host-a finding',
},
(request): CommandResult => {
requests.push(request);
const command = request.args.join(' ');
if (command.includes('push origin HEAD:main')) {
pushes += 1;
return pushes === 1
? { status: 1, stdout: '', stderr: 'non-fast-forward' }
: { status: 0, stdout: '', stderr: '' };
}
if (command.includes('rebase origin/main')) {
rebased = true;
return { status: 0, stdout: '', stderr: '' };
}
if (command.includes('rev-parse HEAD')) {
return {
status: 0,
stdout: `${rebased ? rebasedCommit : firstCommit}\n`,
stderr: '',
};
}
if (command.includes('rev-parse origin/main')) {
return { status: 0, stdout: `${remoteHead}\n`, stderr: '' };
}
return { status: 0, stdout: '', stderr: '' };
},
);
const rendered = requests.map((request) => request.args.join(' '));
const firstPush = rendered.findIndex((command) => command.includes('push origin HEAD:main'));
const rebase = rendered.findIndex((command) => command.includes('rebase origin/main'));
const secondPush = rendered
.map((command): boolean => command.includes('push origin HEAD:main'))
.lastIndexOf(true);
expect(firstPush).toBeGreaterThan(-1);
expect(rebase).toBeGreaterThan(firstPush);
expect(secondPush).toBeGreaterThan(rebase);
expect(pushes).toBe(2);
expect(evidence).toEqual({ commit: rebasedCommit, remoteHead, reachable: true });
});
it('re-proves reachability without inventing a commit when content was already published', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-07 idempotent publication read-back');
const root = tempRoot();
const commit = 'e'.repeat(40);
const requests: CommandRequest[] = [];
const evidence = runtime.publishBrainPaths(
{
root,
identity: 'seat-a',
paths: [join(root, 'lanes', 'lane-a', 'already-present.md')],
message: 'append existing finding',
},
(request): CommandResult => {
requests.push(request);
const command = request.args.join(' ');
if (command.includes(' commit ')) {
return { status: 1, stdout: '', stderr: 'nothing to commit' };
}
if (command.includes('diff --cached --quiet')) {
return { status: 0, stdout: '', stderr: '' };
}
if (command.includes('rev-parse HEAD') || command.includes('rev-parse origin/main')) {
return { status: 0, stdout: `${commit}\n`, stderr: '' };
}
return { status: 0, stdout: '', stderr: '' };
},
);
expect(evidence).toEqual({ commit, remoteHead: commit, reachable: true });
expect(requests.some((request) => request.args.includes('push'))).toBe(true);
});
it('does not manufacture reachability when merge-base rejects the new commit', async (): Promise<void> => {
const runtime = await loadRuntime('MB-REQ-07 publication reachability negative control');
const root = tempRoot();
const commit = 'a'.repeat(40);
const remoteHead = 'b'.repeat(40);
const evidence = runtime.publishBrainPaths(
{
root,
identity: 'seat-a',
paths: [join(root, '.gitignore')],
message: 'seed brain',
},
(request): CommandResult => {
const command = request.args.join(' ');
if (command.includes('rev-parse HEAD')) {
return { status: 0, stdout: `${commit}\n`, stderr: '' };
}
if (command.includes('rev-parse origin/main')) {
return { status: 0, stdout: `${remoteHead}\n`, stderr: '' };
}
if (command.includes('merge-base --is-ancestor')) {
return { status: 1, stdout: '', stderr: '' };
}
return { status: 0, stdout: '', stderr: '' };
},
);
expect(evidence).toEqual({ commit, remoteHead, reachable: false });
});
});
@@ -0,0 +1,393 @@
import { existsSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import { isAbsolute, relative, resolve, sep } from 'node:path';
import {
assessCredentialResult,
deriveBrainTarget,
evaluateBrainDoctor,
planBrainDoctorFix,
type BrainDoctorFinding,
type BrainDoctorObservation,
type CredentialAssessment,
} from './brain-store.js';
const COMMIT = /^[0-9a-f]{40}$/;
export interface CommandRequest {
readonly program: 'git' | 'mosaic';
readonly args: readonly string[];
readonly cwd?: string;
readonly env: Readonly<Record<string, string>>;
}
export interface CommandResult {
readonly status: number;
readonly stdout: string;
readonly stderr: string;
}
export type CommandRunner = (request: CommandRequest) => CommandResult;
export const systemCommandRunner: CommandRunner = (request: CommandRequest): CommandResult => {
const result = spawnSync(request.program, request.args, {
cwd: request.cwd,
env: { ...process.env, ...request.env },
encoding: 'utf8',
maxBuffer: 1024 * 1024,
});
return {
status: result.status ?? 127,
stdout: result.stdout ?? '',
stderr: result.stderr ?? result.error?.message ?? '',
};
};
export interface DoctorRuntimeReport {
readonly findings: readonly BrainDoctorFinding[];
readonly access: CredentialAssessment;
readonly refusalControl: {
readonly observed: boolean;
readonly reasonCode: string | null;
};
}
export interface BrainRefusalControlResult {
readonly observed: boolean;
readonly reasonCode: string | null;
readonly gitReasonCode: string;
readonly apiReasonCode: string;
}
export interface PublishEvidence {
readonly commit: string;
readonly remoteHead: string;
readonly reachable: boolean;
}
function commandEnv(identity: string): Readonly<Record<string, string>> {
return {
MOSAIC_GIT_IDENTITY: identity,
GIT_TERMINAL_PROMPT: '0',
};
}
function integrationFailure(): CredentialAssessment {
return {
outcome: 'indeterminate',
exitCode: 30,
reasonCode: 'unexpected-provider-shape',
diagnostic: 'indeterminate: unexpected-provider-shape',
};
}
function runGit(run: CommandRunner, identity: string, args: readonly string[]): CommandResult {
return run({ program: 'git', args, env: commandEnv(identity) });
}
export function collectBrainDoctorReport(
input: {
readonly registrySource: string;
readonly targetGitUrl: string;
readonly brainNamespace: string;
readonly identity: string;
readonly root: string;
},
run: CommandRunner,
): DoctorRuntimeReport {
const target = deriveBrainTarget(input.registrySource, input.targetGitUrl, input.brainNamespace);
const validation = run({
program: 'mosaic',
args: [
'cred',
'validate',
input.identity,
'--estate',
target.estate,
'--host',
target.host,
'--repo',
target.repo,
'--require',
'write',
'--json',
],
env: commandEnv(input.identity),
});
let access = assessCredentialResult(validation.stdout, {
identity: input.identity,
estate: target.estate,
host: target.host,
repo: target.repo,
});
if (validation.status !== access.exitCode) access = integrationFailure();
const rootExists = existsSync(input.root);
let gitRepository = false;
let remote: string | null = null;
let branch: string | null = null;
let dirty: boolean | null = null;
if (rootExists) {
const repository = runGit(run, input.identity, [
'-C',
input.root,
'rev-parse',
'--is-inside-work-tree',
]);
gitRepository = repository.status === 0 && repository.stdout.trim() === 'true';
if (gitRepository) {
const remoteResult = runGit(run, input.identity, [
'-C',
input.root,
'remote',
'get-url',
'origin',
]);
const branchResult = runGit(run, input.identity, [
'-C',
input.root,
'branch',
'--show-current',
]);
const statusResult = runGit(run, input.identity, ['-C', input.root, 'status', '--porcelain']);
if (remoteResult.status === 0) remote = remoteResult.stdout.trim();
if (branchResult.status === 0) branch = branchResult.stdout.trim();
if (statusResult.status === 0) dirty = statusResult.stdout.trim().length > 0;
}
}
const observation: BrainDoctorObservation = {
rootExists,
gitRepository,
remote,
branch,
dirty,
access,
};
const refusalMarker = `refused reason=${access.reasonCode}`;
const refusalObserved =
validation.status === 10 &&
access.outcome === 'refused' &&
access.reasonCode === 'no-token-for-identity' &&
validation.stderr.includes(refusalMarker);
return {
findings: evaluateBrainDoctor(observation, target.cloneUrl),
access,
refusalControl: {
observed: refusalObserved,
reasonCode: refusalObserved ? access.reasonCode : null,
},
};
}
export function collectBrainRefusalControl(
input: {
readonly registrySource: string;
readonly targetGitUrl: string;
readonly brainNamespace: string;
readonly refusalIdentity: string;
},
run: CommandRunner,
): BrainRefusalControlResult {
const target = deriveBrainTarget(input.registrySource, input.targetGitUrl, input.brainNamespace);
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(input.refusalIdentity)) {
return {
observed: false,
reasonCode: 'permission-evidence-disagrees',
gitReasonCode: 'invalid-control-identity',
apiReasonCode: 'invalid-control-identity',
};
}
const apiResult = run({
program: 'mosaic',
args: [
'cred',
'validate',
input.refusalIdentity,
'--estate',
target.estate,
'--host',
target.host,
'--repo',
target.repo,
'--require',
'write',
'--json',
],
env: commandEnv(input.refusalIdentity),
});
let api = assessCredentialResult(apiResult.stdout, {
identity: input.refusalIdentity,
estate: target.estate,
host: target.host,
repo: target.repo,
});
if (apiResult.status !== api.exitCode) api = integrationFailure();
const gitResult = runGit(run, input.refusalIdentity, ['ls-remote', target.cloneUrl, 'HEAD']);
const marker = /(?:^|\s)reason=([a-z0-9-]+)(?:\s|$)/.exec(gitResult.stderr)?.[1];
const stableRefusals = new Set([
'identity-required',
'estate-required',
'estate-host-mismatch',
'cross-estate-resolution',
'no-token-for-identity',
'tea-login-missing',
'tea-login-host-mismatch',
'provider-identity-mismatch',
'credential-rejected',
'permission-denied',
'organization-membership-required',
'team-membership-required',
]);
const gitReasonCode =
gitResult.status === 0
? 'transport-accepted'
: marker !== undefined && stableRefusals.has(marker) && gitResult.stdout.length === 0
? marker
: 'transport-indeterminate';
const observed =
api.outcome === 'refused' &&
gitReasonCode !== 'transport-accepted' &&
gitReasonCode !== 'transport-indeterminate' &&
gitReasonCode === api.reasonCode;
return {
observed,
reasonCode: observed ? api.reasonCode : 'permission-evidence-disagrees',
gitReasonCode,
apiReasonCode: api.reasonCode,
};
}
export function repairBrainDoctor(
input: {
readonly registrySource: string;
readonly targetGitUrl: string;
readonly brainNamespace: string;
readonly identity: string;
readonly root: string;
},
run: CommandRunner,
): DoctorRuntimeReport {
const target = deriveBrainTarget(input.registrySource, input.targetGitUrl, input.brainNamespace);
let report = collectBrainDoctorReport(input, run);
const actions = planBrainDoctorFix({
findings: report.findings,
target,
identity: input.identity,
root: input.root,
});
for (const action of actions) {
if (action.program === 'mosaic') {
run({ program: 'mosaic', args: action.args, env: commandEnv(input.identity) });
report = collectBrainDoctorReport(input, run);
if (report.access.outcome !== 'ok') return report;
continue;
}
const result = runGit(run, input.identity, action.args);
if (result.status !== 0) return collectBrainDoctorReport(input, run);
}
return collectBrainDoctorReport(input, run);
}
function requireSuccess(result: CommandResult, operation: string): void {
if (result.status !== 0) throw new Error(`${operation}-failed`);
}
function containedRelative(root: string, path: string): string {
if (isAbsolute(path) === false) throw new Error('brain-publish-path-must-be-absolute');
const absoluteRoot = resolve(root);
const absolutePath = resolve(path);
if (absolutePath === absoluteRoot || !absolutePath.startsWith(`${absoluteRoot}${sep}`)) {
throw new Error('brain-publish-path-escaped-root');
}
return relative(absoluteRoot, absolutePath).split(sep).join('/');
}
export function publishBrainPaths(
input: {
readonly root: string;
readonly identity: string;
readonly paths: readonly string[];
readonly message: string;
},
run: CommandRunner,
): PublishEvidence {
if (input.paths.length === 0) throw new Error('brain-publish-paths-empty');
if (input.message.trim().length === 0) throw new Error('brain-publish-message-empty');
const paths = input.paths.map((path: string): string => containedRelative(input.root, path));
requireSuccess(
runGit(run, input.identity, ['-C', input.root, 'add', '--', ...paths]),
'brain-git-add',
);
const commitAttempt = runGit(run, input.identity, [
'-C',
input.root,
'-c',
`user.name=${input.identity}`,
'-c',
`user.email=${input.identity}@fleet.mosaicstack.dev`,
'commit',
'-m',
input.message,
]);
if (commitAttempt.status !== 0) {
const stagedDifference = runGit(run, input.identity, [
'-C',
input.root,
'diff',
'--cached',
'--quiet',
'--exit-code',
]);
if (stagedDifference.status !== 0) throw new Error('brain-git-commit-failed');
}
const readHead = (): string => {
const result = runGit(run, input.identity, ['-C', input.root, 'rev-parse', 'HEAD']);
requireSuccess(result, 'brain-git-read-commit');
const value = result.stdout.trim();
if (!COMMIT.test(value)) throw new Error('brain-git-commit-shape-invalid');
return value;
};
let commit = readHead();
let pushed = false;
for (let attempt = 0; attempt < 3; attempt += 1) {
const push = runGit(run, input.identity, ['-C', input.root, 'push', 'origin', 'HEAD:main']);
if (push.status === 0) {
pushed = true;
break;
}
const concurrentUpdate = /non-fast-forward|fetch first|\[rejected\]/i.test(push.stderr);
if (!concurrentUpdate || attempt === 2) throw new Error('brain-git-push-failed');
requireSuccess(
runGit(run, input.identity, ['-C', input.root, 'fetch', 'origin', 'main']),
'brain-git-fetch-concurrent',
);
requireSuccess(
runGit(run, input.identity, ['-C', input.root, 'rebase', 'origin/main']),
'brain-git-rebase-concurrent',
);
commit = readHead();
}
if (!pushed) throw new Error('brain-git-push-failed');
requireSuccess(
runGit(run, input.identity, ['-C', input.root, 'fetch', 'origin', 'main']),
'brain-git-fetch-readback',
);
const reachableResult = runGit(run, input.identity, [
'-C',
input.root,
'merge-base',
'--is-ancestor',
commit,
'origin/main',
]);
if (reachableResult.status !== 0 && reachableResult.status !== 1) {
throw new Error('brain-git-reachability-check-failed');
}
const remoteResult = runGit(run, input.identity, ['-C', input.root, 'rev-parse', 'origin/main']);
requireSuccess(remoteResult, 'brain-git-read-remote-head');
const remoteHead = remoteResult.stdout.trim();
if (!COMMIT.test(remoteHead)) throw new Error('brain-git-remote-head-shape-invalid');
return { commit, remoteHead, reachable: reachableResult.status === 0 };
}
+341 -64
View File
@@ -1,5 +1,14 @@
import { afterEach, describe, expect, it } from 'vitest'; import { afterEach, describe, expect, it } from 'vitest';
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import {
existsSync,
mkdtempSync,
mkdirSync,
readFileSync,
readdirSync,
rmSync,
symlinkSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
@@ -7,7 +16,7 @@ import { join } from 'node:path';
* Red-first contract checks for stack #1051 / MB-BRAIN-01. * Red-first contract checks for stack #1051 / MB-BRAIN-01.
* *
* These checks are committed before the implementation. They bind to the * These checks are committed before the implementation. They bind to the
* MC-CRED v1.3 terminal classes and stable reason codes, not to the currently * MC-CRED v1.5 terminal classes and stable reason codes, not to the currently
* deployed resolver behavior. Live grant and read/write round-trip tests remain * deployed resolver behavior. Live grant and read/write round-trip tests remain
* gated on MC-CRED-01; these fixtures contain no credential values. * gated on MC-CRED-01; these fixtures contain no credential values.
*/ */
@@ -32,10 +41,23 @@ interface ResolverParityAssessment extends CredentialAssessment {
readonly apiReasonCode: string; readonly apiReasonCode: string;
} }
interface MigrationOwner { interface MigrationOwnerResolution {
readonly name: string; readonly verdict: 'resolved' | 'refused' | 'not-measured';
readonly kind: 'active-lane' | 'durable-team' | 'durable-human' | 'durable-queue'; readonly reasonCode: string;
readonly validated: boolean; readonly principal: {
readonly name: string;
readonly kind:
| 'active-lane'
| 'durable-team'
| 'durable-human'
| 'durable-queue'
| 'mission-seat';
} | null;
readonly authority: {
readonly system: 'gitea' | 'glpi' | 'mosaic-mission-state';
readonly endpoint: string;
readonly contentType: 'application/json';
} | null;
} }
interface MigrationCandidate { interface MigrationCandidate {
@@ -49,7 +71,7 @@ interface MigrationPlan {
readonly status: 'ready' | 'blocked'; readonly status: 'ready' | 'blocked';
readonly candidates: readonly MigrationCandidate[]; readonly candidates: readonly MigrationCandidate[];
readonly reported: readonly { path: string; reason: string }[]; readonly reported: readonly { path: string; reason: string }[];
readonly owner: MigrationOwner | null; readonly owner: MigrationOwnerResolution['principal'];
} }
interface MigrationPublishEvidence { interface MigrationPublishEvidence {
@@ -93,18 +115,24 @@ interface BrainWritePolicy {
} }
interface BrainStoreModule { interface BrainStoreModule {
deriveBrainTarget(registrySource: string, targetGitUrl: string): BrainTarget; deriveBrainTarget(
registrySource: string,
targetGitUrl: string,
brainNamespace: string,
): BrainTarget;
createBrainSkeleton(root: string): { readonly created: readonly string[] }; createBrainSkeleton(root: string): { readonly created: readonly string[] };
assessCredentialResult(source: string): CredentialAssessment; assessCredentialResult(source: string): CredentialAssessment;
assessResolverParity(gitSource: string, apiSource: string): ResolverParityAssessment; assessResolverParity(gitSource: string, apiSource: string): ResolverParityAssessment;
discoverBrainMigration(input: { discoverBrainMigration(
readonly sourceRoot: string; input: {
readonly brainRoot: string; readonly sourceRoot: string;
readonly seat: string; readonly brainRoot: string;
readonly lane: string; readonly seat: string;
readonly laneActive: boolean; readonly lane: string;
readonly owner?: MigrationOwner; readonly laneActive: boolean;
}): MigrationPlan; },
resolveOwner?: (lane: string) => MigrationOwnerResolution,
): MigrationPlan;
migrateBrainState( migrateBrainState(
plan: MigrationPlan, plan: MigrationPlan,
publish: (brainRoot: string, paths: readonly string[]) => MigrationPublishEvidence, publish: (brainRoot: string, paths: readonly string[]) => MigrationPublishEvidence,
@@ -182,6 +210,11 @@ function credentialResult(
outcome: CredentialAssessment['outcome'], outcome: CredentialAssessment['outcome'],
reasonCode: string, reasonCode: string,
message = 'non-secret diagnostic', message = 'non-secret diagnostic',
providerLogin: string | null = outcome === 'ok'
? 'external-seat'
: reasonCode === 'provider-identity-mismatch'
? 'Mos'
: null,
): string { ): string {
const exits = { ok: 0, refused: 10, error: 20, indeterminate: 30 } as const; const exits = { ok: 0, refused: 10, error: 20, indeterminate: 30 } as const;
return JSON.stringify({ return JSON.stringify({
@@ -199,9 +232,41 @@ function credentialResult(
mutation: 'none', mutation: 'none',
reason: { code: reasonCode, message }, reason: { code: reasonCode, message },
evidence: { evidence: {
providerIdentity: null, providerIdentity:
repositoryPermission: null, providerLogin === null
writeDifferential: null, ? null
: {
login: providerLogin,
endpoint: 'GET /api/v1/user',
contentType: 'application/json',
},
repositoryPermission:
outcome === 'ok'
? {
requested: 'write',
effective: 'write',
endpoint: 'GET /api/v1/repos/mosaicstack/mosaic-brain',
contentType: 'application/json',
}
: null,
writeDifferential:
outcome === 'ok'
? {
state: 'can-write',
credentialBinding: 'same-resolution',
transportPrincipal: 'external-seat',
authenticatedReceivePack: 'advertised',
readOnlyControl: {
identity: 'homelab-read-control',
providerPermission: 'read',
receivePack: 'refused',
},
unauthenticatedReceivePack: 'refused',
artifactCreated: false,
proves: 'non-secret evidence',
doesNotProve: 'branch update acceptance',
}
: null,
}, },
audit: { journalId: 'opaque', state: 'sealed' }, audit: { journalId: 'opaque', state: 'sealed' },
}); });
@@ -218,13 +283,17 @@ describe('R2/Q1 — estate and brain discovery have one authority', (): void =>
const sut = await loadSut('MB-REQ-02 target-host estate derivation'); const sut = await loadSut('MB-REQ-02 target-host estate derivation');
expect( expect(
sut.deriveBrainTarget(registry(), 'https://git.mosaicstack.dev/mosaicstack/stack.git'), sut.deriveBrainTarget(
registry(),
'https://git.mosaicstack.dev/mosaicstack/stack.git',
'durable-owner',
),
).toEqual({ ).toEqual({
estate: 'homelab', estate: 'homelab',
host: 'git.mosaicstack.dev', host: 'git.mosaicstack.dev',
owner: 'mosaicstack', owner: 'durable-owner',
repo: 'mosaicstack/mosaic-brain', repo: 'durable-owner/mosaic-brain',
cloneUrl: 'https://git.mosaicstack.dev/mosaicstack/mosaic-brain.git', cloneUrl: 'https://git.mosaicstack.dev/durable-owner/mosaic-brain.git',
}); });
}); });
@@ -234,7 +303,11 @@ describe('R2/Q1 — estate and brain discovery have one authority', (): void =>
process.env['MOSAIC_ESTATE'] = 'homelab'; process.env['MOSAIC_ESTATE'] = 'homelab';
try { try {
expect(() => expect(() =>
sut.deriveBrainTarget(registry(), 'https://unmapped.example.invalid/acme/stack.git'), sut.deriveBrainTarget(
registry(),
'https://unmapped.example.invalid/acme/stack.git',
'durable-owner',
),
).toThrow(/estate-host-unmapped/); ).toThrow(/estate-host-unmapped/);
} finally { } finally {
if (previous === undefined) delete process.env['MOSAIC_ESTATE']; if (previous === undefined) delete process.env['MOSAIC_ESTATE'];
@@ -252,11 +325,41 @@ describe('R6 — brain layout refuses secret material', (): void => {
for (const directory of ['agents', 'lanes', 'board', 'specs', 'methods', 'archives']) { for (const directory of ['agents', 'lanes', 'board', 'specs', 'methods', 'archives']) {
expect(existsSync(join(root, directory)), directory).toBe(true); expect(existsSync(join(root, directory)), directory).toBe(true);
expect(
existsSync(join(root, directory, '.gitkeep')),
`${directory} tracked placeholder`,
).toBe(true);
} }
const rules = readFileSync(join(root, '.gitignore'), 'utf8').trim().split('\n'); const rules = readFileSync(join(root, '.gitignore'), 'utf8').trim().split('\n');
expect(rules).toEqual(['*.token', '*.key', '*.pem', '.env', 'credentials.json']); expect(rules).toEqual(['*.token', '*.key', '*.pem', '.env', 'credentials.json']);
}); });
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();
const brain = join(root, 'brain');
const outside = join(root, 'outside');
mkdirSync(brain);
mkdirSync(outside);
symlinkSync(outside, join(brain, 'agents'));
expect(() => sut.createBrainSkeleton(brain)).toThrow(/brain-layout-directory-unsafe/);
expect(existsSync(join(outside, '.gitkeep'))).toBe(false);
});
it('refuses a symlinked gitignore instead of reading and tracking its external target', async (): Promise<void> => {
const sut = await loadSut('MB-REQ-06 no-follow gitignore');
const root = tempRoot();
const brain = join(root, 'brain');
const outside = join(root, 'outside-secret');
mkdirSync(brain);
writeFileSync(outside, 'DO-NOT-TRACK\n');
symlinkSync(outside, join(brain, '.gitignore'));
expect(() => sut.createBrainSkeleton(brain)).toThrow(/brain-layout-ignore-unsafe/);
expect(readFileSync(outside, 'utf8')).toBe('DO-NOT-TRACK\n');
});
it('never relays broker reason messages that may contain secret-bearing text', async (): Promise<void> => { it('never relays broker reason messages that may contain secret-bearing text', async (): Promise<void> => {
const sut = await loadSut('MB-REQ-06 secret-free error path'); const sut = await loadSut('MB-REQ-06 secret-free error path');
const secretMarker = 'DO-NOT-EMIT-CREDENTIAL-MARKER'; const secretMarker = 'DO-NOT-EMIT-CREDENTIAL-MARKER';
@@ -270,14 +373,16 @@ describe('R6 — brain layout refuses secret material', (): void => {
}); });
}); });
describe('credential caller contract v1.3 terminal classes', (): void => { describe('credential caller contract v1.5 terminal classes', (): void => {
it.each([ it.each([
['ok', 0, 'grant-verified'], ['ok', 0, 'validation-verified'],
['refused', 10, 'no-token-for-identity'], ['refused', 10, 'no-token-for-identity'],
['refused', 10, 'provider-identity-mismatch'],
['error', 20, 'estate-registry-invalid'], ['error', 20, 'estate-registry-invalid'],
['indeterminate', 30, 'provider-unavailable'], ['indeterminate', 30, 'provider-unavailable'],
['indeterminate', 30, 'identity-not-found'], ['indeterminate', 30, 'identity-not-visible'],
['indeterminate', 30, 'credential-rejected'], ['indeterminate', 30, 'identity-not-measured'],
['refused', 10, 'credential-rejected'],
] as const)( ] as const)(
'preserves %s/%i and stable reason %s without parsing prose', 'preserves %s/%i and stable reason %s without parsing prose',
async (outcome, exitCode, reasonCode): Promise<void> => { async (outcome, exitCode, reasonCode): Promise<void> => {
@@ -293,7 +398,7 @@ describe('credential caller contract v1.3 terminal classes', (): void => {
it('makes a missing or inconsistent decision field indeterminate rather than success or refusal', async (): Promise<void> => { it('makes a missing or inconsistent decision field indeterminate rather than success or refusal', async (): Promise<void> => {
const sut = await loadSut('AC-MB-01 malformed broker result fail-closed'); const sut = await loadSut('AC-MB-01 malformed broker result fail-closed');
const malformed = JSON.parse(credentialResult('ok', 'grant-verified')) as Record< const malformed = JSON.parse(credentialResult('ok', 'validation-verified')) as Record<
string, string,
unknown unknown
>; >;
@@ -305,6 +410,66 @@ describe('credential caller contract v1.3 terminal classes', (): void => {
expect(result.exitCode).toBe(30); expect(result.exitCode).toBe(30);
expect(result.reasonCode).toBe('unexpected-provider-shape'); expect(result.reasonCode).toBe('unexpected-provider-shape');
}); });
it('makes an apparent write ok indeterminate when the side-effect-free differential is absent', async (): Promise<void> => {
const sut = await loadSut('AC-MB-01 required write differential');
const raw = JSON.parse(credentialResult('ok', 'validation-verified')) as {
evidence: { writeDifferential: unknown };
};
raw.evidence.writeDifferential = null;
const result = sut.assessCredentialResult(JSON.stringify(raw));
expect(result).toMatchObject({
outcome: 'indeterminate',
exitCode: 30,
reasonCode: 'readback-missing',
});
});
it('rejects the superseded indeterminate/credential-rejected pairing from before the v1.4 correction', async (): Promise<void> => {
const sut = await loadSut('AC-MB-01 credential-rejected stable class');
const legacy = JSON.parse(credentialResult('indeterminate', 'provider-unavailable')) as Record<
string,
unknown
>;
legacy['reason'] = {
code: 'credential-rejected',
message: 'superseded classification',
};
const result = sut.assessCredentialResult(JSON.stringify(legacy));
expect(result.outcome).toBe('indeterminate');
expect(result.exitCode).toBe(30);
expect(result.reasonCode).toBe('unexpected-provider-shape');
});
it('preserves scope-limited identity as not-measured rather than a dead credential refusal', async (): Promise<void> => {
const sut = await loadSut('AC-MB-01 scope-limited identity is not dead credential');
const result = sut.assessCredentialResult(
credentialResult('indeterminate', 'identity-not-measured'),
);
expect(result).toMatchObject({
outcome: 'indeterminate',
exitCode: 30,
reasonCode: 'identity-not-measured',
});
});
it('refuses an apparent ok whose provider /user read-back names a different principal', async (): Promise<void> => {
const sut = await loadSut('AC-MB-01 provider identity MISMATCH is first-class');
const result = sut.assessCredentialResult(
credentialResult('ok', 'validation-verified', 'looks successful', 'Mos'),
);
expect(result.outcome).toBe('refused');
expect(result.exitCode).toBe(10);
expect(result.reasonCode).toBe('provider-identity-mismatch');
});
}); });
describe('R5 — out-of-estate refusal must agree on both resolver axes', (): void => { describe('R5 — out-of-estate refusal must agree on both resolver axes', (): void => {
@@ -324,7 +489,7 @@ describe('R5 — out-of-estate refusal must agree on both resolver axes', (): vo
it.each([ it.each([
[ [
credentialResult('refused', 'no-token-for-identity'), credentialResult('refused', 'no-token-for-identity'),
credentialResult('ok', 'grant-verified'), credentialResult('ok', 'validation-verified'),
], ],
[ [
credentialResult('refused', 'no-token-for-identity'), credentialResult('refused', 'no-token-for-identity'),
@@ -346,8 +511,19 @@ describe('R5 — out-of-estate refusal must agree on both resolver axes', (): vo
}); });
describe('R7 — migration is non-destructive, append-only, and explicit', (): void => { describe('R7 — migration is non-destructive, append-only, and explicit', (): void => {
it('detects canonical lane and current-seat state while explicitly reporting unsupported local state', async (): Promise<void> => { const activeLaneOwner = (): MigrationOwnerResolution => ({
const sut = await loadSut('MB-REQ-07 migration detection/reporting'); verdict: 'resolved',
reasonCode: 'active-lane-owner-verified',
principal: { name: 'lane:lane-a', kind: 'active-lane' },
authority: {
system: 'mosaic-mission-state',
endpoint: 'file:///var/lib/mosaic/missions/lane-a.json',
contentType: 'application/json',
},
});
it('uses an injected authoritative owner resolver for unit mechanics without claiming live owner validation', async (): Promise<void> => {
const sut = await loadSut('MB-REQ-07 injected owner-resolver seam');
const root = tempRoot(); const root = tempRoot();
const sourceRoot = join(root, 'local-memory'); const sourceRoot = join(root, 'local-memory');
const brainRoot = join(root, 'brain'); const brainRoot = join(root, 'brain');
@@ -357,16 +533,13 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v
writeFileSync(join(sourceRoot, 'agents', 'seat-a', 'STATE.md'), 'seat state\n'); writeFileSync(join(sourceRoot, 'agents', 'seat-a', 'STATE.md'), 'seat state\n');
writeFileSync(join(sourceRoot, 'orphan-state.md'), 'must be reported\n'); writeFileSync(join(sourceRoot, 'orphan-state.md'), 'must be reported\n');
const plan = sut.discoverBrainMigration({ const plan = sut.discoverBrainMigration(
sourceRoot, { sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true },
brainRoot, activeLaneOwner,
seat: 'seat-a', );
lane: 'lane-a',
laneActive: true,
owner: { name: 'lane-a', kind: 'active-lane', validated: true },
});
expect(plan.status).toBe('ready'); expect(plan.status).toBe('ready');
expect(plan.owner).toEqual({ name: 'lane:lane-a', kind: 'active-lane' });
expect(plan.candidates.map((candidate) => candidate.kind).sort()).toEqual(['lane', 'seat']); expect(plan.candidates.map((candidate) => candidate.kind).sort()).toEqual(['lane', 'seat']);
expect(plan.reported).toEqual( expect(plan.reported).toEqual(
expect.arrayContaining([ expect.arrayContaining([
@@ -375,20 +548,22 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v
); );
}); });
it('leaves the migration gate blocking when no validated owner exists', async (): Promise<void> => { it('ignores a caller-asserted owner string/validated flag and leaves the gate blocking', async (): Promise<void> => {
const sut = await loadSut('MB-REQ-09 named durable owner gate'); const sut = await loadSut('MB-REQ-09 caller owner assertion cannot satisfy gate');
const root = tempRoot(); const root = tempRoot();
const sourceRoot = join(root, 'local-memory'); const sourceRoot = join(root, 'local-memory');
mkdirSync(join(sourceRoot, 'lanes', 'closed-lane'), { recursive: true }); mkdirSync(join(sourceRoot, 'lanes', 'closed-lane'), { recursive: true });
writeFileSync(join(sourceRoot, 'lanes', 'closed-lane', 'finding.md'), 'finding\n'); writeFileSync(join(sourceRoot, 'lanes', 'closed-lane', 'finding.md'), 'finding\n');
const callerAssertion = {
const plan = sut.discoverBrainMigration({
sourceRoot, sourceRoot,
brainRoot: join(root, 'brain'), brainRoot: join(root, 'brain'),
seat: 'seat-a', seat: 'seat-a',
lane: 'closed-lane', lane: 'closed-lane',
laneActive: false, laneActive: false,
}); owner: { name: 'some-string', kind: 'durable-team', validated: true },
};
const plan = sut.discoverBrainMigration(callerAssertion);
expect(plan.status).toBe('blocked'); expect(plan.status).toBe('blocked');
expect(plan.candidates).toHaveLength(0); expect(plan.candidates).toHaveLength(0);
@@ -399,6 +574,74 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v
); );
}); });
it.each([
['mission seat', { name: 'user:be-coder-07', kind: 'mission-seat' }],
['unicode dash', { name: 'team:platformmaintainers', kind: 'durable-team' }],
['padded', { name: ' team:platform-maintainers ', kind: 'durable-team' }],
['dot presentation', { name: 'team:platform.maintainers', kind: 'durable-team' }],
['space presentation', { name: 'team:platform maintainers', kind: 'durable-team' }],
] as const)(
'rejects %s owner evidence through the injected resolver allowlist',
async (_caseName, principal): Promise<void> => {
const sut = await loadSut('MB-REQ-09 durable-owner allowlist');
const root = tempRoot();
const sourceRoot = join(root, 'local-memory');
mkdirSync(join(sourceRoot, 'lanes', 'closed-lane'), { recursive: true });
writeFileSync(join(sourceRoot, 'lanes', 'closed-lane', 'finding.md'), 'finding\n');
let resolverCalls = 0;
const plan = sut.discoverBrainMigration(
{
sourceRoot,
brainRoot: join(root, 'brain'),
seat: 'seat-a',
lane: 'closed-lane',
laneActive: false,
},
(): MigrationOwnerResolution => {
resolverCalls += 1;
return {
verdict: 'resolved',
reasonCode: 'owner-verified',
principal,
authority: {
system: 'gitea',
endpoint: 'GET /api/v1/teams/1',
contentType: 'application/json',
},
};
},
);
expect(resolverCalls).toBe(1);
expect(plan.status).toBe('blocked');
expect(plan.candidates).toHaveLength(0);
},
);
it('reports secret-shaped legacy files without ever copying them into the brain', async (): Promise<void> => {
const sut = await loadSut('MB-REQ-06 migration secret exclusion');
const root = tempRoot();
const sourceRoot = join(root, 'local-memory');
const laneRoot = join(sourceRoot, 'lanes', 'lane-a');
const brainRoot = join(root, 'brain');
mkdirSync(laneRoot, { recursive: true });
for (const name of ['access.token', 'private.key', 'client.pem', '.env', 'credentials.json']) {
writeFileSync(join(laneRoot, name), 'DO-NOT-MIGRATE\n');
}
const plan = sut.discoverBrainMigration(
{ sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true },
activeLaneOwner,
);
expect(plan.status).toBe('ready');
expect(plan.candidates).toHaveLength(0);
expect(plan.reported).toHaveLength(5);
expect(plan.reported.every((entry) => /secret/i.test(entry.reason))).toBe(true);
expect(existsSync(brainRoot)).toBe(false);
});
it('publishes collision-safe append-only copies before archiving sources and never overwrites a finding', async (): Promise<void> => { it('publishes collision-safe append-only copies before archiving sources and never overwrites a finding', async (): Promise<void> => {
const sut = await loadSut('MB-REQ-07 append-only publish-before-archive migration'); const sut = await loadSut('MB-REQ-07 append-only publish-before-archive migration');
const root = tempRoot(); const root = tempRoot();
@@ -406,14 +649,10 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v
const brainRoot = join(root, 'brain'); const brainRoot = join(root, 'brain');
mkdirSync(join(sourceRoot, 'lanes', 'lane-a'), { recursive: true }); mkdirSync(join(sourceRoot, 'lanes', 'lane-a'), { recursive: true });
writeFileSync(join(sourceRoot, 'lanes', 'lane-a', 'finding.md'), 'new finding\n'); writeFileSync(join(sourceRoot, 'lanes', 'lane-a', 'finding.md'), 'new finding\n');
const plan = sut.discoverBrainMigration({ const plan = sut.discoverBrainMigration(
sourceRoot, { sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true },
brainRoot, activeLaneOwner,
seat: 'seat-a', );
lane: 'lane-a',
laneActive: true,
owner: { name: 'lane-a', kind: 'active-lane', validated: true },
});
const candidate = plan.candidates[0]; const candidate = plan.candidates[0];
expect(candidate).toBeDefined(); expect(candidate).toBeDefined();
if (candidate === undefined) return; if (candidate === undefined) return;
@@ -447,6 +686,39 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v
expect(publishedPaths).toContain(candidate.archive); expect(publishedPaths).toContain(candidate.archive);
}); });
it('refuses nested symlink destinations without copying a migration outside the brain', async (): Promise<void> => {
const sut = await loadSut('MB-REQ-07 migration destination no-follow');
const root = tempRoot();
const sourceRoot = join(root, 'local-memory');
const brainRoot = join(root, 'brain');
const outside = join(root, 'outside');
mkdirSync(join(sourceRoot, 'lanes', 'lane-a'), { recursive: true });
mkdirSync(join(brainRoot, 'lanes'), { recursive: true });
mkdirSync(outside);
const source = join(sourceRoot, 'lanes', 'lane-a', 'finding.md');
writeFileSync(source, 'lane state\n');
symlinkSync(outside, join(brainRoot, 'lanes', 'lane-a'));
const plan = sut.discoverBrainMigration(
{ sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true },
activeLaneOwner,
);
let publishCalls = 0;
const result = sut.migrateBrainState(
plan,
(): MigrationPublishEvidence => {
publishCalls += 1;
return { commit: 'a'.repeat(40), remoteHead: 'a'.repeat(40), reachable: true };
},
brainRoot,
);
expect(result.status).toBe('failed');
expect(readFileSync(source, 'utf8')).toBe('lane state\n');
expect(readdirSync(outside)).toEqual([]);
expect(publishCalls).toBe(0);
});
it('retains every source and reports failure when remote reachability is not established', async (): Promise<void> => { it('retains every source and reports failure when remote reachability is not established', async (): Promise<void> => {
const sut = await loadSut('MB-REQ-07 failed-publish source preservation'); const sut = await loadSut('MB-REQ-07 failed-publish source preservation');
const root = tempRoot(); const root = tempRoot();
@@ -455,14 +727,10 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v
mkdirSync(join(sourceRoot, 'agents', 'seat-a'), { recursive: true }); mkdirSync(join(sourceRoot, 'agents', 'seat-a'), { recursive: true });
const source = join(sourceRoot, 'agents', 'seat-a', 'STATE.md'); const source = join(sourceRoot, 'agents', 'seat-a', 'STATE.md');
writeFileSync(source, 'seat state\n'); writeFileSync(source, 'seat state\n');
const plan = sut.discoverBrainMigration({ const plan = sut.discoverBrainMigration(
sourceRoot, { sourceRoot, brainRoot, seat: 'seat-a', lane: 'lane-a', laneActive: true },
brainRoot, activeLaneOwner,
seat: 'seat-a', );
lane: 'lane-a',
laneActive: true,
owner: { name: 'lane-a', kind: 'active-lane', validated: true },
});
const result = sut.migrateBrainState( const result = sut.migrateBrainState(
plan, plan,
@@ -476,6 +744,9 @@ describe('R7 — migration is non-destructive, append-only, and explicit', (): v
expect(result.status).toBe('failed'); expect(result.status).toBe('failed');
expect(readFileSync(source, 'utf8')).toBe('seat state\n'); expect(readFileSync(source, 'utf8')).toBe('seat state\n');
expect(plan.candidates[0]).toBeDefined();
expect(existsSync(plan.candidates[0]!.destination)).toBe(true);
expect(existsSync(plan.candidates[0]!.archive)).toBe(true);
expect(result.reported).toEqual( expect(result.reported).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ reason: expect.stringMatching(/reachab/i) }), expect.objectContaining({ reason: expect.stringMatching(/reachab/i) }),
@@ -496,11 +767,13 @@ describe('R8 — doctor diagnoses defects and fixes only through approved seams'
remote: null, remote: null,
branch: null, branch: null,
dirty: null, dirty: null,
access: null, access: sut.assessCredentialResult(credentialResult('refused', 'no-token-for-identity')),
}, },
expected, expected,
); );
expect(missing.map((finding) => finding.code)).toContain('brain-clone-missing'); expect(missing.map((finding) => finding.code)).toEqual(
expect.arrayContaining(['brain-clone-missing', 'brain-write-access-refused']),
);
const defects = sut.evaluateBrainDoctor( const defects = sut.evaluateBrainDoctor(
{ {
@@ -529,6 +802,7 @@ describe('R8 — doctor diagnoses defects and fixes only through approved seams'
const target = sut.deriveBrainTarget( const target = sut.deriveBrainTarget(
registry(), registry(),
'https://git.mosaicstack.dev/mosaicstack/stack.git', 'https://git.mosaicstack.dev/mosaicstack/stack.git',
'durable-owner',
); );
const findings: BrainDoctorFinding[] = [ const findings: BrainDoctorFinding[] = [
{ code: 'brain-clone-missing', repairable: true, reasonCode: null }, { code: 'brain-clone-missing', repairable: true, reasonCode: null },
@@ -545,7 +819,9 @@ describe('R8 — doctor diagnoses defects and fixes only through approved seams'
}); });
const rendered = JSON.stringify(actions); const rendered = JSON.stringify(actions);
expect(actions.map((action) => action.program)).toEqual(['git', 'git', 'mosaic']); expect(actions.map((action) => action.program)).toEqual(['mosaic', 'git', 'git']);
expect(actions[0]?.findingCode).toBe('brain-write-access-refused');
expect(actions[1]?.findingCode).toBe('brain-clone-missing');
expect(rendered).toContain('cred'); expect(rendered).toContain('cred');
expect(rendered).toContain('grant'); expect(rendered).toContain('grant');
expect(rendered).toContain('--estate'); expect(rendered).toContain('--estate');
@@ -559,6 +835,7 @@ describe('R8 — doctor diagnoses defects and fixes only through approved seams'
const target = sut.deriveBrainTarget( const target = sut.deriveBrainTarget(
registry(), registry(),
'https://git.mosaicstack.dev/mosaicstack/stack.git', 'https://git.mosaicstack.dev/mosaicstack/stack.git',
'durable-owner',
); );
for (const finding of [ for (const finding of [
+955
View File
@@ -0,0 +1,955 @@
import {
closeSync,
constants as fsConstants,
copyFileSync,
existsSync,
fsyncSync,
lstatSync,
linkSync,
mkdirSync,
openSync,
readdirSync,
readFileSync,
renameSync,
rmSync,
unlinkSync,
writeFileSync,
} from 'node:fs';
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
import { createHash, randomUUID } from 'node:crypto';
import { z } from 'zod';
import { parseCredentialEstateRegistry } from '../credentials/estate-registry.js';
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
const COMMIT = /^[0-9a-f]{40}$/;
const GITIGNORE_RULES = ['*.token', '*.key', '*.pem', '.env', 'credentials.json'] as const;
const BRAIN_DIRECTORIES = ['agents', 'lanes', 'board', 'specs', 'methods', 'archives'] as const;
const TERMINAL_EXITS = {
ok: 0,
refused: 10,
error: 20,
indeterminate: 30,
} as const;
const STABLE_REASON_CLASSES: Readonly<Record<string, CredentialOutcome>> = {
'identity-required': 'refused',
'estate-required': 'refused',
'estate-host-mismatch': 'refused',
'cross-estate-resolution': 'refused',
'no-token-for-identity': 'refused',
'tea-login-missing': 'refused',
'tea-login-host-mismatch': 'refused',
'provider-identity-mismatch': 'refused',
'credential-rejected': 'refused',
'permission-denied': 'refused',
'organization-membership-required': 'refused',
'team-membership-required': 'refused',
'invalid-input': 'error',
'estate-registry-invalid': 'error',
'insecure-credential-source': 'error',
'journal-unavailable': 'error',
'internal-invariant': 'error',
'provider-unavailable': 'indeterminate',
'identity-not-visible': 'indeterminate',
'identity-not-measured': 'indeterminate',
'identity-not-found': 'indeterminate',
'unexpected-content-type': 'indeterminate',
'unexpected-provider-shape': 'indeterminate',
'scope-not-evaluable': 'indeterminate',
'permission-evidence-disagrees': 'indeterminate',
'transport-principal-mismatch': 'indeterminate',
'read-only-control-invalid': 'indeterminate',
'readback-missing': 'indeterminate',
'mutation-state-unknown': 'indeterminate',
};
export interface BrainTarget {
readonly estate: string;
readonly host: string;
readonly owner: string;
readonly repo: string;
readonly cloneUrl: string;
}
export type CredentialOutcome = keyof typeof TERMINAL_EXITS;
export interface CredentialAssessment {
readonly outcome: CredentialOutcome;
readonly exitCode: 0 | 10 | 20 | 30;
readonly reasonCode: string;
readonly diagnostic: string;
}
export interface ResolverParityAssessment extends CredentialAssessment {
readonly gitReasonCode: string;
readonly apiReasonCode: string;
}
export interface MigrationOwnerResolution {
readonly verdict: 'resolved' | 'refused' | 'not-measured';
readonly reasonCode: string;
readonly principal: {
readonly name: string;
readonly kind:
| 'active-lane'
| 'durable-team'
| 'durable-human'
| 'durable-queue'
| 'mission-seat';
} | null;
readonly authority: {
readonly system: 'gitea' | 'glpi' | 'mosaic-mission-state';
readonly endpoint: string;
readonly contentType: 'application/json';
} | null;
}
export interface MigrationCandidate {
readonly source: string;
readonly destination: string;
readonly archive: string;
readonly kind: 'lane' | 'seat';
}
export interface MigrationReport {
readonly path: string;
readonly reason: string;
}
export interface MigrationPlan {
readonly status: 'ready' | 'blocked';
readonly candidates: readonly MigrationCandidate[];
readonly reported: readonly MigrationReport[];
readonly owner: MigrationOwnerResolution['principal'];
}
export interface MigrationPublishEvidence {
readonly commit: string;
readonly remoteHead: string;
readonly reachable: boolean;
}
export interface MigrationResult {
readonly status: 'migrated' | 'reported' | 'failed';
readonly migrated: readonly MigrationCandidate[];
readonly reported: readonly MigrationReport[];
readonly publish: MigrationPublishEvidence | null;
}
export interface BrainDoctorObservation {
readonly rootExists: boolean;
readonly gitRepository: boolean;
readonly remote: string | null;
readonly branch: string | null;
readonly dirty: boolean | null;
readonly access: CredentialAssessment | null;
}
export interface BrainDoctorFinding {
readonly code: string;
readonly repairable: boolean;
readonly reasonCode: string | null;
}
export interface BrainDoctorAction {
readonly program: 'git' | 'mosaic';
readonly args: readonly string[];
readonly findingCode: string;
}
export interface BrainWritePolicy {
readonly allowed: boolean;
readonly mode: 'append-only' | 'single-writer' | 'seat-writer' | 'refused';
readonly reason: string;
}
interface ParsedGitTarget {
readonly host: string;
readonly owner: string;
}
const providerIdentitySchema = z
.object({
login: z.string().min(1),
endpoint: z.literal('GET /api/v1/user'),
contentType: z.string().min(1),
})
.passthrough();
const repositoryPermissionSchema = z
.object({
requested: z.literal('write'),
effective: z.enum(['write', 'admin']),
endpoint: z.string().min(1),
contentType: z.string().min(1),
})
.passthrough();
const writeDifferentialSchema = z
.object({
state: z.literal('can-write'),
credentialBinding: z.literal('same-resolution'),
transportPrincipal: z.string().min(1),
authenticatedReceivePack: z.literal('advertised'),
readOnlyControl: z
.object({
identity: z.string().min(1),
providerPermission: z.literal('read'),
receivePack: z.literal('refused'),
})
.passthrough(),
unauthenticatedReceivePack: z.literal('refused'),
artifactCreated: z.literal(false),
proves: z.string().min(1),
doesNotProve: z.string().min(1),
})
.passthrough();
const credentialResultSchema = z
.object({
schemaVersion: z.literal(1),
operation: z.literal('validate'),
outcome: z.enum(['ok', 'refused', 'error', 'indeterminate']),
exitCode: z.number().int(),
subject: z
.object({
identity: z.string().min(1),
estate: z.string().min(1),
host: z.string().min(1),
repo: z.string().min(1),
})
.strict(),
mutation: z.enum(['none', 'not-started', 'applied', 'unknown']),
reason: z
.object({
code: z.string().min(1),
message: z.string(),
})
.passthrough(),
evidence: z
.object({
providerIdentity: providerIdentitySchema.nullable(),
repositoryPermission: repositoryPermissionSchema.nullable(),
writeDifferential: writeDifferentialSchema.nullable(),
})
.passthrough(),
audit: z
.object({
journalId: z.string().nullable(),
state: z.enum(['not-started', 'open', 'sealed']),
})
.passthrough(),
})
.passthrough();
function safeName(value: string, label: string): string {
if (!SAFE_NAME.test(value)) {
throw new Error(`invalid-${label}`);
}
return value;
}
function parseGitTarget(targetGitUrl: string): ParsedGitTarget {
let host = '';
let pathname = '';
if (/^[^@\s]+@[^:\s]+:.+$/.test(targetGitUrl)) {
const separator = targetGitUrl.indexOf(':');
const authority = targetGitUrl.slice(0, separator);
host = authority.slice(authority.lastIndexOf('@') + 1);
pathname = targetGitUrl.slice(separator + 1);
} else {
let parsed: URL;
try {
parsed = new URL(targetGitUrl);
} catch {
throw new Error('target-git-url-invalid');
}
if (!['https:', 'ssh:'].includes(parsed.protocol) || parsed.password !== '') {
throw new Error('target-git-url-invalid');
}
if (parsed.protocol === 'https:' && parsed.username !== '') {
throw new Error('target-git-url-contains-credential');
}
host = parsed.hostname;
pathname = parsed.pathname;
}
const parts = pathname
.replace(/^\/+/, '')
.replace(/\.git$/, '')
.split('/')
.filter((part: string): boolean => part.length > 0);
if (host.length === 0 || parts.length !== 2) {
throw new Error('target-git-url-invalid');
}
return { host: host.toLowerCase(), owner: safeName(parts[0] ?? '', 'repository-owner') };
}
export function deriveBrainTarget(
registrySource: string,
targetGitUrl: string,
brainNamespace: string,
): BrainTarget {
const target = parseGitTarget(targetGitUrl);
const resolved = parseCredentialEstateRegistry(registrySource).resolveByHost(target.host);
if (resolved === undefined) {
throw new Error(`estate-host-unmapped: ${target.host}`);
}
const owner = safeName(brainNamespace, 'brain-namespace');
const repo = `${owner}/mosaic-brain`;
return {
estate: resolved.estate,
host: target.host,
owner,
repo,
cloneUrl: `${resolved.host.apiBaseUrl}/${repo}.git`,
};
}
function syncFile(path: string): void {
const descriptor = openSync(path, 'r');
try {
fsyncSync(descriptor);
} finally {
closeSync(descriptor);
}
}
export function createBrainSkeleton(root: string): { readonly created: readonly string[] } {
const created: string[] = [];
if (existsSync(root)) {
const rootStatus = lstatSync(root);
if (!rootStatus.isDirectory() || rootStatus.isSymbolicLink()) {
throw new Error('brain-layout-root-unsafe');
}
}
for (const directory of BRAIN_DIRECTORIES) {
const path = join(root, directory);
if (existsSync(path)) {
const status = lstatSync(path);
if (!status.isDirectory() || status.isSymbolicLink()) {
throw new Error('brain-layout-directory-unsafe');
}
}
}
mkdirSync(root, { recursive: true });
for (const directory of BRAIN_DIRECTORIES) {
const path = join(root, directory);
if (!existsSync(path)) {
mkdirSync(path, { recursive: true });
created.push(path);
}
const placeholder = join(path, '.gitkeep');
if (!existsSync(placeholder)) {
writeFileSync(placeholder, '', { encoding: 'utf8', mode: 0o644, flag: 'wx' });
syncFile(placeholder);
created.push(placeholder);
} else if (!lstatSync(placeholder).isFile() || lstatSync(placeholder).isSymbolicLink()) {
throw new Error('brain-layout-placeholder-unsafe');
}
}
const ignorePath = join(root, '.gitignore');
if (existsSync(ignorePath)) {
const ignoreStatus = lstatSync(ignorePath);
if (!ignoreStatus.isFile() || ignoreStatus.isSymbolicLink()) {
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 temporary = `${ignorePath}.tmp-${process.pid}-${randomUUID()}`;
writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o644, flag: 'wx' });
syncFile(temporary);
renameSync(temporary, ignorePath);
created.push(ignorePath);
}
return { created };
}
function indeterminate(reasonCode: string): CredentialAssessment {
return {
outcome: 'indeterminate',
exitCode: 30,
reasonCode,
diagnostic: `indeterminate: ${reasonCode}`,
};
}
export function assessCredentialResult(
source: string,
expectedSubject?: {
readonly identity: string;
readonly estate: string;
readonly host: string;
readonly repo: string;
},
): CredentialAssessment {
let raw: unknown;
try {
raw = JSON.parse(source);
} catch {
return indeterminate('unexpected-provider-shape');
}
const parsed = credentialResultSchema.safeParse(raw);
if (!parsed.success) return indeterminate('unexpected-provider-shape');
if (
expectedSubject !== undefined &&
(parsed.data.subject.identity !== expectedSubject.identity ||
parsed.data.subject.estate !== expectedSubject.estate ||
parsed.data.subject.host !== expectedSubject.host ||
parsed.data.subject.repo !== expectedSubject.repo)
) {
return indeterminate('unexpected-provider-shape');
}
const expectedExit = TERMINAL_EXITS[parsed.data.outcome];
if (parsed.data.exitCode !== expectedExit) {
return indeterminate('unexpected-provider-shape');
}
const stableClass = STABLE_REASON_CLASSES[parsed.data.reason.code];
if (stableClass !== undefined && stableClass !== parsed.data.outcome) {
return indeterminate('unexpected-provider-shape');
}
const identity = parsed.data.evidence.providerIdentity;
if (identity !== null) {
if (!identity.contentType.toLowerCase().startsWith('application/json')) {
return indeterminate('unexpected-content-type');
}
if (identity.login !== parsed.data.subject.identity) {
return {
outcome: 'refused',
exitCode: 10,
reasonCode: 'provider-identity-mismatch',
diagnostic: 'refused: provider-identity-mismatch',
};
}
}
if (parsed.data.outcome === 'ok') {
const permission = parsed.data.evidence.repositoryPermission;
const differential = parsed.data.evidence.writeDifferential;
if (
identity === null ||
permission === null ||
differential === null ||
!permission.contentType.toLowerCase().startsWith('application/json') ||
differential.transportPrincipal !== parsed.data.subject.identity ||
parsed.data.mutation !== 'none' ||
parsed.data.audit.state !== 'sealed' ||
parsed.data.audit.journalId === null
) {
return indeterminate('readback-missing');
}
}
if (
parsed.data.reason.code === 'provider-identity-mismatch' &&
(identity === null || identity.login === parsed.data.subject.identity)
) {
return indeterminate('unexpected-provider-shape');
}
return {
outcome: parsed.data.outcome,
exitCode: expectedExit,
reasonCode: parsed.data.reason.code,
diagnostic: `${parsed.data.outcome}: ${parsed.data.reason.code}`,
};
}
export function assessResolverParity(
gitSource: string,
apiSource: string,
): ResolverParityAssessment {
const git = assessCredentialResult(gitSource);
const api = assessCredentialResult(apiSource);
if (git.outcome === 'refused' && api.outcome === 'refused' && git.reasonCode === api.reasonCode) {
return {
...git,
gitReasonCode: git.reasonCode,
apiReasonCode: api.reasonCode,
};
}
return {
...indeterminate('permission-evidence-disagrees'),
gitReasonCode: git.reasonCode,
apiReasonCode: api.reasonCode,
};
}
function filesBelow(root: string): string[] {
if (!existsSync(root)) return [];
const result: string[] = [];
const walk = (directory: string): void => {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const path = join(directory, entry.name);
if (entry.isDirectory()) walk(path);
else if (entry.isFile()) result.push(path);
else result.push(path);
}
};
walk(root);
return result.sort((left: string, right: string): number => left.localeCompare(right));
}
function ownerIsValid(
resolution: MigrationOwnerResolution,
lane: string,
laneActive: boolean,
): boolean {
if (
resolution.verdict !== 'resolved' ||
resolution.principal === null ||
resolution.authority === null ||
resolution.authority.endpoint.length === 0 ||
resolution.authority.contentType !== 'application/json'
) {
return false;
}
const principal = resolution.principal;
const normalized = principal.name.normalize('NFKC');
if (normalized !== principal.name) return false;
const grammars: Readonly<Record<string, RegExp>> = {
'active-lane': /^lane:[a-z0-9][a-z0-9-]*$/,
'durable-team': /^team:[a-z0-9][a-z0-9-]*$/,
'durable-human': /^user:[a-z0-9][a-z0-9-]*$/,
'durable-queue': /^queue:[a-z0-9][a-z0-9-]*$/,
};
const grammar = grammars[principal.kind];
if (grammar === undefined || !grammar.test(normalized)) return false;
if (principal.kind === 'active-lane') {
return laneActive && principal.name === `lane:${lane}`;
}
return true;
}
function migrationDigest(sourceRoot: string, path: string): string {
const stat = lstatSync(path);
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('migration-source-not-regular');
const key = relative(sourceRoot, path).split(sep).join('/');
return createHash('sha256').update(key).update('\0').update(readFileSync(path)).digest('hex');
}
function migrationCandidate(
sourceRoot: string,
brainRoot: string,
path: string,
kind: 'lane' | 'seat',
seat: string,
lane: string,
): MigrationCandidate {
const digest = migrationDigest(sourceRoot, path).slice(0, 16);
const name = `${digest}-${basename(path)}`;
const destination =
kind === 'lane'
? join(brainRoot, 'lanes', lane, 'findings', 'imports', name)
: join(brainRoot, 'agents', seat, 'state', 'imports', name);
return {
source: path,
destination,
archive: join(brainRoot, 'archives', 'imports', kind, name),
kind,
};
}
function secretShapedPath(path: string): boolean {
const name = basename(path);
return (
name === '.env' ||
name === 'credentials.json' ||
name.endsWith('.token') ||
name.endsWith('.key') ||
name.endsWith('.pem')
);
}
function reportAll(paths: readonly string[], reason: string): MigrationReport[] {
return paths.map((path: string): MigrationReport => ({ path, reason }));
}
export function discoverBrainMigration(
input: {
readonly sourceRoot: string;
readonly brainRoot: string;
readonly seat: string;
readonly lane: string;
readonly laneActive: boolean;
},
resolveOwner?: (lane: string) => MigrationOwnerResolution,
): MigrationPlan {
const seat = safeName(input.seat, 'seat');
const lane = safeName(input.lane, 'lane');
const laneRoot = join(input.sourceRoot, 'lanes', lane);
const seatRoot = join(input.sourceRoot, 'agents', seat);
const laneFiles = filesBelow(laneRoot);
const seatFiles = filesBelow(seatRoot);
const selected = new Set([...laneFiles, ...seatFiles]);
const all = filesBelow(input.sourceRoot);
const unsupported = all.filter((path: string): boolean => !selected.has(path));
let ownerResolution: MigrationOwnerResolution = {
verdict: 'not-measured',
reasonCode: 'owner-resolver-unavailable',
principal: null,
authority: null,
};
if (resolveOwner !== undefined) {
try {
ownerResolution = resolveOwner(lane);
} catch {
ownerResolution = {
verdict: 'not-measured',
reasonCode: 'owner-resolver-failed',
principal: null,
authority: null,
};
}
}
if (!ownerIsValid(ownerResolution, lane, input.laneActive)) {
return {
status: 'blocked',
candidates: [],
reported: reportAll(
all,
'Migration requires a source-of-truth-resolved named durable owner; caller assertions are not evidence.',
),
owner: null,
};
}
const candidates: MigrationCandidate[] = [];
const reported: MigrationReport[] = reportAll(
unsupported,
'Ownership or supported migration shape was not established; retained and reported.',
);
for (const path of laneFiles) {
if (secretShapedPath(path)) {
reported.push({
path,
reason: 'Secret-shaped state is forbidden in the brain; retained and reported.',
});
continue;
}
try {
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.' });
}
}
for (const path of seatFiles) {
if (secretShapedPath(path)) {
reported.push({
path,
reason: 'Secret-shaped state is forbidden in the brain; retained and reported.',
});
continue;
}
try {
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.' });
}
}
return { status: 'ready', candidates, reported, owner: ownerResolution.principal };
}
function isContained(root: string, path: string): boolean {
const absoluteRoot = resolve(root);
const absolutePath = resolve(path);
return absolutePath === absoluteRoot || absolutePath.startsWith(`${absoluteRoot}${sep}`);
}
function assertSafeDestinationAncestors(root: string, destination: string): void {
if (!isContained(root, destination)) throw new Error('migration-destination-escaped-brain');
if (!existsSync(root)) mkdirSync(root, { recursive: true });
const rootStatus = lstatSync(root);
if (!rootStatus.isDirectory() || rootStatus.isSymbolicLink()) {
throw new Error('migration-destination-ancestor-unsafe');
}
const parts = relative(root, dirname(destination)).split(sep).filter(Boolean);
let cursor = root;
for (const part of parts) {
cursor = join(cursor, part);
if (!existsSync(cursor)) continue;
const status = lstatSync(cursor);
if (!status.isDirectory() || status.isSymbolicLink()) {
throw new Error('migration-destination-ancestor-unsafe');
}
}
}
function copyVerified(source: string, destination: string, brainRoot: string): boolean {
assertSafeDestinationAncestors(brainRoot, destination);
mkdirSync(dirname(destination), { recursive: true });
assertSafeDestinationAncestors(brainRoot, destination);
if (existsSync(destination)) {
const status = lstatSync(destination);
if (!status.isFile() || status.isSymbolicLink()) {
throw new Error('append-only-destination-unsafe');
}
const sourceDigest = createHash('sha256').update(readFileSync(source)).digest('hex');
const destinationDigest = createHash('sha256').update(readFileSync(destination)).digest('hex');
if (sourceDigest !== destinationDigest) throw new Error('append-only-collision');
return false;
}
const temporary = `${destination}.tmp-${process.pid}-${randomUUID()}`;
try {
copyFileSync(source, temporary, fsConstants.COPYFILE_EXCL);
const temporaryStatus = lstatSync(temporary);
if (!temporaryStatus.isFile() || temporaryStatus.isSymbolicLink()) {
throw new Error('migration-copy-target-unsafe');
}
syncFile(temporary);
const sourceDigest = createHash('sha256').update(readFileSync(source)).digest('hex');
const copiedDigest = createHash('sha256').update(readFileSync(temporary)).digest('hex');
if (sourceDigest !== copiedDigest) throw new Error('migration-copy-verification-failed');
assertSafeDestinationAncestors(brainRoot, destination);
linkSync(temporary, destination);
syncFile(destination);
return true;
} finally {
rmSync(temporary, { force: true });
}
}
export function migrateBrainState(
plan: MigrationPlan,
publish: (brainRoot: string, paths: readonly string[]) => MigrationPublishEvidence,
brainRoot: string,
): MigrationResult {
if (plan.status !== 'ready' || plan.candidates.length === 0) {
return {
status: 'reported',
migrated: [],
reported: plan.reported,
publish: null,
};
}
const created: string[] = [];
const published: string[] = [];
let publicationAttempted = false;
let evidence: MigrationPublishEvidence;
try {
for (const candidate of plan.candidates) {
if (
!isContained(brainRoot, candidate.destination) ||
!isContained(brainRoot, candidate.archive)
) {
throw new Error('migration-destination-escaped-brain');
}
if (copyVerified(candidate.source, candidate.destination, brainRoot)) {
created.push(candidate.destination);
}
if (copyVerified(candidate.source, candidate.archive, brainRoot)) {
created.push(candidate.archive);
}
published.push(candidate.destination, candidate.archive);
}
publicationAttempted = true;
evidence = publish(brainRoot, published);
if (!COMMIT.test(evidence.commit) || !COMMIT.test(evidence.remoteHead) || !evidence.reachable) {
throw new Error('remote reachability was not established');
}
} catch (error: unknown) {
// Once publication is attempted its remote mutation state may be unknown.
// Keep the local copies so the checkout does not silently diverge from a
// commit that may already be reachable; sources always remain intact.
if (!publicationAttempted) {
for (const path of created.reverse()) rmSync(path, { force: true });
}
const detail = error instanceof Error ? error.message : 'migration failed';
return {
status: 'failed',
migrated: [],
reported: [
...plan.reported,
...plan.candidates.map(
(candidate: MigrationCandidate): MigrationReport => ({
path: candidate.source,
reason: `Migration retained source: ${detail}`,
}),
),
],
publish: null,
};
}
const migrated: MigrationCandidate[] = [];
const removalReports: MigrationReport[] = [];
for (const candidate of plan.candidates) {
try {
unlinkSync(candidate.source);
migrated.push(candidate);
} catch {
removalReports.push({
path: candidate.source,
reason:
'Published migration is reachable but source cleanup failed; retained and reported.',
});
}
}
return {
status: removalReports.length === 0 ? 'migrated' : 'reported',
migrated,
reported: [...plan.reported, ...removalReports],
publish: evidence,
};
}
function accessFinding(access: CredentialAssessment | null): BrainDoctorFinding | null {
if (access === null) {
return {
code: 'brain-write-access-indeterminate',
repairable: false,
reasonCode: 'readback-missing',
};
}
if (access.outcome === 'ok') return null;
return {
code: `brain-write-access-${access.outcome}`,
repairable:
access.outcome === 'refused' &&
['permission-denied', 'no-token-for-identity'].includes(access.reasonCode),
reasonCode: access.reasonCode,
};
}
export function evaluateBrainDoctor(
observation: BrainDoctorObservation,
expectedRemote: string,
): readonly BrainDoctorFinding[] {
const access = accessFinding(observation.access);
if (!observation.rootExists) {
return [
{ code: 'brain-clone-missing', repairable: true, reasonCode: null },
...(access === null ? [] : [access]),
];
}
const findings: BrainDoctorFinding[] = [];
if (!observation.gitRepository) {
findings.push({ code: 'brain-not-git-repository', repairable: true, reasonCode: null });
return findings;
}
if (observation.remote !== expectedRemote) {
findings.push({ code: 'brain-remote-mismatch', repairable: true, reasonCode: null });
}
if (observation.branch !== 'main') {
findings.push({ code: 'brain-branch-mismatch', repairable: false, reasonCode: null });
}
if (observation.dirty === true) {
findings.push({ code: 'brain-uncommitted-state', repairable: false, reasonCode: null });
}
if (access !== null) findings.push(access);
return findings;
}
export function planBrainDoctorFix(input: {
readonly findings: readonly BrainDoctorFinding[];
readonly target: BrainTarget;
readonly identity: string;
readonly root: string;
}): readonly BrainDoctorAction[] {
safeName(input.identity, 'identity');
const actions: BrainDoctorAction[] = [];
const priority: Readonly<Record<string, number>> = {
'brain-write-access-refused': 0,
'brain-clone-missing': 1,
'brain-remote-mismatch': 2,
};
const ordered = [...input.findings].sort(
(left: BrainDoctorFinding, right: BrainDoctorFinding): number =>
(priority[left.code] ?? 99) - (priority[right.code] ?? 99),
);
for (const finding of ordered) {
if (!finding.repairable) continue;
if (finding.code === 'brain-clone-missing') {
actions.push({
program: 'git',
args: ['clone', '--branch', 'main', '--single-branch', input.target.cloneUrl, input.root],
findingCode: finding.code,
});
} else if (finding.code === 'brain-remote-mismatch') {
actions.push({
program: 'git',
args: ['-C', input.root, 'remote', 'set-url', 'origin', input.target.cloneUrl],
findingCode: finding.code,
});
} else if (finding.code === 'brain-write-access-refused') {
actions.push({
program: 'mosaic',
args: [
'cred',
'grant',
input.identity,
'--estate',
input.target.estate,
'--host',
input.target.host,
'--repo',
input.target.repo,
'--permission',
'write',
'--json',
],
findingCode: finding.code,
});
}
}
return actions;
}
function safeRelativePath(path: string): string[] | null {
if (isAbsolute(path) || path.includes('\\')) return null;
const parts = path.split('/').filter((part: string): boolean => part.length > 0);
if (parts.length === 0 || parts.some((part: string): boolean => part === '.' || part === '..')) {
return null;
}
return parts;
}
export function classifyBrainWrite(input: {
readonly path: string;
readonly actor: string;
readonly seat: string;
readonly boardWriter?: string;
}): BrainWritePolicy {
const parts = safeRelativePath(input.path);
if (parts === null || !SAFE_NAME.test(input.actor) || !SAFE_NAME.test(input.seat)) {
return { allowed: false, mode: 'refused', reason: 'invalid-write-subject' };
}
if (parts[0] === 'lanes' && parts.length >= 3) {
return { allowed: true, mode: 'append-only', reason: 'lane-content-is-findings' };
}
if (parts[0] === 'board') {
if (input.boardWriter !== undefined && input.actor === input.boardWriter) {
return { allowed: true, mode: 'single-writer', reason: 'named-board-writer' };
}
return { allowed: false, mode: 'refused', reason: 'board-writer-mismatch' };
}
if (parts[0] === 'agents' && parts.length >= 3) {
if (parts[1] === input.seat && input.actor === input.seat) {
return { allowed: true, mode: 'seat-writer', reason: 'seat-owned-state' };
}
return { allowed: false, mode: 'refused', reason: 'seat-writer-mismatch' };
}
return { allowed: false, mode: 'refused', reason: 'unsupported-write-path' };
}
+27 -4
View File
@@ -29,6 +29,11 @@ import { readPersonaContractBlock } from '../fleet/persona-contract.js';
import { canonicalizeRoleClass } from './fleet-personas.js'; import { canonicalizeRoleClass } from './fleet-personas.js';
import { launchClaudex, type ClaudexHarnessAdapter } from './claudex.js'; import { launchClaudex, type ClaudexHarnessAdapter } from './claudex.js';
import { runLeaseEnforcementDoctorCheck } from './lease-doctor-check.js'; import { runLeaseEnforcementDoctorCheck } from './lease-doctor-check.js';
import {
defaultInstalledBrainDoctorOptions,
runInstalledBrainDoctorCheck,
} from './brain-doctor-check.js';
import { systemCommandRunner } from './brain-store-runtime.js';
const MOSAIC_HOME = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic'); const MOSAIC_HOME = process.env['MOSAIC_HOME'] ?? join(homedir(), '.config', 'mosaic');
const MAX_INSTALLED_TOOLS_BYTES = 256 * 1024; const MAX_INSTALLED_TOOLS_BYTES = 256 * 1024;
@@ -1257,8 +1262,11 @@ export function registerLaunchCommands(program: Command): void {
}); });
} }
// `doctor` — the framework drift audit (bash script) PLUS the #869 // `doctor` — the framework drift audit (bash script), the #869
// Point-1 C5 lease-enforcement activation check (TS, reusing C1's // Point-1 C5 lease-enforcement activation check, and the #1051 per-estate
// durable brain check. Both TS checks run before the bash audit and can
// force a non-zero result for hard/indeterminate failures.
// The lease check reuses C1's
// `leaseEnforcementActivatable()` and C3's `checkBrokerSupervisorHealth()`). // `leaseEnforcementActivatable()` and C3's `checkBrokerSupervisorHealth()`).
// Kept out of the generic `directCommands` loop above because this check // Kept out of the generic `directCommands` loop above because this check
// must run and report BEFORE the bash script's own exit, and must be able // must run and report BEFORE the bash script's own exit, and must be able
@@ -1267,14 +1275,29 @@ export function registerLaunchCommands(program: Command): void {
// undiagnosed (see lease-doctor-check.ts docstring). // undiagnosed (see lease-doctor-check.ts docstring).
program program
.command('doctor') .command('doctor')
.description('Health audit — detect drift, missing files, and #869 lease-activation gaps') .description('Health audit — detect drift, lease gaps, and per-estate brain defects')
.allowUnknownOption(true) .allowUnknownOption(true)
.allowExcessArguments(true) .allowExcessArguments(true)
.action(async (_opts: unknown, cmd: Command) => { .action(async (_opts: unknown, cmd: Command) => {
checkMosaicHome(); checkMosaicHome();
const leaseCheck = await runLeaseEnforcementDoctorCheck(); const leaseCheck = await runLeaseEnforcementDoctorCheck();
const leaseCheckFailed = printLeaseDoctorCheck(leaseCheck); const leaseCheckFailed = printLeaseDoctorCheck(leaseCheck);
runDoctorScriptAndExit(fwScript('mosaic-doctor'), cmd.args, leaseCheckFailed); const fix = cmd.args.includes('--fix');
const brainCheck = runInstalledBrainDoctorCheck(
defaultInstalledBrainDoctorOptions(fix),
systemCommandRunner,
);
for (const line of brainCheck.lines) {
(brainCheck.status === 'ok' ? console.log : console.error)(line);
}
const brainCheckFailed =
brainCheck.status === 'error' ||
(brainCheck.status === 'warn' && cmd.args.includes('--fail-on-warn'));
runDoctorScriptAndExit(
fwScript('mosaic-doctor'),
cmd.args,
leaseCheckFailed || brainCheckFailed,
);
}); });
} }