import { test } from 'node:test'; import assert from 'node:assert/strict'; import { mkdtempSync, cpSync, readdirSync, lstatSync, chmodSync, readFileSync, writeFileSync, rmSync, mkdirSync, symlinkSync } from 'node:fs'; import { join, resolve } from 'node:path'; import { tmpdir } from 'node:os'; import { spawnSync } from 'node:child_process'; import { loadRegistry } from '../src/registry.mjs'; import { validateProvider, validateAccount, validateSettingsProfile, validateSeatSelection, validateHarnessManifest } from '../src/records.mjs'; const fixture = resolve(import.meta.dirname, 'fixtures/valid'); const cli = resolve(import.meta.dirname, '../src/cli/main.mjs'); const providerFile = 'auth/providers/openai-codex.json'; const accountDir = 'auth/accounts/openai-codex/homelab-openai'; const profileFile = 'auth/settings/research-default.json'; const marker = 'FIXTURE_PRIVATE_MARKER_1500'; function secure(dir) { chmodSync(dir, 0o700); for (const name of readdirSync(dir)) { const p = join(dir, name); if (lstatSync(p).isDirectory()) secure(p); else chmodSync(p, 0o600); } } function setup(t) { const base = mkdtempSync(join(tmpdir(), 'registry-safety-')); t.after(() => rmSync(base, { recursive: true, force: true })); const root = join(base, 'registry'); cpSync(fixture, root, { recursive: true }); secure(root); return { root, base }; } function edit(root, name, mutate) { const p = join(root, name); const data = JSON.parse(readFileSync(p, 'utf8')); mutate(data); writeFileSync(p, JSON.stringify(data), { mode: 0o600 }); } function run(root, cmd = 'validate') { return spawnSync(process.execPath, [cli, cmd, '--registry-root', root], { encoding: 'utf8', timeout: 5000 }); } function refuses(root, code) { for (const cmd of ['validate', 'list']) { const r = run(root, cmd); assert.equal(r.status, 1, r.stderr); assert.equal(r.stdout, ''); assert.match(r.stderr, new RegExp(code)); assert.ok(!r.stderr.includes(marker)); assert.ok(!r.stderr.includes(' at ')); } } test('D1 missing, empty and structurally empty roots refuse, no list projection', t => { const { root, base } = setup(t); refuses(join(base, 'absent'), 'missing-path'); mkdirSync(join(base, 'empty'), { mode: 0o700 }); refuses(join(base, 'empty'), 'missing-path'); for (const p of ['auth/providers', 'auth/accounts', 'auth/settings', 'harnesses']) { rmSync(join(root, p), { recursive: true }); mkdirSync(join(root, p), { mode: 0o700 }); } refuses(root, 'empty-registry'); }); for (const path of ['auth', 'auth/providers', 'auth/accounts', 'auth/settings', 'harnesses']) test(`D1 required directory ${path} cannot be absent`, t => { const { root } = setup(t); rmSync(join(root, path), { recursive: true }); refuses(root, 'missing-path'); }); test('D1 root file and unreadable metadata refuse', t => { const { root, base } = setup(t); const f = join(base, 'not-dir'); writeFileSync(f, '{}', { mode: 0o600 }); refuses(f, 'not-a-directory'); chmodSync(join(root, providerFile), 0o000); refuses(root, 'insecure-permissions'); }); for (const path of [providerFile, accountDir, 'auth/providers', 'auth']) test(`D2 no symlink traversal at ${path}`, t => { const { root, base } = setup(t), outside = join(base, 'outside'); cpSync(join(root, path), outside, { recursive: true }); rmSync(join(root, path), { recursive: true }); symlinkSync(outside, join(root, path)); refuses(root, 'symlink-forbidden'); }); test('D2 root and ancestor symlinks and lexical traversal refuse', t => { const { root, base } = setup(t); symlinkSync(root, join(base, 'alias')); refuses(join(base, 'alias'), 'symlink-forbidden'); symlinkSync(base, join(base, 'ancestor')); refuses(join(base, 'ancestor/registry'), 'symlink-forbidden'); refuses(`${root}/../registry`, 'invalid-root'); }); for (const path of ['', 'auth', providerFile, `${accountDir}/account.json`]) test(`private filesystem modes enforced for ${path || 'root'}`, t => { const { root } = setup(t); chmodSync(join(root, path), path.endsWith('.json') ? 0o644 : 0o755); refuses(root, 'insecure-permissions'); }); test('D3 numeric version 1 only across all record kinds', () => { const pairs = [ [validateProvider, JSON.parse(readFileSync(join(fixture, providerFile))), 'providerVersion'], [validateAccount, JSON.parse(readFileSync(join(fixture, accountDir, 'account.json'))), 'accountVersion'], [validateSettingsProfile, JSON.parse(readFileSync(join(fixture, profileFile))), 'settingsVersion'], [validateHarnessManifest, JSON.parse(readFileSync(join(fixture, 'harnesses/pi.json'))), 'harnessVersion'], [validateSeatSelection, { selectionVersion: 1, profile: 'research-default' }, 'selectionVersion'], ]; for (const [validate, record, key] of pairs) { assert.equal(validate(record).length, 0); for (const value of [2, '1', 'banana-schema', null, true]) assert.ok(validate({ ...record, [key]: value }).some(e => e.code === 'unsupported-version')); } }); test('D4 nested unknown keys and missing per-kind required fields refuse', t => { const { root } = setup(t); edit(root, providerFile, p => { p.harnesses.pi[marker] = marker; }); refuses(root, 'unknown-field'); const p = { providerVersion: 1, id: 'remote', kind: 'custom-endpoint', credentialTypes: ['none'], harnesses: { claude: { baseUrl: 'https://example.test', models: ['m'] } } }; assert.ok(validateProvider(p).some(e => e.code === 'missing-field')); assert.ok(validateProvider({ ...p, harnesses: {} }).some(e => e.code === 'empty-harnesses')); }); test('D5 unenrolled default refuses even when account exists', t => { const { root } = setup(t); edit(root, profileFile, p => { p.allowedAccounts = ['openai-codex/other']; }); refuses(root, 'default-account-not-enrolled'); }); test('D6 provider/account credential type must match', t => { const { root } = setup(t); edit(root, providerFile, p => { p.credentialTypes = ['api_key']; }); refuses(root, 'credential-type-not-supported'); }); test('D7 every harness endpoint enforces HTTP opt-in and shape', () => { for (const h of ['pi', 'claude', 'codex']) { const p = { providerVersion: 1, id: 'remote', kind: 'custom-endpoint', credentialTypes: ['none'], harnesses: { [h]: { api: 'openai-completions', baseUrl: 'http://example.test/v1', models: ['qwen:7b'] } } }; assert.ok(validateProvider(p).some(e => e.code === 'insecure-transport-not-allowed')); assert.equal(validateProvider({ ...p, allowInsecureTransport: true }).length, 0); p.harnesses[h][marker] = marker; assert.ok(validateProvider(p).some(e => e.code === 'unknown-field')); } }); test('D8 URLs reject embedded credentials and unsupported protocols without echo', t => { const { root } = setup(t); for (const url of [`https://user:${marker}@example.test/v1`, 'file:///tmp/test', 'ftp://example.test']) { edit(root, 'auth/providers/ollama-remote.json', p => { p.harnesses.pi.baseUrl = url; }); refuses(root, 'url-credentials-forbidden|invalid-url'); } }); test('D9 malformed JSON diagnostics contain no content excerpt', t => { const { root } = setup(t); writeFileSync(join(root, providerFile), `${marker} not json`); refuses(root, '^registry: invalid-json'); }); test('D10 missing metadata is missing-path, not invalid-json', t => { const { root } = setup(t); rmSync(join(root, accountDir, 'account.json')); refuses(root, '^registry: missing-path'); }); test('D10 library returns no partial entries on any invalid record', async t => { const { root } = setup(t); edit(root, 'harnesses/pi.json', p => { p[marker] = marker; }); const result = await loadRegistry(root); assert.equal(result.errors.length, 1); for (const values of Object.values(result.entries)) assert.equal(Object.keys(values).length, 0); refuses(root, 'unknown-field'); }); test('null/scalar/array metadata refuses without stack or echo', t => { const { root } = setup(t); for (const value of [null, marker, [], 17]) { writeFileSync(join(root, providerFile), JSON.stringify(value)); refuses(root, 'not-an-object'); } }); test('credential sibling is never opened, even when an unreadable symlink', t => { const { root, base } = setup(t); const secret = join(base, 'secret'); writeFileSync(secret, marker, { mode: 0o000 }); symlinkSync(secret, join(root, accountDir, 'credential.json')); const r = run(root, 'list'); assert.equal(r.status, 0, r.stderr); assert.ok(!r.stdout.includes(marker)); }); test('oversized metadata refuses before parsing', t => { const { root } = setup(t); writeFileSync(join(root, providerFile), ' '.repeat(1024 * 1024 + 1)); refuses(root, 'record-too-large'); });