465 lines
16 KiB
TypeScript
465 lines
16 KiB
TypeScript
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;
|
|
readonly approveMigrationContent?: (path: string, content: Uint8Array) => boolean;
|
|
},
|
|
): 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',
|
|
approveMigrationContent: (): boolean => true,
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
status: 'provisioned',
|
|
reasonCode: 'brain-provisioned',
|
|
owner: { verdict: 'resolved', reasonCode: 'owner-verified' },
|
|
migration: { status: 'reported' },
|
|
});
|
|
expect(existsSync(source)).toBe(true);
|
|
const imported = result.migration?.reported ?? [];
|
|
expect(imported).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ path: source, reason: expect.stringMatching(/retained/i) }),
|
|
]),
|
|
);
|
|
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);
|
|
});
|
|
});
|