Add packages/mosaic registry schemas, validation, read-only CLI; pin pi 0.85.1 (#1499)
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "@mosaic/registry",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Mosaic auth/provider/harness registry schemas and validation (increment 1: schemas, validation, read-only CLI).",
|
||||
"license": "UNLICENSED",
|
||||
"type": "module",
|
||||
"engines": { "node": ">=24" },
|
||||
"bin": { "mosaic-registry": "src/cli/main.mjs" },
|
||||
"exports": {
|
||||
".": "./src/index.mjs",
|
||||
"./ids.mjs": "./src/ids.mjs",
|
||||
"./records.mjs": "./src/records.mjs",
|
||||
"./registry.mjs": "./src/registry.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node --test tests/"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { loadRegistry } from "../registry.mjs";
|
||||
|
||||
// Read-only registry CLI: validate and list. No mutation commands (increment 1).
|
||||
|
||||
function usage() {
|
||||
process.stdout.write(`usage: mosaic-registry <command> --registry-root <dir>
|
||||
commands:
|
||||
validate validate the registry tree; exit 0 valid, 1 invalid, 2 usage
|
||||
list list providers, accounts, profiles (no credential fields ever)
|
||||
`);
|
||||
}
|
||||
|
||||
async function main(argv) {
|
||||
const command = argv[0];
|
||||
const rootIndex = argv.indexOf("--registry-root");
|
||||
const root = rootIndex !== -1 ? argv[rootIndex + 1] : undefined;
|
||||
if ((command !== "validate" && command !== "list") || !root) {
|
||||
usage();
|
||||
return 2;
|
||||
}
|
||||
const { entries, errors } = await loadRegistry(root);
|
||||
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");
|
||||
return 0;
|
||||
}
|
||||
// list: strictly non-secret projection.
|
||||
const providers = Object.keys(entries.providers).sort().map((id) => ({ id, kind: entries.providers[id].kind }));
|
||||
const accounts = Object.keys(entries.accounts).sort().map((ref) => ({
|
||||
ref,
|
||||
name: entries.accounts[ref].name,
|
||||
type: entries.accounts[ref].type,
|
||||
}));
|
||||
const profiles = Object.keys(entries.profiles).sort().map((id) => ({ id }));
|
||||
process.stdout.write(`${JSON.stringify({ providers, accounts, profiles }, null, 2)}\n`);
|
||||
return errors.length > 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
process.exitCode = await main(process.argv.slice(2));
|
||||
@@ -0,0 +1,4 @@
|
||||
import { validateProvider, validateAccount, validateSettingsProfile, validateSeatSelection, validateHarnessManifest, ValidationError } from "./records.mjs";
|
||||
import { loadRegistry } from "./registry.mjs";
|
||||
|
||||
export { validateProvider, validateAccount, validateSettingsProfile, validateSeatSelection, validateHarnessManifest, ValidationError, loadRegistry };
|
||||
@@ -0,0 +1,184 @@
|
||||
// Registry record validation for the Mosaic auth/provider/harness domain.
|
||||
// Increment 1 per docs/plans/2026-09-10_m20-increment1-charter.md: pure
|
||||
// schema/reference validation, no I/O, no secrets, no materialization.
|
||||
|
||||
const ID_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
||||
|
||||
export class ValidationError extends Error {
|
||||
constructor(path, code, detail) {
|
||||
super(`${path}: ${code}${detail ? `: ${detail}` : ""}`);
|
||||
this.name = "ValidationError";
|
||||
this.path = path;
|
||||
this.code = code;
|
||||
this.detail = detail ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
const fail = (errors, path, code, detail) => errors.push(new ValidationError(path, code, detail));
|
||||
|
||||
function isPlainObject(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const errors = [];
|
||||
checkShape(errors, "provider", record, {
|
||||
providerVersion: "id",
|
||||
id: "id",
|
||||
kind: "string",
|
||||
harnesses: "record",
|
||||
credentialTypes: "string[]",
|
||||
allowInsecureTransport: "boolean",
|
||||
}, ["providerVersion", "id", "kind", "harnesses", "credentialTypes"]);
|
||||
if (errors.length === 0) {
|
||||
if (record.kind !== "native" && record.kind !== "custom-endpoint") {
|
||||
fail(errors, "provider.kind", "unsupported-kind", record.kind);
|
||||
}
|
||||
for (const [harness, cfg] of Object.entries(record.harnesses)) {
|
||||
if (!ID_PATTERN.test(harness)) fail(errors, `provider.harnesses.${harness}`, "invalid-id");
|
||||
if (!isPlainObject(cfg)) fail(errors, `provider.harnesses.${harness}`, "not-an-object");
|
||||
}
|
||||
const harnessCfg = record.harnesses.pi;
|
||||
if (record.kind === "custom-endpoint" && isPlainObject(harnessCfg)) {
|
||||
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");
|
||||
}
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
|
||||
export function validateAccount(record, providerId) {
|
||||
const errors = [];
|
||||
checkShape(errors, "account", record, {
|
||||
accountVersion: "id",
|
||||
id: "id",
|
||||
name: "string",
|
||||
provider: "id",
|
||||
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;
|
||||
}
|
||||
|
||||
export function validateSettingsProfile(record) {
|
||||
const errors = [];
|
||||
checkShape(errors, "profile", record, {
|
||||
settingsVersion: "id",
|
||||
id: "id",
|
||||
allowedAccounts: "accountRef[]",
|
||||
providers: "string[]",
|
||||
defaultAccounts: "record",
|
||||
models: "record",
|
||||
}, ["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;
|
||||
}
|
||||
|
||||
export function validateSeatSelection(record) {
|
||||
const errors = [];
|
||||
checkShape(errors, "selection", record, {
|
||||
selectionVersion: "id",
|
||||
profile: "id",
|
||||
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;
|
||||
}
|
||||
|
||||
export function validateHarnessManifest(record) {
|
||||
const errors = [];
|
||||
checkShape(errors, "harness", record, {
|
||||
harnessVersion: "id",
|
||||
id: "id",
|
||||
executable: "string",
|
||||
adapter: "string",
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Registry tree loading and cross-record reference validation.
|
||||
// Reads a supplied registry root; never writes, never touches credentials.
|
||||
|
||||
import { readFile, readdir, stat } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { validateProvider, validateAccount, validateSettingsProfile, validateSeatSelection, validateHarnessManifest } from "./records.mjs";
|
||||
|
||||
export async function loadRegistry(root) {
|
||||
const entries = { providers: {}, accounts: {}, profiles: {}, selections: {}, harnesses: {} };
|
||||
const errors = [];
|
||||
|
||||
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 {
|
||||
const s = await stat(dir);
|
||||
if (!s.isDirectory()) throw new Error("not-a-directory");
|
||||
return await readdir(dir);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function readJson(path, errors) {
|
||||
try {
|
||||
return JSON.parse(await readFile(path, "utf8"));
|
||||
} catch (err) {
|
||||
errors.push(new ValidationErrorCompat(path, "invalid-json", err.message));
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
class ValidationErrorCompat extends Error {
|
||||
constructor(path, code, detail) {
|
||||
super(`${path}: ${code}: ${detail}`);
|
||||
this.name = "ValidationError";
|
||||
this.path = path;
|
||||
this.code = code;
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"accountVersion":"1","id":"homelab-openai","name":"Homelab OpenAI","provider":"openai-codex","type":"oauth","createdAt":"2026-09-10T00:00:00Z"}
|
||||
@@ -0,0 +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"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"providerVersion":"1","id":"openai-codex","kind":"native","harnesses":{"pi":{"providerId":"openai-codex"}},"credentialTypes":["oauth","api_key"]}
|
||||
@@ -0,0 +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"]}}
|
||||
@@ -0,0 +1 @@
|
||||
{"harnessVersion":"1","id":"pi","executable":"pi","adapter":"pi","compatibleRange":">=0.85.1 <0.86.0","executionMode":"container","materializers":["auth-json","models-json"]}
|
||||
@@ -0,0 +1,149 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, dirname, resolve } from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { validateProvider, validateAccount, validateSettingsProfile, validateSeatSelection, validateHarnessManifest } from "../src/records.mjs";
|
||||
|
||||
const pkgRoot = resolve(import.meta.dirname, "..");
|
||||
const cli = join(pkgRoot, "src", "cli", "main.mjs");
|
||||
const validFixture = join(pkgRoot, "tests", "fixtures", "valid");
|
||||
|
||||
function makeRoot() {
|
||||
const root = mkdtempSync(join(tmpdir(), "mosaic-registry-test-"));
|
||||
return root;
|
||||
}
|
||||
|
||||
function writeTree(root, files) {
|
||||
for (const [name, content] of Object.entries(files)) {
|
||||
const path = join(root, name);
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, typeof content === "string" ? content : JSON.stringify(content));
|
||||
}
|
||||
}
|
||||
|
||||
function runCli(root, command = "validate") {
|
||||
return spawnSync(process.execPath, [cli, command, "--registry-root", root], { encoding: "utf8", timeout: 15000 });
|
||||
}
|
||||
|
||||
const validTree = () => ({
|
||||
"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/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"] },
|
||||
});
|
||||
|
||||
test("valid fixture tree validates and lists without secrets", () => {
|
||||
const r = runCli(validFixture);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.match(r.stdout, /^valid\n$/);
|
||||
const l = runCli(validFixture, "list");
|
||||
assert.equal(l.status, 0, l.stderr);
|
||||
const listed = JSON.parse(l.stdout);
|
||||
assert.equal(listed.providers.length, 2);
|
||||
assert.equal(listed.accounts.length, 1);
|
||||
assert.equal(listed.profiles.length, 1);
|
||||
assert.ok(!JSON.stringify(listed).includes("credential"));
|
||||
assert.ok(!JSON.stringify(listed).includes("token"));
|
||||
});
|
||||
|
||||
test("unknown-field refuses", () => {
|
||||
const errors = validateProvider({ providerVersion: "1", id: "p", kind: "native", harnesses: {}, credentialTypes: ["none"], extra: 1 });
|
||||
assert.equal(errors[0].code, "unknown-field");
|
||||
});
|
||||
|
||||
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(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", () => {
|
||||
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.equal(validateProvider({ ...base, allowInsecureTransport: true }).length, 0);
|
||||
const https = JSON.parse(JSON.stringify(base));
|
||||
https.harnesses.pi.baseUrl = "https://h/v1";
|
||||
assert.equal(validateProvider(https).length, 0);
|
||||
});
|
||||
|
||||
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"));
|
||||
});
|
||||
|
||||
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(validateProvider({ providerVersion: "1", id: "p", kind: "weird", harnesses: {}, credentialTypes: ["none"] }).some((e) => e.code === "unsupported-kind"));
|
||||
});
|
||||
|
||||
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"));
|
||||
});
|
||||
|
||||
test("profile account refs must be provider/account shaped", () => {
|
||||
const errors = validateSettingsProfile({ settingsVersion: "1", id: "s", allowedAccounts: ["justone"], providers: [], defaultAccounts: {} });
|
||||
assert.ok(errors.some((e) => e.code === "invalid-account-ref-list"));
|
||||
});
|
||||
|
||||
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.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)", () => {
|
||||
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", () => {
|
||||
const root = makeRoot();
|
||||
try {
|
||||
const tree = validTree();
|
||||
tree["auth/providers/dup.json"] = { ...tree["auth/providers/openai-codex.json"] };
|
||||
writeTree(root, tree);
|
||||
const r = runCli(root);
|
||||
assert.equal(r.status, 1);
|
||||
assert.match(r.stderr, /duplicate-id|id-path-mismatch/);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("CLI validate: missing referenced provider/account refuse", () => {
|
||||
const root = makeRoot();
|
||||
try {
|
||||
const tree = validTree();
|
||||
delete tree["auth/providers/openai-codex.json"];
|
||||
writeTree(root, tree);
|
||||
const r = runCli(root);
|
||||
assert.equal(r.status, 1);
|
||||
assert.match(r.stderr, /missing-provider/);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("CLI validate: broken JSON refuses without secret echo", () => {
|
||||
const root = makeRoot();
|
||||
try {
|
||||
const tree = validTree();
|
||||
tree["auth/providers/openai-codex.json"] = "{ not json";
|
||||
writeTree(root, tree);
|
||||
const r = runCli(root);
|
||||
assert.equal(r.status, 1);
|
||||
assert.match(r.stderr, /invalid-json/);
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
|
||||
test("CLI usage errors exit 2", () => {
|
||||
assert.equal(runCli(validFixture, "bogus").status, 2);
|
||||
assert.equal(spawnSync(process.execPath, [cli, "validate"], { encoding: "utf8" }).status, 2);
|
||||
});
|
||||
|
||||
test("credential.json sibling presence does not break validation and is never read", () => {
|
||||
const root = makeRoot();
|
||||
try {
|
||||
const tree = validTree();
|
||||
tree["auth/accounts/openai-codex/homelab/credential.json"] = { secretMarker: "FIXTURE-NOT-A-SECRET" };
|
||||
writeTree(root, tree);
|
||||
const r = runCli(root);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
const l = runCli(root, "list");
|
||||
assert.ok(!l.stdout.includes("FIXTURE-NOT-A-SECRET"));
|
||||
} finally { rmSync(root, { recursive: true, force: true }); }
|
||||
});
|
||||
Reference in New Issue
Block a user