UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

627 lines (588 loc) • 30.8 kB
import { createHash } from "node:crypto"; import { RestrictedYamlError, parseRestrictedYaml } from "./restricted-yaml.mjs"; export const POLICY_DRAFT_MODEL_SCHEMA = "kxm.model.v2"; export const POLICY_DRAFT_ROLE_SCHEMA = "kxm.role.v2"; export const POLICY_DRAFT_PURPOSES = Object.freeze([ "writer", "planner", "reviewer-arch", "reviewer-cli", "experiment", ]); export const POLICY_DRAFT_PERMISSIONS = Object.freeze(["edit", "read-only"]); export const POLICY_DRAFT_STATUSES = Object.freeze(["admitted", "candidate", "retired"]); const IDENTIFIER = /^[a-z][a-z0-9]*(?:[-_][a-z0-9]+)*$/; const ROUTE_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/; const SHA256 = /^[a-f0-9]{64}$/; const TOKEN = /[\s\x00-\x1f]/u; const EFFORTS = Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"]); const MODES = Object.freeze(["headless", "interactive", "either"]); const MODEL_KEYS = Object.freeze([ "schema", "id", "aliases", "harness", "model", "vendor", "status", "permissions", "origin", "thinking", "tags", "capabilities", "priority", "fallbacks", "limits", ]); const ROLE_KEYS = Object.freeze([ "schema", "id", "purpose", "permission", "description", "extends", "roster", "skills", "tools", "produces", "consumes", "policy", ]); const ORIGIN_KEYS = Object.freeze(["source", "sha256"]); const LIMIT_KEYS = Object.freeze(["contextTokens", "outputTokens", "timeoutMs"]); const TOOL_KEYS = Object.freeze(["preset", "allow", "deny"]); const TEMPLATE_KEYS = Object.freeze(["template", "schema"]); const POLICY_KEYS = Object.freeze(["vendorIndependenceRequired", "maxTransitions", "requiresGateVerification", "fallback"]); const FALLBACK_KEYS = Object.freeze(["onError", "maxSwitches", "revert"]); const FALLBACK_ERRORS = Object.freeze(["rate_limit", "transport", "provider_unavailable", "context_overflow"]); const FALLBACK_REVERT = Object.freeze(["next_run", "never"]); const ROSTER_ENTRY_KEYS = Object.freeze(["route", "effort", "mode"]); const CRITIC_PURPOSES = Object.freeze(["reviewer-arch", "reviewer-cli"]); function compareCodeUnits(left, right) { return left < right ? -1 : left > right ? 1 : 0; } function sortIssues(issues) { return [...issues].sort((left, right) => compareCodeUnits(left.file, right.file) || compareCodeUnits(left.phase, right.phase) || compareCodeUnits(left.code, right.code) || compareCodeUnits(left.message, right.message)); } function issue(phase, code, file, message) { return { phase, code, file, message }; } function isObject(value) { return value !== null && typeof value === "object" && !Array.isArray(value); } function own(object, key) { return Object.hasOwn(object, key); } function extraKeys(value, allowed) { return Object.keys(value).filter((key) => !allowed.includes(key)); } function freezeDeep(value) { if (value && typeof value === "object") { for (const child of Object.values(value)) freezeDeep(child); Object.freeze(value); } return value; } function cloneJson(value) { return JSON.parse(JSON.stringify(value)); } function sha256Bytes(input) { const bytes = typeof input === "string" ? Buffer.from(input, "utf8") : Buffer.from(input); return createHash("sha256").update(bytes).digest("hex"); } function canonicalVendor(value, aliases) { if (typeof value !== "string") return ""; const lower = value.toLowerCase(); return own(aliases, lower) ? aliases[lower] : lower; } function identifier(value, max = 64) { return typeof value === "string" && value.length >= 1 && value.length <= max && IDENTIFIER.test(value); } function routeId(value) { return typeof value === "string" && value.length >= 1 && value.length <= 64 && ROUTE_ID.test(value); } function nonemptyToken(value, max = 512) { return typeof value === "string" && value.length >= 1 && value.length <= max && !TOKEN.test(value); } function codePointCount(value) { return [...value].length; } function sourcePath(value) { if (!nonemptyToken(value)) return false; if (value.startsWith("/") || value.includes("\\")) return false; const parts = value.split("/"); return parts.length > 0 && parts.every((part) => part && part !== "." && part !== ".."); } function uniqueStringList(value, allowed, { min = 1, max = 64 } = {}) { if (!Array.isArray(value) || value.length < min || value.length > max) return false; if (new Set(value).size !== value.length) return false; return value.every((item) => allowed.includes(item)); } function identifierList(value, { maxItems = 32, itemMax = 64 } = {}) { if (!Array.isArray(value) || value.length > maxItems) return false; if (new Set(value).size !== value.length) return false; return value.every((item) => identifier(item, itemMax)); } function parseDocument(value, label, issues) { if (typeof value === "string" || value instanceof Uint8Array) { try { return parseRestrictedYaml(value, label); } catch (error) { if (error instanceof RestrictedYamlError) issues.push(...error.issues); else issues.push(issue("parse", "invalid_yaml", label, error instanceof Error ? error.message : "invalid YAML")); return undefined; } } if (isObject(value)) return value; issues.push(issue("schema", "root_not_object", label, "resource root must be a mapping")); return undefined; } function collectDocuments(raw, kind, issues) { const collected = []; const seen = new Set(); if (raw === undefined) return collected; if (Array.isArray(raw)) { for (const [index, value] of raw.entries()) { const label = `${kind}[${index}]`; const document = parseDocument(value, label, issues); if (!document) continue; const declared = document.id; if (!routeId(declared) && !(kind === "roles" && identifier(declared))) { issues.push(issue("schema", "missing_id", label, `${kind.slice(0, -1)} identity is missing or invalid`)); continue; } if (seen.has(declared)) { issues.push(issue("schema", kind === "models" ? "duplicate_route_id" : "duplicate_role_id", label, `duplicate ${kind.slice(0, -1)} identity ${declared}`)); continue; } seen.add(declared); collected.push({ id: declared, label: `${kind}/${declared}`, document }); } return collected; } if (!isObject(raw)) { issues.push(issue("schema", "root_not_object", kind, `${kind} must be a mapping or array`)); return collected; } for (const id of Object.keys(raw).sort(compareCodeUnits)) { const label = `${kind}/${id}`; if (kind === "models" ? !routeId(id) : !identifier(id)) { issues.push(issue("schema", "invalid_id", label, `invalid ${kind.slice(0, -1)} identity`)); continue; } if (seen.has(id)) { issues.push(issue("schema", kind === "models" ? "duplicate_route_id" : "duplicate_role_id", label, `duplicate ${kind.slice(0, -1)} identity ${id}`)); continue; } seen.add(id); const document = parseDocument(raw[id], label, issues); if (!document) continue; if (document.id !== undefined && document.id !== id) { issues.push(issue("schema", "identity_mismatch", label, `declared id ${String(document.id)} does not match ${id}`)); continue; } collected.push({ id, label, document }); } return collected; } function closedObject(value, allowed, label, issues, code = "schema_additionalProperties") { if (!isObject(value)) { issues.push(issue("schema", "root_not_object", label, "value must be a mapping")); return false; } const unexpected = extraKeys(value, allowed); if (unexpected.length > 0) { issues.push(issue("schema", code, label, `unexpected properties: ${unexpected.join(", ")}`)); return false; } return true; } function required(value, keys, label, issues) { for (const key of keys) { if (!own(value, key)) issues.push(issue("schema", "schema_required", label, `missing ${key}`)); } } function validateOrigin(origin, label, issues) { if (!closedObject(origin, ORIGIN_KEYS, `${label}.origin`, issues)) return; required(origin, ORIGIN_KEYS, `${label}.origin`, issues); if (origin.source !== undefined && !sourcePath(origin.source)) { issues.push(issue("schema", "origin_source_invalid", `${label}.origin`, "origin source must be a contained relative path")); } if (origin.sha256 !== undefined && (typeof origin.sha256 !== "string" || !SHA256.test(origin.sha256))) { issues.push(issue("schema", "origin_hash_invalid", `${label}.origin`, "origin sha256 must be a 64-character hex digest")); } } function validateModelShape(document, id, label, issues) { if (!closedObject(document, MODEL_KEYS, label, issues)) return; required(document, ["schema", "harness", "model", "vendor", "status", "permissions"], label, issues); if (document.schema !== undefined && document.schema !== POLICY_DRAFT_MODEL_SCHEMA) { issues.push(issue("schema", "schema_identity_mismatch", label, `expected ${POLICY_DRAFT_MODEL_SCHEMA}, received ${String(document.schema)}`)); } if (document.id !== undefined && document.id !== id) { issues.push(issue("schema", "identity_mismatch", label, `declared id ${String(document.id)} does not match ${id}`)); } if (document.aliases !== undefined && !identifierList(document.aliases)) { issues.push(issue("schema", "schema_pattern", label, "aliases must be unique identifiers")); } if (document.harness !== undefined && !identifier(document.harness)) { issues.push(issue("schema", "schema_pattern", label, "harness must be an identifier")); } if (document.model !== undefined && (typeof document.model !== "string" || codePointCount(document.model) < 1 || codePointCount(document.model) > 200)) { issues.push(issue("schema", "schema_pattern", label, "model must be a nonempty string")); } if (document.vendor !== undefined && !nonemptyToken(document.vendor, 64)) { issues.push(issue("schema", "schema_pattern", label, "vendor must be a nonempty token")); } if (document.status !== undefined && !POLICY_DRAFT_STATUSES.includes(document.status)) { issues.push(issue("schema", "unsupported_status", label, "status must be admitted, candidate, or retired")); } if (document.permissions !== undefined && !uniqueStringList(document.permissions, POLICY_DRAFT_PERMISSIONS, { min: 1, max: 2 })) { issues.push(issue("schema", "schema_enum", label, "permissions must be unique edit and/or read-only")); } if (document.harness === "pi" && !own(document, "origin")) { issues.push(issue("schema", "schema_required", label, "Pi routes require origin source and sha256")); } if (own(document, "origin")) validateOrigin(document.origin, label, issues); if (document.thinking !== undefined && (typeof document.thinking !== "string" || document.thinking.length < 1 || document.thinking.length > 64)) { issues.push(issue("schema", "schema_pattern", label, "thinking must be a short string")); } if (document.tags !== undefined && !identifierList(document.tags)) { issues.push(issue("schema", "schema_pattern", label, "tags must be unique identifiers")); } if (document.capabilities !== undefined && !identifierList(document.capabilities)) { issues.push(issue("schema", "schema_pattern", label, "capabilities must be unique identifiers")); } if (document.priority !== undefined && (!Number.isInteger(document.priority) || document.priority < -10000 || document.priority > 10000)) { issues.push(issue("schema", "schema_type", label, "priority must be an integer in range")); } if (document.fallbacks !== undefined) { if (!Array.isArray(document.fallbacks) || document.fallbacks.length > 8) { issues.push(issue("schema", "schema_type", label, "fallbacks must be a bounded array")); } else { for (const [index, fallback] of document.fallbacks.entries()) { if (!isObject(fallback)) { issues.push(issue("schema", "root_not_object", `${label}.fallbacks[${index}]`, "fallback must be a mapping")); continue; } const keys = Object.keys(fallback); const profile = keys.length === 1 && own(fallback, "profile") && identifier(fallback.profile); const tag = own(fallback, "tag") && identifier(fallback.tag) && extraKeys(fallback, ["tag", "capabilities"]).length === 0 && (fallback.capabilities === undefined || identifierList(fallback.capabilities)); const provider = own(fallback, "provider") && own(fallback, "model") && extraKeys(fallback, ["provider", "model"]).length === 0 && identifier(fallback.provider) && typeof fallback.model === "string" && codePointCount(fallback.model) >= 1 && codePointCount(fallback.model) <= 200; if (!profile && !tag && !provider) { issues.push(issue("schema", "schema_oneOf", `${label}.fallbacks[${index}]`, "fallback does not match a retained v1 selector")); } } } } if (document.limits !== undefined) { if (!closedObject(document.limits, LIMIT_KEYS, `${label}.limits`, issues)) return; for (const key of ["contextTokens", "outputTokens"]) { if (document.limits[key] !== undefined && (!Number.isInteger(document.limits[key]) || document.limits[key] < 1)) { issues.push(issue("schema", "schema_type", `${label}.limits`, `${key} must be a positive integer`)); } } if (document.limits.timeoutMs !== undefined && (!Number.isInteger(document.limits.timeoutMs) || document.limits.timeoutMs < 0 || document.limits.timeoutMs > 31536000000)) { issues.push(issue("schema", "schema_type", `${label}.limits`, "timeoutMs must be a non-negative integer between 0 and 31536000000")); } } } function validateTemplateList(value, label, issues) { if (!Array.isArray(value)) { issues.push(issue("schema", "schema_type", label, "must be an array")); return; } for (const [index, entry] of value.entries()) { const path = `${label}[${index}]`; if (!closedObject(entry, TEMPLATE_KEYS, path, issues)) continue; if (typeof entry.template !== "string" || !entry.template) { issues.push(issue("schema", "schema_required", path, "template is required")); } if (entry.schema !== undefined && typeof entry.schema !== "string") { issues.push(issue("schema", "schema_type", path, "schema must be a string")); } } } function validateRoleShape(document, id, label, issues) { if (!closedObject(document, ROLE_KEYS, label, issues)) return; required(document, ["schema", "purpose", "permission", "description", "roster"], label, issues); if (document.schema !== undefined && document.schema !== POLICY_DRAFT_ROLE_SCHEMA) { issues.push(issue("schema", "schema_identity_mismatch", label, `expected ${POLICY_DRAFT_ROLE_SCHEMA}, received ${String(document.schema)}`)); } if (document.id !== undefined && document.id !== id) { issues.push(issue("schema", "identity_mismatch", label, `declared id ${String(document.id)} does not match ${id}`)); } if (document.extends !== undefined && !identifier(document.extends)) { issues.push(issue("schema", "schema_pattern", label, "extends must be a role identifier")); } if (document.purpose !== undefined && !POLICY_DRAFT_PURPOSES.includes(document.purpose)) { issues.push(issue("schema", "unsupported_purpose", label, "purpose must be a runner role label")); } if (document.permission !== undefined && !POLICY_DRAFT_PERMISSIONS.includes(document.permission)) { issues.push(issue("schema", "schema_enum", label, "permission must be edit or read-only")); } if (document.description !== undefined && (typeof document.description !== "string" || document.description.length < 1 || document.description.length > 2000)) { issues.push(issue("schema", "schema_type", label, "description must be a bounded string")); } if (document.roster !== undefined) { if (!Array.isArray(document.roster) || document.roster.length > 32) { issues.push(issue("schema", "schema_type", label, "roster must be a bounded array")); } else { const seenRoutes = new Set(); for (const [index, entry] of document.roster.entries()) { const path = `${label}.roster[${index}]`; if (!closedObject(entry, ROSTER_ENTRY_KEYS, path, issues)) continue; if (!routeId(entry.route)) { issues.push(issue("schema", "schema_pattern", path, "roster route must be a route identity")); continue; } if (seenRoutes.has(entry.route)) { issues.push(issue("schema", "duplicate_route_id", path, `duplicate roster route ${entry.route}`)); } seenRoutes.add(entry.route); if (entry.effort !== undefined && !EFFORTS.includes(entry.effort)) { issues.push(issue("schema", "schema_enum", path, "effort is not a known helper effort")); } if (entry.mode !== undefined && !MODES.includes(entry.mode)) { issues.push(issue("schema", "schema_enum", path, "mode must be headless, interactive, or either")); } } } } if (document.skills !== undefined) { if (!Array.isArray(document.skills) || document.skills.length > 64 || document.skills.some((item) => typeof item !== "string" || item.length < 1 || item.length > 128)) { issues.push(issue("schema", "schema_type", label, "skills must be bounded strings")); } } if (document.tools !== undefined) { if (!closedObject(document.tools, TOOL_KEYS, `${label}.tools`, issues)) return; if (document.tools.preset !== undefined && typeof document.tools.preset !== "string") { issues.push(issue("schema", "schema_type", `${label}.tools`, "preset must be a string")); } for (const key of ["allow", "deny"]) { if (document.tools[key] !== undefined && (!Array.isArray(document.tools[key]) || document.tools[key].some((item) => typeof item !== "string"))) { issues.push(issue("schema", "schema_type", `${label}.tools`, `${key} must be a string array`)); } } } if (document.produces !== undefined) validateTemplateList(document.produces, `${label}.produces`, issues); if (document.consumes !== undefined) validateTemplateList(document.consumes, `${label}.consumes`, issues); if (document.policy !== undefined) { if (!closedObject(document.policy, POLICY_KEYS, `${label}.policy`, issues)) return; if (document.policy.vendorIndependenceRequired !== undefined && typeof document.policy.vendorIndependenceRequired !== "boolean") { issues.push(issue("schema", "schema_type", `${label}.policy`, "vendorIndependenceRequired must be boolean")); } if (document.policy.requiresGateVerification !== undefined && typeof document.policy.requiresGateVerification !== "boolean") { issues.push(issue("schema", "schema_type", `${label}.policy`, "requiresGateVerification must be boolean")); } if (document.policy.maxTransitions !== undefined && (!Number.isInteger(document.policy.maxTransitions) || document.policy.maxTransitions < 1)) { issues.push(issue("schema", "schema_type", `${label}.policy`, "maxTransitions must be a positive integer")); } if (document.policy.fallback !== undefined) { const fallback = document.policy.fallback; const path = `${label}.policy.fallback`; if (!closedObject(fallback, FALLBACK_KEYS, path, issues)) return; if (fallback.onError !== undefined) { if (!Array.isArray(fallback.onError) || new Set(fallback.onError).size !== fallback.onError.length || fallback.onError.some((item) => !FALLBACK_ERRORS.includes(item))) { issues.push(issue("schema", "schema_enum", path, "onError must be unique rate_limit, transport, provider_unavailable, or context_overflow values")); } } if (fallback.maxSwitches !== undefined && (!Number.isInteger(fallback.maxSwitches) || fallback.maxSwitches < 0)) { issues.push(issue("schema", "schema_type", path, "maxSwitches must be an integer >= 0")); } if (fallback.revert !== undefined && !FALLBACK_REVERT.includes(fallback.revert)) { issues.push(issue("schema", "schema_enum", path, "revert must be next_run or never")); } } } } function subset(values, allowed) { return values.every((value) => allowed.includes(value)); } function validateModelSemantics(model, label, options, evidence, issues) { const aliases = options.vendorAliases ?? {}; const ceiling = options.ceilings[model.harness]; if (!ceiling) { issues.push(issue("semantic", "unsupported_harness", label, `no code-owned ceiling for harness ${String(model.harness)}`)); return; } const vendor = canonicalVendor(model.vendor, aliases); if (options.piAllowedProviders.includes(vendor)) { issues.push(issue("semantic", "vendor_aggregator", label, "vendor must name the model vendor, not a billing aggregator")); } if (!subset(model.permissions, ceiling.permissions)) { issues.push(issue("semantic", "permission_escalation", label, "route permissions exceed the harness ceiling")); } if (Array.isArray(ceiling.models) && ceiling.models.length > 0 && !ceiling.models.includes(model.model)) { issues.push(issue("semantic", "model_not_in_ceiling", label, "exact model is not listed on the harness ceiling")); } if (model.harness === "pi") { const parts = typeof model.model === "string" ? model.model.split("/") : []; const nativeVendors = isObject(options.piNativeVendorProviders) ? options.piNativeVendorProviders : {}; const provider = parts[0]; const owned = own(nativeVendors, provider) ? canonicalVendor(nativeVendors[provider], aliases) : ""; const minParts = owned ? 2 : 3; const maxParts = owned ? 2 : Number.POSITIVE_INFINITY; if ( parts.length < minParts || parts.length > maxParts || parts.some((part) => !part) || !options.piAllowedProviders.includes(provider) || (owned && !/^gemini-[a-z0-9.-]+$/.test(parts[1])) ) { issues.push(issue("semantic", "unsupported_pi_model", label, "Pi model must use an allowlisted aggregator prefix")); } else if (owned) { if (owned !== vendor) { issues.push(issue("semantic", "pi_native_vendor_forbidden", label, "native vendor cannot use Pi")); } } else { const prefix = canonicalVendor(parts[1], aliases); if (options.nativePiBrakeProviders.includes(prefix) || options.nativePiBrakeProviders.includes(vendor)) { issues.push(issue("semantic", "pi_native_vendor_forbidden", label, "native vendor cannot use Pi")); } } } else if (vendor !== ceiling.provider || (typeof model.model === "string" && model.model.includes("/"))) { issues.push(issue("semantic", "native_vendor_mismatch", label, "native route vendor or model does not match the harness ceiling")); } const origin = model.origin; if (isObject(origin) && typeof origin.source === "string" && typeof origin.sha256 === "string") { if (!own(evidence, origin.source)) { issues.push(issue("semantic", "origin_evidence_missing", label, `missing evidence bytes for ${origin.source}`)); } else if (sha256Bytes(evidence[origin.source]) !== origin.sha256) { issues.push(issue("semantic", "origin_hash_mismatch", label, "origin evidence hash does not match supplied bytes")); } } } function admittedRoute(model) { return model.status === "admitted"; } function validateRoleSemantics(role, id, label, models, options, issues) { if (!Array.isArray(role.roster)) return; for (const [index, entry] of role.roster.entries()) { const path = `${label}.roster[${index}]`; const model = models.get(entry.route); if (!model) { issues.push(issue("reference", "route_unknown", path, `roster references unknown route ${entry.route}`)); continue; } const ceiling = options.ceilings[model.harness]; if (!ceiling) continue; if (Array.isArray(ceiling.roles) && ceiling.roles.length > 0 && !ceiling.roles.includes(role.purpose)) { issues.push(issue("semantic", "purpose_ceiling_mismatch", path, `purpose ${role.purpose} is not allowed on harness ${model.harness}`)); } if (!model.permissions.includes(role.permission)) { issues.push(issue("semantic", "permission_escalation", path, "role permission is not granted by the route")); } if (!ceiling.permissions.includes(role.permission)) { issues.push(issue("semantic", "permission_escalation", path, "role permission exceeds the harness ceiling")); } if (Array.isArray(ceiling.efforts) && ceiling.efforts.length > 0 && entry.effort !== undefined && !ceiling.efforts.includes(entry.effort)) { issues.push(issue("semantic", "effort_ceiling_mismatch", path, "effort is not allowed on the harness ceiling")); } if (!admittedRoute(model)) { issues.push(issue("semantic", "route_not_admitted", path, `route ${entry.route} is ${model.status} and is not treated as admitted`)); } if (model.harness === "pi") { if (role.permission === "edit" && !["writer", "experiment"].includes(role.purpose)) { issues.push(issue("semantic", "permission_escalation", path, "Pi critic/planner cannot edit")); } if (role.purpose === "writer" && (model.permissions.length !== 1 || model.permissions[0] !== "edit")) { issues.push(issue("semantic", "permission_escalation", path, "Pi writer requires edit permission only")); } } } if (id === role.purpose && CRITIC_PURPOSES.includes(role.purpose)) { const admitted = role.roster .map((entry) => models.get(entry.route)) .filter((model) => model && admittedRoute(model)); // The first roster entry is the required critic. Later entries are // fallbacks so one provider outage does not stop the role. Every entry // stays read-only; vendor independence uses the first entry only. if (admitted.length < 1) { issues.push(issue("semantic", "critic_route_count", label, "required critic purpose files must list at least one admitted route")); } else if (role.permission !== "read-only" || admitted.some((model) => model.permissions.length !== 1 || model.permissions[0] !== "read-only")) { issues.push(issue("semantic", "permission_escalation", label, "critic purpose files must be read-only")); } } } function validateExtends(roles, issues) { for (const [id, role] of roles) { if (role.extends === undefined) continue; const label = `roles/${id}`; if (!roles.has(role.extends)) { issues.push(issue("reference", "role_extends_unknown", label, `extends names unknown role ${role.extends}`)); continue; } const seen = new Set([id]); let current = role.extends; while (typeof current === "string" && current.length > 0) { if (seen.has(current)) { issues.push(issue("semantic", "role_extends_cycle", label, `extends cycle: ${[...seen, current].join(" -> ")}`)); break; } seen.add(current); current = roles.get(current)?.extends; } } } function validateCriticVendors(roles, models, options, issues) { const aliases = options.vendorAliases ?? {}; const critics = []; for (const purpose of CRITIC_PURPOSES) { const role = roles.get(purpose); if (!role || role.purpose !== purpose) continue; const admitted = (role.roster ?? []) .map((entry) => models.get(entry.route)) .filter((model) => model && admittedRoute(model)); if (admitted.length >= 1) critics.push({ purpose, vendor: canonicalVendor(admitted[0].vendor, aliases) }); } if (critics.length === 2 && critics[0].vendor === critics[1].vendor) { issues.push(issue("semantic", "critic_vendor_collision", "roles/reviewer-arch", "required critics must have independent vendors")); } const writer = roles.get("writer"); if (writer && critics.length > 0) { for (const entry of writer.roster ?? []) { const model = models.get(entry.route); if (!model || !admittedRoute(model)) continue; const vendor = canonicalVendor(model.vendor, aliases); if (critics.some((critic) => critic.vendor === vendor)) { issues.push(issue("semantic", "writer_critic_vendor_collision", "roles/writer", "writer and critics must have independent vendors")); } } } } /** Pure draft validation. Not admission, not a trusted loader, and not file I/O. */ export function validatePolicyDraft(input, options) { const issues = []; if (!isObject(input)) { return { ok: false, issues: [issue("schema", "root_not_object", "<draft>", "draft input must be a mapping")] }; } const unexpected = extraKeys(input, ["models", "roles", "evidence"]); if (unexpected.length > 0) { issues.push(issue("schema", "schema_additionalProperties", "<draft>", `unexpected properties: ${unexpected.join(", ")}`)); } if (!isObject(options) || !isObject(options.ceilings) || !Array.isArray(options.nativePiBrakeProviders) || !Array.isArray(options.piAllowedProviders)) { issues.push(issue("semantic", "ceilings_required", "<options>", "code-owned ceilings and vendor brakes must be supplied explicitly")); return { ok: false, issues: sortIssues(issues) }; } const evidence = isObject(input.evidence) ? input.evidence : {}; if (input.evidence !== undefined && !isObject(input.evidence)) { issues.push(issue("schema", "root_not_object", "evidence", "evidence must be a mapping of source path to bytes")); } const modelDocs = collectDocuments(input.models, "models", issues); const roleDocs = collectDocuments(input.roles, "roles", issues); for (const entry of modelDocs) validateModelShape(entry.document, entry.id, entry.label, issues); for (const entry of roleDocs) validateRoleShape(entry.document, entry.id, entry.label, issues); if (issues.length > 0) return { ok: false, issues: sortIssues(issues) }; const models = new Map(); for (const entry of modelDocs) { validateModelSemantics(entry.document, entry.label, options, evidence, issues); const normalized = cloneJson(entry.document); normalized.id = entry.id; models.set(entry.id, normalized); } const roles = new Map(); for (const entry of roleDocs) { validateRoleSemantics(entry.document, entry.id, entry.label, models, options, issues); const normalized = cloneJson(entry.document); normalized.id = entry.id; roles.set(entry.id, normalized); } validateExtends(roles, issues); validateCriticVendors(roles, models, options, issues); if (issues.length > 0) return { ok: false, issues: sortIssues(issues) }; const data = freezeDeep({ models: Object.fromEntries([...models.entries()].sort(([left], [right]) => compareCodeUnits(left, right))), roles: Object.fromEntries([...roles.entries()].sort(([left], [right]) => compareCodeUnits(left, right))), }); return { ok: true, data }; }