/** * The plugin **runtime store**: resolve which code a harness runs *once*, at * install time, instead of re-resolving an npm package spec on every hook * invocation. * * Historically every harness plugin wrote its runtime hook command as a * version-pinned npx spec (`npx -y -p @ory/@ ory--hook`). That * froze the *resolution strategy* into each harness's config file: which code * runs was decided by npm on every `PreToolUse`, forever — one `npm exec` * resolution per tool call, dependent on network and npm cache state, and * untestable locally without publishing to a registry first. * * This module replaces that with three artifacts: * * 1. **A runtime store** at `/runtime//`, an npm prefix * holding the plugin package and its dependency closure. Populated by a * single `npm install --prefix` at install time — the only package * resolution that ever happens. Several harnesses installed at the same * version share one store directory. * 2. **A stable entry shim** per harness at * `/bin/ory-hook-.js` (and one for the MCP server). * Harness configs reference the *shim*, whose path never changes across * upgrades — so changing the runtime, or the resolution policy itself, * rewrites one small file instead of N harness config formats. * 3. **A manifest** at `/runtime/manifest.json` recording what is * wired where, so `status` can report the runtime a harness will actually * load, upgrades can rewire, and unreferenced stores can be pruned. * * A **linked** runtime (`linkedRuntime`) points the shim straight at a * workspace build instead of a materialized store. That is the development * loop: `pnpm build` + relaunch, with no publish, no version bump, and no * registry involved. * * ## The shim must fail open * * The shim is not an indirection for its own sake: it is the only place that * can degrade gracefully. If the runtime store is missing — a wiped config * dir, a half-finished upgrade, dotfiles synced to a new machine — a raw * `node "/…/hook.js"` command would exit non-zero, and a harness that * treats an unanswered gate as a block (Cursor registers `preToolUse` with * `failClosed: true`) would then block *every* tool call. So the generated * hook shim catches an unloadable runtime, explains itself on stderr, and * exits 0 with the harness's pass-through response. The MCP shim does the * opposite and exits non-zero: an MCP server that cannot start gates nothing, * and a silent no-op server is harder to diagnose than a loud failure. */ /** Root of the runtime store: `/runtime`. */ export declare function getRuntimeRoot(): string; /** Store directory for one version: `/runtime/`. */ export declare function getRuntimeStoreDir(version: string): string; /** Directory holding the generated entry shims: `/bin`. */ export declare function getShimDir(): string; /** Stable hook-shim path for a harness. Never changes across upgrades. */ export declare function getHookShimPath(harness: string): string; /** Stable MCP-server shim path. Shared by every harness. */ export declare function getMcpShimPath(): string; /** The npm package providing the MCP server, materialized alongside plugins. */ export declare const MCP_SERVER_PACKAGE = "@ory/mcp-server"; /** * A resolved runtime: a concrete directory on disk holding the package whose * code the harness will run. * * `materialized` is the production shape — an npm prefix under the data dir, * populated at install time. `linked` points at an already-built package * directory (a workspace checkout), used by explicit source installs. */ export interface RuntimeTarget { kind: "materialized" | "linked"; /** npm package name, e.g. `@ory/goose`. */ packageName: string; /** Absolute path to the package directory (contains `package.json`, `dist/`). */ packageDir: string; /** Version found in the resolved package's `package.json`. */ version: string; /** The npm prefix the package was installed into (materialized only). */ storeDir?: string; } /** * How the runtime store is populated. Injectable so tests exercise the * materialization logic without reaching the network, and so a caller can * route the install through a different registry. */ export type NpmInstaller = (args: { prefix: string; specs: string[]; }) => { ok: boolean; output: string; }; /** * Default installer: one `npm install --prefix …`. * * `--prefix` keeps the install entirely inside the store directory (npm * creates `/node_modules` and, if absent, a minimal `package.json`), * and `--no-save` keeps it from rewriting a manifest we do not own. The * user's own npm configuration — registry, auth, proxy — applies, which is * the point: this is the *one* resolution, done with the user's settings, at * a moment where a failure is visible and actionable. */ export declare const defaultNpmInstaller: NpmInstaller; /** * When this install runs from inside an `npx`/`npm exec` wrapper (the * documented `npx -p @ory/ ory- install` entry point), the * outer npm resolves the user's config and *projects it into the environment* * as `npm_config_*` vars for child processes. Our inner `npm install …` * inherits those — which is how the user's registry/proxy/`min-release-age` * reach the store install, so we keep them. * * The one exception is `allow-scripts`: npm ≥ 11.6 rejects an env/CLI-level * `allow-scripts` in a project-scoped install (`EALLOWSCRIPTS`), because that * source is meant for one-off/global contexts. A user with * `allow-scripts=…` in `~/.npmrc` (an increasingly common supply-chain * hardening) would otherwise be unable to install the runtime at all. Dropping * only the projected env var lets the child re-read the same setting from the * `.npmrc` cascade (user/global level, which *is* permitted here), so packages * that legitimately need a build step — e.g. `tree-sitter-bash` for shell * decomposition — still get it. */ export declare function sanitizeNpmEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv; /** * Materialize a runtime into the store and return it. * * The MCP server is installed into the same prefix by default: it is fetched * by the harness at runtime in exactly the same way the hook was, so it needs * the same treatment for the registry to drop out of the runtime path * entirely. * * Throws when the install fails or the expected package directory is absent * afterwards — an install that cannot materialize its runtime must say so * rather than write a shim pointing at nothing. */ export declare function materializeRuntime(opts: { packageName: string; version: string; /** Extra specs to install into the same prefix. Defaults to the MCP server. */ extraPackages?: string[]; installer?: NpmInstaller; }): RuntimeTarget; /** * Use an already-built package directory as the runtime — the development * path (`install --link `), where the workspace build *is* the artifact. * * Validates that the directory looks like a built package so a typo surfaces * at install time instead of as hooks that silently never run. */ export declare function linkedRuntime(opts: { packageName: string; packageDir: string; }): RuntimeTarget; /** * Absolute path to an entry point inside a runtime, e.g. the hook script. * Throws when the file is absent — the shim is written from this, so a bad * relative path must fail at install time, not at the first tool call. */ export declare function resolveRuntimeEntry(target: RuntimeTarget, relativePath: string): string; /** * The MCP server inside a runtime, or `undefined` when it isn't there. * * A materialized store installs it as a sibling; a linked workspace build has * it two levels up (`packages/mcp-server`). Optional by design: a plugin whose * MCP registration is missing should degrade to no MCP server, not fail the * whole install. */ export declare function resolveMcpRuntimeEntry(target: RuntimeTarget): string | undefined; /** * Body of a generated hook shim. * * Deliberately tiny and dependency-free: it must be loadable even when the * runtime it points at is not. `require` (not `import`) so the shim works * regardless of how the harness invokes it, and the pass-through response is * written *before* exiting 0 so a fail-closed gate still sees a valid answer. */ export declare function hookShimBody(opts: { entry: string; packageName: string; version: string; installCommand: string; fallbackStdout: string; }): string; /** * Write (or rewrite) the stable hook shim for a harness and record the wiring * in the manifest. Returns the shim path — what the harness config should * reference, via {@link hookCommand}. * * `fallbackStdout` is the harness's pass-through response for the * runtime-missing case. Most subprocess harnesses treat empty stdout plus * exit 0 as "no opinion, proceed", which is the default; pass a JSON string * for a harness that needs an explicit allow payload. */ export declare function writeHookShim(opts: { harness: string; target: RuntimeTarget; /** Entry inside the package. Defaults to the conventional `dist/hook.js`. */ entryRelPath?: string; installCommand: string; fallbackStdout?: string; /** Recorded in the manifest so a bootstrap can find the MCP server. */ mcpEntry?: string; }): string; /** * Write (or rewrite) the shared MCP-server shim. Returns the shim path, or * `undefined` when this runtime has no MCP server to point at — callers then * skip the MCP registration rather than writing a broken one. */ export declare function writeMcpShim(opts: { target: RuntimeTarget; installCommand: string; }): string | undefined; /** * The hook command a harness config should carry: `node ""`. * * Quoted because a data dir can contain spaces, and several harnesses run the * command through a shell. */ export declare function hookCommand(shimPath: string): string; /** * True when a command in a harness config is one of ours. * * Matches the runtime shims this module writes **and** the version-pinned * `npx -y -p @ory/ …` commands earlier releases wrote. The legacy pattern * has to stay recognized even though nothing emits it any more: uninstall and * the non-destructive install merges use this predicate, and a legacy entry * left behind would keep invoking the last published release alongside the new * shim. */ export declare function isOryRuntimeCommand(command: string | undefined): boolean; /** The MCP server registration a harness config should carry. */ export declare function mcpCommand(shimPath: string): { command: string; args: string[]; }; /** * Module specifier for an *in-process* plugin: the absolute package directory. * * In-process harnesses (OpenCode, OpenClaw, Amp, Pi) load the plugin as a * module rather than spawning a hook, so they reference the runtime directly * instead of through a shim — a loader file re-exporting this path, or a * `file://` plugin spec built from it. */ export declare function moduleSpecifier(target: RuntimeTarget): string; /** What a single harness has wired up, as recorded in the manifest. */ export interface RuntimeWiring { harness: string; packageName: string; version: string; kind: RuntimeTarget["kind"]; /** Absolute package dir the shim loads from. */ packageDir: string; /** The store prefix, for a materialized runtime. */ storeDir?: string; /** Generated shim path referenced by the harness config. */ hookShim: string; /** Entry inside the runtime that the shim requires. */ hookEntry: string; /** * MCP server entry inside the runtime, when this runtime has one. Recorded so * a dependency-free bootstrap (the one committed to the public install-surface * mirrors) can find the server without resolving a package. */ mcpEntry?: string; /** ISO-8601 timestamp the wiring was written. */ wiredAt: string; } export interface RuntimeManifest { version: 1; harnesses: Record; } /** Read the manifest. Returns an empty manifest when absent or unreadable. */ export declare function readRuntimeManifest(): RuntimeManifest; /** Record (or replace) one harness's wiring. Called by {@link writeHookShim}. */ export declare function recordRuntimeWiring(opts: { harness: string; target: RuntimeTarget; hookShim: string; hookEntry: string; mcpEntry?: string; }): RuntimeWiring; /** The wiring for one harness, or `undefined` when it isn't installed. */ export declare function getRuntimeWiring(harness: string): RuntimeWiring | undefined; /** * Forget a harness's wiring and remove its shim. Used by `uninstall`. The * store itself is left to {@link pruneRuntimeStores}, which removes it only * once no harness references it. */ export declare function removeRuntimeWiring(harness: string): void; /** * Delete store directories no harness references any more — the GC step of an * upgrade. Always called *after* the shims have been rewritten, so a store in * use is never removed. Returns the directories deleted. */ export declare function pruneRuntimeStores(): string[]; /** * Where an `install` should get the runtime from. * * `--runtime-dir ` → link that directory * `ORY_PLUGIN_RUNTIME_DIR` → link that directory * otherwise → materialize the CLI's own version * * The flag and the env var support explicit source installs that point at an * already-built workspace package instead of materializing from a registry. * * The flag deliberately requires an explicit directory. A bare "link whatever * package I am running from" would resolve to npx's ephemeral cache dir under * the common `npx -p @ory/` invocation — a runtime that disappears * between sessions — and `--link` is already taken by at least one harness * (Gemini CLI symlinks its *extension* with it), so the name has to be * unambiguous too. */ export declare function resolveRuntimeForInstall(opts: { packageName: string; /** Package root of the *running* CLI — supplies the version to materialize. */ packageRoot: string; args?: readonly string[]; installer?: NpmInstaller; }): RuntimeTarget; /** * The hook command of a subprocess-hook runtime. * * `PreparedRuntime.hookCommand` is optional because an in-process integration * imports a module instead of spawning a hook. A harness that *does* spawn one * cannot be wired without it, so its absence is a programming error — better to * say so than to write `undefined` into a config file. */ export declare function requireHookCommand(runtime: PreparedRuntime): string; /** Everything a plugin's install needs to write into its harness config. */ export interface PreparedRuntime { target: RuntimeTarget; /** * Stable shim path the harness config references. For an in-process * integration there is no shim — this is the module the harness imports. */ hookShim: string; /** * Ready-to-write hook command: `node ""`. Absent for an in-process * integration, which imports a module instead of spawning a hook. */ hookCommand?: string; /** MCP shim path, absent when this runtime ships no MCP server. */ mcpShim?: string; /** Ready-to-write MCP registration, absent for the same reason. */ mcpServer?: { command: string; args: string[]; }; /** Store directories GC'd after rewiring — reported by `install`. */ prunedStores: string[]; } /** Serialize installs because npm mutates and prunes the shared prefix in place. */ export declare function withRuntimeInstallLock(action: () => T, options?: { timeoutMs?: number; pollMs?: number; }): T; /** * One call per plugin install: resolve the runtime, write the shims, record * the wiring, and GC stores nothing references any more. * * Ordering matters — the shims are rewritten *before* pruning, so the store a * harness is about to use is never a GC candidate. */ export declare function wireRuntime(opts: { harness: string; packageName: string; packageRoot: string; /** Copy-pasteable command that regenerates this wiring, for shim messages. */ installCommand: string; args?: readonly string[]; /** Entry inside the package. Defaults to `dist/hook.js`. */ entryRelPath?: string; /** Harness pass-through payload for the runtime-missing case. */ fallbackStdout?: string; /** Set false for a harness that registers no MCP server (e.g. Pi). */ includeMcp?: boolean; /** * The harness loads the plugin as a module rather than spawning a hook * (OpenCode, OpenClaw, Pi). No hook shim is written — a shim named for a hook * that nothing invokes is just a file whose top-level `require` could * surprise someone later — and the wiring records the module entry instead, * so `status` still reports what the harness will load. */ inProcess?: boolean; installer?: NpmInstaller; }): PreparedRuntime; /** Health of the runtime a harness is wired to. */ export type RuntimeHealth = { state: "not_installed"; } | { state: "ok"; wiring: RuntimeWiring; } | { state: "shim_missing"; wiring: RuntimeWiring; } | { state: "runtime_missing"; wiring: RuntimeWiring; } | { state: "version_drift"; wiring: RuntimeWiring; runtimeVersion: string; }; /** * Inspect what a harness will actually load. * * With the runtime resolved at install time, the wired code and the CLI a user * happens to run (`npx @ory/@latest status`) can legitimately differ — * so `status` reports the runtime version found on disk, not the CLI's own. * The unhappy states are each distinguishable because they have different * fixes: a missing shim means the harness config points at nothing, a missing * runtime means the shim will fail open (tools unchecked), and drift means the * store was replaced underneath a stale shim. */ export declare function checkRuntimeHealth(harness: string): RuntimeHealth; /** One-line human summary of {@link checkRuntimeHealth}, for `status`. */ export declare function describeRuntimeHealth(health: RuntimeHealth): string;