import { type PlanResult } from "../../../internals/execute.js"; import { type Action } from "../../../internals/plan.js"; import type { Runner } from "../../../internals/proc.js"; import type { BindingLock, BindingOwnershipEntry, BindingWrite } from "../../lock.js"; import { type ResolvedGitSource, type ScanDisposition } from "../../scan-gate.js"; import { type PluginCacheLocator, type PluginIdentity } from "./plugin-identity.js"; import type { ClaudeDriftEntry } from "./removal.js"; /** * Claude host plugin-binding SERVICES (W3b) — the bind/verify/remove plugin * lifecycle a W4 `host-plugin` FrameworkAdapter calls. This module is NOT a * FrameworkAdapter, a CLI command, or a contamination/context-cost surface; it is * the host-side machinery under the D6 `provision`/`remove` steps. * * MECHANISM (orchestrator ruling; pinned on Claude Code 2.1.214). A binding starts * from an already-scan-authorized source (W2 produced a brand-protected * {@link ScanDisposition} for the exact digest of the resolved checkout). The * service ASSERTS that authorization ({@link assertProvisionAuthorized}) — it never * re-implements policy — then: * 1. registers the SCANNED CHECKOUT ITSELF as the marketplace source * (`claude plugin marketplace add `), so the bytes the host * installs are the bytes AIH scanned, by construction; * 2. materializes the plugin into the loadable cache * (`claude plugin install @ --scope project|local`); * 3. re-digests that cache tree and compares it to the scanned digest (D7). A * mismatch FAILS CLOSED: it disables + uninstalls and throws, writing no lock; * 4. on a match, records the D18-owned project enable + the machine-scope * ownership the lock reconciles on removal. * * PLAN-vs-DIRECT-RUNNER (D14). The repo-relative D18 field write — `enabledPlugins` * in `.claude/settings.json` — flows through the canonical W3a plan path * ({@link ClaudeManagedWriteEngine} -> `executePlan` -> {@link finalizeClaudeOwnership}), * keeping plan/apply separate for the owned project surface. The `claude plugin …` * lifecycle is IMPERATIVE through the injected {@link Runner}, because those are * machine-scoped side effects — not repo-relative writes `executePlan` contains — * and D7 verification is a mid-sequence control-flow gate (install -> digest -> * conditionally uninstall + abort) a static action list cannot express. This * mirrors `scan-gate.ts`, which drives git through the Runner directly rather than * through a plan. * * enabledPlugins WRITER (design tension; chosen: AIH authors it). `claude plugin * install --scope project` itself writes `enabledPlugins` (D4.1). D18 requires the * LOCK to own that field. This service AUTHORS the field via {@link ClaudeManagedWriteEngine} * (option (b)), because that reuses the fully-tested W3a D18 path with an * AIH-authored value: removal is byte-exact (`planClaudeRemoval`: equal -> restore * pre-existing / prune, drift -> preserve + report) and re-bind renders identical * bytes (`unchanged`, no backup churn). The pre-existing state is captured BEFORE * any CLI call (the engine reads at `jsonField` time), so even though the CLI also * flips the same bit, the lock records the true pre-AIH state and stays the single * D18 owner. The alternative — letting the CLI write it and capturing ownership * post-hoc (option (a)) — records a value AIH did not author and leaves the CLI as * a co-writer of a field the lock claims to own; it is strictly weaker on the * "exact removal + idempotent re-bind" criterion. */ /** Plugin scope: `project` -> `.claude/settings.json`; `local` -> `.claude/settings.local.json`. */ export type PluginScope = "project" | "local"; /** Validate a plugin or marketplace name (safe-key rules + CLI/pointer atomicity). */ export declare function assertSafePluginName(name: string, kind: "plugin" | "marketplace"): void; /** The `@` key used for install/enable and the `enabledPlugins` map. */ export declare function pluginEnableKey(plugin: string, marketplace: string): string; /** The `.claude/settings*.json` surface a scope's `enabledPlugins` field lives in. */ export declare function settingsFileForScope(scope: PluginScope): string; export interface PluginCliDeps { /** The subprocess seam — a fake in tests; never spawns a real `claude` there. */ runner: Runner; /** Environment for the CLI (home-dir resolution). Defaults to none. */ env?: NodeJS.ProcessEnv; /** Per-call timeout override. */ timeoutMs?: number; } /** * Register a LOCAL scanned checkout as a marketplace source. The path must be * absolute (it is `resolved.treePath`, an AIH-owned cache path) — a relative or * `-`-leading value is refused so nothing user-influenced can be read as a CLI flag. */ export declare function marketplaceAdd(deps: PluginCliDeps, sourcePath: string): Promise; /** Remove a registered marketplace by name. */ export declare function marketplaceRemove(deps: PluginCliDeps, marketplace: string): Promise; /** Install (materialize) a plugin from a marketplace at the given scope. */ export declare function installPlugin(deps: PluginCliDeps, plugin: string, marketplace: string, scope: PluginScope): Promise; /** Enable an installed plugin (lifecycle wrapper). */ export declare function enablePlugin(deps: PluginCliDeps, plugin: string, marketplace: string): Promise; /** Disable an installed plugin (lifecycle wrapper). */ export declare function disablePlugin(deps: PluginCliDeps, plugin: string, marketplace: string): Promise; /** * Uninstall an installed plugin (lifecycle + removal wrapper). The scope is * REQUIRED: `claude plugin uninstall` defaults to `--scope user` (2.1.214 * empirical, W4 live-run correction), so a project/local install must be * uninstalled at its own scope or the host refuses with "enabled at project * scope". */ export declare function uninstallPlugin(deps: PluginCliDeps, plugin: string, marketplace: string, scope: PluginScope): Promise; /** `claude plugin list --json`, parsed. Fails closed on a non-zero exit or unparseable JSON. */ export declare function listPlugins(deps: PluginCliDeps): Promise; /** * `claude plugin details @`, raw stdout TEXT. * * Empirically corrected (2.1.214): this CLI has NO `--json` flag — its * output is a human-readable component inventory (Skills/Agents/Hooks/MCP * servers/LSP servers with counts and names) plus a host-projected token-cost * line (`Always-on: ~N tok`). Parse it with * `contextCostFromPluginDetailsText` in `./context-cost.js`. Fails closed * ONLY on a non-zero exit / spawn failure — there is no JSON to fail to parse. */ export declare function pluginDetails(deps: PluginCliDeps, plugin: string, marketplace: string): Promise; export interface BindPluginRequest { /** The brand-protected disposition W2 minted for the exact source digest (D12). */ disposition: ScanDisposition; /** The scanned checkout: `treePath` (bytes to register) + `treeDigest` (D7 anchor). */ resolved: ResolvedGitSource; /** The plugin name to install. */ plugin: string; /** The marketplace name to register the scanned checkout under. */ marketplace: string; /** Enable scope; defaults to `project`. */ scope?: PluginScope; /** * On a re-bind, the prior lock. When it already owns the `enabledPlugins` field, * its ORIGINAL pre-existing value is preserved instead of re-reading disk (which * would capture the prior bind's own value), so removal still restores the true * pre-AIH state. */ previousLock?: BindingLock; } export interface BindPluginDeps { /** The project root the D18 `enabledPlugins` field is owned under. */ root: string; /** The subprocess seam for the `claude plugin …` lifecycle. */ runner: Runner; /** Environment (home-dir resolution for machine-scope targets + the cache locator). */ env?: NodeJS.ProcessEnv; /** Injectable cache locator; defaults to {@link defaultPluginCacheLocator}. */ locateCache?: PluginCacheLocator; /** Injectable apply seam for the settings plan; defaults to the real `executePlan`. */ applyActions?: (root: string, actions: Action[]) => Promise; /** Per-call CLI timeout override. */ timeoutMs?: number; } export interface BindPluginResult { plugin: string; marketplace: string; /** The `@` enable/cache key. */ pluginKey: string; scope: PluginScope; /** The `.claude/settings*.json` surface the enable was written to. */ settingsFile: string; /** The single D18 `enabledPlugins` write record. */ writes: BindingWrite[]; /** Sealed ownership: the `enabledPlugins` field + the two `home:` machine-scope entries. */ ownership: BindingOwnershipEntry[]; /** D7 fields for the lock (`match` is always `true` here — a mismatch throws). */ identity: PluginIdentity; /** The scanned checkout registered as the marketplace source. */ marketplaceSourcePath: string; /** The tree that was digested for D7 (the loaded cache). */ loadedTreePath: string; } /** * Bind a plugin end to end: assert the scan authorization, register the scanned * checkout as the marketplace, install, verify D7 identity, and — only on a match * — apply the D18-owned `enabledPlugins` write and seal ownership. A digest * mismatch disables + uninstalls and throws {@link ClaudePluginIdentityError} with * no lock/ownership produced (fail closed, no partial state). */ export declare function bindPlugin(request: BindPluginRequest, deps: BindPluginDeps): Promise; export interface RemovePluginRequest { /** The lock's ownership entries (the `home:` machine-scope ones are reconciled here). */ ownership: readonly BindingOwnershipEntry[]; plugin: string; marketplace: string; /** The scope the plugin was installed at — uninstall must name it (the CLI * defaults to user scope; W4 live-run correction). */ scope: PluginScope; } export interface RemovePluginDeps { runner: Runner; env?: NodeJS.ProcessEnv; /** Injectable cache locator; defaults to {@link defaultPluginCacheLocator}. */ locateCache?: PluginCacheLocator; timeoutMs?: number; } export interface RemovePluginResult { /** Machine-scope targets reconciled clean and torn down via the CLI. */ removed: string[]; /** Drifted machine-scope entries: preserved, never torn down, and reported. */ drift: ClaudeDriftEntry[]; /** The CLI teardown steps attempted (for the caller's report/telemetry). */ cli: { describe: string; ok: boolean; }[]; } /** * Conservatively reconcile the MACHINE-SCOPE (`home:`) plugin ownership on removal, * exactly like the repo-relative reconciler: the loaded cache tree is the one * observable, digestible surface, so its current digest is the drift test. * - cache tree absent -> already gone; idempotent no-op; * - cache digest == the recorded -> clean; tear down (uninstall + marketplace remove); * - cache digest != the recorded -> user-modified; PRESERVE both + report drift. * * The repo-relative `enabledPlugins` field is reconciled by W3a's `planClaudeRemoval` * (a `home:` target reads as absent there, so those entries are inert no-ops); the * caller partitions ownership with {@link isHomeScopedTarget}. Machine state is * rebuildable, so a torn-down-then-absent surface is never an error. */ export declare function removePlugin(request: RemovePluginRequest, deps: RemovePluginDeps): Promise;