Files
stack/extensions/mosaic-core/lib/loader.ts
T

274 lines
10 KiB
TypeScript

// 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<string, unknown>;
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<LoaderIO["lstatSync"]>;
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<LoaderIO["lstatSync"]>;
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");
}