import { type ActiveManagedMcpProjectionOwnership, type ManagedMcpProjectionOwnership } from "../config/marker.js"; import type { Cli } from "../internals/clis.js"; import { type Action, type PlanContext, type WriteAction } from "../internals/plan.js"; import type { ManagedMcpAllowlistSettings } from "./allowlist.js"; /** * THE ONE managed-MCP projection lifecycle. Every command that records, subtracts, * or reports the Claude managed-MCP allowlist shares this module — `aih policy * project`, `aih mcp`, `aih prune`, `aih uninstall`, and the doctor probes — so the * ownership rules exist in exactly one place (issues #566, #567, #568). It replaces * the copies that had grown in `src/org-policy/project.ts` and `src/mcp/index.ts`. * * The rules are bounded by what the marker actually proves: * * - The marker records the exact expected VALUES plus a hash for exactly two keys, * `allowManagedMcpServersOnly` and `allowedMcpServers`, so key-level subtraction * of those two is authorized on an exact match and NOTHING else is. * - FILE DELETION IS NEVER AUTHORIZED. The marker proves two keys, never the file. * - `organizationPolicy` / `sandbox` are never subtracted: no provenance is * recorded, and `sandbox` is co-written by `aih guardrails` and `aih sandbox`. * - A missing, malformed, revoked, or hash-invalid marker — or a live pair that * drifted from the record — is REPORT-ONLY. The claim is revoked; the file is * never mutated. * * Ordering mirrors the ECC reconciliation driver (`src/ecc/reconcile-driver.ts:485`, * `:511`, `:514`): owned content first, ownership state next, ledger last. Clearing * the ownership record before the subtraction lands would destroy the only evidence * that the residue was ever aih's. Every action here carries an apply-time content * pin, and the executor stages them all in ONE filesystem transaction, so an * interrupted apply leaves either the old state or the new one — never a cleared * marker beside unsubtracted content. */ /** * The ONLY two keys the ownership marker records provenance for, so the only two * aih may ever subtract. `organizationPolicy` / `sandbox` carry no provenance (and * `sandbox` is co-written by `aih guardrails` and `aih sandbox`), so they are * reported but never removed. */ export declare const MANAGED_MCP_PROJECTION_KEYS: readonly ["allowManagedMcpServersOnly", "allowedMcpServers"]; /** The one projected managed-settings path; its owning CLI scopes every probe on it. */ export declare const MANAGED_SETTINGS_PATH = ".claude/managed-settings.json"; /** * Return the Claude managed-MCP fields only when their on-disk pair exactly * matches an AIH projection. This excludes same-key operator configuration. */ export declare function matchingGeneratedManagedMcpProjectionKeys(value: unknown, generated: ManagedMcpAllowlistSettings): readonly string[]; export declare function matchesManagedMcpProjectionOwnership(value: unknown, ownership: ManagedMcpProjectionOwnership | undefined): ownership is ManagedMcpProjectionOwnership; /** Why aih cannot prove it owns what is on disk — the escalation reason an agent reads. */ export type UnprovableReason = "no-ownership-record" | "not-a-regular-file" | "settings-absent" | "pair-drifted"; export interface ManagedMcpProjectionResidue { /** Repo-relative path of the projected managed settings. */ path: string; /** The recorded, still-active ownership claim (an inactive claim never reaches here). */ ownership: ActiveManagedMcpProjectionOwnership; /** True only when the live pair EXACTLY matches the recorded projection. */ matches: boolean; /** Why `matches` is false — `undefined` when it is true. */ unprovable: Exclude | undefined; /** Marker bytes observed while planning (the apply-time pin for marker writes). */ markerSource: string | undefined; /** Managed-settings bytes observed while planning (the apply-time pin for the subtraction). */ settingsSource: string | undefined; } /** Human-readable escalation reason for a residue aih must not touch. */ export declare function unprovableResidueReason(reason: UnprovableReason): string; /** * NO-FOLLOW presence: does anything at all occupy this path? `lstat`, never * `existsSync` (which follows links, so a DANGLING symlink would read as absent and * the stale ownership claim would survive untouched) and never a content read (which * throws EISDIR on a directory). */ export declare function occupied(abs: string): boolean; /** * True when any PARENT directory between `root` and the file is a symlink. The * no-follow read guards only the leaf, but the executor refuses a symlinked parent * outright (`assertNoSymlinkParents`) — so classifying such a path as repairable * would name `aih prune` for a finding prune is guaranteed to refuse, breaking the * one agent-facing promise this lifecycle makes: the command it names clears it. */ export declare function hasSymlinkParent(root: string, rel: string): boolean; /** * The projected managed-settings bytes, or `undefined` when the path is anything * other than a readable regular file. Callers that need the on-disk content for an * apply-time pin must use THIS, not `readIfExists`: a directory planted at the path * makes a plain content read throw EISDIR and take the whole command down. */ export declare function readManagedSettings(root: string, settingsRel?: string): string | undefined; /** * The on-disk managed-MCP ownership state, or `undefined` when the marker records no * ACTIVE claim (absent / malformed / revoked / hash-invalid) — the case where aih has * nothing to reconcile and must not touch the file. * * The settings read is a no-follow, regular-file read ({@link readRegularFile}), not * a plain `readIfExists`: a symlink substituted for the projected path is refused * rather than followed, so a subtraction can never be redirected onto a file the * ownership claim does not cover. */ export declare function managedMcpProjectionOnDisk(root: string, settingsRel?: string): ManagedMcpProjectionResidue | undefined; /** Read-only ownership verdict shared by policy report and doctor consumers. */ export declare function managedMcpProjectionState(root: string): { state: "clean" | "missing" | "altered" | "revoked" | "malformed" | "unsafe-path"; detail: string; }; /** Bind a write to the exact bytes observed while planning (apply-time content pin). */ export declare function withExpectedContents(action: WriteAction, contents: string | undefined): WriteAction; /** * Subtract ONLY the two marker-proven keys, in place. Never a delete: every other * key in the file — operator content, `organizationPolicy`, `sandbox` — is * merge-preserved byte-for-byte. */ export declare function managedMcpSubtractionAction(residue: ManagedMcpProjectionResidue, describe?: string): WriteAction; /** Record ownership of a freshly projected managed-MCP pair. */ export declare function managedMcpProjectionOwnershipAction(ctx: PlanContext, targets: readonly Cli[] | readonly string[], generated: ManagedMcpAllowlistSettings): Action; /** Drop the ownership record after its content was successfully subtracted. */ export declare function clearManagedMcpProjectionOwnershipAction(source: string | undefined): Action; /** Give up the claim on content aih can no longer prove it wrote, without touching it. */ export declare function revokeManagedMcpProjectionOwnershipAction(ownership: ManagedMcpProjectionOwnership, source: string | undefined): Action; /** * The ordered deactivation pair: owned-content subtraction FIRST, ownership state * SECOND. An unprovable residue yields the revoke alone — the file is left exactly * as the operator left it. */ export declare function managedMcpDeactivationActions(residue: ManagedMcpProjectionResidue, describe?: string): Action[];