import { AihError } from "../../../errors.js"; /** * D7 plugin-content identity verification for the Claude host, plus the * machine-scope ownership + cache-locator conventions the plugin services * ({@link ./plugins.ts}) build on. * * D7 requires that the bytes the host actually LOADS are the bytes AIH scanned. * After the host materializes a plugin into its loadable cache, this module * re-digests that cache tree with the SAME canonical routine the scan gate uses * ({@link hashComponentTree} — the routine `resolveGitSource` folds a checkout * with) and compares it to the scanned `treeDigest`. A mismatch fails closed; * only an exact match may be recorded in the lock (`scannedDigest` == `loadedDigest`). * * `scan-gate.ts` is a READ-ONLY module, so its module-private top-level-path * selection cannot be imported; {@link loadedTopLevelPaths} mirrors it exactly * (every entry except `.git`, sorted) and feeds the SAME `hashComponentTree`, so * a loaded tree digests comparably to a scanned one — no second digest algorithm. */ /** * The user's home directory — injected env first (hermetic tests), then the OS. * Mirrors `homeDir` in `src/internals/cli-detect.ts` (USERPROFILE || HOME || os). */ export declare function claudeHomeDir(env: NodeJS.ProcessEnv): string; /** * Machine-scoped binding effects (marketplace registration, the plugin cache * under the user's Claude dir) are recorded as OWNERSHIP entries whose `target` * carries a `home:`-prefixed, POSIX, repo-style path. This is schema-legal today: * the lock's `ownership[].target` is a free string (only `writes[].path` is * `SafeRelPath`-bound). Repo-relative D18 writes keep bare relative targets; a * `home:` prefix is the single, documented marker that a target lives under the * user's home rather than the project root, so removal can route it to the * machine-scope reconciler instead of the repo-relative one. */ export declare const HOME_OWNERSHIP_PREFIX = "home:"; /** `/.claude/plugins` — the Claude plugins state root (repo-style POSIX). */ export declare const CLAUDE_PLUGINS_DIR_REL = ".claude/plugins"; /** `/.claude/plugins/cache` — where installed plugin trees materialize. */ export declare const CLAUDE_PLUGINS_CACHE_REL = ".claude/plugins/cache"; /** * `/.claude/settings.json` — where the host records registered * marketplaces (empirically verified on 2.1.214). A DIFFERENT file from the * repo-relative project `.claude/settings.json` `surfaces.ts` exports as * `CLAUDE_SETTINGS_PATH` (same relative shape, different root — home vs. * project); named distinctly here so the two are never conflated. * * `plugins/config.json` — this module's PRE-empirical model — is NOT used by * the real host; there is no exported constant for it (dead, deleted, not * repointed). The host ALSO derives `plugins/known_marketplaces.json` and * `plugins/installed_plugins.json` as its own read caches — AIH never owns or * writes either; they exist only as context for a future reader of this file. */ export declare const CLAUDE_HOME_SETTINGS_REL = ".claude/settings.json"; /** Top-level key in the home settings file holding the marketplace map. */ export declare const CLAUDE_EXTRA_KNOWN_MARKETPLACES_KEY = "extraKnownMarketplaces"; /** Ownership target for a registered marketplace (kind `json-pointer`). */ export declare function homeMarketplaceTarget(marketplace: string): string; /** * Ownership target for a materialized plugin cache tree (kind `file`). * Empirically corrected in the W4 live run: the host materializes to * `cache////` (see * {@link defaultPluginCacheLocator}), NOT `cache/` — ownership * roots at the plugin level so every version directory under it is covered by * the same recorded surface. */ export declare function homePluginCacheTarget(marketplace: string, plugin: string): string; /** True for a machine-scope ownership target (the `home:`-prefixed convention). */ export declare function isHomeScopedTarget(target: string): boolean; export interface PluginCacheLocatorParams { /** Resolved user home dir (see {@link claudeHomeDir}). */ home: string; /** The marketplace name the plugin was installed from. */ marketplace: string; /** The plugin name. */ plugin: string; /** The `@` enable/cache key. */ pluginKey: string; /** The scanned checkout registered as the marketplace source (`resolved.treePath`). */ marketplaceSourcePath: string; /** * The plugin's version (from its own `.claude-plugin/plugin.json`, e.g. via * {@link pluginSourceSubtreeDigest}), when known. Refines the cache-path * guess to the exact versioned directory; callers should prefer * {@link installPathFromPluginList}'s authoritative path over this locator * whenever the host's own report is available. */ version?: string; } /** Resolve the on-disk tree the host will LOAD for an installed plugin (D7 subject). */ export type PluginCacheLocator = (params: PluginCacheLocatorParams) => string; /** * The DEFAULT cache layout — empirically verified on 2.1.214 (2026-07): * `/.claude/plugins/cache////`, containing * the plugin SOURCE SUBTREE only (not the whole marketplace checkout). * * When `version` is unknown, this falls back to the `/` * PARENT directory — a documented, less-precise guess (it may contain more * than one version subdirectory, or none yet). Callers should prefer * {@link installPathFromPluginList}'s authoritative, host-reported path * whenever available and treat this default as the last-resort guess it is. */ export declare const defaultPluginCacheLocator: PluginCacheLocator; /** * Extract the authoritative `installPath` for `pluginKey` from a parsed * `claude plugin list --json` payload — shape (2.1.214, empirical): * `{version: 2, plugins: {"@": [{scope, installPath, version, * installedAt, lastUpdated, projectPath}]}}`. When more than one scope entry * exists for the key, the first is used (the payload carries no scope filter * here; a caller needing a specific scope should inspect the raw payload * itself). The host may report a long-form path, so the result is normalized. * * Callers PREFER this over {@link defaultPluginCacheLocator}'s layout guess — * it is the host's own report of where it materialized the plugin, not a * guess. NEVER throws: any absent key, wrong shape, or missing field yields * `undefined` so the caller can fall back to the locator instead of failing * the whole bind over an inventory-parsing hiccup. */ export declare function installPathFromPluginList(listPayload: unknown, pluginKey: string): string | undefined; /** Fail-closed plugin-service error (CLI failure, unparseable output, bad name, empty/unreadable cache). */ export declare class ClaudePluginError extends AihError { constructor(message: string); } /** * The materialized cache tree the locator points at does not EXIST — the one * case conservative removal may treat as an idempotent "already gone". It is a * DISTINCT subclass so the reconciler can discriminate a genuinely missing tree * from a present-but-unreadable one (an unreadable/empty tree stays a plain * {@link ClaudePluginError} and is preserved + reported, never dropped). */ export declare class ClaudePluginCacheMissingError extends ClaudePluginError { } /** D7 identity fields recorded in the lock (`match` is `scannedDigest === loadedDigest`). */ export interface PluginIdentity { scannedDigest: string; loadedDigest: string; match: boolean; } /** * A D7 fail-closed: the tree the host would load does not hash to the scanned * digest. Carries the {@link PluginIdentity} so the caller can surface both * digests; the binding must NOT write a lock when this is thrown. */ export declare class ClaudePluginIdentityError extends ClaudePluginError { readonly identity: PluginIdentity; constructor(message: string, identity: PluginIdentity); } /** {@link pluginSourceSubtreeDigest}'s result. */ export interface PluginSourceSubtree { /** The resolved on-disk path of the plugin's own source subtree. */ subtreePath: string; /** The sha256 tree digest of `subtreePath` (the D7 anchor — see the module doc). */ digest: string; /** The plugin's own version, from its `.claude-plugin/plugin.json`. */ version: string; } /** * The marketplace name the checkout's own manifest declares. The claude host * registers a marketplace under THIS name — `claude plugin marketplace add` * takes no name argument, so the registrar never chooses it (W4 live-run * correction). `bindPlugin` asserts the adapter's pinned expectation matches * this value before any host mutation; a missing or malformed name fails * closed the same way. */ export declare function marketplaceManifestName(checkoutPath: string): string; /** * The D7 anchor (empirically corrected): digest the plugin's own SOURCE * SUBTREE — the exact bytes `claude plugin install` materializes — not the * whole scanned checkout. Reads `.claude-plugin/marketplace.json` from * `checkoutPath` to find `plugin`'s `source` entry, resolves it INSIDE the * checkout (fail closed on an absolute or traversing source), then digests * that subtree with the SAME `hashComponentTree` non-`.git` routine * `resolveGitSource`/{@link hashLoadedPluginTree} use. A degenerate `"./"` (or * `"."`, or empty) source — the common single-plugin-at-root marketplace * shape (e.g. obra/superpowers) — resolves to the checkout root itself, so * its digest EQUALS the checkout's own whole-tree digest. * * The whole-checkout identity (repository/commitSha/treeDigest) remains the * D7 upstream authority enforced by the scan disposition + declaration * (`assertResolvedMatchesDeclaration`); THIS digest is what the lock's * `scannedDigest` means from here on — the scanned bytes the host actually * loads, which for a multi-plugin marketplace is a strict subset of the * checkout. */ export declare function pluginSourceSubtreeDigest(checkoutPath: string, plugin: string): PluginSourceSubtree; /** * Digest a materialized plugin cache tree the SAME way the scan gate digests a * checkout: {@link hashComponentTree} over the non-`.git` top-level roots. Fails * closed when the tree is missing (the host did not materialize the cache the * locator points at). */ export declare function hashLoadedPluginTree(treePath: string): string; /** * D7 verification: digest the loaded tree and compare it to the scanned digest, * returning the exact `{scannedDigest, loadedDigest, match}` the lock records. * This function only COMPUTES the verdict (and fails closed when the tree cannot * be read); the caller enforces the fail-closed cleanup on a `match: false`. */ export declare function verifyPluginIdentity(scannedDigest: string, loadedTreePath: string): PluginIdentity;