// lib/loader.ts — trusted-path role-manifest and v4 Goal-policy loading. // // Version-3 loading retains its established manifest path and digest behavior. // Version-4 adds one trusted, non-symlinked contract read at session start and // emits an immutable policy attestation. There is no cwd-relative path, // environment override, second manifest reader, or policy fallback. import { createHash } from "node:crypto"; import { validateManifest, type RoleManifest, type RoleManifestV4 } from "./policy.ts"; import { GOAL_POLICY_ENFORCEMENT, GOAL_POLICY_IDENTIFIER_RESOLUTION, GOAL_REPORT_CONTRACT_BLOB, GOAL_REPORT_CONTRACT_PATH, GOAL_REPORT_CONTRACT_SECTION, GOAL_REPORT_CONTRACT_SHA256, GOAL_REPORT_FORMAT, isOpaqueGoalPolicyId, type GoalPolicyAttestationV1, } from "./goal-policy.ts"; export const MANIFEST_BASENAME = "mosaic-core.manifest.json"; export type LoadFailure = | { stage: "no-agent-name" } | { stage: "no-seat-profile" } | { stage: "no-role-in-profile" } | { stage: "manifest-unreadable"; detail: string } | { stage: "manifest-symlink" } | { stage: "invalid-json"; detail: string } | { stage: "schema"; reason: string; detail?: string } | { stage: "role-mismatch"; expected: string; got: string } | { stage: "goal-policy-unreadable" } | { stage: "goal-policy-root-symlink" } | { stage: "goal-policy-parent-symlink" } | { stage: "goal-policy-symlink" } | { stage: "goal-policy-path-escape" } | { stage: "goal-policy-non-file" } | { stage: "goal-policy-invalid-utf8" } | { stage: "goal-policy-digest" } | { stage: "goal-policy-section" } | { stage: "goal-policy-launch-generation" } | { stage: "goal-policy-incarnation" } | { stage: "goal-policy-publication" }; export interface TrustedGoalPolicy { attestation: GoalPolicyAttestationV1; } export type LoadOutcome = | { ok: true; manifest: RoleManifest; digest: string; path: string; goalPolicy?: TrustedGoalPolicy } | { ok: false; failure: LoadFailure }; export interface LoaderIO { readFileSync(path: string): string | Uint8Array; lstatSync(path: string): { isSymbolicLink(): boolean; isFile(): boolean }; /** Required for v4 contract containment. Missing capability fails v4 closed. */ realpathSync?(path: string): string; } export interface LoadContext { incarnationId?: string; launchGeneration?: number; } export function manifestPath(brainHome: string, role: string): string { return `${brainHome}/fleet/roles/${role}/${MANIFEST_BASENAME}`; } export function seatRole(brainHome: string, agentName: string | undefined, io: LoaderIO): string | null { if (!agentName || !/^[a-z][a-z0-9-]*$/.test(agentName)) return null; let profileRaw: string; try { profileRaw = decodeUtf8(io.readFileSync(`${brainHome}/fleet/agents/${agentName}/profile.json`)); } catch { return null; } try { const profile = JSON.parse(profileRaw) as Record; const role = profile.role; if (typeof role === "string" && /^[a-z][a-z0-9-]*$/.test(role)) return role; } catch { // fall through } return null; } /** Trusted manifest loader. v4 context is mandatory only for a v4 manifest. */ export function loadTrustedManifest(brainHome: string, role: string, io: LoaderIO, context: LoadContext = {}): LoadOutcome { const path = manifestPath(brainHome, role); let st: ReturnType; try { st = io.lstatSync(path); } catch (error) { return { ok: false, failure: { stage: "manifest-unreadable", detail: String(error) } }; } if (st.isSymbolicLink()) return { ok: false, failure: { stage: "manifest-symlink" } }; let rawBytes: Uint8Array; let raw: string; try { rawBytes = toBytes(io.readFileSync(path)); raw = decodeUtf8(rawBytes); } catch (error) { return { ok: false, failure: { stage: "manifest-unreadable", detail: String(error) } }; } let parsed: unknown; try { parsed = JSON.parse(raw); } catch (error) { return { ok: false, failure: { stage: "invalid-json", detail: String(error) } }; } const verdict = validateManifest(parsed); if (!verdict.ok) { return { ok: false, failure: { stage: "schema", reason: verdict.reason ?? "invalid", detail: verdict.detail } }; } if (verdict.manifest!.role !== role) { return { ok: false, failure: { stage: "role-mismatch", expected: role, got: verdict.manifest!.role } }; } const digest = sha256(rawBytes); if (verdict.manifest!.schemaVersion !== 4) { return { ok: true, manifest: verdict.manifest!, digest, path }; } const goalPolicy = loadGoalPolicy(brainHome, verdict.manifest!, digest, io, context); if (!goalPolicy.ok) return goalPolicy; return { ok: true, manifest: verdict.manifest!, digest, path, goalPolicy: goalPolicy.goalPolicy }; } function loadGoalPolicy( brainHome: string, manifest: RoleManifestV4, manifestSha256: string, io: LoaderIO, context: LoadContext, ): { ok: true; goalPolicy: TrustedGoalPolicy } | { ok: false; failure: LoadFailure } { const policy = manifest.goalReportPolicy; // validateManifest already pins every field. Repeat explicit constants here // so a future policy-validator regression cannot silently change the loader. if ( policy.format !== GOAL_REPORT_FORMAT || policy.contractPath !== GOAL_REPORT_CONTRACT_PATH || policy.contractSection !== GOAL_REPORT_CONTRACT_SECTION || policy.contractBlob !== GOAL_REPORT_CONTRACT_BLOB || policy.contractSha256 !== GOAL_REPORT_CONTRACT_SHA256 || policy.enforcement !== GOAL_POLICY_ENFORCEMENT || policy.identifierResolution !== GOAL_POLICY_IDENTIFIER_RESOLUTION ) { return { ok: false, failure: { stage: "goal-policy-digest" } }; } const trustedPath = trustedContractPath(brainHome, policy.contractPath, io); if (!trustedPath.ok) return { ok: false, failure: { stage: trustedPath.stage } }; const contractPath = trustedPath.path; let stat: ReturnType; try { stat = io.lstatSync(contractPath); } catch { return { ok: false, failure: { stage: "goal-policy-unreadable" } }; } if (stat.isSymbolicLink()) return { ok: false, failure: { stage: "goal-policy-symlink" } }; if (!stat.isFile()) return { ok: false, failure: { stage: "goal-policy-non-file" } }; let bytes: Uint8Array; let source: string; try { bytes = toBytes(io.readFileSync(contractPath)); source = decodeUtf8(bytes); } catch { return { ok: false, failure: { stage: "goal-policy-invalid-utf8" } }; } if (sha256(bytes) !== policy.contractSha256 || gitBlobSha1(bytes) !== policy.contractBlob) { return { ok: false, failure: { stage: "goal-policy-digest" } }; } const heading = `## ${policy.contractSection}`; const first = source.indexOf(heading); if (first < 0 || source.indexOf(heading, first + heading.length) !== -1) { return { ok: false, failure: { stage: "goal-policy-section" } }; } const generation = context.launchGeneration; if (typeof generation !== "number" || !Number.isSafeInteger(generation) || generation < 1) { return { ok: false, failure: { stage: "goal-policy-launch-generation" } }; } if (!isOpaqueGoalPolicyId(context.incarnationId)) { return { ok: false, failure: { stage: "goal-policy-incarnation" } }; } const attestation: GoalPolicyAttestationV1 = Object.freeze({ schemaVersion: 1, role: manifest.role, roleRevision: manifest.revision, manifestSha256, format: policy.format, contractPath: policy.contractPath, contractSection: policy.contractSection, contractBlob: policy.contractBlob, contractSha256: policy.contractSha256, enforcement: policy.enforcement, identifierResolution: policy.identifierResolution, launchGeneration: generation, incarnationId: context.incarnationId, }); return { ok: true, goalPolicy: { attestation } }; } type TrustedContractPath = | Readonly<{ ok: true; path: string }> | Readonly<{ ok: false; stage: "goal-policy-unreadable" | "goal-policy-root-symlink" | "goal-policy-parent-symlink" | "goal-policy-symlink" | "goal-policy-path-escape"; }>; /** * Resolve each path component without allowing a contract parent to redirect * a trusted-root read. The leaf check alone is insufficient because * `skills-local` itself can be a symlink to matching bytes outside the brain. */ function trustedContractPath(brainHome: string, relative: string, io: LoaderIO): TrustedContractPath { if (relative !== GOAL_REPORT_CONTRACT_PATH || relative.startsWith("/") || relative.split("/").some((part) => part === "" || part === "." || part === "..")) { return Object.freeze({ ok: false, stage: "goal-policy-unreadable" }); } const root = brainHome.replace(/\/+$/, ""); if (!root.startsWith("/") || root === "" || !io.realpathSync) { return Object.freeze({ ok: false, stage: "goal-policy-unreadable" }); } try { const rootStat = io.lstatSync(root); if (rootStat.isSymbolicLink()) return Object.freeze({ ok: false, stage: "goal-policy-root-symlink" }); const canonicalRoot = io.realpathSync(root).replace(/\/+$/, ""); if (!canonicalRoot.startsWith("/") || canonicalRoot === "") { return Object.freeze({ ok: false, stage: "goal-policy-unreadable" }); } const parts = relative.split("/"); let path = root; for (let index = 0; index < parts.length; index++) { path = `${path}/${parts[index]}`; const stat = io.lstatSync(path); if (stat.isSymbolicLink()) { return Object.freeze({ ok: false, stage: index === parts.length - 1 ? "goal-policy-symlink" : "goal-policy-parent-symlink" }); } } const canonicalPath = io.realpathSync(path); if (canonicalPath !== canonicalRoot && !canonicalPath.startsWith(`${canonicalRoot}/`)) { return Object.freeze({ ok: false, stage: "goal-policy-path-escape" }); } return Object.freeze({ ok: true, path }); } catch { return Object.freeze({ ok: false, stage: "goal-policy-unreadable" }); } } function toBytes(value: string | Uint8Array): Uint8Array { return typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value); } function decodeUtf8(value: string | Uint8Array): string { if (typeof value === "string") return value; return new TextDecoder("utf-8", { fatal: true }).decode(value); } export function sha256(value: string | Uint8Array): string { return createHash("sha256").update(value).digest("hex"); } export function gitBlobSha1(bytes: Uint8Array): string { const header = Buffer.from(`blob ${bytes.byteLength}\0`, "utf8"); return createHash("sha1").update(header).update(bytes).digest("hex"); }