// lib/policy.ts — pure role-manifest schema validation (PRD R2, AC1; NG-4 v3). // // NG-1 keeps PURE validation only: no filesystem load, no trusted-path // resolution (NG-2 owns those). The schema is deliberately strict — // everything not named here is rejected, because a manifest that silently // tolerates extra fields is a manifest whose future drift cannot be caught. // // NG-4 (velma B2, memo 9c426fa9): the manifest separates CAPABILITIES from // tool bindings. Capabilities are the authority; Pi tools are their bound // projections. The strict cross-check: the tools array must be EXACTLY the // set of Pi bindings of the bound capabilities — neither more (an // unbound-to-capability tool is undeclared authority) nor less. // // NG-4 remediation F6: the manifest carries the FULL authority set — // schemaVersion, extensions.load order, workspace.readRoots, credentials // (deny-all), evidence permissions (journal-only), and digest/integrity // rules. Settings are generated projections; they never become authority. export interface CapabilityBinding { id: string; effect: CapabilityEffect; binding?: string; status: CapabilityStatus; } export interface RoleManifestV3 { schemaVersion: 3; role: string; revision: number; capabilities: CapabilityBinding[]; tools: string[]; extensions: { load: string[] }; workspace: { readRoots: string[] }; credentials: Record; evidence: { journal: true; journalEvents: string[]; external: "denied" }; digestRules: { algorithm: "sha256"; scope: "manifest-bytes"; postcondition: "active-set-exact" }; forbiddenTools: string[]; shell: { mode: "denied" }; } export interface GoalReportPolicy { format: "ms-executive-update/v1"; contractPath: "skills-local/ms-executive-update/SKILL.md"; contractSection: "Machine contract (for `goal_report` payloads and any parser)"; contractBlob: "df30c6fbb54b4a65a298c9e51c07f742610d171c"; contractSha256: "bbea48a46b1f8da7bc759f86856fb52830b7dde456b826317163c6dc6ccab319"; enforcement: "pre-state-change-fail-closed"; identifierResolution: "consumer-fail-closed"; } export interface RoleManifestV4 { schemaVersion: 4; role: "gate-merge-ng" | "plan-ng" | "review-ng"; revision: 3 | 4; capabilities: CapabilityBinding[]; tools: string[]; extensions: { load: string[] }; workspace: { readRoots: string[] }; credentials: Record; evidence: Record; digestRules: { algorithm: "sha256"; scope: "manifest-bytes"; postcondition: "active-set-exact" }; forbiddenTools: string[]; shell: { mode: "denied" }; goalReportPolicy: GoalReportPolicy; targetPolicy?: { kind: "pull-request"; hosts: string[]; repositories: string[]; pinSource: "assignment" }; } export type RoleManifest = RoleManifestV3 | RoleManifestV4; export type CapabilityEffect = "observe" | "propose" | "transition" | "communicate"; export type CapabilityStatus = "bound" | "unbound"; /** Closed capability ids. C1-C8 remain version-3 only; C9-C11 are version-4 gate authority. */ const V3_CAPABILITY_IDS = new Set([ "policy.status", // C1 "improvement.propose", // C2 "repo.file.read", // C3 "goal.report", // C4 "reason.structured", // C5 "coord.route_implementation", // C6 "coord.request", // C7 "comms.send", // C8 ]); export const CAPABILITY_IDS = new Set([ ...V3_CAPABILITY_IDS, "repo.target.inspect", // C9 "gate.evidence.observe", // C10 "gate.verdict", // C11 ]); export const CAPABILITY_EFFECTS = new Set(["observe", "propose", "transition", "communicate"]); /** Fixed normative id-to-binding maps. Version 3 never learns C9-C11. */ const V3_NORMATIVE_BINDINGS: Record = { "policy.status": { binding: "mosaic_policy_status", status: "bound" }, "improvement.propose": { binding: "mosaic_improvement_propose", status: "bound" }, "repo.file.read": { binding: "read", status: "bound" }, "goal.report": { binding: "goal_report", status: "bound" }, "reason.structured": { status: "unbound" }, "coord.route_implementation": { status: "unbound" }, "coord.request": { status: "unbound" }, "comms.send": { status: "unbound" }, }; export const NORMATIVE_BINDINGS: Record = { ...V3_NORMATIVE_BINDINGS, "repo.target.inspect": { binding: "mosaic_gate_target_read", status: "bound" }, "gate.evidence.observe": { binding: "mosaic_gate_evidence_observe", status: "bound" }, "gate.verdict": { binding: "mosaic_gate_verdict", status: "bound" }, }; /** * NG4-CRIT: the MANDATORY reviewed forbidden raw-tool set. The manifest's * forbiddenTools must CONTAIN this minimum set (it may add more, never * remove). This prevents the caller from deleting entries to smuggle a * forbidden tool back into scope. */ export const MANDATORY_FORBIDDEN = ["bash", "edit", "write", "powershell", "tmux", "send_message", "git"]; export const CAPABILITY_STATUSES = new Set(["bound", "unbound"]); export type PolicyRejection = | "not-an-object" | "unknown-field" | "missing-field" | "invalid-role" | "invalid-revision" | "invalid-schema-version" | "invalid-tools" | "duplicate-tool" | "unsafe-empty-policy" | "invalid-capabilities" | "duplicate-capability" | "unknown-capability" | "invalid-capability-effect" | "invalid-capability-status" | "bound-without-binding" | "unbound-with-binding" | "invalid-capability-binding" | "tools-bindings-mismatch" | "invalid-extensions" | "invalid-workspace" | "invalid-credentials" | "invalid-evidence" | "invalid-digest-rules" | "missing-capability" | "invalid-forbidden-tools" | "invalid-shell" | "forbidden-tool-overlap" | "invalid-journal-events" | "invalid-role-class" | "invalid-goal-report-policy" | "invalid-target-policy" | "version4-exact-set-mismatch"; export interface PolicyResult { ok: boolean; manifest?: RoleManifest; reason?: PolicyRejection; detail?: string; } const V3_ALLOWED_FIELDS = new Set([ "schemaVersion", "role", "revision", "tools", "capabilities", "extensions", "workspace", "credentials", "evidence", "digestRules", "forbiddenTools", "shell", ]); const ROLE_RE = /^[a-z][a-z0-9-]*$/; const TOOL_RE = /^[a-z0-9][a-z0-9_-]*$/; const EXT_RE = /^[a-z][a-z0-9-]*$/; export function validateManifest(input: unknown): PolicyResult { if (typeof input !== "object" || input === null || Array.isArray(input)) { return { ok: false, reason: "not-an-object", detail: "manifest must be a JSON object" }; } const obj = input as Record; // Version 3 remains on its original closed validator. Version 4 is a // separate role-class contract and never widens the v3 capability map. if (obj.schemaVersion === 4) return validateV4Manifest(obj); for (const key of Object.keys(obj)) { if (!V3_ALLOWED_FIELDS.has(key)) { return { ok: false, reason: "unknown-field", detail: `unexpected field "${key}"` }; } } for (const field of V3_ALLOWED_FIELDS) { if (!(field in obj)) { return { ok: false, reason: "missing-field", detail: `missing field "${field}"` }; } } if (obj.schemaVersion !== 3) { return { ok: false, reason: "invalid-schema-version", detail: "schemaVersion must be 3" }; } const role = obj.role; if (typeof role !== "string" || !ROLE_RE.test(role)) { return { ok: false, reason: "invalid-role", detail: `role must match ${ROLE_RE}` }; } const revision = obj.revision; if (typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 1) { return { ok: false, reason: "invalid-revision", detail: "revision must be a positive integer" }; } // ---- capabilities: the authority layer ------------------------------------ const rawCaps = obj.capabilities; if (!Array.isArray(rawCaps) || rawCaps.length === 0 || rawCaps.some((c) => typeof c !== "object" || c === null || Array.isArray(c))) { return { ok: false, reason: "invalid-capabilities", detail: "capabilities must be a non-empty array of objects" }; } const seenCaps = new Set(); const boundTools: string[] = []; const capabilities: CapabilityBinding[] = []; for (const raw of rawCaps) { const c = raw as Record; for (const key of Object.keys(c)) { if (!["id", "effect", "binding", "status"].includes(key)) { return { ok: false, reason: "invalid-capabilities", detail: `unknown capability field "${key}"` }; } } if (typeof c.id !== "string" || !V3_CAPABILITY_IDS.has(c.id)) { return { ok: false, reason: "unknown-capability", detail: `capability id "${String(c.id)}" is outside the reviewed C1-C8 map` }; } if (seenCaps.has(c.id)) { return { ok: false, reason: "duplicate-capability", detail: `duplicate capability "${c.id}"` }; } seenCaps.add(c.id); if (typeof c.effect !== "string" || !CAPABILITY_EFFECTS.has(c.effect as CapabilityEffect)) { return { ok: false, reason: "invalid-capability-effect", detail: `capability "${c.id}"` }; } // F6H-(5): enforce the normative id->effect mapping from the C1-C8 map const NORMATIVE_EFFECT: Record = { "policy.status": "observe", "improvement.propose": "propose", "repo.file.read": "observe", "goal.report": "transition", "reason.structured": "observe", "coord.route_implementation": "transition", "coord.request": "transition", "comms.send": "communicate", }; if (c.effect !== NORMATIVE_EFFECT[c.id]) { return { ok: false, reason: "invalid-capability-effect", detail: `capability "${c.id}" must have effect "${NORMATIVE_EFFECT[c.id]}" (normative C-map), got "${c.effect}"` }; } if (typeof c.status !== "string" || !CAPABILITY_STATUSES.has(c.status as CapabilityStatus)) { return { ok: false, reason: "invalid-capability-status", detail: `capability "${c.id}"` }; } // NG4-CRIT: enforce the NORMATIVE binding (or mandatory unbound status) const normative = V3_NORMATIVE_BINDINGS[c.id]; if (normative.status === "unbound" && c.status === "bound") { return { ok: false, reason: "invalid-capability-status", detail: `capability "${c.id}" must be UNBOUND in this slice (no backend)` }; } if (c.status === "bound") { if (normative.status !== "bound") { return { ok: false, reason: "invalid-capability-status", detail: `capability "${c.id}" cannot be bound in this slice` }; } if (c.binding !== normative.binding) { return { ok: false, reason: "invalid-capability-binding", detail: `capability "${c.id}" must bind exactly "${normative.binding}" (normative C-map), got "${c.binding}"` }; } if (typeof c.binding !== "string" || !TOOL_RE.test(c.binding)) { return { ok: false, reason: "bound-without-binding", detail: `capability "${c.id}" is bound but has no valid tool binding` }; } boundTools.push(c.binding); } else if (c.binding !== undefined) { return { ok: false, reason: "unbound-with-binding", detail: `capability "${c.id}" is unbound but declares a binding` }; } capabilities.push({ id: c.id, effect: c.effect as CapabilityEffect, binding: c.binding as string | undefined, status: c.status as CapabilityStatus, }); } // ---- tools: the PROJECTION of bound capabilities, exactly ------------------ const tools = obj.tools; if (!Array.isArray(tools) || tools.some((t) => typeof t !== "string" || !TOOL_RE.test(t))) { return { ok: false, reason: "invalid-tools", detail: `every tool must match ${TOOL_RE}` }; } if (tools.length === 0) { return { ok: false, reason: "unsafe-empty-policy", detail: "tools must not be empty" }; } const seen = new Set(); for (const t of tools) { if (seen.has(t)) { return { ok: false, reason: "duplicate-tool", detail: `duplicate tool "${t}"` }; } seen.add(t); } const expected = [...new Set(boundTools)].sort(); const declared = [...new Set(tools)].sort(); if (JSON.stringify(expected) !== JSON.stringify(declared)) { return { ok: false, reason: "tools-bindings-mismatch", detail: `tools must be exactly the bound capabilities' bindings (expected [${expected.join(", ")}])`, }; } // ---- NG4-CRIT3: C1-C4 MUST be present and bound (canary contract) ------------- const REQUIRED_BOUND = ["policy.status", "improvement.propose", "repo.file.read", "goal.report"]; for (const reqId of REQUIRED_BOUND) { const cap = capabilities.find((c) => c.id === reqId); if (!cap) { return { ok: false, reason: "missing-capability", detail: `canary roles must declare capability "${reqId}" (C1-C4 are mandatory)` }; } if (cap.status !== "bound") { return { ok: false, reason: "invalid-capability-status", detail: `capability "${reqId}" must be BOUND (C1-C4 are mandatory for canary roles)` }; } } // C5-C8: if present, must be unbound (first slice — no backend) for (const cap of capabilities) { if (V3_NORMATIVE_BINDINGS[cap.id]?.status === "unbound" && cap.status === "bound") { return { ok: false, reason: "invalid-capability-status", detail: `capability "${cap.id}" must be UNBOUND in this slice` }; } } // ---- extensions: load order authority ---------------------------------------- const exts = obj.extensions; if (typeof exts !== "object" || exts === null || Array.isArray(exts) || !Array.isArray((exts as Record).load)) { return { ok: false, reason: "invalid-extensions", detail: "extensions.load must be an ordered array" }; } // F6H-(2): reject unknown nested keys in extensions for (const ek of Object.keys(exts)) { if (ek !== "load") { return { ok: false, reason: "invalid-extensions", detail: `unknown extensions field "${ek}"` }; } } const loadOrder = (exts as { load: unknown[] }).load; if (loadOrder.some((e) => typeof e !== "string" || !EXT_RE.test(e))) { return { ok: false, reason: "invalid-extensions", detail: "extension names must be slugs" }; } if (new Set(loadOrder).size !== loadOrder.length) { return { ok: false, reason: "invalid-extensions", detail: "duplicate extension in load order" }; } // F6H-(3): the reviewed required load order is exact — goal FIRST (registers // goal_report), mosaic-core LAST (reconciles the complete registry). // Anything else is unreviewed authority. const REQUIRED_LOAD = ["goal", "mosaic-core"]; if (JSON.stringify(loadOrder) !== JSON.stringify(REQUIRED_LOAD)) { return { ok: false, reason: "invalid-extensions", detail: `extensions.load must be exactly [${REQUIRED_LOAD.join(", ")}] — goal first (registers goal_report), mosaic-core last (reconciles the complete registry)` }; } // ---- workspace: read-scope roots (symbolic @brain or absolute) --------------- const ws = obj.workspace; if (typeof ws !== "object" || ws === null || Array.isArray(ws) || !Array.isArray((ws as Record).readRoots)) { return { ok: false, reason: "invalid-workspace", detail: "workspace.readRoots must be an array" }; } // F6H-(2): reject unknown nested keys in workspace for (const wk of Object.keys(ws)) { if (wk !== "readRoots") { return { ok: false, reason: "invalid-workspace", detail: `unknown workspace field "${wk}"` }; } } const readRoots = (ws as { readRoots: unknown[] }).readRoots; if (readRoots.some((r) => typeof r !== "string" || !(r.startsWith("/") || r === "@brain"))) { return { ok: false, reason: "invalid-workspace", detail: "readRoots must be absolute paths or the symbolic @brain root" }; } // ---- credentials: deny-all, EXACT (F6) ---------------------------------------- const creds = obj.credentials; if (typeof creds !== "object" || creds === null || Array.isArray(creds)) { return { ok: false, reason: "invalid-credentials", detail: "credentials must be an object" }; } const credKeys = Object.keys(creds); if (credKeys.some((k) => !["store", "helper", "providerTokens"].includes(k))) { return { ok: false, reason: "invalid-credentials", detail: "unknown credentials field" }; } if (creds.store !== "none" || creds.helper !== "none") { return { ok: false, reason: "invalid-credentials", detail: "canary roles carry no credential store or helper" }; } if (creds.providerTokens !== "denied") { return { ok: false, reason: "invalid-credentials", detail: 'providerTokens must be exactly "denied" (mandatory deny-all)' }; } // ---- evidence: journal-only (F6) ---------------------------------------------- const ev = obj.evidence; if (typeof ev !== "object" || ev === null || Array.isArray(ev)) { return { ok: false, reason: "invalid-evidence", detail: "evidence must be an object" }; } const evKeys = Object.keys(ev); if (evKeys.some((k) => !["journal", "journalEvents", "external"].includes(k))) { return { ok: false, reason: "invalid-evidence", detail: "unknown evidence field" }; } if (ev.journal !== true) { return { ok: false, reason: "invalid-evidence", detail: "canary roles must journal (the only evidence sink)" }; } if (ev.external !== "denied") { return { ok: false, reason: "invalid-evidence", detail: "external evidence must be denied for canary roles" }; } const REVIEWED_EVENTS = ["reconciliation", "denial", "improvement-proposal"]; if (!Array.isArray(ev.journalEvents) || ev.journalEvents.length !== REVIEWED_EVENTS.length) { return { ok: false, reason: "invalid-journal-events", detail: `journalEvents must be exactly the reviewed set [${REVIEWED_EVENTS.join(", ")}]` }; } const evSeen = new Set(); for (const e of ev.journalEvents) { if (typeof e !== "string" || !REVIEWED_EVENTS.includes(e)) { return { ok: false, reason: "invalid-journal-events", detail: `unknown journal event "${String(e)}"` }; } if (evSeen.has(e)) { return { ok: false, reason: "invalid-journal-events", detail: `duplicate journal event "${e}"` }; } evSeen.add(e); } // ---- digest/integrity rules: explicit and closed (F6) -------------------------- const dig = obj.digestRules; if (typeof dig !== "object" || dig === null || Array.isArray(dig)) { return { ok: false, reason: "invalid-digest-rules", detail: "digestRules must be an object" }; } const digKeys = Object.keys(dig); if (digKeys.some((k) => !["algorithm", "scope", "postcondition"].includes(k))) { return { ok: false, reason: "invalid-digest-rules", detail: "unknown digestRules field" }; } if (dig.algorithm !== "sha256") { return { ok: false, reason: "invalid-digest-rules", detail: "digest algorithm must be sha256" }; } if (dig.scope !== "manifest-bytes") { return { ok: false, reason: "invalid-digest-rules", detail: "digest scope must be manifest-bytes" }; } if (dig.postcondition !== "active-set-exact") { return { ok: false, reason: "invalid-digest-rules", detail: "postcondition must be active-set-exact" }; } // ---- forbiddenTools (auth): the reviewed absent set — these tools must // never appear in the bound tools; overlap is a typed rejection const forbidden = obj.forbiddenTools; if (!Array.isArray(forbidden) || forbidden.length === 0 || forbidden.some((t) => typeof t !== "string" || !TOOL_RE.test(t))) { return { ok: false, reason: "invalid-forbidden-tools", detail: "forbiddenTools must be a non-empty array of valid tool names" }; } const forbiddenSet = new Set(forbidden as string[]); if (forbiddenSet.size !== (forbidden as string[]).length) { return { ok: false, reason: "invalid-forbidden-tools", detail: "duplicate entry in forbiddenTools" }; } // NG4-CRIT: the manifest MUST contain the mandatory reviewed forbidden set // (it may add more, never remove) for (const m of MANDATORY_FORBIDDEN) { if (!forbiddenSet.has(m)) { return { ok: false, reason: "invalid-forbidden-tools", detail: `mandatory forbidden tool "${m}" is absent from forbiddenTools (cannot be removed)` }; } } for (const t of tools) { if (forbiddenSet.has(t)) { return { ok: false, reason: "forbidden-tool-overlap", detail: `tool "${t}" is both bound and forbidden` }; } } // ---- shell policy (auth): canary roles deny all shell access const shell = obj.shell; if (typeof shell !== "object" || shell === null || Array.isArray(shell)) { return { ok: false, reason: "invalid-shell", detail: "shell must be an object" }; } const shellKeys = Object.keys(shell); if (shellKeys.some((k) => !["mode"].includes(k))) { return { ok: false, reason: "invalid-shell", detail: "unknown shell field" }; } if (shell.mode !== "denied") { return { ok: false, reason: "invalid-shell", detail: "canary roles deny all shell access (shell.mode must be \"denied\")" }; } // ---- C3 scope (F7): bound read capability requires declared scope --------------- const c3 = capabilities.find((c) => c.id === "repo.file.read"); if (c3 && c3.status === "bound" && readRoots.length === 0) { return { ok: false, reason: "invalid-workspace", detail: "C3 repo.file.read is bound but workspace.readRoots is empty — declare the read scope or leave C3 unbound" }; } return { ok: true, manifest: { schemaVersion: 3, role, revision, capabilities, tools: [...tools], extensions: { load: [...loadOrder] as string[] }, workspace: { readRoots: [...readRoots] as string[] }, credentials: { ...(creds as Record) }, evidence: { journal: true, journalEvents: [...(ev as { journalEvents: string[] }).journalEvents], external: "denied" }, digestRules: { algorithm: "sha256", scope: "manifest-bytes", postcondition: "active-set-exact" }, forbiddenTools: [...forbidden] as string[], shell: { mode: "denied" }, }, }; } // ---- schema version 4: closed, role-class-specific authority ----------------- type V4Role = "gate-merge-ng" | "plan-ng" | "review-ng"; interface V4RoleSpec { revision: 3 | 4; capabilities: ReadonlyArray>; tools: readonly string[]; extensions: readonly string[]; readRoots: readonly string[]; credentials: Record; evidence: Record; forbiddenTools: readonly string[]; targetPolicy?: Record; } const GOAL_POLICY_FIELDS = [ "format", "contractPath", "contractSection", "contractBlob", "contractSha256", "enforcement", "identifierResolution", ] as const; const GOAL_POLICY_VALUES: GoalReportPolicy = { format: "ms-executive-update/v1", contractPath: "skills-local/ms-executive-update/SKILL.md", contractSection: "Machine contract (for `goal_report` payloads and any parser)", contractBlob: "df30c6fbb54b4a65a298c9e51c07f742610d171c", contractSha256: "bbea48a46b1f8da7bc759f86856fb52830b7dde456b826317163c6dc6ccab319", enforcement: "pre-state-change-fail-closed", identifierResolution: "consumer-fail-closed", }; const V4_COMMON_POLICY = { algorithm: "sha256", scope: "manifest-bytes", postcondition: "active-set-exact" } as const; const PLAN_REVIEW_CAPABILITIES = [ { id: "policy.status", effect: "observe", binding: "mosaic_policy_status", status: "bound" }, { id: "improvement.propose", effect: "propose", binding: "mosaic_improvement_propose", status: "bound" }, { id: "repo.file.read", effect: "observe", binding: "read", status: "bound" }, { id: "goal.report", effect: "transition", binding: "goal_report", status: "bound" }, { id: "coord.route_implementation", effect: "transition", status: "unbound" }, { id: "comms.send", effect: "communicate", status: "unbound" }, ] as const; const GATE_CAPABILITIES = [ { id: "policy.status", effect: "observe", binding: "mosaic_policy_status", status: "bound" }, { id: "goal.report", effect: "transition", binding: "goal_report", status: "bound" }, { id: "repo.target.inspect", effect: "observe", binding: "mosaic_gate_target_read", status: "bound" }, { id: "gate.evidence.observe", effect: "observe", binding: "mosaic_gate_evidence_observe", status: "bound" }, { id: "gate.verdict", effect: "transition", binding: "mosaic_gate_verdict", status: "bound" }, ] as const; const PLAN_REVIEW_FORBIDDEN = ["bash", "edit", "git", "powershell", "send_message", "tmux", "write"] as const; const GATE_FORBIDDEN = [ "bash", "browser", "edit", "fetch", "find", "git", "grep", "http", "ls", "mosaic_improvement_propose", "powershell", "pr_merge", "read", "send_message", "tmux", "web", "write", ] as const; const V4_SPECS: Record = { "gate-merge-ng": { revision: 3, capabilities: GATE_CAPABILITIES, tools: ["goal_report", "mosaic_gate_evidence_observe", "mosaic_gate_target_read", "mosaic_gate_verdict", "mosaic_policy_status"], extensions: ["goal", "gate-merge", "mosaic-core"], readRoots: [], credentials: { seatStore: "none", helper: "none", providerTokens: "denied", brokerChannel: "launcher-inherited-attested-fd" }, evidence: { localJournal: true, journalEvents: ["reconciliation", "denial", "gate-observation", "gate-verdict-attempt"], canonicalWriter: "mosaic-gate-broker", external: "gate-evidence-and-verdict-only" }, forbiddenTools: GATE_FORBIDDEN, targetPolicy: { kind: "pull-request", hosts: ["git.mosaicstack.dev"], repositories: ["mosaicstack/stack"], pinSource: "assignment" }, }, "plan-ng": { revision: 4, capabilities: PLAN_REVIEW_CAPABILITIES, tools: ["goal_report", "mosaic_improvement_propose", "mosaic_policy_status", "read"], extensions: ["goal", "mosaic-core"], readRoots: ["@brain"], credentials: { store: "none", helper: "none", providerTokens: "denied" }, evidence: { journal: true, journalEvents: ["reconciliation", "denial", "improvement-proposal"], external: "denied" }, forbiddenTools: PLAN_REVIEW_FORBIDDEN, }, "review-ng": { revision: 4, capabilities: PLAN_REVIEW_CAPABILITIES, tools: ["goal_report", "mosaic_improvement_propose", "mosaic_policy_status", "read"], extensions: ["goal", "mosaic-core"], readRoots: ["@brain"], credentials: { store: "none", helper: "none", providerTokens: "denied" }, evidence: { journal: true, journalEvents: ["reconciliation", "denial", "improvement-proposal"], external: "denied" }, forbiddenTools: PLAN_REVIEW_FORBIDDEN, }, }; function exactObject(value: unknown, expected: Record): boolean { if (typeof value !== "object" || value === null || Array.isArray(value)) return false; const obj = value as Record; const keys = Object.keys(obj).sort(); const expectedKeys = Object.keys(expected).sort(); if (keys.length !== expectedKeys.length || keys.some((key, index) => key !== expectedKeys[index])) return false; return expectedKeys.every((key) => exactValue(obj[key], expected[key])); } function exactValue(actual: unknown, expected: unknown): boolean { if (Array.isArray(expected)) { return Array.isArray(actual) && actual.length === expected.length && actual.every((entry, index) => exactValue(entry, expected[index])); } if (typeof expected === "object" && expected !== null) return exactObject(actual, expected as Record); return actual === expected; } function sameStringSet(value: unknown, expected: readonly string[]): boolean { return Array.isArray(value) && value.every((entry) => typeof entry === "string") && new Set(value).size === value.length && value.length === expected.length && [...value].sort().every((entry, index) => entry === [...expected].sort()[index]); } function exactCapabilitySet(value: unknown, expected: V4RoleSpec["capabilities"]): boolean { if (!Array.isArray(value) || value.length !== expected.length) return false; const byId = new Map>(); for (const raw of value) { if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return false; const cap = raw as Record; if (typeof cap.id !== "string" || byId.has(cap.id)) return false; byId.set(cap.id, cap); } return expected.every((definition) => { const cap = byId.get(definition.id); if (!cap) return false; const expectedKeys = definition.status === "bound" ? ["id", "effect", "binding", "status"] : ["id", "effect", "status"]; if (Object.keys(cap).length !== expectedKeys.length || Object.keys(cap).some((key) => !expectedKeys.includes(key))) return false; const normative = NORMATIVE_BINDINGS[definition.id]; return !!normative && definition.effect === cap.effect && definition.status === cap.status && cap.effect === (definition.effect as string) && cap.status === definition.status && (definition.status === "unbound" || ("binding" in definition && cap.binding === definition.binding && normative.status === "bound" && normative.binding === definition.binding)); }); } function validateV4Manifest(obj: Record): PolicyResult { const role = obj.role; if (role !== "gate-merge-ng" && role !== "plan-ng" && role !== "review-ng") { return { ok: false, reason: "invalid-role-class", detail: "schema version 4 supports only gate-merge-ng, plan-ng, and review-ng" }; } const spec = V4_SPECS[role]; const rootFields = [ "schemaVersion", "role", "revision", "capabilities", "tools", "extensions", "workspace", "credentials", "evidence", "digestRules", "forbiddenTools", "shell", "goalReportPolicy", ...(spec.targetPolicy ? ["targetPolicy"] : []), ]; if (Object.keys(obj).length !== rootFields.length || Object.keys(obj).some((key) => !rootFields.includes(key))) { return { ok: false, reason: "unknown-field", detail: "schema version 4 manifest fields must be exact" }; } if (obj.schemaVersion !== 4) return { ok: false, reason: "invalid-schema-version", detail: "schemaVersion must be 4" }; if (obj.revision !== spec.revision) return { ok: false, reason: "invalid-revision", detail: `${role} must have revision ${spec.revision}` }; if (!exactCapabilitySet(obj.capabilities, spec.capabilities)) return { ok: false, reason: "version4-exact-set-mismatch", detail: "capabilities must be the exact reviewed role-class set" }; if (!sameStringSet(obj.tools, spec.tools)) return { ok: false, reason: "tools-bindings-mismatch", detail: "tools must be the exact reviewed bound capability projection" }; if (!exactObject(obj.extensions, { load: spec.extensions })) return { ok: false, reason: "invalid-extensions", detail: "extensions.load must be the exact reviewed order" }; if (!exactObject(obj.workspace, { readRoots: spec.readRoots })) return { ok: false, reason: "invalid-workspace", detail: "workspace.readRoots must be exact for the role class" }; if (!exactObject(obj.credentials, spec.credentials)) return { ok: false, reason: "invalid-credentials", detail: "credentials policy must be exact" }; if (!exactObject(obj.evidence, spec.evidence)) return { ok: false, reason: "invalid-evidence", detail: "evidence policy must be exact" }; if (!exactObject(obj.digestRules, V4_COMMON_POLICY)) return { ok: false, reason: "invalid-digest-rules", detail: "digest rules must be exact" }; if (!sameStringSet(obj.forbiddenTools, spec.forbiddenTools)) return { ok: false, reason: "invalid-forbidden-tools", detail: "forbidden tools must be the exact reviewed set" }; if (!exactObject(obj.shell, { mode: "denied" })) return { ok: false, reason: "invalid-shell", detail: "shell.mode must be denied" }; if (!exactObject(obj.goalReportPolicy, GOAL_POLICY_VALUES)) return { ok: false, reason: "invalid-goal-report-policy", detail: "goalReportPolicy must pin the reviewed contract exactly" }; if (spec.targetPolicy) { if (!exactObject(obj.targetPolicy, spec.targetPolicy)) return { ok: false, reason: "invalid-target-policy", detail: "targetPolicy must pin the reviewed assignment scope" }; } return { ok: true, manifest: { schemaVersion: 4, role, revision: spec.revision, capabilities: (obj.capabilities as CapabilityBinding[]).map((capability) => ({ ...capability })), tools: [...obj.tools as string[]], extensions: { load: [...(obj.extensions as { load: string[] }).load] }, workspace: { readRoots: [...(obj.workspace as { readRoots: string[] }).readRoots] }, credentials: { ...(obj.credentials as Record) }, evidence: { ...(obj.evidence as Record) }, digestRules: { ...V4_COMMON_POLICY }, forbiddenTools: [...obj.forbiddenTools as string[]], shell: { mode: "denied" }, goalReportPolicy: { ...GOAL_POLICY_VALUES }, ...(spec.targetPolicy ? { targetPolicy: { ...(spec.targetPolicy as { kind: "pull-request"; hosts: string[]; repositories: string[]; pinSource: "assignment" }) } } : {}), }, }; }