Correct registry validation and contain metadata reads (#1500)

This commit is contained in:
2026-09-10 16:40:16 -05:00
parent b3fa221060
commit 6335342873
12 changed files with 470 additions and 289 deletions
@@ -0,0 +1,17 @@
# #1500 prerequisite correction, pre-review
Jason approved the narrow corrective slice by answering yes to the request for D1-D10 repairs, adversarial tests, independent review and selective commit/push. This does not authorize materialization or live operations. Baseline b3fa221060abb820cd809f15c3a54ba84518b7a1.
Implemented only in packages/mosaic:
- Fail-closed required roots/directories and empty registries; no partial results.
- Linux descriptor-anchored traversal, lstat and O_NOFOLLOW checks for metadata and directories, inode/device rechecks and private owner/modes. Unsupported platforms refuse. No external dependencies.
- Numeric version 1, all-harness strict nested metadata, URL userinfo/protocol refusal and explicit HTTP opt-in.
- Default enrollment, provider credential-type and reference checks.
- Bounded JSON reads and fixed diagnostics, no parser-content excerpts or input-key/value reflection.
- Numeric fixture migration, private temporary fixture copies, D1-D10 regressions, null/malformed data and unreadable credential-sibling controls. No real credential files read.
Verification: 43 package tests passed; combined package/launcher command passed 48/48. Syntax checks and diff whitespace check passed. Logs /tmp/i1500-safety.log and /tmp/i1500-combined.log. An earlier package run was 14/15 because the missing-provider control received empty-registry first; reordered reference validation before final emptiness check, retained original failure at /tmp/i1500-initial.log, rerun green. No missing registry is accepted in either version of that correction.
Limits: Linux/procfs is explicit in package README; no claim of transactional multi-record snapshot consistency under concurrent writers. Credential sibling is never opened/stat'ed. No root pin/dependency changes or launch-service edits. Docker/task/release suites have not been rerun for this package-only correction; tests with image/state side effects are not implied authorized by their names. Prior full-task failure is not declared fixed. No code committed or pushed before independent review.
Next: Filbert independently reviews exact package overlay and reproduces adversarial controls in a clean baseline copy. No dependent materialization work until approval and correction publication.
+25
View File
@@ -0,0 +1,25 @@
# Registry metadata validation
Read-only metadata CLI. No materialization, live credential handling or refresh service.
```
node packages/mosaic/src/cli/main.mjs validate --registry-root /absolute/fixture-root
node packages/mosaic/src/cli/main.mjs list --registry-root /absolute/fixture-root
node --test packages/mosaic/tests/
```
## Filesystem contract
The reader currently supports Linux with procfs and descriptor-relative paths. Other platforms refuse explicitly rather than falling back to unsafe traversal. Root and all traversed registry directories must belong to the invoking user and have mode 0700. Metadata files must be regular, owned by that user and mode 0600. Symlinks are rejected in the root path, directories and metadata files. Required directories are auth/providers, auth/accounts, auth/settings and harnesses. A missing or empty registry is invalid, not an empty successful listing.
Metadata is limited to 1 MiB per file. Directory handles anchor child access while paths are being checked, preventing a renamed ancestor from redirecting later opens. Files are opened without following symlinks and checked against their prior inode/device and permissions. This is not a claim of transactionally consistent multi-record snapshots under concurrent writers; registry mutation and materialization are outside this package slice.
Account metadata lives at auth/accounts/<provider>/<account>/account.json. The credential.json sibling is never opened or inspected. No real credentials are needed for tests. Public Git fixtures do not preserve private modes: the tests copy them into temporary directories and set 0700/0600 before invoking the reader. Do not run the CLI against your real credential stores for a smoke test.
## Validation contract
Record version fields are numeric 1. Unknown versions, top-level/nested unknown fields and malformed records refuse. Every custom-endpoint harness entry uses the declared api/baseUrl/models metadata shape; an unknown adapter shape requires a reviewed schema extension. URL userinfo and non-HTTP(S) protocols refuse; plain HTTP requires allowInsecureTransport: true. No secret-valued apiKey field is accepted in provider metadata.
Profiles may only default to an enrolled account. Account types must be supported by their providers, and references must resolve. Failed validation returns no partial entries or CLI listing. Diagnostics use fixed paths/codes, never input values, unknown keys or JSON parser excerpts. Invalid input exits 1; command usage errors exit 2.
The safety regression suite covers independently reproduced findings D1-D10 from issue #1500. Passing it establishes those tested properties, not a production security certification or authorization to build materialization on unreviewed code.
+4 -5
View File
@@ -19,12 +19,11 @@ async function main(argv) {
return 2; return 2;
} }
const { entries, errors } = await loadRegistry(root); const { entries, errors } = await loadRegistry(root);
if (errors.length > 0) {
for (const err of errors) process.stderr.write(`${err.path}: ${err.code}\n`);
return 1;
}
if (command === "validate") { if (command === "validate") {
if (errors.length > 0) {
for (const err of errors) process.stderr.write(`${err.path}: ${err.code}${err.detail ? `: ${err.detail}` : ""}\n`);
process.stderr.write(`${errors.length} validation error(s)\n`);
return 1;
}
process.stdout.write("valid\n"); process.stdout.write("valid\n");
return 0; return 0;
} }
+91 -157
View File
@@ -1,184 +1,118 @@
// Registry record validation for the Mosaic auth/provider/harness domain. // Pure metadata validation. Diagnostics never include input values or unknown keys.
// Increment 1 per docs/plans/2026-09-10_m20-increment1-charter.md: pure const ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
// schema/reference validation, no I/O, no secrets, no materialization. const REF = /^[a-z0-9][a-z0-9._-]{0,63}\/[a-z0-9][a-z0-9._-]{0,63}$/;
export const validId = value => typeof value === 'string' && ID.test(value);
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/; const object = v => v !== null && typeof v === 'object' && !Array.isArray(v) &&
[Object.prototype, null].includes(Object.getPrototypeOf(v));
const text = v => typeof v === 'string' && v.length > 0;
const list = predicate => v => Array.isArray(v) && v.length > 0 &&
v.every(predicate) && new Set(v).size === v.length;
const types = {
version: v => v === 1,
id: validId,
string: text,
ids: list(validId),
strings: list(text),
refs: list(v => typeof v === 'string' && REF.test(v)),
object,
boolean: v => typeof v === 'boolean',
timestamp: v => typeof v === 'string' &&
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.test(v) && Number.isFinite(Date.parse(v)),
};
const codes = { version: 'unsupported-version', id: 'invalid-id', string: 'invalid-string',
ids: 'invalid-id-list', strings: 'invalid-model-list', refs: 'invalid-account-ref-list', object: 'not-an-object',
boolean: 'invalid-boolean', timestamp: 'invalid-timestamp' };
export class ValidationError extends Error { export class ValidationError extends Error {
constructor(path, code, detail) { constructor(path, code) {
super(`${path}: ${code}${detail ? `: ${detail}` : ""}`); super(`${path}: ${code}`);
this.name = "ValidationError"; this.name = 'ValidationError'; this.path = path; this.code = code; this.detail = '';
this.path = path;
this.code = code;
this.detail = detail ?? "";
} }
} }
const fail = (errors, path, code) => errors.push(new ValidationError(path, code));
const fail = (errors, path, code, detail) => errors.push(new ValidationError(path, code, detail)); function shape(errors, path, value, fields, required) {
if (!object(value)) { fail(errors, path, 'not-an-object'); return false; }
function isPlainObject(value) { if (Object.keys(value).some(k => !Object.hasOwn(fields, k))) fail(errors, path, 'unknown-field');
return typeof value === "object" && value !== null && !Array.isArray(value); for (const [key, type] of Object.entries(fields)) {
if (!Object.hasOwn(value, key)) {
if (required.includes(key)) fail(errors, `${path}.${key}`, 'missing-field');
} else if (!types[type](value[key])) fail(errors, `${path}.${key}`, codes[type]);
}
return errors.length === 0;
} }
// Rejects unknown fields: every record lists its allowed keys and a type map.
function checkShape(errors, path, value, allowed, requireAll) {
if (!isPlainObject(value)) {
fail(errors, path, "not-an-object");
return;
}
for (const key of Object.keys(value)) {
if (!(key in allowed)) fail(errors, `${path}.${key}`, "unknown-field");
}
for (const [key, type] of Object.entries(allowed)) {
if (!(key in value)) {
if (requireAll.includes(key)) fail(errors, `${path}.${key}`, "missing-field");
continue;
}
const v = value[key];
if (type === "id") {
if (typeof v !== "string" || !ID_PATTERN.test(v)) fail(errors, `${path}.${key}`, "invalid-id");
} else if (type === "string") {
if (typeof v !== "string" || v.length === 0) fail(errors, `${path}.${key}`, "invalid-string");
} else if (type === "accountRef[]") {
if (!Array.isArray(v) || v.length === 0 || v.some((e) => typeof e !== "string" || !/^[a-z0-9][a-z0-9._-]{0,63}\/[a-z0-9][a-z0-9._-]{0,63}$/.test(e))) {
fail(errors, `${path}.${key}`, "invalid-account-ref-list");
}
} else if (type === "string[]") {
if (!Array.isArray(v) || v.length === 0 || v.some((e) => typeof e !== "string" || !ID_PATTERN.test(e))) {
fail(errors, `${path}.${key}`, "invalid-id-list");
}
} else if (type === "record") {
if (!isPlainObject(v)) fail(errors, `${path}.${key}`, "not-an-object");
} else if (type === "timestamp") {
if (typeof v !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/.test(v)) {
fail(errors, `${path}.${key}`, "invalid-timestamp");
}
} else if (type === "boolean") {
if (typeof v !== "boolean") fail(errors, `${path}.${key}`, "invalid-boolean");
}
}
}
export function validateProvider(record) { export function validateProvider(record) {
const errors = []; const errors = [];
checkShape(errors, "provider", record, { if (!shape(errors, 'provider', record, { providerVersion: 'version', id: 'id', kind: 'string',
providerVersion: "id", harnesses: 'object', credentialTypes: 'ids', allowInsecureTransport: 'boolean' },
id: "id", ['providerVersion', 'id', 'kind', 'harnesses', 'credentialTypes'])) return errors;
kind: "string", if (!['native', 'custom-endpoint'].includes(record.kind)) fail(errors, 'provider.kind', 'unsupported-kind');
harnesses: "record", if (record.credentialTypes.some(t => !['oauth', 'api_key', 'none'].includes(t)))
credentialTypes: "string[]", fail(errors, 'provider.credentialTypes', 'unsupported-credential-type');
allowInsecureTransport: "boolean", if (!Object.keys(record.harnesses).length) fail(errors, 'provider.harnesses', 'empty-harnesses');
}, ["providerVersion", "id", "kind", "harnesses", "credentialTypes"]); if (record.kind === 'native' && record.allowInsecureTransport === true)
if (errors.length === 0) { fail(errors, 'provider.allowInsecureTransport', 'insecure-transport-unsupported-for-native');
if (record.kind !== "native" && record.kind !== "custom-endpoint") { for (const [id, cfg] of Object.entries(record.harnesses)) {
fail(errors, "provider.kind", "unsupported-kind", record.kind); const p = 'provider.harnesses.entry';
} if (!validId(id)) fail(errors, p, 'invalid-id');
for (const [harness, cfg] of Object.entries(record.harnesses)) { const local = [];
if (!ID_PATTERN.test(harness)) fail(errors, `provider.harnesses.${harness}`, "invalid-id"); if (record.kind === 'native') {
if (!isPlainObject(cfg)) fail(errors, `provider.harnesses.${harness}`, "not-an-object"); shape(local, p, cfg, { providerId: 'id' }, ['providerId']);
} } else if (record.kind === 'custom-endpoint') {
const harnessCfg = record.harnesses.pi; if (shape(local, p, cfg, { api: 'string', baseUrl: 'string', models: 'strings' }, ['api', 'baseUrl', 'models'])) {
if (record.kind === "custom-endpoint" && isPlainObject(harnessCfg)) { checkUrl(local, p, cfg.baseUrl, record.allowInsecureTransport);
const url = harnessCfg.baseUrl;
if (typeof url !== "string" || !/^https?:\/\//.test(url)) {
fail(errors, "provider.harnesses.pi.baseUrl", "invalid-url");
} else if (url.startsWith("http://") && record.allowInsecureTransport !== true) {
fail(errors, "provider.allowInsecureTransport", "insecure-transport-not-allowed");
} }
if (!Array.isArray(harnessCfg.models) || harnessCfg.models.length === 0) {
fail(errors, "provider.harnesses.pi.models", "invalid-model-list");
}
} else if (record.kind === "native" && isPlainObject(harnessCfg) && harnessCfg.baseUrl === undefined && Array.isArray(harnessCfg.models) === false) {
// native: pi entry is providerId only; models/bases come from provider catalog
}
if (record.kind === "native" && record.allowInsecureTransport === true) {
fail(errors, "provider.allowInsecureTransport", "insecure-transport-unsupported-for-native");
} }
errors.push(...local);
} }
return errors; return errors;
} }
function checkUrl(errors, path, value, allowHttp) {
try {
const u = new URL(value);
if (!['http:', 'https:'].includes(u.protocol) || !u.hostname || u.hash || /[\s\\]/.test(value))
fail(errors, path, 'invalid-url');
if (u.username || u.password) fail(errors, path, 'url-credentials-forbidden');
if (u.protocol === 'http:' && allowHttp !== true) fail(errors, path, 'insecure-transport-not-allowed');
} catch { fail(errors, path, 'invalid-url'); }
}
export function validateAccount(record, providerId) { export function validateAccount(record, providerId) {
const errors = []; const errors = [];
checkShape(errors, "account", record, { if (!shape(errors, 'account', record, { accountVersion: 'version', id: 'id', name: 'string',
accountVersion: "id", provider: 'id', type: 'string', createdAt: 'timestamp' },
id: "id", ['accountVersion', 'id', 'name', 'provider', 'type', 'createdAt'])) return errors;
name: "string", if (providerId !== undefined && record.provider !== providerId) fail(errors, 'account.provider', 'provider-path-mismatch');
provider: "id", if (!['oauth', 'api_key', 'none'].includes(record.type)) fail(errors, 'account.type', 'unsupported-credential-type');
type: "string",
createdAt: "timestamp",
}, ["accountVersion", "id", "name", "provider", "type", "createdAt"]);
if (errors.length === 0) {
if (providerId !== undefined && record.provider !== providerId) {
fail(errors, "account.provider", "provider-path-mismatch", `${record.provider} vs ${providerId}`);
}
if (record.type !== "oauth" && record.type !== "api_key" && record.type !== "none") {
fail(errors, "account.type", "unsupported-credential-type", record.type);
}
}
return errors; return errors;
} }
function accountMap(errors, path, map) {
for (const [provider, ref] of Object.entries(map ?? {})) {
if (!validId(provider) || typeof ref !== 'string' || !REF.test(ref)) fail(errors, path, 'invalid-account-ref');
else if (ref.split('/')[0] !== provider) fail(errors, path, 'account-provider-mismatch');
}
}
export function validateSettingsProfile(record) { export function validateSettingsProfile(record) {
const errors = []; const errors = [];
checkShape(errors, "profile", record, { if (!shape(errors, 'profile', record, { settingsVersion: 'version', id: 'id', allowedAccounts: 'refs',
settingsVersion: "id", providers: 'ids', defaultAccounts: 'object', models: 'object' }, ['settingsVersion', 'id', 'allowedAccounts'])) return errors;
id: "id", accountMap(errors, 'profile.defaultAccounts', record.defaultAccounts);
allowedAccounts: "accountRef[]", for (const ref of Object.values(record.defaultAccounts ?? {}))
providers: "string[]", if (!record.allowedAccounts.includes(ref)) fail(errors, 'profile.defaultAccounts', 'default-account-not-enrolled');
defaultAccounts: "record", for (const [id, models] of Object.entries(record.models ?? {}))
models: "record", if (!validId(id) || !list(text)(models)) fail(errors, 'profile.models', 'invalid-model-list');
}, ["settingsVersion", "id", "allowedAccounts"]);
if (errors.length === 0) {
for (const [provider, account] of Object.entries(record.defaultAccounts ?? {})) {
if (!ID_PATTERN.test(provider)) fail(errors, `profile.defaultAccounts.${provider}`, "invalid-id");
if (typeof account !== "string" || !/^[a-z0-9][a-z0-9._-]{0,63}\/[a-z0-9][a-z0-9._-]{0,63}$/.test(account)) {
fail(errors, `profile.defaultAccounts.${provider}`, "invalid-account-ref");
}
}
}
return errors; return errors;
} }
export function validateSeatSelection(record) { export function validateSeatSelection(record) {
const errors = []; const errors = [];
checkShape(errors, "selection", record, { if (shape(errors, 'selection', record, { selectionVersion: 'version', profile: 'id', accounts: 'object',
selectionVersion: "id", updatedAt: 'timestamp', pinnedFromSession: 'string' }, ['selectionVersion', 'profile']))
profile: "id", accountMap(errors, 'selection.accounts', record.accounts);
accounts: "record",
updatedAt: "timestamp",
// Fork pin (gate 6): present only when this selection was pinned at fork time.
pinnedFromSession: "string",
}, ["selectionVersion", "profile"]);
if (errors.length === 0) {
for (const [provider, account] of Object.entries(record.accounts ?? {})) {
if (!ID_PATTERN.test(provider)) fail(errors, `selection.accounts.${provider}`, "invalid-id");
if (typeof account !== "string" || !/^[a-z0-9][a-z0-9._-]{0,63}\/[a-z0-9][a-z0-9._-]{0,63}$/.test(account)) {
fail(errors, `selection.accounts.${provider}`, "invalid-account-ref");
}
}
}
return errors; return errors;
} }
export function validateHarnessManifest(record) { export function validateHarnessManifest(record) {
const errors = []; const errors = [];
checkShape(errors, "harness", record, { if (!shape(errors, 'harness', record, { harnessVersion: 'version', id: 'id', executable: 'id',
harnessVersion: "id", adapter: 'id', compatibleRange: 'string', executionMode: 'string', materializers: 'ids' },
id: "id", ['harnessVersion', 'id', 'executable', 'adapter', 'compatibleRange', 'executionMode', 'materializers'])) return errors;
executable: "string", if (record.id !== record.executable) fail(errors, 'harness.executable', 'id-executable-mismatch');
adapter: "string", if (!['container', 'host'].includes(record.executionMode)) fail(errors, 'harness.executionMode', 'unsupported-mode');
compatibleRange: "string",
executionMode: "string",
materializers: "string[]",
}, ["harnessVersion", "id", "executable", "adapter", "compatibleRange", "executionMode", "materializers"]);
if (errors.length === 0) {
if (record.id !== record.executable) {
fail(errors, "harness.executable", "id-executable-mismatch", "gate 1: canonical IDs are executable names");
}
if (record.executionMode !== "container" && record.executionMode !== "host") {
fail(errors, "harness.executionMode", "unsupported-mode", record.executionMode);
}
}
return errors; return errors;
} }
+135 -103
View File
@@ -1,106 +1,138 @@
// Registry tree loading and cross-record reference validation. // Linux descriptor-anchored metadata reader. Never opens credential.json.
// Reads a supplied registry root; never writes, never touches credentials. // Directory FDs keep traversal inside the checked tree even during rename races.
import { open, readdir, lstat } from 'node:fs/promises';
import { readFile, readdir, stat } from "node:fs/promises"; import { constants } from 'node:fs';
import { join } from "node:path"; import { resolve } from 'node:path';
import { validateProvider, validateAccount, validateSettingsProfile, validateSeatSelection, validateHarnessManifest } from "./records.mjs"; import { ValidationError, validId, validateProvider, validateAccount,
validateSettingsProfile, validateHarnessManifest } from './records.mjs';
const fail = code => { throw new ValidationError('registry', code); };
const fdPath = handle => `/proc/self/fd/${handle.fd}`;
const ioCode = e => e instanceof ValidationError ? e : new ValidationError('registry',
({ ENOENT: 'missing-path', EACCES: 'inaccessible-path', EPERM: 'inaccessible-path',
ENOTDIR: 'not-a-directory', ELOOP: 'symlink-forbidden' })[e.code] ?? 'read-failed');
function privateMode(s, directory) {
if (s.uid !== process.getuid() || (s.mode & 0o777) !== (directory ? 0o700 : 0o600))
fail('insecure-permissions');
}
async function directory(path, privateRequired = true) {
const before = await lstat(path);
if (before.isSymbolicLink()) fail('symlink-forbidden');
if (!before.isDirectory()) fail('not-a-directory');
if (privateRequired) privateMode(before, true);
const h = await open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
try {
const after = await h.stat();
if (before.dev !== after.dev || before.ino !== after.ino) fail('path-changed');
if (privateRequired) privateMode(after, true);
return h;
} catch (e) { await h.close(); throw e; }
}
async function rootDirectory(root) {
if (process.platform !== 'linux' || typeof process.getuid !== 'function') fail('unsupported-platform');
if (typeof root !== 'string' || !root.length || root.split('/').includes('..')) fail('invalid-root');
const parts = resolve(root).split('/').filter(Boolean);
if (!parts.length) fail('invalid-root');
let h = await directory('/', false);
try {
for (let i = 0; i < parts.length; i++) {
const next = await directory(`${fdPath(h)}/${parts[i]}`, i === parts.length - 1);
await h.close(); h = next;
}
return h;
} catch (e) { await h.close(); throw e; }
}
async function withDirectory(parent, name, fn) {
const h = await directory(`${fdPath(parent)}/${name}`);
try { return await fn(h); } finally { await h.close(); }
}
async function jsonFile(parent, name) {
const path = `${fdPath(parent)}/${name}`;
const before = await lstat(path);
if (before.isSymbolicLink()) fail('symlink-forbidden');
if (!before.isFile()) fail('not-a-regular-file');
privateMode(before, false);
if (before.size > 1024 * 1024) fail('record-too-large');
const h = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
try {
const after = await h.stat();
if (!after.isFile() || before.dev !== after.dev || before.ino !== after.ino) fail('path-changed');
privateMode(after, false);
// Bounded read even if the writer grows the file after stat.
const buffer = Buffer.alloc(1024 * 1024 + 1);
let length = 0;
while (length < buffer.length) {
const { bytesRead } = await h.read(buffer, length, buffer.length - length, null);
if (!bytesRead) break;
length += bytesRead;
}
if (length > 1024 * 1024) fail('record-too-large');
try { return JSON.parse(buffer.toString('utf8', 0, length)); }
catch { fail('invalid-json'); }
} finally { await h.close(); }
}
async function records(dir, validator, target) {
for (const name of (await readdir(fdPath(dir))).sort()) {
if (!name.endsWith('.json') || !validId(name.slice(0, -5))) fail('invalid-record-name');
const id = name.slice(0, -5), record = await jsonFile(dir, name);
const errors = validator(record);
if (errors.length) throw errors[0];
if (record.id !== id) fail('id-path-mismatch');
if (Object.hasOwn(target, id)) fail('duplicate-id');
target[id] = record;
}
}
function emptyEntries() {
return Object.fromEntries(['providers', 'accounts', 'profiles', 'selections', 'harnesses']
.map(k => [k, Object.create(null)]));
}
export async function loadRegistry(root) { export async function loadRegistry(root) {
const entries = { providers: {}, accounts: {}, profiles: {}, selections: {}, harnesses: {} }; const entries = emptyEntries();
const errors = []; let handle;
const providersDir = join(root, "auth", "providers");
for (const file of await safeList(providersDir, errors)) {
if (!file.endsWith(".json")) continue;
const id = file.slice(0, -5);
const record = await readJson(join(providersDir, file), errors);
if (record === undefined) continue;
errors.push(...validateProvider(record));
if (record.id !== id) errors.push(new (await import("./records.mjs")).ValidationError(`providers/${file}`, "id-path-mismatch", `${record.id} vs ${id}`));
if (entries.providers[id]) errors.push(new (await import("./records.mjs")).ValidationError(`providers/${file}`, "duplicate-id", id));
entries.providers[id] = record;
}
const accountsDir = join(root, "auth", "accounts");
for (const provider of await safeList(accountsDir, errors)) {
for (const accountDir of await safeList(join(accountsDir, provider), errors)) {
// account.json sits in a per-account directory; credential.json is never read.
const record = await readJson(join(accountsDir, provider, accountDir, "account.json"), errors);
if (record === undefined) continue;
errors.push(...validateAccount(record, provider));
const accountRef = `${provider}/${accountDir}`;
if (record.id !== accountDir) errors.push(new (await import("./records.mjs")).ValidationError(`accounts/${provider}/${accountDir}`, "id-path-mismatch", `${record.id} vs ${accountDir}`));
if (entries.accounts[accountRef]) errors.push(new (await import("./records.mjs")).ValidationError(`accounts/${provider}/${accountDir}`, "duplicate-id", accountRef));
entries.accounts[accountRef] = record;
}
}
const settingsDir = join(root, "auth", "settings");
for (const file of await safeList(settingsDir, errors)) {
if (!file.endsWith(".json")) continue;
const record = await readJson(join(settingsDir, file), errors);
if (record === undefined) continue;
errors.push(...validateSettingsProfile(record));
if (record.id !== file.slice(0, -5)) errors.push(new (await import("./records.mjs")).ValidationError(`settings/${file}`, "id-path-mismatch"));
entries.profiles[file.slice(0, -5)] = record;
}
const harnessesDir = join(root, "harnesses");
for (const file of await safeList(harnessesDir, errors)) {
if (!file.endsWith(".json")) continue;
const record = await readJson(join(harnessesDir, file), errors);
if (record === undefined) continue;
errors.push(...validateHarnessManifest(record));
entries.harnesses[file.slice(0, -5)] = record;
}
// Cross-record reference integrity.
for (const [ref, account] of Object.entries(entries.accounts)) {
if (!entries.providers[account.provider]) {
errors.push(new (await import("./records.mjs")).ValidationError(`accounts/${ref}`, "missing-provider", account.provider));
}
}
for (const [pid, profile] of Object.entries(entries.profiles)) {
for (const ref of profile.allowedAccounts ?? []) {
if (!entries.accounts[ref]) errors.push(new (await import("./records.mjs")).ValidationError(`profiles/${pid}`, "missing-account", ref));
}
for (const provider of profile.providers ?? []) {
if (!entries.providers[provider]) errors.push(new (await import("./records.mjs")).ValidationError(`profiles/${pid}`, "missing-provider", provider));
}
for (const [provider, ref] of Object.entries(profile.defaultAccounts ?? {})) {
if (!entries.accounts[ref]) errors.push(new (await import("./records.mjs")).ValidationError(`profiles/${pid}`, "missing-default-account", ref));
else if (!ref.startsWith(`${provider}/`)) errors.push(new (await import("./records.mjs")).ValidationError(`profiles/${pid}`, "default-account-provider-mismatch", ref));
}
}
return { entries, errors };
}
async function safeList(dir, errors) {
try { try {
const s = await stat(dir); handle = await rootDirectory(root);
if (!s.isDirectory()) throw new Error("not-a-directory"); await withDirectory(handle, 'auth', async auth => {
return await readdir(dir); await withDirectory(auth, 'providers', d => records(d, validateProvider, entries.providers));
} catch { await withDirectory(auth, 'accounts', async accounts => {
return []; for (const provider of (await readdir(fdPath(accounts))).sort()) {
} if (!validId(provider)) fail('invalid-provider-directory');
} await withDirectory(accounts, provider, async pd => {
for (const id of (await readdir(fdPath(pd))).sort()) {
async function readJson(path, errors) { if (!validId(id)) fail('invalid-account-directory');
try { await withDirectory(pd, id, async ad => {
return JSON.parse(await readFile(path, "utf8")); // Do not stat, open or parse the credential sibling.
} catch (err) { const account = await jsonFile(ad, 'account.json');
errors.push(new ValidationErrorCompat(path, "invalid-json", err.message)); const errors = validateAccount(account, provider);
return undefined; if (errors.length) throw errors[0];
} if (account.id !== id) fail('id-path-mismatch');
} entries.accounts[`${provider}/${id}`] = account;
});
class ValidationErrorCompat extends Error { }
constructor(path, code, detail) { });
super(`${path}: ${code}: ${detail}`); }
this.name = "ValidationError"; });
this.path = path; await withDirectory(auth, 'settings', d => records(d, validateSettingsProfile, entries.profiles));
this.code = code; });
this.detail = detail; await withDirectory(handle, 'harnesses', d => records(d, validateHarnessManifest, entries.harnesses));
} for (const provider of Object.values(entries.providers))
for (const id of Object.keys(provider.harnesses))
if (!Object.hasOwn(entries.harnesses, id)) fail('missing-harness');
for (const account of Object.values(entries.accounts)) {
const provider = entries.providers[account.provider];
if (!provider) fail('missing-provider');
if (!provider.credentialTypes.includes(account.type)) fail('credential-type-not-supported');
}
for (const profile of Object.values(entries.profiles)) {
for (const ref of profile.allowedAccounts) if (!entries.accounts[ref]) fail('missing-account');
for (const id of [...(profile.providers ?? []), ...Object.keys(profile.models ?? {})])
if (!entries.providers[id]) fail('missing-provider');
for (const ref of Object.values(profile.defaultAccounts ?? {}))
if (!entries.accounts[ref]) fail('missing-default-account');
}
if (!Object.keys(entries.providers).length || !Object.keys(entries.profiles).length ||
!Object.keys(entries.harnesses).length) fail('empty-registry');
return { entries, errors: [] };
} catch (e) {
// Never return partially trusted data after a refusal.
return { entries: emptyEntries(), errors: [ioCode(e)] };
} finally { if (handle) await handle.close(); }
} }
@@ -1 +1 @@
{"accountVersion":"1","id":"homelab-openai","name":"Homelab OpenAI","provider":"openai-codex","type":"oauth","createdAt":"2026-09-10T00:00:00Z"} {"accountVersion":1,"id":"homelab-openai","name":"Homelab OpenAI","provider":"openai-codex","type":"oauth","createdAt":"2026-09-10T00:00:00Z"}
@@ -1 +1 @@
{"providerVersion":"1","id":"ollama-remote","kind":"custom-endpoint","allowInsecureTransport":true,"harnesses":{"pi":{"api":"openai-completions","baseUrl":"http://ollama.internal:11434/v1","models":["qwen2.5-coder:7b"]}},"credentialTypes":["none"]} {"providerVersion":1,"id":"ollama-remote","kind":"custom-endpoint","allowInsecureTransport":true,"harnesses":{"pi":{"api":"openai-completions","baseUrl":"http://ollama.internal:11434/v1","models":["qwen2.5-coder:7b"]}},"credentialTypes":["none"]}
@@ -1 +1 @@
{"providerVersion":"1","id":"openai-codex","kind":"native","harnesses":{"pi":{"providerId":"openai-codex"}},"credentialTypes":["oauth","api_key"]} {"providerVersion":1,"id":"openai-codex","kind":"native","harnesses":{"pi":{"providerId":"openai-codex"}},"credentialTypes":["oauth","api_key"]}
@@ -1 +1 @@
{"settingsVersion":"1","id":"research-default","allowedAccounts":["openai-codex/homelab-openai"],"providers":["openai-codex","ollama-remote"],"defaultAccounts":{"openai-codex":"openai-codex/homelab-openai"},"models":{"ollama-remote":["qwen2.5-coder:7b"]}} {"settingsVersion":1,"id":"research-default","allowedAccounts":["openai-codex/homelab-openai"],"providers":["openai-codex","ollama-remote"],"defaultAccounts":{"openai-codex":"openai-codex/homelab-openai"},"models":{"ollama-remote":["qwen2.5-coder:7b"]}}
+1 -1
View File
@@ -1 +1 @@
{"harnessVersion":"1","id":"pi","executable":"pi","adapter":"pi","compatibleRange":">=0.85.1 <0.86.0","executionMode":"container","materializers":["auth-json","models-json"]} {"harnessVersion":1,"id":"pi","executable":"pi","adapter":"pi","compatibleRange":">=0.85.1 <0.86.0","executionMode":"container","materializers":["auth-json","models-json"]}
+35 -19
View File
@@ -1,6 +1,6 @@
import { test } from "node:test"; import { test } from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs"; import { mkdtempSync, mkdirSync, writeFileSync, rmSync, cpSync, chmodSync, readdirSync, lstatSync } from "node:fs";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { join, dirname, resolve } from "node:path"; import { join, dirname, resolve } from "node:path";
import { spawnSync } from "node:child_process"; import { spawnSync } from "node:child_process";
@@ -15,12 +15,23 @@ function makeRoot() {
return root; return root;
} }
function privateTree(root) {
chmodSync(root, 0o700);
for (const name of readdirSync(root)) {
const p = join(root, name);
if (lstatSync(p).isDirectory()) privateTree(p);
else chmodSync(p, 0o600);
}
}
function writeTree(root, files) { function writeTree(root, files) {
for (const name of ['auth/providers', 'auth/accounts', 'auth/settings', 'harnesses'])
mkdirSync(join(root, name), { recursive: true });
for (const [name, content] of Object.entries(files)) { for (const [name, content] of Object.entries(files)) {
const path = join(root, name); const path = join(root, name);
mkdirSync(dirname(path), { recursive: true }); mkdirSync(dirname(path), { recursive: true });
writeFileSync(path, typeof content === "string" ? content : JSON.stringify(content)); writeFileSync(path, typeof content === "string" ? content : JSON.stringify(content));
} }
privateTree(root);
} }
function runCli(root, command = "validate") { function runCli(root, command = "validate") {
@@ -28,17 +39,21 @@ function runCli(root, command = "validate") {
} }
const validTree = () => ({ const validTree = () => ({
"auth/providers/openai-codex.json": { providerVersion: "1", id: "openai-codex", kind: "native", harnesses: { pi: { providerId: "openai-codex" } }, credentialTypes: ["oauth", "api_key"] }, "auth/providers/openai-codex.json": { providerVersion: 1, id: "openai-codex", kind: "native", harnesses: { pi: { providerId: "openai-codex" } }, credentialTypes: ["oauth", "api_key"] },
"auth/accounts/openai-codex/homelab/account.json": { accountVersion: "1", id: "homelab", name: "Homelab", provider: "openai-codex", type: "oauth", createdAt: "2026-09-10T00:00:00Z" }, "auth/accounts/openai-codex/homelab/account.json": { accountVersion: 1, id: "homelab", name: "Homelab", provider: "openai-codex", type: "oauth", createdAt: "2026-09-10T00:00:00Z" },
"auth/settings/research.json": { settingsVersion: "1", id: "research", allowedAccounts: ["openai-codex/homelab"], providers: ["openai-codex"], defaultAccounts: { "openai-codex": "openai-codex/homelab" } }, "auth/settings/research.json": { settingsVersion: 1, id: "research", allowedAccounts: ["openai-codex/homelab"], providers: ["openai-codex"], defaultAccounts: { "openai-codex": "openai-codex/homelab" } },
"harnesses/pi.json": { harnessVersion: "1", id: "pi", executable: "pi", adapter: "pi", compatibleRange: ">=0.85.1 <0.86.0", executionMode: "container", materializers: ["auth-json"] }, "harnesses/pi.json": { harnessVersion: 1, id: "pi", executable: "pi", adapter: "pi", compatibleRange: ">=0.85.1 <0.86.0", executionMode: "container", materializers: ["auth-json"] },
}); });
test("valid fixture tree validates and lists without secrets", () => { test("valid fixture tree validates and lists without secrets", () => {
const r = runCli(validFixture); const root = makeRoot();
try {
cpSync(validFixture, root, { recursive: true });
privateTree(root);
const r = runCli(root);
assert.equal(r.status, 0, r.stderr); assert.equal(r.status, 0, r.stderr);
assert.match(r.stdout, /^valid\n$/); assert.match(r.stdout, /^valid\n$/);
const l = runCli(validFixture, "list"); const l = runCli(root, "list");
assert.equal(l.status, 0, l.stderr); assert.equal(l.status, 0, l.stderr);
const listed = JSON.parse(l.stdout); const listed = JSON.parse(l.stdout);
assert.equal(listed.providers.length, 2); assert.equal(listed.providers.length, 2);
@@ -46,20 +61,21 @@ test("valid fixture tree validates and lists without secrets", () => {
assert.equal(listed.profiles.length, 1); assert.equal(listed.profiles.length, 1);
assert.ok(!JSON.stringify(listed).includes("credential")); assert.ok(!JSON.stringify(listed).includes("credential"));
assert.ok(!JSON.stringify(listed).includes("token")); assert.ok(!JSON.stringify(listed).includes("token"));
} finally { rmSync(root, { recursive: true, force: true }); }
}); });
test("unknown-field refuses", () => { test("unknown-field refuses", () => {
const errors = validateProvider({ providerVersion: "1", id: "p", kind: "native", harnesses: {}, credentialTypes: ["none"], extra: 1 }); const errors = validateProvider({ providerVersion: 1, id: "p", kind: "native", harnesses: {}, credentialTypes: ["none"], extra: 1 });
assert.equal(errors[0].code, "unknown-field"); assert.equal(errors[0].code, "unknown-field");
}); });
test("invalid-id refuses uppercase and traversal shapes", () => { test("invalid-id refuses uppercase and traversal shapes", () => {
assert.ok(validateProvider({ providerVersion: "1", id: "BadID", kind: "native", harnesses: {}, credentialTypes: ["none"] }).some((e) => e.code === "invalid-id")); assert.ok(validateProvider({ providerVersion: 1, id: "BadID", kind: "native", harnesses: {}, credentialTypes: ["none"] }).some((e) => e.code === "invalid-id"));
assert.ok(validateAccount({ accountVersion: "1", id: "../escape", name: "x", provider: "p", type: "oauth", createdAt: "2026-09-10T00:00:00Z" }).some((e) => e.code === "invalid-id")); assert.ok(validateAccount({ accountVersion: 1, id: "../escape", name: "x", provider: "p", type: "oauth", createdAt: "2026-09-10T00:00:00Z" }).some((e) => e.code === "invalid-id"));
}); });
test("plain-http baseUrl requires allowInsecureTransport", () => { test("plain-http baseUrl requires allowInsecureTransport", () => {
const base = { providerVersion: "1", id: "o", kind: "custom-endpoint", harnesses: { pi: { api: "openai-completions", baseUrl: "http://h:11434/v1", models: ["m"] } }, credentialTypes: ["none"] }; const base = { providerVersion: 1, id: "o", kind: "custom-endpoint", harnesses: { pi: { api: "openai-completions", baseUrl: "http://h:11434/v1", models: ["m"] } }, credentialTypes: ["none"] };
assert.ok(validateProvider(base).some((e) => e.code === "insecure-transport-not-allowed")); assert.ok(validateProvider(base).some((e) => e.code === "insecure-transport-not-allowed"));
assert.equal(validateProvider({ ...base, allowInsecureTransport: true }).length, 0); assert.equal(validateProvider({ ...base, allowInsecureTransport: true }).length, 0);
const https = JSON.parse(JSON.stringify(base)); const https = JSON.parse(JSON.stringify(base));
@@ -68,30 +84,30 @@ test("plain-http baseUrl requires allowInsecureTransport", () => {
}); });
test("native provider rejects allowInsecureTransport", () => { test("native provider rejects allowInsecureTransport", () => {
assert.ok(validateProvider({ providerVersion: "1", id: "p", kind: "native", harnesses: {}, credentialTypes: ["none"], allowInsecureTransport: true }).some((e) => e.code === "insecure-transport-unsupported-for-native")); assert.ok(validateProvider({ providerVersion: 1, id: "p", kind: "native", harnesses: {}, credentialTypes: ["none"], allowInsecureTransport: true }).some((e) => e.code === "insecure-transport-unsupported-for-native"));
}); });
test("unsupported credential type and kind refuse", () => { test("unsupported credential type and kind refuse", () => {
assert.ok(validateAccount({ accountVersion: "1", id: "a", name: "x", provider: "p", type: "basic", createdAt: "2026-09-10T00:00:00Z" }).some((e) => e.code === "unsupported-credential-type")); assert.ok(validateAccount({ accountVersion: 1, id: "a", name: "x", provider: "p", type: "basic", createdAt: "2026-09-10T00:00:00Z" }).some((e) => e.code === "unsupported-credential-type"));
assert.ok(validateProvider({ providerVersion: "1", id: "p", kind: "weird", harnesses: {}, credentialTypes: ["none"] }).some((e) => e.code === "unsupported-kind")); assert.ok(validateProvider({ providerVersion: 1, id: "p", kind: "weird", harnesses: {}, credentialTypes: ["none"] }).some((e) => e.code === "unsupported-kind"));
}); });
test("account provider-path mismatch refuses", () => { test("account provider-path mismatch refuses", () => {
assert.ok(validateAccount({ accountVersion: "1", id: "a", name: "x", provider: "other", type: "oauth", createdAt: "2026-09-10T00:00:00Z" }, "right").some((e) => e.code === "provider-path-mismatch")); assert.ok(validateAccount({ accountVersion: 1, id: "a", name: "x", provider: "other", type: "oauth", createdAt: "2026-09-10T00:00:00Z" }, "right").some((e) => e.code === "provider-path-mismatch"));
}); });
test("profile account refs must be provider/account shaped", () => { test("profile account refs must be provider/account shaped", () => {
const errors = validateSettingsProfile({ settingsVersion: "1", id: "s", allowedAccounts: ["justone"], providers: [], defaultAccounts: {} }); const errors = validateSettingsProfile({ settingsVersion: 1, id: "s", allowedAccounts: ["justone"], providers: [], defaultAccounts: {} });
assert.ok(errors.some((e) => e.code === "invalid-account-ref-list")); assert.ok(errors.some((e) => e.code === "invalid-account-ref-list"));
}); });
test("seat selection accepts fork pin field, validates account refs", () => { test("seat selection accepts fork pin field, validates account refs", () => {
assert.equal(validateSeatSelection({ selectionVersion: "1", profile: "research", accounts: { "openai-codex": "openai-codex/homelab" }, updatedAt: "2026-09-10T00:00:00Z", pinnedFromSession: "abc123" }).length, 0); assert.equal(validateSeatSelection({ selectionVersion: 1, profile: "research", accounts: { "openai-codex": "openai-codex/homelab" }, updatedAt: "2026-09-10T00:00:00Z", pinnedFromSession: "abc123" }).length, 0);
assert.ok(validateSeatSelection({ selectionVersion: "1", profile: "research", accounts: { "openai-codex": "nope" } }).some((e) => e.code === "invalid-account-ref")); assert.ok(validateSeatSelection({ selectionVersion: 1, profile: "research", accounts: { "openai-codex": "nope" } }).some((e) => e.code === "invalid-account-ref"));
}); });
test("harness manifest id must equal executable (gate 1)", () => { test("harness manifest id must equal executable (gate 1)", () => {
assert.ok(validateHarnessManifest({ harnessVersion: "1", id: "pi", executable: "claude", adapter: "x", compatibleRange: ">=1", executionMode: "container", materializers: ["m"] }).some((e) => e.code === "id-executable-mismatch")); assert.ok(validateHarnessManifest({ harnessVersion: 1, id: "pi", executable: "claude", adapter: "x", compatibleRange: ">=1", executionMode: "container", materializers: ["m"] }).some((e) => e.code === "id-executable-mismatch"));
}); });
test("CLI validate: duplicate provider id across files refuses", () => { test("CLI validate: duplicate provider id across files refuses", () => {
+158
View File
@@ -0,0 +1,158 @@
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');
});