Add packages/mosaic registry schemas, validation, read-only CLI; pin pi 0.85.1 (#1499)
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user