@ory/argus 0.14.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/README.md +31 -46
  2. package/assets/commands/temporal-up.md +1 -1
  3. package/assets/skills/auth-setup/SKILL.md +1 -1
  4. package/assets/skills/local-dev/SKILL.md +17 -7
  5. package/assets/skills/ory-build-agent/SKILL.md +39 -91
  6. package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -17
  7. package/assets/skills/ory-temporal-worker/SKILL.md +31 -33
  8. package/assets/skills/permissions-onboarding/SKILL.md +131 -104
  9. package/dist/adapters.d.ts +88 -32
  10. package/dist/adapters.js +443 -164
  11. package/dist/agent-auth.d.ts +226 -67
  12. package/dist/agent-auth.js +951 -205
  13. package/dist/auth-store.d.ts +37 -2
  14. package/dist/auth-store.js +37 -3
  15. package/dist/auth.d.ts +33 -4
  16. package/dist/auth.js +163 -20
  17. package/dist/bash-parser.d.ts +98 -0
  18. package/dist/bash-parser.js +396 -0
  19. package/dist/branding.d.ts +77 -16
  20. package/dist/branding.js +93 -23
  21. package/dist/build-info.json +4 -4
  22. package/dist/cli-invocation.d.ts +1 -1
  23. package/dist/cli-invocation.js +2 -1
  24. package/dist/cli.d.ts +20 -29
  25. package/dist/cli.js +271 -239
  26. package/dist/client.d.ts +175 -138
  27. package/dist/client.js +672 -391
  28. package/dist/config.d.ts +217 -54
  29. package/dist/config.js +461 -42
  30. package/dist/context.d.ts +10 -0
  31. package/dist/context.js +21 -0
  32. package/dist/contract-suite.d.ts +3 -5
  33. package/dist/contract-suite.js +75 -47
  34. package/dist/denial.d.ts +36 -3
  35. package/dist/denial.js +79 -10
  36. package/dist/event-reporter.d.ts +77 -0
  37. package/dist/event-reporter.js +776 -0
  38. package/dist/external-registrations-main.d.ts +10 -0
  39. package/dist/external-registrations-main.js +38 -0
  40. package/dist/external-registrations.d.ts +79 -0
  41. package/dist/external-registrations.js +188 -0
  42. package/dist/help-cli.d.ts +39 -0
  43. package/dist/help-cli.js +55 -0
  44. package/dist/hook-timeout.d.ts +64 -0
  45. package/dist/hook-timeout.js +88 -0
  46. package/dist/index.d.ts +28 -23
  47. package/dist/index.js +173 -58
  48. package/dist/lifecycle.d.ts +3 -3
  49. package/dist/lifecycle.js +38 -6
  50. package/dist/local/cli.js +11 -6
  51. package/dist/local/configs.d.ts +74 -18
  52. package/dist/local/configs.js +291 -84
  53. package/dist/local/health.js +7 -7
  54. package/dist/local/index.d.ts +2 -2
  55. package/dist/local/index.js +24 -10
  56. package/dist/local/manager.d.ts +20 -1
  57. package/dist/local/manager.js +159 -36
  58. package/dist/local/ports.d.ts +158 -0
  59. package/dist/local/ports.js +443 -0
  60. package/dist/local/seed.d.ts +13 -5
  61. package/dist/local/seed.js +62 -38
  62. package/dist/logger.d.ts +54 -25
  63. package/dist/logger.js +329 -63
  64. package/dist/mcp.d.ts +2 -2
  65. package/dist/mcp.js +10 -5
  66. package/dist/mirror-bootstrap.d.ts +48 -0
  67. package/dist/mirror-bootstrap.js +254 -0
  68. package/dist/opl.d.ts +289 -0
  69. package/dist/opl.js +446 -0
  70. package/dist/permission-mode.d.ts +87 -0
  71. package/dist/permission-mode.js +307 -0
  72. package/dist/permissions-cli.d.ts +13 -49
  73. package/dist/permissions-cli.js +154 -348
  74. package/dist/permissions.d.ts +148 -38
  75. package/dist/permissions.js +591 -45
  76. package/dist/post-install.d.ts +33 -0
  77. package/dist/post-install.js +127 -0
  78. package/dist/read-credential.d.ts +65 -0
  79. package/dist/read-credential.js +86 -0
  80. package/dist/registry/cli.js +5 -2
  81. package/dist/registry/config.d.ts +0 -17
  82. package/dist/registry/config.js +0 -23
  83. package/dist/registry/index.d.ts +1 -1
  84. package/dist/registry/index.js +2 -2
  85. package/dist/registry/manager.d.ts +4 -21
  86. package/dist/registry/manager.js +21 -37
  87. package/dist/runtime-credential.d.ts +140 -0
  88. package/dist/runtime-credential.js +572 -0
  89. package/dist/runtime.d.ts +413 -0
  90. package/dist/runtime.js +825 -0
  91. package/dist/setup.d.ts +23 -47
  92. package/dist/setup.js +59 -116
  93. package/dist/skills.js +0 -7
  94. package/dist/status-cli.d.ts +27 -11
  95. package/dist/status-cli.js +121 -115
  96. package/dist/status-data.d.ts +116 -17
  97. package/dist/status-data.js +121 -38
  98. package/dist/subject.d.ts +126 -20
  99. package/dist/subject.js +215 -30
  100. package/dist/testing.d.ts +74 -38
  101. package/dist/testing.js +185 -68
  102. package/dist/tool-catalog.d.ts +53 -11
  103. package/dist/tool-catalog.js +164 -13
  104. package/dist/tool-metadata.d.ts +7 -6
  105. package/dist/tool-metadata.js +6 -5
  106. package/dist/types.d.ts +11 -1
  107. package/dist/uninstall.d.ts +71 -34
  108. package/dist/uninstall.js +217 -85
  109. package/dist/user-login.d.ts +9 -10
  110. package/dist/user-login.js +56 -75
  111. package/dist/watch-cli.d.ts +6 -0
  112. package/dist/watch-cli.js +217 -0
  113. package/package.json +4 -27
  114. package/assets/commands/dashboard.md +0 -34
  115. package/dist/dashboard-cli.d.ts +0 -8
  116. package/dist/dashboard-cli.js +0 -70
  117. package/dist/dev.d.ts +0 -103
  118. package/dist/dev.js +0 -583
  119. package/dist/interactive-setup.d.ts +0 -286
  120. package/dist/interactive-setup.js +0 -1734
  121. package/dist/local/jaeger-main.d.ts +0 -13
  122. package/dist/local/jaeger-main.js +0 -85
  123. package/dist/local/jaeger.d.ts +0 -50
  124. package/dist/local/jaeger.js +0 -162
  125. package/dist/otel/exporter.d.ts +0 -17
  126. package/dist/otel/exporter.js +0 -12
  127. package/dist/otel/index.d.ts +0 -2
  128. package/dist/otel/index.js +0 -8
  129. package/dist/otel/otlp.d.ts +0 -103
  130. package/dist/otel/otlp.js +0 -385
  131. package/dist/project-api-key.d.ts +0 -69
  132. package/dist/project-api-key.js +0 -147
  133. package/dist/setup-actions.d.ts +0 -232
  134. package/dist/setup-actions.js +0 -507
  135. package/dist/tracer.d.ts +0 -190
  136. package/dist/tracer.js +0 -481
  137. package/dist/watch-sandbox.d.ts +0 -9
  138. package/dist/watch-sandbox.js +0 -81
  139. package/dist/web/api.d.ts +0 -33
  140. package/dist/web/api.js +0 -294
  141. package/dist/web/launch.d.ts +0 -11
  142. package/dist/web/launch.js +0 -96
  143. package/dist/web/server.d.ts +0 -20
  144. package/dist/web/server.js +0 -233
  145. package/dist/web/types.d.ts +0 -65
  146. package/dist/web/types.js +0 -2
  147. package/dist/webapp/assets/index-Wucl4SZs.css +0 -1
  148. package/dist/webapp/assets/index-m-GtEdq0.js +0 -49
  149. package/dist/webapp/favicon.ico +0 -0
  150. package/dist/webapp/index.html +0 -15
@@ -0,0 +1,825 @@
1
+ "use strict";
2
+ /**
3
+ * The plugin **runtime store**: resolve which code a harness runs *once*, at
4
+ * install time, instead of re-resolving an npm package spec on every hook
5
+ * invocation.
6
+ *
7
+ * Historically every harness plugin wrote its runtime hook command as a
8
+ * version-pinned npx spec (`npx -y -p @ory/<pkg>@<ver> ory-<h>-hook`). That
9
+ * froze the *resolution strategy* into each harness's config file: which code
10
+ * runs was decided by npm on every `PreToolUse`, forever — one `npm exec`
11
+ * resolution per tool call, dependent on network and npm cache state, and
12
+ * untestable locally without publishing to a registry first.
13
+ *
14
+ * This module replaces that with three artifacts:
15
+ *
16
+ * 1. **A runtime store** at `<dataDir>/runtime/<version>/`, an npm prefix
17
+ * holding the plugin package and its dependency closure. Populated by a
18
+ * single `npm install --prefix` at install time — the only package
19
+ * resolution that ever happens. Several harnesses installed at the same
20
+ * version share one store directory.
21
+ * 2. **A stable entry shim** per harness at
22
+ * `<dataDir>/bin/ory-hook-<harness>.js` (and one for the MCP server).
23
+ * Harness configs reference the *shim*, whose path never changes across
24
+ * upgrades — so changing the runtime, or the resolution policy itself,
25
+ * rewrites one small file instead of N harness config formats.
26
+ * 3. **A manifest** at `<dataDir>/runtime/manifest.json` recording what is
27
+ * wired where, so `status` can report the runtime a harness will actually
28
+ * load, upgrades can rewire, and unreferenced stores can be pruned.
29
+ *
30
+ * A **linked** runtime (`linkedRuntime`) points the shim straight at a
31
+ * workspace build instead of a materialized store. That is the development
32
+ * loop: `pnpm build` + relaunch, with no publish, no version bump, and no
33
+ * registry involved.
34
+ *
35
+ * ## The shim must fail open
36
+ *
37
+ * The shim is not an indirection for its own sake: it is the only place that
38
+ * can degrade gracefully. If the runtime store is missing — a wiped config
39
+ * dir, a half-finished upgrade, dotfiles synced to a new machine — a raw
40
+ * `node "<store>/…/hook.js"` command would exit non-zero, and a harness that
41
+ * treats an unanswered gate as a block (Cursor registers `preToolUse` with
42
+ * `failClosed: true`) would then block *every* tool call. So the generated
43
+ * hook shim catches an unloadable runtime, explains itself on stderr, and
44
+ * exits 0 with the harness's pass-through response. The MCP shim does the
45
+ * opposite and exits non-zero: an MCP server that cannot start gates nothing,
46
+ * and a silent no-op server is harder to diagnose than a loud failure.
47
+ */
48
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
49
+ if (k2 === undefined) k2 = k;
50
+ var desc = Object.getOwnPropertyDescriptor(m, k);
51
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
52
+ desc = { enumerable: true, get: function() { return m[k]; } };
53
+ }
54
+ Object.defineProperty(o, k2, desc);
55
+ }) : (function(o, m, k, k2) {
56
+ if (k2 === undefined) k2 = k;
57
+ o[k2] = m[k];
58
+ }));
59
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
60
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
61
+ }) : function(o, v) {
62
+ o["default"] = v;
63
+ });
64
+ var __importStar = (this && this.__importStar) || (function () {
65
+ var ownKeys = function(o) {
66
+ ownKeys = Object.getOwnPropertyNames || function (o) {
67
+ var ar = [];
68
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
69
+ return ar;
70
+ };
71
+ return ownKeys(o);
72
+ };
73
+ return function (mod) {
74
+ if (mod && mod.__esModule) return mod;
75
+ var result = {};
76
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
77
+ __setModuleDefault(result, mod);
78
+ return result;
79
+ };
80
+ })();
81
+ Object.defineProperty(exports, "__esModule", { value: true });
82
+ exports.defaultNpmInstaller = exports.MCP_SERVER_PACKAGE = void 0;
83
+ exports.getRuntimeRoot = getRuntimeRoot;
84
+ exports.getRuntimeStoreDir = getRuntimeStoreDir;
85
+ exports.getShimDir = getShimDir;
86
+ exports.getHookShimPath = getHookShimPath;
87
+ exports.getMcpShimPath = getMcpShimPath;
88
+ exports.sanitizeNpmEnv = sanitizeNpmEnv;
89
+ exports.materializeRuntime = materializeRuntime;
90
+ exports.linkedRuntime = linkedRuntime;
91
+ exports.resolveRuntimeEntry = resolveRuntimeEntry;
92
+ exports.resolveMcpRuntimeEntry = resolveMcpRuntimeEntry;
93
+ exports.hookShimBody = hookShimBody;
94
+ exports.writeHookShim = writeHookShim;
95
+ exports.writeMcpShim = writeMcpShim;
96
+ exports.hookCommand = hookCommand;
97
+ exports.isOryRuntimeCommand = isOryRuntimeCommand;
98
+ exports.mcpCommand = mcpCommand;
99
+ exports.moduleSpecifier = moduleSpecifier;
100
+ exports.readRuntimeManifest = readRuntimeManifest;
101
+ exports.recordRuntimeWiring = recordRuntimeWiring;
102
+ exports.getRuntimeWiring = getRuntimeWiring;
103
+ exports.removeRuntimeWiring = removeRuntimeWiring;
104
+ exports.pruneRuntimeStores = pruneRuntimeStores;
105
+ exports.resolveRuntimeForInstall = resolveRuntimeForInstall;
106
+ exports.requireHookCommand = requireHookCommand;
107
+ exports.withRuntimeInstallLock = withRuntimeInstallLock;
108
+ exports.wireRuntime = wireRuntime;
109
+ exports.checkRuntimeHealth = checkRuntimeHealth;
110
+ exports.describeRuntimeHealth = describeRuntimeHealth;
111
+ const fs = __importStar(require("node:fs"));
112
+ const path = __importStar(require("node:path"));
113
+ const node_child_process_1 = require("node:child_process");
114
+ const node_crypto_1 = require("node:crypto");
115
+ const config_js_1 = require("./config.js");
116
+ // ─── Paths ─────────────────────────────────────────────────────────
117
+ /** Root of the runtime store: `<dataDir>/runtime`. */
118
+ function getRuntimeRoot() {
119
+ return path.join((0, config_js_1.getDataDir)(), "runtime");
120
+ }
121
+ /** Store directory for one version: `<dataDir>/runtime/<version>`. */
122
+ function getRuntimeStoreDir(version) {
123
+ return path.join(getRuntimeRoot(), version);
124
+ }
125
+ function getRuntimeInstallLockPath() {
126
+ return path.join(getRuntimeRoot(), ".install.lock");
127
+ }
128
+ /** Directory holding the generated entry shims: `<dataDir>/bin`. */
129
+ function getShimDir() {
130
+ return path.join((0, config_js_1.getDataDir)(), "bin");
131
+ }
132
+ /** Stable hook-shim path for a harness. Never changes across upgrades. */
133
+ function getHookShimPath(harness) {
134
+ return path.join(getShimDir(), `ory-hook-${harness}.js`);
135
+ }
136
+ /** Stable MCP-server shim path. Shared by every harness. */
137
+ function getMcpShimPath() {
138
+ return path.join(getShimDir(), "ory-mcp-server.js");
139
+ }
140
+ function getManifestPath() {
141
+ return path.join(getRuntimeRoot(), "manifest.json");
142
+ }
143
+ // ─── Runtime targets ───────────────────────────────────────────────
144
+ /** The npm package providing the MCP server, materialized alongside plugins. */
145
+ exports.MCP_SERVER_PACKAGE = "@ory/mcp-server";
146
+ /** Read a package version from a package directory, if present. */
147
+ function readPackageVersion(packageDir) {
148
+ try {
149
+ const parsed = JSON.parse(fs.readFileSync(path.join(packageDir, "package.json"), "utf-8"));
150
+ return typeof parsed.version === "string" ? parsed.version : undefined;
151
+ }
152
+ catch {
153
+ return undefined;
154
+ }
155
+ }
156
+ /**
157
+ * Default installer: one `npm install --prefix <store> <spec>…`.
158
+ *
159
+ * `--prefix` keeps the install entirely inside the store directory (npm
160
+ * creates `<store>/node_modules` and, if absent, a minimal `package.json`),
161
+ * and `--no-save` keeps it from rewriting a manifest we do not own. The
162
+ * user's own npm configuration — registry, auth, proxy — applies, which is
163
+ * the point: this is the *one* resolution, done with the user's settings, at
164
+ * a moment where a failure is visible and actionable.
165
+ */
166
+ const defaultNpmInstaller = ({ prefix, specs }) => {
167
+ const result = (0, node_child_process_1.spawnSync)("npm", [
168
+ "install",
169
+ "--prefix",
170
+ prefix,
171
+ ...specs,
172
+ "--no-audit",
173
+ "--no-fund",
174
+ "--no-save",
175
+ "--loglevel=error",
176
+ ], {
177
+ encoding: "utf-8",
178
+ stdio: ["ignore", "pipe", "pipe"],
179
+ env: sanitizeNpmEnv(process.env),
180
+ });
181
+ const output = ((result.stdout ?? "") + (result.stderr ?? "")).trim();
182
+ return { ok: !result.error && result.status === 0, output };
183
+ };
184
+ exports.defaultNpmInstaller = defaultNpmInstaller;
185
+ /**
186
+ * When this install runs from inside an `npx`/`npm exec` wrapper (the
187
+ * documented `npx -p @ory/<harness> ory-<harness> install` entry point), the
188
+ * outer npm resolves the user's config and *projects it into the environment*
189
+ * as `npm_config_*` vars for child processes. Our inner `npm install <spec>…`
190
+ * inherits those — which is how the user's registry/proxy/`min-release-age`
191
+ * reach the store install, so we keep them.
192
+ *
193
+ * The one exception is `allow-scripts`: npm ≥ 11.6 rejects an env/CLI-level
194
+ * `allow-scripts` in a project-scoped install (`EALLOWSCRIPTS`), because that
195
+ * source is meant for one-off/global contexts. A user with
196
+ * `allow-scripts=…` in `~/.npmrc` (an increasingly common supply-chain
197
+ * hardening) would otherwise be unable to install the runtime at all. Dropping
198
+ * only the projected env var lets the child re-read the same setting from the
199
+ * `.npmrc` cascade (user/global level, which *is* permitted here), so packages
200
+ * that legitimately need a build step — e.g. `tree-sitter-bash` for shell
201
+ * decomposition — still get it.
202
+ */
203
+ function sanitizeNpmEnv(env) {
204
+ const cleaned = {};
205
+ for (const [key, value] of Object.entries(env)) {
206
+ if (/^npm_config_allow[-_]scripts$/i.test(key)) {
207
+ continue;
208
+ }
209
+ cleaned[key] = value;
210
+ }
211
+ return cleaned;
212
+ }
213
+ /**
214
+ * Materialize a runtime into the store and return it.
215
+ *
216
+ * The MCP server is installed into the same prefix by default: it is fetched
217
+ * by the harness at runtime in exactly the same way the hook was, so it needs
218
+ * the same treatment for the registry to drop out of the runtime path
219
+ * entirely.
220
+ *
221
+ * Throws when the install fails or the expected package directory is absent
222
+ * afterwards — an install that cannot materialize its runtime must say so
223
+ * rather than write a shim pointing at nothing.
224
+ */
225
+ function materializeRuntime(opts) {
226
+ const { packageName, version } = opts;
227
+ const storeDir = getRuntimeStoreDir(version);
228
+ fs.mkdirSync(storeDir, { recursive: true });
229
+ const extras = opts.extraPackages ?? [`${exports.MCP_SERVER_PACKAGE}@${version}`];
230
+ // npm --no-save prunes packages omitted from a later install into the same
231
+ // prefix. Reinstall every runtime already wired to this shared store so
232
+ // adding one harness cannot remove another harness's package.
233
+ const retained = Object.values(readRuntimeManifest().harnesses)
234
+ .filter((wiring) => wiring.storeDir &&
235
+ path.resolve(wiring.storeDir) === path.resolve(storeDir))
236
+ .map((wiring) => `${wiring.packageName}@${wiring.version}`);
237
+ const specs = [
238
+ ...new Set([`${packageName}@${version}`, ...retained, ...extras]),
239
+ ];
240
+ const installer = opts.installer ?? exports.defaultNpmInstaller;
241
+ const result = installer({ prefix: storeDir, specs });
242
+ if (!result.ok) {
243
+ throw new Error(`Failed to materialize the Ory runtime into ${storeDir}.\n` +
244
+ ` npm install ${specs.join(" ")}\n` +
245
+ (result.output ? ` ${result.output}\n` : ""));
246
+ }
247
+ const packageDir = path.join(storeDir, "node_modules", packageName);
248
+ const resolvedVersion = readPackageVersion(packageDir);
249
+ if (!resolvedVersion) {
250
+ throw new Error(`The Ory runtime install reported success but ${packageDir} is missing or ` +
251
+ `has no readable package.json.`);
252
+ }
253
+ return {
254
+ kind: "materialized",
255
+ packageName,
256
+ packageDir,
257
+ version: resolvedVersion,
258
+ storeDir,
259
+ };
260
+ }
261
+ /**
262
+ * Use an already-built package directory as the runtime — the development
263
+ * path (`install --link <dir>`), where the workspace build *is* the artifact.
264
+ *
265
+ * Validates that the directory looks like a built package so a typo surfaces
266
+ * at install time instead of as hooks that silently never run.
267
+ */
268
+ function linkedRuntime(opts) {
269
+ const packageDir = path.resolve(opts.packageDir);
270
+ const version = readPackageVersion(packageDir);
271
+ if (!version) {
272
+ throw new Error(`Cannot link the Ory runtime: ${packageDir} has no readable package.json.`);
273
+ }
274
+ if (!fs.existsSync(path.join(packageDir, "dist"))) {
275
+ throw new Error(`Cannot link the Ory runtime: ${packageDir}/dist is missing. Build the ` +
276
+ `package first (pnpm build).`);
277
+ }
278
+ return { kind: "linked", packageName: opts.packageName, packageDir, version };
279
+ }
280
+ /**
281
+ * Absolute path to an entry point inside a runtime, e.g. the hook script.
282
+ * Throws when the file is absent — the shim is written from this, so a bad
283
+ * relative path must fail at install time, not at the first tool call.
284
+ */
285
+ function resolveRuntimeEntry(target, relativePath) {
286
+ const entry = path.join(target.packageDir, relativePath);
287
+ if (!fs.existsSync(entry)) {
288
+ throw new Error(`Ory runtime entry not found: ${entry} (package ${target.packageName}@${target.version}).`);
289
+ }
290
+ return entry;
291
+ }
292
+ /**
293
+ * The MCP server inside a runtime, or `undefined` when it isn't there.
294
+ *
295
+ * A materialized store installs it as a sibling; a linked workspace build has
296
+ * it two levels up (`packages/mcp-server`). Optional by design: a plugin whose
297
+ * MCP registration is missing should degrade to no MCP server, not fail the
298
+ * whole install.
299
+ */
300
+ function resolveMcpRuntimeEntry(target) {
301
+ const candidates = target.kind === "materialized"
302
+ ? [path.join(target.storeDir, "node_modules", exports.MCP_SERVER_PACKAGE)]
303
+ : [
304
+ // pnpm workspace: the plugin's own node_modules links siblings.
305
+ path.join(target.packageDir, "node_modules", exports.MCP_SERVER_PACKAGE),
306
+ // …and the packages/ directory holds the source checkout.
307
+ path.join(target.packageDir, "..", "mcp-server"),
308
+ ];
309
+ for (const dir of candidates) {
310
+ const entry = path.join(dir, "dist", "index.js");
311
+ if (fs.existsSync(entry))
312
+ return entry;
313
+ }
314
+ return undefined;
315
+ }
316
+ // ─── Shims ─────────────────────────────────────────────────────────
317
+ /**
318
+ * Body of a generated hook shim.
319
+ *
320
+ * Deliberately tiny and dependency-free: it must be loadable even when the
321
+ * runtime it points at is not. `require` (not `import`) so the shim works
322
+ * regardless of how the harness invokes it, and the pass-through response is
323
+ * written *before* exiting 0 so a fail-closed gate still sees a valid answer.
324
+ */
325
+ function hookShimBody(opts) {
326
+ const { entry, packageName, version, installCommand, fallbackStdout } = opts;
327
+ return `#!/usr/bin/env node
328
+ // Ory hook entry shim — generated by ${packageName}@${version}. Do not edit.
329
+ //
330
+ // Resolves the Ory plugin runtime once, at install time: this file is the
331
+ // stable path harness configs reference, and it is rewritten (in place) when
332
+ // the runtime changes. No package resolution happens on the tool-call path.
333
+ //
334
+ // Regenerate with: ${installCommand}
335
+ "use strict";
336
+
337
+ const ENTRY = ${JSON.stringify(entry)};
338
+
339
+ try {
340
+ const mod = require(ENTRY);
341
+ // Most hook entries run their pipeline as a load-time side effect, so the
342
+ // require() above is the whole invocation. An entry that must stay
343
+ // side-effect-free on import — Amp's delegate, whose tests import it to call
344
+ // decideToolPermission directly — exports \`main\` instead and is run here.
345
+ //
346
+ // Without this, such an entry silently does nothing under the shim:
347
+ // \`require.main === module\` is false because require.main is THIS file, so its
348
+ // own guard never fires, the process exits 0, and a harness reading exit 0 as
349
+ // "allow" lets every tool through unchecked (#217).
350
+ if (mod && typeof mod.main === "function") {
351
+ Promise.resolve(mod.main()).catch(function (err) {
352
+ process.stderr.write(
353
+ "[ory-agent] hook entry failed: " + ((err && err.message) || String(err)) + "\\n"
354
+ );
355
+ ${fallbackStdout ? ` process.stdout.write(${JSON.stringify(fallbackStdout)});\n` : ""} process.exit(0);
356
+ });
357
+ }
358
+ } catch (err) {
359
+ // Fail OPEN. A harness that treats an unanswered permission gate as a block
360
+ // would otherwise block every tool call because our runtime went missing.
361
+ process.stderr.write(
362
+ "[ory-agent] plugin runtime is missing or unloadable at " +
363
+ ENTRY +
364
+ "\\n[ory-agent] tools are NOT being checked. Reinstall with: ${installCommand}\\n" +
365
+ "[ory-agent] cause: " +
366
+ ((err && err.message) || String(err)) +
367
+ "\\n"
368
+ );
369
+ ${fallbackStdout ? ` process.stdout.write(${JSON.stringify(fallbackStdout)});\n` : ""} process.exit(0);
370
+ }
371
+ `;
372
+ }
373
+ /**
374
+ * Body of the MCP-server shim. Fails **loudly** (exit 1): an MCP server that
375
+ * cannot start blocks nothing, so a clear error beats a silent no-op server.
376
+ */
377
+ function mcpShimBody(opts) {
378
+ const { entry, packageName, version, installCommand } = opts;
379
+ return `#!/usr/bin/env node
380
+ // Ory MCP server entry shim — generated by ${packageName}@${version}. Do not edit.
381
+ // Regenerate with: ${installCommand}
382
+ "use strict";
383
+
384
+ const ENTRY = ${JSON.stringify(entry)};
385
+
386
+ try {
387
+ require(ENTRY);
388
+ } catch (err) {
389
+ process.stderr.write(
390
+ "[ory-agent] MCP server runtime is missing or unloadable at " +
391
+ ENTRY +
392
+ "\\n[ory-agent] reinstall with: ${installCommand}\\n" +
393
+ "[ory-agent] cause: " +
394
+ ((err && err.message) || String(err)) +
395
+ "\\n"
396
+ );
397
+ process.exit(1);
398
+ }
399
+ `;
400
+ }
401
+ function writeExecutable(file, body) {
402
+ fs.mkdirSync(path.dirname(file), { recursive: true });
403
+ fs.writeFileSync(file, body, { mode: 0o755 });
404
+ // writeFileSync honors `mode` only when creating the file, so re-assert it
405
+ // for an overwrite of an existing shim.
406
+ try {
407
+ fs.chmodSync(file, 0o755);
408
+ }
409
+ catch {
410
+ /* best-effort — a non-executable shim still runs via `node <path>` */
411
+ }
412
+ }
413
+ /**
414
+ * Write (or rewrite) the stable hook shim for a harness and record the wiring
415
+ * in the manifest. Returns the shim path — what the harness config should
416
+ * reference, via {@link hookCommand}.
417
+ *
418
+ * `fallbackStdout` is the harness's pass-through response for the
419
+ * runtime-missing case. Most subprocess harnesses treat empty stdout plus
420
+ * exit 0 as "no opinion, proceed", which is the default; pass a JSON string
421
+ * for a harness that needs an explicit allow payload.
422
+ */
423
+ function writeHookShim(opts) {
424
+ const entry = resolveRuntimeEntry(opts.target, opts.entryRelPath ?? path.join("dist", "hook.js"));
425
+ const shimPath = getHookShimPath(opts.harness);
426
+ writeExecutable(shimPath, hookShimBody({
427
+ entry,
428
+ packageName: opts.target.packageName,
429
+ version: opts.target.version,
430
+ installCommand: opts.installCommand,
431
+ fallbackStdout: opts.fallbackStdout ?? "",
432
+ }));
433
+ recordRuntimeWiring({
434
+ harness: opts.harness,
435
+ target: opts.target,
436
+ hookShim: shimPath,
437
+ hookEntry: entry,
438
+ mcpEntry: opts.mcpEntry,
439
+ });
440
+ return shimPath;
441
+ }
442
+ /**
443
+ * Write (or rewrite) the shared MCP-server shim. Returns the shim path, or
444
+ * `undefined` when this runtime has no MCP server to point at — callers then
445
+ * skip the MCP registration rather than writing a broken one.
446
+ */
447
+ function writeMcpShim(opts) {
448
+ const entry = resolveMcpRuntimeEntry(opts.target);
449
+ if (!entry)
450
+ return undefined;
451
+ const shimPath = getMcpShimPath();
452
+ writeExecutable(shimPath, mcpShimBody({
453
+ entry,
454
+ packageName: opts.target.packageName,
455
+ version: opts.target.version,
456
+ installCommand: opts.installCommand,
457
+ }));
458
+ return shimPath;
459
+ }
460
+ // ─── Rendering into harness config ─────────────────────────────────
461
+ /**
462
+ * The hook command a harness config should carry: `node "<shim>"`.
463
+ *
464
+ * Quoted because a data dir can contain spaces, and several harnesses run the
465
+ * command through a shell.
466
+ */
467
+ function hookCommand(shimPath) {
468
+ return `node "${shimPath}"`;
469
+ }
470
+ /**
471
+ * True when a command in a harness config is one of ours.
472
+ *
473
+ * Matches the runtime shims this module writes **and** the version-pinned
474
+ * `npx -y -p @ory/<pkg> …` commands earlier releases wrote. The legacy pattern
475
+ * has to stay recognized even though nothing emits it any more: uninstall and
476
+ * the non-destructive install merges use this predicate, and a legacy entry
477
+ * left behind would keep invoking the last published release alongside the new
478
+ * shim.
479
+ */
480
+ function isOryRuntimeCommand(command) {
481
+ if (!command)
482
+ return false;
483
+ return (/ory-hook-[a-z0-9-]+\.(js|cjs|mjs)/.test(command) ||
484
+ /ory-mcp-server\.(js|cjs|mjs)/.test(command) ||
485
+ // Legacy npx form: `npx … -p @ory/<pkg>[@ver] ory-<h>-hook`.
486
+ /@ory\/[a-z0-9-]+/.test(command) ||
487
+ /\bory-[a-z0-9-]+-hook\b/.test(command));
488
+ }
489
+ /** The MCP server registration a harness config should carry. */
490
+ function mcpCommand(shimPath) {
491
+ return { command: "node", args: [shimPath] };
492
+ }
493
+ /**
494
+ * Module specifier for an *in-process* plugin: the absolute package directory.
495
+ *
496
+ * In-process harnesses (OpenCode, OpenClaw, Amp, Pi) load the plugin as a
497
+ * module rather than spawning a hook, so they reference the runtime directly
498
+ * instead of through a shim — a loader file re-exporting this path, or a
499
+ * `file://` plugin spec built from it.
500
+ */
501
+ function moduleSpecifier(target) {
502
+ return target.packageDir;
503
+ }
504
+ const EMPTY_MANIFEST = { version: 1, harnesses: {} };
505
+ /** Read the manifest. Returns an empty manifest when absent or unreadable. */
506
+ function readRuntimeManifest() {
507
+ try {
508
+ const parsed = JSON.parse(fs.readFileSync(getManifestPath(), "utf-8"));
509
+ const harnesses = parsed.harnesses && typeof parsed.harnesses === "object"
510
+ ? parsed.harnesses
511
+ : {};
512
+ return { version: 1, harnesses };
513
+ }
514
+ catch {
515
+ return { ...EMPTY_MANIFEST, harnesses: {} };
516
+ }
517
+ }
518
+ function writeRuntimeManifest(manifest) {
519
+ const file = getManifestPath();
520
+ fs.mkdirSync(path.dirname(file), { recursive: true });
521
+ fs.writeFileSync(file, JSON.stringify(manifest, null, 2) + "\n");
522
+ }
523
+ /** Record (or replace) one harness's wiring. Called by {@link writeHookShim}. */
524
+ function recordRuntimeWiring(opts) {
525
+ const wiring = {
526
+ harness: opts.harness,
527
+ packageName: opts.target.packageName,
528
+ version: opts.target.version,
529
+ kind: opts.target.kind,
530
+ packageDir: opts.target.packageDir,
531
+ ...(opts.target.storeDir ? { storeDir: opts.target.storeDir } : {}),
532
+ hookShim: opts.hookShim,
533
+ hookEntry: opts.hookEntry,
534
+ ...(opts.mcpEntry ? { mcpEntry: opts.mcpEntry } : {}),
535
+ wiredAt: new Date().toISOString(),
536
+ };
537
+ const manifest = readRuntimeManifest();
538
+ manifest.harnesses[opts.harness] = wiring;
539
+ writeRuntimeManifest(manifest);
540
+ return wiring;
541
+ }
542
+ /** The wiring for one harness, or `undefined` when it isn't installed. */
543
+ function getRuntimeWiring(harness) {
544
+ return readRuntimeManifest().harnesses[harness];
545
+ }
546
+ /**
547
+ * Forget a harness's wiring and remove its shim. Used by `uninstall`. The
548
+ * store itself is left to {@link pruneRuntimeStores}, which removes it only
549
+ * once no harness references it.
550
+ */
551
+ function removeRuntimeWiring(harness) {
552
+ const manifest = readRuntimeManifest();
553
+ const wiring = manifest.harnesses[harness];
554
+ if (wiring) {
555
+ try {
556
+ fs.rmSync(wiring.hookShim, { force: true });
557
+ }
558
+ catch {
559
+ /* best-effort */
560
+ }
561
+ delete manifest.harnesses[harness];
562
+ writeRuntimeManifest(manifest);
563
+ }
564
+ }
565
+ /**
566
+ * Delete store directories no harness references any more — the GC step of an
567
+ * upgrade. Always called *after* the shims have been rewritten, so a store in
568
+ * use is never removed. Returns the directories deleted.
569
+ */
570
+ function pruneRuntimeStores() {
571
+ const root = getRuntimeRoot();
572
+ let entries;
573
+ try {
574
+ entries = fs.readdirSync(root);
575
+ }
576
+ catch {
577
+ return [];
578
+ }
579
+ const inUse = new Set(Object.values(readRuntimeManifest().harnesses)
580
+ .map((w) => w.storeDir)
581
+ .filter((d) => !!d)
582
+ .map((d) => path.resolve(d)));
583
+ const removed = [];
584
+ for (const name of entries) {
585
+ const dir = path.join(root, name);
586
+ if (name === "manifest.json")
587
+ continue;
588
+ if (!fs.statSync(dir).isDirectory())
589
+ continue;
590
+ if (inUse.has(path.resolve(dir)))
591
+ continue;
592
+ fs.rmSync(dir, { recursive: true, force: true });
593
+ removed.push(dir);
594
+ }
595
+ return removed;
596
+ }
597
+ // ─── The install-side entry point ──────────────────────────────────
598
+ /**
599
+ * Where an `install` should get the runtime from.
600
+ *
601
+ * `--runtime-dir <dir>` → link that directory
602
+ * `ORY_PLUGIN_RUNTIME_DIR` → link that directory
603
+ * otherwise → materialize the CLI's own version
604
+ *
605
+ * The flag and the env var support explicit source installs that point at an
606
+ * already-built workspace package instead of materializing from a registry.
607
+ *
608
+ * The flag deliberately requires an explicit directory. A bare "link whatever
609
+ * package I am running from" would resolve to npx's ephemeral cache dir under
610
+ * the common `npx -p @ory/<pkg>` invocation — a runtime that disappears
611
+ * between sessions — and `--link` is already taken by at least one harness
612
+ * (Gemini CLI symlinks its *extension* with it), so the name has to be
613
+ * unambiguous too.
614
+ */
615
+ function resolveRuntimeForInstall(opts) {
616
+ const linkDir = parseRuntimeDirFlag(opts.args ?? []);
617
+ if (linkDir) {
618
+ return linkedRuntime({ packageName: opts.packageName, packageDir: linkDir });
619
+ }
620
+ const version = readPackageVersion(opts.packageRoot);
621
+ if (!version) {
622
+ throw new Error(`Cannot determine the version to install: ${opts.packageRoot}/package.json ` +
623
+ `is missing or unreadable.`);
624
+ }
625
+ return materializeRuntime({
626
+ packageName: opts.packageName,
627
+ version,
628
+ installer: opts.installer,
629
+ });
630
+ }
631
+ /**
632
+ * Read `--runtime-dir <dir>` off an argv slice, falling back to the
633
+ * `ORY_PLUGIN_RUNTIME_DIR` env var. A flag with no value is an error rather
634
+ * than a silent fallback to materialization — someone passing it means to run
635
+ * against a local build, and quietly installing from npm instead would look
636
+ * like the build simply had no effect.
637
+ */
638
+ function parseRuntimeDirFlag(args) {
639
+ const idx = args.indexOf("--runtime-dir");
640
+ if (idx !== -1) {
641
+ const next = args[idx + 1];
642
+ if (!next || next.startsWith("-")) {
643
+ throw new Error("--runtime-dir requires a directory argument.");
644
+ }
645
+ return path.resolve(next);
646
+ }
647
+ const fromEnv = process.env.ORY_PLUGIN_RUNTIME_DIR?.trim();
648
+ return fromEnv ? path.resolve(fromEnv) : undefined;
649
+ }
650
+ /**
651
+ * The hook command of a subprocess-hook runtime.
652
+ *
653
+ * `PreparedRuntime.hookCommand` is optional because an in-process integration
654
+ * imports a module instead of spawning a hook. A harness that *does* spawn one
655
+ * cannot be wired without it, so its absence is a programming error — better to
656
+ * say so than to write `undefined` into a config file.
657
+ */
658
+ function requireHookCommand(runtime) {
659
+ if (!runtime.hookCommand) {
660
+ throw new Error(`The resolved runtime for ${runtime.target.packageName} has no hook ` +
661
+ `command (wired as in-process?).`);
662
+ }
663
+ return runtime.hookCommand;
664
+ }
665
+ const RUNTIME_LOCK_TIMEOUT_MS = 5 * 60_000;
666
+ const RUNTIME_LOCK_POLL_MS = 50;
667
+ /** Serialize installs because npm mutates and prunes the shared prefix in place. */
668
+ function withRuntimeInstallLock(action, options = {}) {
669
+ const root = getRuntimeRoot();
670
+ const lockPath = getRuntimeInstallLockPath();
671
+ const deadline = Date.now() + (options.timeoutMs ?? RUNTIME_LOCK_TIMEOUT_MS);
672
+ fs.mkdirSync(root, { recursive: true });
673
+ let fd;
674
+ const token = `${process.pid}:${(0, node_crypto_1.randomUUID)()}`;
675
+ for (;;) {
676
+ try {
677
+ fd = fs.openSync(lockPath, "wx", 0o600);
678
+ fs.writeSync(fd, token);
679
+ break;
680
+ }
681
+ catch (err) {
682
+ if (err.code !== "EEXIST")
683
+ throw err;
684
+ if (Date.now() > deadline) {
685
+ throw new Error(`Timed out waiting for the Ory runtime install lock at ${lockPath}. ` +
686
+ "Remove it only if no other plugin install is running.");
687
+ }
688
+ sleepRuntimeLock(options.pollMs ?? RUNTIME_LOCK_POLL_MS);
689
+ }
690
+ }
691
+ try {
692
+ return action();
693
+ }
694
+ finally {
695
+ try {
696
+ fs.closeSync(fd);
697
+ }
698
+ catch {
699
+ /* best-effort */
700
+ }
701
+ try {
702
+ if (fs.readFileSync(lockPath, "utf8") === token)
703
+ fs.unlinkSync(lockPath);
704
+ }
705
+ catch {
706
+ /* best-effort */
707
+ }
708
+ }
709
+ }
710
+ let runtimeLockSleepBuffer;
711
+ function sleepRuntimeLock(ms) {
712
+ if (runtimeLockSleepBuffer === undefined) {
713
+ try {
714
+ runtimeLockSleepBuffer = new Int32Array(new SharedArrayBuffer(4));
715
+ }
716
+ catch {
717
+ runtimeLockSleepBuffer = null;
718
+ }
719
+ }
720
+ if (!runtimeLockSleepBuffer) {
721
+ throw new Error("This runtime cannot wait for another Ory plugin install to finish.");
722
+ }
723
+ Atomics.wait(runtimeLockSleepBuffer, 0, 0, ms);
724
+ }
725
+ /**
726
+ * One call per plugin install: resolve the runtime, write the shims, record
727
+ * the wiring, and GC stores nothing references any more.
728
+ *
729
+ * Ordering matters — the shims are rewritten *before* pruning, so the store a
730
+ * harness is about to use is never a GC candidate.
731
+ */
732
+ function wireRuntime(opts) {
733
+ return withRuntimeInstallLock(() => wireRuntimeUnlocked(opts));
734
+ }
735
+ function wireRuntimeUnlocked(opts) {
736
+ const target = resolveRuntimeForInstall({
737
+ packageName: opts.packageName,
738
+ packageRoot: opts.packageRoot,
739
+ args: opts.args,
740
+ installer: opts.installer,
741
+ });
742
+ const mcpEntry = opts.includeMcp === false ? undefined : resolveMcpRuntimeEntry(target);
743
+ let hookShim;
744
+ let hookCmd;
745
+ if (opts.inProcess) {
746
+ hookShim = resolveRuntimeEntry(target, opts.entryRelPath ?? path.join("dist", "index.js"));
747
+ recordRuntimeWiring({
748
+ harness: opts.harness,
749
+ target,
750
+ hookShim,
751
+ hookEntry: hookShim,
752
+ mcpEntry,
753
+ });
754
+ }
755
+ else {
756
+ hookShim = writeHookShim({
757
+ harness: opts.harness,
758
+ target,
759
+ entryRelPath: opts.entryRelPath,
760
+ installCommand: opts.installCommand,
761
+ fallbackStdout: opts.fallbackStdout,
762
+ mcpEntry,
763
+ });
764
+ hookCmd = hookCommand(hookShim);
765
+ }
766
+ const mcpShim = opts.includeMcp === false
767
+ ? undefined
768
+ : writeMcpShim({ target, installCommand: opts.installCommand });
769
+ // Record the harness in the shared config's install registry. Every install
770
+ // funnels through here, so this is the one place that knows a plugin has
771
+ // been added — and `uninstall` reads it to tell whether the shared config
772
+ // still has other consumers before wiping it.
773
+ (0, config_js_1.registerInstalledHarness)(opts.harness);
774
+ return {
775
+ target,
776
+ hookShim,
777
+ ...(hookCmd ? { hookCommand: hookCmd } : {}),
778
+ ...(mcpShim ? { mcpShim, mcpServer: mcpCommand(mcpShim) } : {}),
779
+ prunedStores: pruneRuntimeStores(),
780
+ };
781
+ }
782
+ /**
783
+ * Inspect what a harness will actually load.
784
+ *
785
+ * With the runtime resolved at install time, the wired code and the CLI a user
786
+ * happens to run (`npx @ory/<pkg>@latest status`) can legitimately differ —
787
+ * so `status` reports the runtime version found on disk, not the CLI's own.
788
+ * The unhappy states are each distinguishable because they have different
789
+ * fixes: a missing shim means the harness config points at nothing, a missing
790
+ * runtime means the shim will fail open (tools unchecked), and drift means the
791
+ * store was replaced underneath a stale shim.
792
+ */
793
+ function checkRuntimeHealth(harness) {
794
+ const wiring = getRuntimeWiring(harness);
795
+ if (!wiring)
796
+ return { state: "not_installed" };
797
+ if (!fs.existsSync(wiring.hookShim))
798
+ return { state: "shim_missing", wiring };
799
+ if (!fs.existsSync(wiring.hookEntry)) {
800
+ return { state: "runtime_missing", wiring };
801
+ }
802
+ const onDisk = readPackageVersion(wiring.packageDir);
803
+ if (onDisk && onDisk !== wiring.version) {
804
+ return { state: "version_drift", wiring, runtimeVersion: onDisk };
805
+ }
806
+ return { state: "ok", wiring };
807
+ }
808
+ /** One-line human summary of {@link checkRuntimeHealth}, for `status`. */
809
+ function describeRuntimeHealth(health) {
810
+ switch (health.state) {
811
+ case "not_installed":
812
+ return "not installed (no runtime wired)";
813
+ case "ok":
814
+ return (`${health.wiring.packageName}@${health.wiring.version} ` +
815
+ `(${health.wiring.kind}) → ${health.wiring.hookEntry}`);
816
+ case "shim_missing":
817
+ return `shim missing at ${health.wiring.hookShim} — re-run install`;
818
+ case "runtime_missing":
819
+ return (`runtime missing at ${health.wiring.hookEntry} — hooks are failing ` +
820
+ `open (tools are NOT being checked); re-run install`);
821
+ case "version_drift":
822
+ return (`wired to ${health.wiring.version} but ${health.runtimeVersion} is on ` +
823
+ `disk at ${health.wiring.packageDir} — re-run install`);
824
+ }
825
+ }