// lib/adapter.ts — Pi adapter binding (PRD R3-R5; NG-2). // // Deliberately dependency-light (type-only imports + node builtins) so the // hermetic suite can import and exercise the REAL binding against a fake pi // — including the AC4 red control, which sabotages this file's enforcement // registration block and proves the same call then passes. // // The binding: // session_start (every reason: startup/new/resume/reload/fork — policy may // have changed on disk between sessions) -> // resolve seat role from the launcher-established agent // name -> load trusted manifest -> build application -> // setActiveTools(exact) -> VERIFY getActiveTools() equals // the exact set; any drift at any stage -> fail-closed. // tool_call -> decideToolCall; blocked calls return the stable code. import { incarnationIdentity } from "./incarnation.ts"; import { buildApplication, decideToolCall, failClosedState, statusSnapshot, STATUS_TOOL, PROPOSAL_TOOL, SAFE_READ_TOOLS, type EnforcementState, } from "./enforce.ts"; import { loadTrustedManifest, seatRole, type LoadOutcome, type LoaderIO } from "./loader.ts"; import { createJournal, type Journal, type JournalIO } from "./journal.ts"; import { sanitizeProposal } from "./proposal.ts"; import { publishGoalPolicy, verifyGoalPolicyPublication, type GoalPolicyAttestationV1 } from "./goal-policy.ts"; import { readFileSync, lstatSync, realpathSync } from "node:fs"; import { homedir } from "node:os"; export interface PiToolCallEvent { toolName: string; input?: unknown; } export interface PiLike { on(event: "session_start", handler: (event: { reason: string }) => void | Promise): void; on(event: "tool_call", handler: (event: PiToolCallEvent) => unknown): void; getActiveTools(): string[]; setActiveTools(names: string[]): void; registerTool(tool: Record): void; } export interface AdapterOptions { env?: Record; io?: LoaderIO; journalIO?: JournalIO; stateHome?: string; incarnationId?: string; launchGeneration?: number; } function defaultIO(): LoaderIO { return { readFileSync(path: string): string | Uint8Array { return readFileSync(path); }, lstatSync, realpathSync, }; } function launchGeneration(env: Record, supplied: number | undefined): number | undefined { if (supplied !== undefined) return supplied; const raw = env.MOSAIC_LAUNCH_GENERATION; if (!raw || !/^[1-9][0-9]*$/.test(raw)) return undefined; const value = Number(raw); return Number.isSafeInteger(value) ? value : undefined; } export function bindEnforcement(pi: PiLike, opts: AdapterOptions = {}): void { const env = opts.env ?? process.env; const io = opts.io ?? defaultIO(); const incarnationId = opts.incarnationId ?? incarnationIdentity(); const configuredLaunchGeneration = launchGeneration(env, opts.launchGeneration); let enforcement: EnforcementState = { state: "not-applied", incarnationId }; let activeGoalPolicy: GoalPolicyAttestationV1 | undefined; // A version-4 role has exactly one trusted manifest/contract load per // incarnation. Reloads reuse this immutable result and never republish. let v4Load: Extract | undefined; let v4Published = false; const brainHome = env.MOSAIC_BRAIN_HOME || `${homedir()}/.mosaic`; const journal: Journal = createJournal({ incarnationId, stateHome: opts.stateHome ?? env.XDG_STATE_HOME, io: opts.journalIO, }); pi.registerTool({ name: STATUS_TOOL, label: "Mosaic policy status", description: "Read-only report of the applied mosaic-core role policy: role, revision, digest, " + "active tools, and enforcement state. Cannot modify anything.", parameters: { type: "object", properties: {}, additionalProperties: false }, async execute() { return { content: [{ type: "text", text: JSON.stringify(statusSnapshot(enforcement, pi.getActiveTools())) }], details: {}, }; }, }); pi.registerTool({ name: PROPOSAL_TOOL, label: "Mosaic improvement proposal", description: "Record a structured improvement proposal for the mosaic-core role policy. Proposals " + "are appended to the per-incarnation journal for coordinator review; they never " + "modify manifests, settings, or active tools.", parameters: { type: "object", properties: { target: { type: "string", description: "What the proposal concerns: a role, tool, or policy field" }, summary: { type: "string", description: "One line, at most 500 characters" }, motivation: { type: "string", description: "Why this improvement matters, at most 2000 characters" }, evidence: { type: "array", items: { type: "string" }, description: "Optional evidence pointers, at most 8 items of 500 characters" }, }, required: ["target", "summary", "motivation"], additionalProperties: false, }, async execute(_id: string, params: unknown) { const verdict = sanitizeProposal(params); if (!verdict.ok) { return { content: [{ type: "text", text: `proposal rejected (${verdict.reason}${verdict.detail ? `: ${verdict.detail}` : ""}) — recorded nothing` }], details: { rejected: verdict.reason }, }; } try { journal.append({ kind: "improvement-proposal", proposal: verdict.proposal }); } catch { // Journal failure: the proposal is NOT recorded, nothing is applied, // and the caller sees a fixed message with no raw exception return { content: [{ type: "text", text: "proposal recording unavailable (journal failure); recorded nothing; nothing applied" }], details: { recorded: false, cause: "journal-failure" }, }; } return { content: [{ type: "text", text: `proposal recorded for coordinator review (target: ${verdict.proposal.target}); nothing was applied` }], details: { recorded: true }, }; }, }); // [NG-2 enforcement binding begin] // The enforcement surface under AC4's removal red control: exactly these // two registrations. Sabotage that deletes this block leaves the status // tool but no interception and no application — the same call then passes. pi.on("session_start", async () => { // Resolve the profile role before entering degraded mode. A known gate role // gets status only if its manifest, contract, resolver, or publication // fails; it must not inherit generic safe reads or proposal authority. const profileRole = seatRole(brainHome, env.MOSAIC_AGENT_NAME, io); let roleHint = profileRole ?? undefined; // NG4-RECON: enter TRUSTED FAIL-CLOSED BEFORE any platform call. // If setActiveTools or getActiveTools throws mid-reconciliation, the // previous applied policy does NOT remain authoritative (R5). enforcement = failClosedState(incarnationId, "reconciliation-in-progress", brainHome, roleHint); try { let load: LoadOutcome; activeGoalPolicy = undefined; if (v4Load) { load = v4Load; roleHint = v4Load.manifest.role; } else { if (profileRole === null) { const stage = env.MOSAIC_AGENT_NAME ? "no-seat-profile" : "no-agent-name"; load = { ok: false, failure: { stage } as LoadOutcome extends { ok: false; failure: infer F } ? F : never }; } else { load = loadTrustedManifest(brainHome, profileRole, io, { incarnationId, launchGeneration: configuredLaunchGeneration }); if (load.ok) roleHint = load.manifest.role; } if (load.ok && load.manifest.schemaVersion === 4) v4Load = load; } if (load.ok && load.manifest.schemaVersion === 4) { const policy = load.goalPolicy?.attestation; if (!policy) { load = { ok: false, failure: { stage: "goal-policy-publication" } }; } else if (!v4Published) { const publication = publishGoalPolicy(policy, (reason) => { try { journal.append({ kind: "denial", tool: "goal_report", reason: `mosaic-core:goal-policy-denied:${reason}` }); } catch { console.warn("[mosaic-core] journal append failed (enforcement unaffected; details suppressed)"); } }); if (!publication.ok) { load = { ok: false, failure: { stage: "goal-policy-publication" } }; } else { v4Published = true; activeGoalPolicy = publication.attestation; } } else { activeGoalPolicy = policy; } } const application = buildApplication({ load, activeTools: pi.getActiveTools(), incarnationId, roleHint, brainHome, }); pi.setActiveTools(application.applyTools); const got = [...pi.getActiveTools()].sort(); const want = [...application.applyTools].sort(); if (JSON.stringify(got) !== JSON.stringify(want)) { enforcement = failClosedState(incarnationId, "postcondition-failed", brainHome, roleHint); // A mismatch is not merely a status value. Remove the broad/incorrect // active projection before returning; interception remains the final // denial if the platform rejects this recovery call too. try { pi.setActiveTools([...enforcement.allowed].sort()); } catch { /* interception governs */ } try { journal.append({ kind: "reconciliation", state: "fail-closed", cause: "postcondition-failed", removed: [], added: [] }); } catch (e) { console.warn("[mosaic-core] journal append failed (enforcement unaffected; details suppressed)"); } return; } enforcement = application.state; try { journal.append({ kind: "reconciliation", state: application.state.state, role: application.state.state === "applied" ? application.state.role : undefined, revision: application.state.state === "applied" ? application.state.revision : undefined, digest: application.state.state === "applied" ? application.state.digest : undefined, cause: application.state.state === "fail-closed" ? application.state.cause : undefined, removed: application.plan ? application.plan.toRemove : [], added: application.plan ? application.plan.toAdd : [], }); } catch (e) { console.warn("[mosaic-core] journal append failed (enforcement unaffected; details suppressed)"); } } catch (reconErr) { // Platform call threw mid-reconciliation: the provisional fail-closed // state GOVERNS — the old applied policy is dead. // NG4-CAUSE: STABLE cause only — never embed the raw exception. // status EXPOSES cause; an injected/platform error could carry // sensitive content into model-visible denial reasons. enforcement = failClosedState(incarnationId, "reconciliation-threw", brainHome, roleHint); try { pi.setActiveTools([...enforcement.allowed].sort()); } catch { /* best-effort; interception governs */ } try { journal.append({ kind: "reconciliation", state: "fail-closed", cause: "reconciliation-threw", removed: [], added: [] }); } catch { /* evidence never bypasses enforcement */ } } }); pi.on("tool_call", async (event: PiToolCallEvent) => { // F7-(1): wire event.input through so the read-containment check sees // the path argument. F7-B precision: for read-family tools (read, // grep, find, ls) with an ABSENT path, Pi executes against the tool's // own cwd — an uncontrolled location. We MUTATE the input to pin the // path to the trusted brain home (Pi guarantees mutation affects // execution and is not revalidated). The test suite asserts the // executed input IS pinned, not just the decision result. // F7E: read.path is REQUIRED in Pi — absent/malformed input is DENIED // by the enforcement layer (no synthesis, no cwd ambiguity). Only ls // gets absent-path pinning (it lists a directory, not a file, and its // path is genuinely optional in Pi). if ((SAFE_READ_TOOLS as readonly string[]).includes(event.toolName)) { // (2) ls with UNDEFINED input: assign {path: brainHome} (safe dir listing) // FIX2-(1): synthesize ONLY when event.input is EXACTLY undefined. // null / 42 / [] are malformed and must reach stable DENIAL, not be // silently mutated into a valid input. if (event.toolName === "ls" && event.input === undefined) { event.input = { path: brainHome }; } if (event.input && typeof event.input === "object") { const input = event.input as { path?: unknown }; // ls with absent path in an existing object: pin to brainHome if (event.toolName === "ls" && (input.path === undefined || input.path === "")) { input.path = brainHome; } // (3) Resolve RELATIVE paths against brainHome BEFORE realpath if (typeof input.path === "string" && input.path !== "" && !input.path.startsWith("/")) { input.path = brainHome + "/" + input.path; } // F7C-(1): realpath existing targets — symlink escape resolution if (typeof input.path === "string" && input.path !== "") { try { input.path = realpathSync(input.path); } catch { // path doesn't exist yet — leave as-is; containment handles traversal } } } } if (event.toolName === "goal_report" && activeGoalPolicy) { const policy = verifyGoalPolicyPublication(activeGoalPolicy); if (!policy.ok) { const reason = `mosaic-core:goal-policy-denied:${policy.reason}`; try { journal.append({ kind: "denial", tool: event.toolName, reason }); } catch { console.warn("[mosaic-core] journal append failed (enforcement unaffected; details suppressed)"); } return { block: true, reason }; } } const decision = decideToolCall(enforcement, event.toolName, event.input, brainHome); if (decision.block) { // PART B: evidence failure must NEVER bypass enforcement — the denial // returns even if the journal append throws (disk full, permission, // injected failure) try { journal.append({ kind: "denial", tool: event.toolName, reason: decision.reason }); } catch (journalErr) { console.warn("[mosaic-core] journal append failed (enforcement unaffected; details suppressed)"); } return { block: true, reason: decision.reason }; } return undefined; }); // [NG-2 enforcement binding end] }