@ory/argus 0.13.9 → 1.0.0

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 (133) hide show
  1. package/README.md +31 -45
  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 +43 -97
  6. package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -18
  7. package/assets/skills/ory-temporal-worker/SKILL.md +38 -42
  8. package/assets/skills/permissions-onboarding/SKILL.md +131 -104
  9. package/dist/adapters.d.ts +93 -30
  10. package/dist/adapters.js +464 -136
  11. package/dist/agent-auth.d.ts +258 -68
  12. package/dist/agent-auth.js +998 -202
  13. package/dist/auth-store.d.ts +37 -2
  14. package/dist/auth-store.js +37 -3
  15. package/dist/auth.d.ts +40 -4
  16. package/dist/auth.js +247 -19
  17. package/dist/bash-parser.d.ts +98 -0
  18. package/dist/bash-parser.js +396 -0
  19. package/dist/branding.d.ts +128 -0
  20. package/dist/branding.js +151 -0
  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 -278
  26. package/dist/client.d.ts +175 -138
  27. package/dist/client.js +672 -391
  28. package/dist/config.d.ts +249 -57
  29. package/dist/config.js +486 -62
  30. package/dist/context.d.ts +10 -0
  31. package/dist/context.js +21 -0
  32. package/dist/contract-suite.d.ts +8 -8
  33. package/dist/contract-suite.js +88 -69
  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 +31 -19
  47. package/dist/index.js +182 -31
  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.d.ts +14 -0
  54. package/dist/local/health.js +50 -4
  55. package/dist/local/index.d.ts +2 -2
  56. package/dist/local/index.js +24 -10
  57. package/dist/local/manager.d.ts +20 -1
  58. package/dist/local/manager.js +160 -39
  59. package/dist/local/ports.d.ts +158 -0
  60. package/dist/local/ports.js +443 -0
  61. package/dist/local/seed.d.ts +22 -25
  62. package/dist/local/seed.js +88 -56
  63. package/dist/logger.d.ts +54 -25
  64. package/dist/logger.js +329 -63
  65. package/dist/mcp.d.ts +2 -2
  66. package/dist/mcp.js +10 -5
  67. package/dist/mirror-bootstrap.d.ts +48 -0
  68. package/dist/mirror-bootstrap.js +254 -0
  69. package/dist/opl.d.ts +289 -0
  70. package/dist/opl.js +446 -0
  71. package/dist/permission-mode.d.ts +87 -0
  72. package/dist/permission-mode.js +307 -0
  73. package/dist/permissions-cli.d.ts +13 -49
  74. package/dist/permissions-cli.js +154 -348
  75. package/dist/permissions.d.ts +148 -38
  76. package/dist/permissions.js +591 -45
  77. package/dist/post-install.d.ts +33 -0
  78. package/dist/post-install.js +127 -0
  79. package/dist/read-credential.d.ts +65 -0
  80. package/dist/read-credential.js +86 -0
  81. package/dist/registry/cli.js +5 -2
  82. package/dist/registry/config.d.ts +0 -17
  83. package/dist/registry/config.js +0 -23
  84. package/dist/registry/index.d.ts +1 -1
  85. package/dist/registry/index.js +2 -2
  86. package/dist/registry/manager.d.ts +4 -21
  87. package/dist/registry/manager.js +83 -55
  88. package/dist/runtime-credential.d.ts +140 -0
  89. package/dist/runtime-credential.js +572 -0
  90. package/dist/runtime.d.ts +408 -0
  91. package/dist/runtime.js +748 -0
  92. package/dist/setup.d.ts +23 -28
  93. package/dist/setup.js +57 -84
  94. package/dist/status-cli.d.ts +29 -13
  95. package/dist/status-cli.js +124 -144
  96. package/dist/status-data.d.ts +195 -0
  97. package/dist/status-data.js +333 -0
  98. package/dist/status-system.d.ts +24 -0
  99. package/dist/status-system.js +56 -0
  100. package/dist/subject.d.ts +126 -20
  101. package/dist/subject.js +215 -30
  102. package/dist/testing.d.ts +74 -38
  103. package/dist/testing.js +185 -68
  104. package/dist/tool-catalog.d.ts +53 -11
  105. package/dist/tool-catalog.js +164 -13
  106. package/dist/tool-metadata.d.ts +7 -6
  107. package/dist/tool-metadata.js +6 -5
  108. package/dist/types.d.ts +11 -1
  109. package/dist/uninstall.d.ts +74 -19
  110. package/dist/uninstall.js +224 -49
  111. package/dist/user-login.d.ts +22 -16
  112. package/dist/user-login.js +67 -96
  113. package/dist/watch-cli.d.ts +6 -0
  114. package/dist/watch-cli.js +217 -0
  115. package/package.json +3 -11
  116. package/dist/dev.d.ts +0 -103
  117. package/dist/dev.js +0 -584
  118. package/dist/interactive-setup.d.ts +0 -165
  119. package/dist/interactive-setup.js +0 -1546
  120. package/dist/local/jaeger-main.d.ts +0 -13
  121. package/dist/local/jaeger-main.js +0 -85
  122. package/dist/local/jaeger.d.ts +0 -50
  123. package/dist/local/jaeger.js +0 -162
  124. package/dist/otel/exporter.d.ts +0 -17
  125. package/dist/otel/exporter.js +0 -12
  126. package/dist/otel/index.d.ts +0 -2
  127. package/dist/otel/index.js +0 -8
  128. package/dist/otel/otlp.d.ts +0 -103
  129. package/dist/otel/otlp.js +0 -385
  130. package/dist/tracer.d.ts +0 -190
  131. package/dist/tracer.js +0 -481
  132. package/dist/watch-sandbox.d.ts +0 -9
  133. package/dist/watch-sandbox.js +0 -81
@@ -1,1546 +0,0 @@
1
- "use strict";
2
- /**
3
- * Interactive install wizard. Runs after a harness plugin has been
4
- * registered, stepping the user through connecting the plugin to Ory:
5
- *
6
- * 1. Choose a connection: **Ory Network** (browser login / account
7
- * creation) or **local / audit-only** (no project, audit logging only).
8
- * 2. Ory Network branch: shell out to the official `ory` CLI to log in,
9
- * pick a workspace, pick a project (→ `projectUrl`), and create the
10
- * public OAuth2 client the PKCE user login needs (→ `oauth2ClientId`).
11
- * 3. Persist the resolved values to the shared config file and enable the
12
- * interactive user login.
13
- * 4. Best-effort "full setup": run the PKCE user login now (we hold the
14
- * project URL + client id), resolve the agent DCR identity, and let the
15
- * caller bootstrap permissions from the freshly-cached user identity.
16
- *
17
- * The whole wizard is **fail-open and skippable**: on a missing TTY, a
18
- * `--no-configure` flag, an already-configured plugin (without
19
- * `--reconfigure`), a missing `ory` CLI, or any `ory` failure it prints the
20
- * manual `configure` instructions and returns without throwing. Install must
21
- * never crash because setup was declined or the network was unreachable.
22
- *
23
- * The Ory Network integration deliberately reuses the official `ory` CLI
24
- * rather than re-implementing the Console API: the CLI already owns Network
25
- * browser login + account creation, workspace/project listing, and OAuth2
26
- * client creation, and the client it creates is exactly the one the plugin
27
- * READMEs document for manual setup. The one exception is minting the project
28
- * API key that runtime permission checks need — the CLI exposes no command for
29
- * it, so that single step calls the Console API directly, reusing the CLI's
30
- * stored session for auth (see {@link createProjectApiKeyViaConsole}).
31
- */
32
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
33
- if (k2 === undefined) k2 = k;
34
- var desc = Object.getOwnPropertyDescriptor(m, k);
35
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
36
- desc = { enumerable: true, get: function() { return m[k]; } };
37
- }
38
- Object.defineProperty(o, k2, desc);
39
- }) : (function(o, m, k, k2) {
40
- if (k2 === undefined) k2 = k;
41
- o[k2] = m[k];
42
- }));
43
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
44
- Object.defineProperty(o, "default", { enumerable: true, value: v });
45
- }) : function(o, v) {
46
- o["default"] = v;
47
- });
48
- var __importStar = (this && this.__importStar) || (function () {
49
- var ownKeys = function(o) {
50
- ownKeys = Object.getOwnPropertyNames || function (o) {
51
- var ar = [];
52
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
53
- return ar;
54
- };
55
- return ownKeys(o);
56
- };
57
- return function (mod) {
58
- if (mod && mod.__esModule) return mod;
59
- var result = {};
60
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
61
- __setModuleDefault(result, mod);
62
- return result;
63
- };
64
- })();
65
- Object.defineProperty(exports, "__esModule", { value: true });
66
- exports.ORY_CLI_NPM_SPEC = exports.LOOPBACK_REDIRECT_URIS = void 0;
67
- exports.createOryRunner = createOryRunner;
68
- exports.runPostInstall = runPostInstall;
69
- exports.runInteractiveSetup = runInteractiveSetup;
70
- exports.projectUrlFromSlug = projectUrlFromSlug;
71
- exports.createProjectApiKeyViaConsole = createProjectApiKeyViaConsole;
72
- exports.apiKeyAuthenticates = apiKeyAuthenticates;
73
- const node_child_process_1 = require("node:child_process");
74
- const fs = __importStar(require("node:fs"));
75
- const os = __importStar(require("node:os"));
76
- const path = __importStar(require("node:path"));
77
- const tty = __importStar(require("node:tty"));
78
- const readline = __importStar(require("node:readline"));
79
- const config_js_1 = require("./config.js");
80
- const setup_js_1 = require("./setup.js");
81
- const cli_js_1 = require("./cli.js");
82
- const cli_invocation_js_1 = require("./cli-invocation.js");
83
- const auth_js_1 = require("./auth.js");
84
- const client_js_1 = require("./client.js");
85
- const logger_js_1 = require("./logger.js");
86
- const user_login_js_1 = require("./user-login.js");
87
- const agent_auth_js_1 = require("./agent-auth.js");
88
- const tool_catalog_js_1 = require("./tool-catalog.js");
89
- const index_js_1 = require("./local/index.js");
90
- const subject_js_1 = require("./subject.js");
91
- const permissions_cli_js_1 = require("./permissions-cli.js");
92
- const version_cli_js_1 = require("./version-cli.js");
93
- const ui = __importStar(require("./ui.js"));
94
- /** Loopback redirect URIs the PKCE user login expects on the OAuth2 client. */
95
- exports.LOOPBACK_REDIRECT_URIS = auth_js_1.LOOPBACK_PORTS.map((port) => `http://127.0.0.1:${port}/callback`);
96
- /**
97
- * Build a runner bound to a specific `ory` binary (a bare command resolved on
98
- * `PATH`, or an absolute path to a plugin-installed binary). {@link defaultRunner}
99
- * uses the bare `"ory"`; {@link installOryCli} returns one bound to the binary
100
- * it just installed.
101
- */
102
- function createOryRunner(oryBin) {
103
- return {
104
- exec(args, opts) {
105
- const r = (0, node_child_process_1.spawnSync)(oryBin, args, {
106
- encoding: "utf-8",
107
- ...(opts?.input !== undefined ? { input: opts.input } : {}),
108
- });
109
- return {
110
- status: r.status,
111
- stdout: r.stdout ?? "",
112
- stderr: r.stderr ?? "",
113
- };
114
- },
115
- execInteractive(args) {
116
- const r = (0, node_child_process_1.spawnSync)(oryBin, args, { stdio: "inherit" });
117
- return r.status ?? 1;
118
- },
119
- };
120
- }
121
- const defaultRunner = createOryRunner("ory");
122
- /**
123
- * npm spec for the Ory CLI installed on demand when the `ory` binary isn't on
124
- * `PATH`. `@ory/cli` ships the platform `ory` binary and exposes it as its
125
- * `bin`, so an isolated `npm install` gives us a working CLI without touching
126
- * the user's global environment. Pinned to a major to stay reproducible while
127
- * still picking up patch/minor fixes.
128
- */
129
- exports.ORY_CLI_NPM_SPEC = "@ory/cli@^1";
130
- /**
131
- * Best-effort install of the Ory CLI into a plugin-managed directory under the
132
- * shared data dir, returning a runner bound to the installed binary (or null
133
- * on any failure). Uses `npm install --prefix` so nothing global is modified;
134
- * the binary lands at `<dataDir>/cli/node_modules/.bin/ory`. Never throws.
135
- */
136
- async function installOryCli() {
137
- const dir = path.join((0, config_js_1.getDataDir)(), "cli");
138
- try {
139
- fs.mkdirSync(dir, { recursive: true });
140
- }
141
- catch {
142
- return null;
143
- }
144
- const npm = process.platform === "win32" ? "npm.cmd" : "npm";
145
- const r = (0, node_child_process_1.spawnSync)(npm, ["install", "--prefix", dir, "--no-save", "--no-audit", "--no-fund", exports.ORY_CLI_NPM_SPEC], { encoding: "utf-8", stdio: ["ignore", "ignore", "pipe"] });
146
- if (r.status !== 0)
147
- return null;
148
- const binName = process.platform === "win32" ? "ory.cmd" : "ory";
149
- const bin = path.join(dir, "node_modules", ".bin", binName);
150
- if (!fs.existsSync(bin))
151
- return null;
152
- const runner = createOryRunner(bin);
153
- // Confirm the freshly installed binary actually runs before handing it back.
154
- return oryCliPresent(runner) ? runner : null;
155
- }
156
- /**
157
- * The shared post-install entry point every harness's `install` command
158
- * calls. Runs the interactive setup wizard, then (using whatever identity the
159
- * wizard cached) bootstraps permissions and prints the onboarding banner.
160
- *
161
- * This is the single seam that centralizes what each harness previously
162
- * open-coded as a local `postInstallPermissions` helper.
163
- */
164
- async function runPostInstall(binName, harness, args = [], deps = {}) {
165
- // Surface the running CLI version up front. `npx -p @ory/<harness>` (no
166
- // version pin) will happily reuse a previously-cached install rather than
167
- // re-resolve to latest, so a user can silently be running an old CLI whose
168
- // install flow predates newer behavior (e.g. the interactive setup wizard)
169
- // while believing they're on the current release. Printing the version makes
170
- // that mismatch visible at a glance; the README's troubleshooting note
171
- // explains how to clear a stale npx cache.
172
- const cliVersion = (0, version_cli_js_1.resolveCoreVersion)();
173
- if (cliVersion !== "unknown") {
174
- ui.info(`${binName} v${cliVersion}`);
175
- }
176
- const result = await runInteractiveSetup(binName, harness, args, deps);
177
- // The Ory Network wizard path bootstraps permissions itself (via the
178
- // admin-authenticated `ory` CLI). Only fall back to the DCR-token bootstrap
179
- // when the wizard didn't handle it — otherwise we'd emit 403s on a hosted
180
- // project whose relation-tuple API rejects the agent/user token.
181
- const bootstrapped = result.skipAutoBootstrap
182
- ? false
183
- : await (0, permissions_cli_js_1.maybeAutoBootstrap)(binName, harness);
184
- (0, permissions_cli_js_1.printPermissionsOnboardingHelp)(binName, harness, {
185
- bootstrappedAutomatically: bootstrapped,
186
- });
187
- // Flush any next-steps guidance the harness's install() deferred, so it
188
- // lands at the very end of the interactive flow rather than above the
189
- // wizard's first prompt. When the wizard already connected Ory, the renderer
190
- // prints a short launch summary instead of the manual setup steps — those
191
- // would only contradict the values the wizard just saved.
192
- (0, setup_js_1.emitDeferredNextSteps)(isConfiguredOutcome(result.outcome));
193
- }
194
- /**
195
- * Outcomes where Ory is already wired up by the time the wizard returns —
196
- * either the user connected it now (network / local / audit-only) or it was
197
- * configured on a previous run. For these the closing next-steps should be a
198
- * short "launch the harness" summary, not the manual env-var guidance.
199
- */
200
- function isConfiguredOutcome(outcome) {
201
- return (outcome === "network_configured" ||
202
- outcome === "local_configured" ||
203
- outcome === "audit_only" ||
204
- outcome === "skipped_configured");
205
- }
206
- /**
207
- * Run the interactive setup wizard. Always resolves; never throws.
208
- */
209
- async function runInteractiveSetup(binName, harness, args = [], deps = {}) {
210
- const runner = deps.runner ?? defaultRunner;
211
- const ttyAvailable = deps.isTtyAvailableFn ?? cli_js_1.isTtyAvailable;
212
- const noConfigure = args.includes("--no-configure");
213
- const reconfigure = args.includes("--reconfigure");
214
- if (noConfigure) {
215
- return { outcome: "skipped_flag" };
216
- }
217
- if (!ttyAvailable()) {
218
- // Non-interactive (CI/headless) — preserve the pre-wizard behavior and
219
- // point the operator at the manual, scriptable path.
220
- printManualSetup(binName);
221
- return { outcome: "skipped_no_tty" };
222
- }
223
- const resolved = (0, config_js_1.resolveConfig)();
224
- if ((resolved.projectUrl || resolved.auditOnly) && !reconfigure) {
225
- ui.banner("Ory Agent Plugin", "Already connected to Ory");
226
- ui.blank();
227
- ui.success("Ory is already configured for the agent plugins.");
228
- ui.summary([
229
- ["Mode", resolved.auditOnly ? "audit-only (no project)" : "connected"],
230
- ["Project URL", resolved.auditOnly ? undefined : resolved.projectUrl],
231
- ["Config", (0, config_js_1.getConfigPath)()],
232
- ]);
233
- ui.blank();
234
- ui.hint(`Re-run with ${ui.inlineCommand("--reconfigure")} to change it.`);
235
- return { outcome: "skipped_configured" };
236
- }
237
- // The wizard prompts several times in a row. Opening `/dev/tty` afresh for
238
- // each prompt (as `promptOnTty` does) leaves a blocking read stuck in
239
- // libuv's threadpool per call, and after a few prompts the pool is
240
- // exhausted and the next prompt hangs. So when we're driving real TTY input
241
- // (no injected promptFn), open one readline interface and reuse it for the
242
- // whole flow. Tests inject their own promptFn and skip this entirely.
243
- // One persistent tty prompter, but recreated around any interactive child
244
- // process. A readline over `/dev/tty` that stays open across a
245
- // `spawnSync(..., { stdio: "inherit" })` (how `execInteractive` runs the
246
- // `ory auth` browser login) stops receiving input once the child returns —
247
- // the terminal is left in a state the parent readline no longer reads from,
248
- // so the *next* prompt (e.g. workspace selection) hangs forever with no
249
- // output. `runInteractive` closes the prompter before the child and reopens
250
- // a fresh one after, which reliably restores line input.
251
- let ttyPrompter = deps.promptFn ? null : createTtyPrompter();
252
- const prompt = (q) => (deps.promptFn ?? ttyPrompter?.prompt ?? cli_js_1.promptOnTty)(q);
253
- const runInteractive = (fn) => {
254
- if (deps.promptFn || !ttyPrompter)
255
- return fn();
256
- ttyPrompter.close();
257
- ttyPrompter = null;
258
- try {
259
- return fn();
260
- }
261
- finally {
262
- ttyPrompter = createTtyPrompter();
263
- }
264
- };
265
- // Quiet the structured stderr firehose for the duration of the wizard so
266
- // `[ory-agent] {...}` debug lines don't interleave with the human-facing UI
267
- // when a session runs with ORY_AGENT_DEBUG=true (the dev launcher always
268
- // sets it). Debug still lands in the log file.
269
- try {
270
- return await (0, logger_js_1.withQuietStderr)(() => runWizard(binName, harness, { runner, prompt, deps, runInteractive }));
271
- }
272
- catch (err) {
273
- const msg = err instanceof Error ? err.message : String(err);
274
- console.warn("");
275
- ui.warning(`Interactive setup did not complete: ${msg}`);
276
- printManualSetup(binName);
277
- return { outcome: "network_fallback" };
278
- }
279
- finally {
280
- ttyPrompter?.close();
281
- }
282
- }
283
- /**
284
- * Open `/dev/tty` once and wrap it in a single readline interface, reused for
285
- * every prompt in the wizard.
286
- *
287
- * Crucially this uses `tty.ReadStream`, not `fs.createReadStream`. Reading a
288
- * tty via an fs stream issues a *blocking* `read()` in libuv's threadpool that
289
- * can't be cancelled; a pending one exhausts the pool (hanging mid-flow) and,
290
- * worse, blocks `process.exit()` from ever completing (hanging at the very
291
- * end). `tty.ReadStream` uses libuv's non-blocking tty handle instead, which is
292
- * event-loop integrated and tears down cleanly on exit. Returns null if
293
- * `/dev/tty` can't be opened (caller falls back to `promptOnTty`).
294
- */
295
- function createTtyPrompter() {
296
- let fd;
297
- try {
298
- fd = fs.openSync("/dev/tty", "r");
299
- }
300
- catch {
301
- return null;
302
- }
303
- let input;
304
- try {
305
- input = new tty.ReadStream(fd);
306
- }
307
- catch {
308
- try {
309
- fs.closeSync(fd);
310
- }
311
- catch {
312
- /* ignore */
313
- }
314
- return null;
315
- }
316
- const rl = readline.createInterface({ input, output: process.stderr });
317
- return {
318
- prompt: (question) => new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim()))),
319
- close: () => {
320
- rl.close();
321
- input.destroy();
322
- try {
323
- fs.closeSync(fd);
324
- }
325
- catch {
326
- /* fd may already be closed by the stream */
327
- }
328
- },
329
- };
330
- }
331
- async function runWizard(binName, harness, ctx) {
332
- ui.banner("Ory Agent Plugin", "Connect your agent to Ory");
333
- ui.heading("How do you want to connect?");
334
- ui.blank();
335
- ui.menu([
336
- {
337
- key: "1",
338
- label: "Ory Network",
339
- description: "sign in (or create an account) and connect a hosted project",
340
- },
341
- {
342
- key: "2",
343
- label: "Local stack",
344
- description: "self-hosted Ory via `local up` (localhost:4000), seeded for you",
345
- },
346
- {
347
- key: "3",
348
- label: "Audit-only",
349
- description: "no Ory project (traces tool calls locally, no auth or checks)",
350
- },
351
- ]);
352
- ui.blank();
353
- const choice = await ctx.prompt(ui.promptLine("Enter 1, 2 or 3", { hint: "[1]" }));
354
- // Default to the Ory Network path (empty / "1"). "2" wires up the local
355
- // stack — where every value comes from the seeded stack, no prompts needed.
356
- // "3" is the no-project audit-only fallback.
357
- if (isAuditChoice(choice)) {
358
- return configureAuditOnly(binName);
359
- }
360
- if (isLocalChoice(choice)) {
361
- return configureLocalStack(binName, harness, ctx);
362
- }
363
- return configureNetwork(binName, harness, ctx);
364
- }
365
- function configureAuditOnly(binName) {
366
- (0, config_js_1.saveConfig)({ auditOnly: true });
367
- ui.heading("Audit-only mode");
368
- ui.success("Configured for local / audit-only mode.");
369
- ui.hint("Tool calls are traced locally; no Ory project, auth, or permission checks.");
370
- ui.summary([["Config", (0, config_js_1.getConfigPath)()]]);
371
- ui.blank();
372
- ui.hint(`To connect to Ory later, re-run install with ${ui.inlineCommand("--reconfigure")}, or run:`);
373
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --project-url <URL> --oauth2-client-id <CLIENT_ID>`);
374
- return { outcome: "audit_only" };
375
- }
376
- /**
377
- * Configure the plugin against the local Ory stack. Every value the plugin
378
- * needs — the project URL (`http://localhost:4000`), the public OAuth2 client
379
- * for the PKCE login (the seeded `ory-user-local`), and the tool permissions —
380
- * is supplied by the stack itself, so this path takes **no** prompts: the local
381
- * stack is the default source of otherwise-unprovided values.
382
- *
383
- * The flow brings the stack up (idempotent; Docker required), seeds the test
384
- * identity + `use` permissions, persists the local connection details, and runs
385
- * the login + agent DCR so the next session is fully wired. Every step is
386
- * fail-open: if Docker isn't available we still persist the local config and
387
- * tell the user to run `local up` when they can, so the plugin is pointed at
388
- * the local stack either way.
389
- */
390
- async function configureLocalStack(binName, harness, ctx) {
391
- const bringUp = ctx.deps.localStackFn ?? index_js_1.ensureLocalOryStack;
392
- const seed = ctx.deps.seedFn ?? index_js_1.seedLocalEnvironment;
393
- ui.heading("Local Ory stack");
394
- ui.step("Setting up the local Ory stack (Kratos, Keto, Hydra, gateway on :4000).");
395
- ui.hint("This requires Docker and may take a minute on first run.");
396
- let stackStatus;
397
- let stackDetail;
398
- try {
399
- const stack = await bringUp();
400
- stackStatus = stack.status;
401
- stackDetail = stack.detail;
402
- }
403
- catch (err) {
404
- stackStatus = "compose-failed";
405
- stackDetail = err instanceof Error ? err.message : String(err);
406
- }
407
- const running = stackStatus === "started" || stackStatus === "already-running";
408
- // Point the plugin at the local stack regardless of whether it came up: the
409
- // seeded PKCE client id is stable, so this config is valid the moment the
410
- // stack is running (now, or after a later `local up`). Persist the seeded
411
- // subject namespace too so permission checks address the user as the same
412
- // SubjectSet (`User:<id>`) the seed writes — without it, enforce mode would
413
- // never match the seeded tuples.
414
- (0, config_js_1.saveConfig)({
415
- projectUrl: index_js_1.GATEWAY_URL,
416
- oauth2ClientId: index_js_1.USER_CLIENT_ID,
417
- userSubjectNamespace: index_js_1.USER_SUBJECT_NAMESPACE,
418
- userLogin: true,
419
- auditOnly: false,
420
- });
421
- if (!running) {
422
- console.warn("");
423
- ui.warning(`Could not start the local stack: ${stackDetail ?? stackStatus}.`);
424
- ui.warnDetail("The plugin is still configured for the local stack — start it when Docker is ready:");
425
- ui.warnDetail(`${(0, cli_invocation_js_1.oryNpx)(binName)} local up`);
426
- printLocalStackSummary();
427
- return {
428
- outcome: "local_configured",
429
- projectUrl: index_js_1.GATEWAY_URL,
430
- oauth2ClientId: index_js_1.USER_CLIENT_ID,
431
- userAuthenticated: false,
432
- // The stack seeds its own permissions on `local up`; never run the
433
- // hosted-project admin bootstrap against localhost.
434
- skipAutoBootstrap: true,
435
- };
436
- }
437
- // Seed the identity + `use` permissions into the local stack. Best-effort:
438
- // observe mode (the install default) keeps tools working even if this fails.
439
- let credentials = null;
440
- try {
441
- const result = await seed();
442
- credentials = {
443
- email: result.user.identity.email,
444
- password: result.user.password,
445
- };
446
- ui.success(`Seeded ${result.permissions.tuples} permissions in '${result.permissions.namespace}' for ${result.permissions.subject}.`);
447
- }
448
- catch (err) {
449
- const msg = err instanceof Error ? err.message : String(err);
450
- ui.warning(`Could not seed the local stack: ${msg}`);
451
- ui.warnDetail(`Seed it later with: ${(0, cli_invocation_js_1.oryNpx)(binName)} local seed`);
452
- }
453
- printLocalStackSummary();
454
- if (credentials) {
455
- ui.heading("Sign in to the browser login with the seeded test user:");
456
- ui.summary([
457
- ["Email", credentials.email],
458
- ["Password", credentials.password],
459
- ]);
460
- }
461
- const { authenticated } = await completeFullSetup(binName, harness, ctx);
462
- return {
463
- outcome: "local_configured",
464
- projectUrl: index_js_1.GATEWAY_URL,
465
- oauth2ClientId: index_js_1.USER_CLIENT_ID,
466
- userAuthenticated: authenticated,
467
- skipAutoBootstrap: true,
468
- };
469
- }
470
- function printLocalStackSummary() {
471
- ui.heading("Local stack configured:");
472
- ui.summary([
473
- ["Project URL", index_js_1.GATEWAY_URL],
474
- ["OAuth2 client id", index_js_1.USER_CLIENT_ID],
475
- ["Subject namespace", `${index_js_1.USER_SUBJECT_NAMESPACE} (matches seeded permissions)`],
476
- ["Config", (0, config_js_1.getConfigPath)()],
477
- ]);
478
- }
479
- async function configureNetwork(binName, harness, ctx) {
480
- const { prompt } = ctx;
481
- // 1. Ensure the `ory` CLI is available — offer to install it if it isn't,
482
- // so a from-scratch user (no CLI on PATH) doesn't dead-end here. On accept
483
- // we install `@ory/cli` into a plugin-managed dir and switch the runner to
484
- // the freshly installed binary; on decline/failure we fall back to manual.
485
- let runner = ctx.runner;
486
- if (!oryCliPresent(runner)) {
487
- const installed = await maybeInstallOryCli(ctx, prompt);
488
- if (!installed) {
489
- printOryCliMissing();
490
- printManualSetup(binName);
491
- return { outcome: "network_fallback" };
492
- }
493
- runner = installed;
494
- }
495
- // 2. Ensure the user is signed in. `ory` persists its own session, so probe
496
- // first (listing workspaces) and only launch the browser sign-in when the
497
- // probe fails — otherwise we'd promise a browser that never opens because
498
- // the user is already authenticated.
499
- let wsList = runner.exec(["list", "workspaces", "--format", "json"]);
500
- if (wsList.status !== 0) {
501
- ui.heading("Sign in to Ory Network");
502
- ui.info("A browser window will open to sign in or create an account.");
503
- ui.hint("If it doesn't, watch this terminal for a URL to open manually.");
504
- ui.hint("The browser handles sign-in only — this terminal drives the rest of setup,");
505
- ui.hint("so once the browser says you're signed in, come back here to continue.");
506
- const loginCode = ctx.runInteractive(() => runner.execInteractive(["auth"]));
507
- // The `ory auth` browser page is rendered by the Ory CLI, so we can't tell
508
- // the user there to return to the terminal. Ring the bell instead — their
509
- // focus is still on the browser, and this pulls it back to the terminal
510
- // (dock bounce / tab badge) where the next steps live.
511
- ui.bell();
512
- if (loginCode !== 0) {
513
- console.warn("");
514
- ui.warning("Ory Network sign-in did not complete.");
515
- printManualSetup(binName);
516
- return { outcome: "network_fallback" };
517
- }
518
- ui.success("Signed in to Ory Network — back in the terminal now.");
519
- ui.step("Next, choose your workspace and project here. Loading your workspaces…");
520
- wsList = runner.exec(["list", "workspaces", "--format", "json"]);
521
- }
522
- else {
523
- ui.heading("Ory Network");
524
- ui.success("Already signed in to Ory Network.");
525
- }
526
- // 3. Pick a workspace (offer to create one when the account has none — a
527
- // brand-new account may have no workspace yet).
528
- let workspaces = wsList.status === 0 ? parseList(wsList.stdout, "workspaces") : null;
529
- if (!workspaces || workspaces.length === 0) {
530
- const created = await maybeCreateWorkspace(runner, prompt, harness);
531
- if (created)
532
- workspaces = [created];
533
- }
534
- if (!workspaces || workspaces.length === 0) {
535
- console.warn("");
536
- ui.warning("No Ory Network workspace selected.");
537
- ui.warnDetail("Create one at https://console.ory.sh/ and re-run install with --reconfigure.");
538
- printManualSetup(binName);
539
- return { outcome: "network_fallback" };
540
- }
541
- const workspace = await selectFromList(prompt, "workspace", workspaces, (w) => w.name ?? w.id);
542
- if (!workspace) {
543
- printManualSetup(binName);
544
- return { outcome: "network_fallback" };
545
- }
546
- // 4. Pick a project. A brand-new workspace with none gets the create prompt;
547
- // otherwise the existing projects are listed for selection, and while the
548
- // workspace is still under the Developer-plan cap of two we also offer to
549
- // create a fresh project dedicated to the agent plugin — so a single-project
550
- // account isn't forced to reuse its existing project.
551
- ui.step("Loading projects…");
552
- const existingProjects = listProjects(runner, workspace.id);
553
- const project = !existingProjects || existingProjects.length === 0
554
- ? await maybeCreateProject(runner, prompt, workspace, harness)
555
- : await selectOrCreateProject(runner, prompt, workspace, harness, existingProjects);
556
- if (!project) {
557
- console.warn("");
558
- ui.warning("No Ory Network project selected.");
559
- printManualSetup(binName);
560
- return { outcome: "network_fallback" };
561
- }
562
- // The SDK URL is built from the project *slug*. Require a real slug — a
563
- // missing one, or a value that looks like the project id (UUID), can't
564
- // produce a correct URL, so fall back to manual setup rather than guess.
565
- if (!project.slug || (0, config_js_1.looksLikeProjectId)(project.slug)) {
566
- console.warn("");
567
- ui.warning("Could not resolve the selected project's slug (needed for the SDK URL).");
568
- printManualSetup(binName);
569
- return { outcome: "network_fallback" };
570
- }
571
- const projectUrl = projectUrlFromSlug(project.slug);
572
- // 5. Enable OAuth2 Dynamic Client Registration so the agent identity can
573
- // self-register at runtime. Ory Network projects ship with DCR disabled,
574
- // so without this the default agent-identity path would never resolve.
575
- await maybeEnableDcr(runner, prompt, project.id);
576
- // 6. Provision the public OAuth2 client for the PKCE user login. Reuse an
577
- // existing one on this project first — a re-run (`install --reconfigure`)
578
- // against the same project must not mint a duplicate public client and
579
- // orphan the old one (the CLI-created client is never cleaned up by
580
- // uninstall). Only create when no prior client is found.
581
- ui.heading("OAuth2 client");
582
- let clientId = findExistingOAuth2Client(runner, project.id);
583
- if (clientId) {
584
- ui.step(`Reusing the existing OAuth2 client for user login (${clientId}).`);
585
- }
586
- else {
587
- ui.step("Creating the OAuth2 client for user login...");
588
- clientId = createOAuth2Client(runner, project.id);
589
- }
590
- if (!clientId) {
591
- ui.warning("Could not create the OAuth2 client automatically.");
592
- ui.warnDetail(`Project URL resolved: ${projectUrl}`);
593
- printManualOAuth2ClientHelp(binName, project.id);
594
- // Still persist the project URL + id so the user only needs the client id.
595
- (0, config_js_1.saveConfig)({ projectUrl, projectId: project.id, auditOnly: false });
596
- return { outcome: "network_fallback", projectUrl };
597
- }
598
- // 7. Persist and enable user login. The project id is stored alongside the
599
- // URL (it's the `ORY_PROJECT_ID` value) so later admin operations and
600
- // `status` have it without re-selecting a project.
601
- (0, config_js_1.saveConfig)({
602
- projectUrl,
603
- projectId: project.id,
604
- oauth2ClientId: clientId,
605
- userLogin: true,
606
- auditOnly: false,
607
- });
608
- ui.heading("Ory Network connected:");
609
- ui.success("Project connected and OAuth2 client created.");
610
- ui.summary([
611
- ["Project URL", projectUrl],
612
- ["OAuth2 client id", clientId],
613
- ["Config", (0, config_js_1.getConfigPath)()],
614
- ]);
615
- // 8. Best-effort full setup: log the user in now and resolve the agent
616
- // identity, so the very next session is fully wired.
617
- const { authenticated, subject } = await completeFullSetup(binName, harness, ctx);
618
- // 9. Bootstrap permissions via the admin-authenticated `ory` CLI. The
619
- // runtime agent/user tokens can't write relation tuples on a hosted
620
- // project (that's a project-admin operation), but the `ory` session can.
621
- if (subject) {
622
- // Persist the subject *shape* this grant used. If the install resolved a
623
- // SubjectSet (ORY_USER_SUBJECT_NAMESPACE was set), a later runtime session
624
- // that lacks the env var would otherwise resolve a direct subject_id and
625
- // check/write a divergent tuple for the same user. Persisting the namespace
626
- // keeps install-time and runtime writes on one canonical subject shape.
627
- if ("subjectSet" in subject) {
628
- (0, config_js_1.saveConfig)({ userSubjectNamespace: subject.subjectSet.namespace });
629
- }
630
- await bootstrapPermissionsViaOry(runner, project.id, harness, subject, prompt);
631
- }
632
- // 10. Provision the project API key runtime permission *checks* need. Same
633
- // root cause as bootstrap's admin path — the DCR OAuth2 token can't
634
- // authenticate Ory Network's Permission API — but this credential is used
635
- // at runtime, so it's persisted to config. Placed last so its prompt
636
- // doesn't consume answers meant for earlier steps.
637
- await maybeProvisionProjectApiKey(binName, harness, project.id, projectUrl, prompt, ctx.deps.createProjectApiKeyFn ?? createProjectApiKeyViaConsole, ctx.deps.validateApiKeyFn ?? apiKeyAuthenticates);
638
- return {
639
- outcome: "network_configured",
640
- projectUrl,
641
- oauth2ClientId: clientId,
642
- userAuthenticated: authenticated,
643
- // We just handled bootstrap with admin scope; don't let runPostInstall
644
- // retry it with the scope-less DCR token (which would only 403).
645
- skipAutoBootstrap: true,
646
- };
647
- }
648
- async function completeFullSetup(binName, harness, ctx) {
649
- const clientFactory = ctx.deps.clientFactory ?? client_js_1.OryAgentClient.fromEnv;
650
- const userLogin = ctx.deps.userLoginFn ?? user_login_js_1.ensureUserAuthenticated;
651
- const agentGate = ctx.deps.agentGateFn ?? agent_auth_js_1.ensureAgentIdentity;
652
- try {
653
- const client = clientFactory(harness);
654
- ui.heading("Signing you in to the project as the permission subject.");
655
- ui.hint("If a sign-in is needed, a browser will open (watch for a URL here too).");
656
- const decision = await userLogin(client, {
657
- binName,
658
- harness,
659
- allowBlock: false,
660
- });
661
- // Resolve the agent identity regardless — it bootstraps from the user
662
- // token when present and no-ops otherwise. Never blocks.
663
- await agentGate(client, { harness });
664
- const authenticated = decision.proceed && !!decision.subject;
665
- const subject = authenticated ? (0, subject_js_1.resolveUserSubject)(client) : null;
666
- return {
667
- authenticated,
668
- subject: subject && (0, subject_js_1.subjectLabel)(subject) !== "agent:unknown" ? subject : null,
669
- };
670
- }
671
- catch (err) {
672
- const msg = err instanceof Error ? err.message : String(err);
673
- ui.warning(`Skipping automatic login (will run on first session): ${msg}`);
674
- return { authenticated: false, subject: null };
675
- }
676
- }
677
- /**
678
- * Grant the signed-in user `use` on every built-in tool by writing relation
679
- * tuples through the admin-authenticated `ory` CLI (`ory create
680
- * relationships`). This is the one credential in the flow with project-admin
681
- * scope — the agent's DCR token and the user's PKCE token can't write tuples
682
- * on a hosted Ory Network project.
683
- *
684
- * The tuples reference the permission namespace (default `AgentTools`), which
685
- * must be defined in the project's permission model or the write fails with
686
- * `NotFound`. So we inspect the model first (see {@link inspectPermissionModel})
687
- * and act on what's actually there. We provision in every case *except* when
688
- * the namespace already exists:
689
- *
690
- * - **Namespace already defined** → grant directly, touch nothing.
691
- * - **No model yet** (a freshly created Ory Network project) → offer to
692
- * provision a minimal `<namespace>` model (`ory update opl`), then grant.
693
- * - **Model exists (OPL source fetchable) but lacks `<namespace>`** → offer
694
- * to *merge* the namespace into the existing OPL and re-upload it, leaving
695
- * the other namespaces untouched, then grant. Ory Network serves the OPL
696
- * source at a `location` URL in the permission config, which makes this
697
- * non-destructive merge possible.
698
- * - **Model provably exists but its source couldn't be read** → we can't
699
- * merge and `ory update opl` overwrites the whole OPL, so we refuse to
700
- * clobber it: print the exact snippet to add and skip the grant.
701
- *
702
- * Best-effort throughout: any failure prints a short, actionable note rather
703
- * than a wall of errors. Observe mode (the install default) keeps tools working
704
- * regardless, so this is an optimization, not a hard requirement.
705
- */
706
- async function bootstrapPermissionsViaOry(runner, projectId, harness, subject, prompt) {
707
- const namespace = process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
708
- const tools = (0, tool_catalog_js_1.getToolCatalog)(harness);
709
- if (tools.length === 0)
710
- return;
711
- // Ensure the namespace the tuples reference exists, or the write NotFounds.
712
- // We only skip provisioning when the namespace is already defined; every
713
- // other state provisions. When we can fetch the existing OPL source we merge
714
- // our namespace into it (non-destructive); otherwise we create a fresh
715
- // minimal model. The one case we refuse is a model that provably exists but
716
- // whose source we couldn't read — overwriting it would clobber it.
717
- const model = await inspectPermissionModel(runner, projectId, namespace);
718
- if (model.kind === "merge") {
719
- // A model exists and we have its OPL source — append our namespace without
720
- // disturbing the others (prompted).
721
- ui.heading(`This project has a permission model, but it doesn't define '${namespace}'.`);
722
- ui.hint("It can be added to the existing model without changing the other namespaces.");
723
- ui.blank();
724
- const answer = await prompt(ui.promptLine(`Add '${namespace}' to this project's permission model now?`, {
725
- hint: "[Y/n]",
726
- }));
727
- if (isNo(answer)) {
728
- printPermissionModelHelp(namespace);
729
- return;
730
- }
731
- if (!provisionPermissionModel(runner, projectId, namespace, model.opl)) {
732
- ui.warning(`Could not add '${namespace}' to the permission model automatically.`);
733
- printPermissionModelHelp(namespace);
734
- return;
735
- }
736
- ui.success(`Added '${namespace}' to the permission model.`);
737
- }
738
- else if (model.kind === "empty" || model.kind === "unknown") {
739
- // No model (or one we couldn't read at all) — provision a minimal one
740
- // (prompted). Reading the config failing is treated as "nothing there".
741
- ui.heading(`This project has no '${namespace}' permission model yet —`);
742
- ui.hint("without it, permission checks can't be granted or enforced.");
743
- ui.blank();
744
- const answer = await prompt(ui.promptLine(`Create a minimal '${namespace}' permission model on this project now?`, { hint: "[Y/n]" }));
745
- if (isNo(answer)) {
746
- printPermissionModelHelp(namespace);
747
- return;
748
- }
749
- if (!provisionPermissionModel(runner, projectId, namespace)) {
750
- ui.warning(`Could not create the '${namespace}' permission model automatically.`);
751
- printPermissionModelHelp(namespace);
752
- return;
753
- }
754
- ui.success(`Created the '${namespace}' permission model.`);
755
- }
756
- else if (model.kind === "blocked") {
757
- // A model provably exists but we couldn't fetch its source, so we can't
758
- // merge and `ory update opl` would overwrite it. Guide instead of clobber.
759
- ui.heading(`This project has a permission model, but it doesn't define '${namespace}'`);
760
- ui.hint("and its source couldn't be read, so adding it automatically might overwrite it.");
761
- printPermissionModelHelp(namespace);
762
- return;
763
- }
764
- // "present" (namespace already defined) falls through and grants directly.
765
- const subjectPatch = "subjectSet" in subject
766
- ? {
767
- subject_set: {
768
- namespace: subject.subjectSet.namespace,
769
- object: subject.subjectSet.object,
770
- relation: subject.subjectSet.relation,
771
- },
772
- }
773
- : { subject_id: subject.subjectId };
774
- const tuples = tools.map((object) => ({
775
- namespace,
776
- object,
777
- relation: "use",
778
- ...subjectPatch,
779
- }));
780
- ui.heading("Permissions");
781
- ui.step(`Granting ${(0, subject_js_1.subjectLabel)(subject)} 'use' on ${tools.length} tools (via the Ory CLI)...`);
782
- const r = runner.exec(["create", "relationships", "--project", projectId, "--format", "json", "-f", "-", "--yes"], { input: JSON.stringify(tuples) });
783
- if (r.status === 0) {
784
- ui.success(`Granted permissions for ${tools.length} tools in '${namespace}'.`);
785
- return;
786
- }
787
- ui.warning("Could not pre-grant permissions automatically.");
788
- const detail = r.stderr.trim();
789
- if (detail)
790
- ui.warnDetail(detail.split("\n")[0]);
791
- printPermissionModelHelp(namespace);
792
- }
793
- /**
794
- * Inspect the project's permission model. Ory Network reports the config in one
795
- * of two shapes: `{ namespaces: [] }` (or `null`) for a project with no model,
796
- * and `{ namespaces: { location: "https://….txt" } }` for an OPL-configured
797
- * one — where the `.txt` is the full OPL *source*. We fetch that source so a
798
- * missing namespace can be merged in rather than clobbering the whole model.
799
- */
800
- async function inspectPermissionModel(runner, projectId, namespace) {
801
- const r = runner.exec([
802
- "get",
803
- "permission-config",
804
- "--project",
805
- projectId,
806
- "--format",
807
- "json",
808
- ]);
809
- if (r.status !== 0)
810
- return { kind: "unknown" };
811
- const cfg = safeJsonParse(r.stdout);
812
- if (!cfg || typeof cfg !== "object")
813
- return { kind: "unknown" };
814
- const ns = cfg.namespaces;
815
- // A fresh project reports no model as null / an absent key.
816
- if (ns == null)
817
- return { kind: "empty" };
818
- // OPL-configured form: { namespaces: { location: "https://….txt" } }.
819
- // The .txt holds the full OPL source, so we can merge into it.
820
- if (!Array.isArray(ns) && typeof ns === "object") {
821
- const location = ns.location;
822
- if (typeof location !== "string" || !location)
823
- return { kind: "empty" };
824
- const opl = await fetchOplSource(location);
825
- // A model exists but its source is unreadable — can't merge, mustn't clobber.
826
- if (opl == null)
827
- return { kind: "blocked" };
828
- return oplDefinesClass(opl, namespace)
829
- ? { kind: "present" }
830
- : { kind: "merge", opl };
831
- }
832
- // Inline compiled form: { namespaces: [{ name, id }, …] } (legacy / self-hosted).
833
- // We only have names, not source, so a missing namespace can't be merged.
834
- if (Array.isArray(ns)) {
835
- if (ns.length === 0)
836
- return { kind: "empty" };
837
- const names = ns
838
- .map((n) => n && typeof n === "object"
839
- ? n.name
840
- : undefined)
841
- .filter((n) => typeof n === "string");
842
- return names.includes(namespace) ? { kind: "present" } : { kind: "blocked" };
843
- }
844
- return { kind: "unknown" };
845
- }
846
- /** Fetch the OPL source served at a permission-config `location` URL. */
847
- async function fetchOplSource(url) {
848
- try {
849
- const res = await fetch(url);
850
- if (!res.ok)
851
- return null;
852
- return await res.text();
853
- }
854
- catch {
855
- return null;
856
- }
857
- }
858
- /** Whether an OPL source defines `class <name>`. */
859
- function oplDefinesClass(opl, name) {
860
- const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
861
- return new RegExp(`\\bclass\\s+${escaped}\\b`).test(opl);
862
- }
863
- /**
864
- * Provision a permission model that defines `<namespace>` with a `use`
865
- * relation, via `ory update opl`. The namespace becomes an OPL class, so it
866
- * must be a valid identifier; if not, we bail (caller guides instead).
867
- *
868
- * When `existingOpl` is given, the namespace is appended to that source
869
- * (preserving the model's other namespaces); otherwise a fresh minimal model
870
- * is generated. Writes the OPL to a temp file because `ory update opl` reads
871
- * `--file` only (no stdin). Returns true on success.
872
- */
873
- function provisionPermissionModel(runner, projectId, namespace, existingOpl) {
874
- if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(namespace))
875
- return false;
876
- const opl = existingOpl !== undefined
877
- ? mergeNamespaceIntoOpl(existingOpl, namespace)
878
- : freshOpl(namespace);
879
- let dir;
880
- try {
881
- dir = fs.mkdtempSync(path.join(os.tmpdir(), "ory-opl-"));
882
- }
883
- catch {
884
- return false;
885
- }
886
- const file = path.join(dir, "namespace_config.ts");
887
- try {
888
- fs.writeFileSync(file, opl);
889
- const r = runner.exec([
890
- "update",
891
- "opl",
892
- "--project",
893
- projectId,
894
- "-f",
895
- file,
896
- "--format",
897
- "json",
898
- "--yes",
899
- ]);
900
- return r.status === 0;
901
- }
902
- catch {
903
- return false;
904
- }
905
- finally {
906
- try {
907
- fs.rmSync(dir, { recursive: true, force: true });
908
- }
909
- catch {
910
- /* ignore */
911
- }
912
- }
913
- }
914
- /** A minimal standalone OPL defining `User` and `<namespace>` with `use`. */
915
- function freshOpl(namespace) {
916
- return [
917
- 'import { Namespace } from "@ory/permission-namespace-types"',
918
- "",
919
- "class User implements Namespace {}",
920
- "",
921
- `class ${namespace} implements Namespace {`,
922
- " related: {",
923
- " use: User[]",
924
- " }",
925
- "}",
926
- "",
927
- ].join("\n");
928
- }
929
- /**
930
- * Append a `<namespace>` class (with a `use: User[]` relation) to an existing
931
- * OPL source, leaving every other namespace untouched. Adds a `User` class too
932
- * when the source doesn't already define one, since `use` is typed `User[]`.
933
- */
934
- function mergeNamespaceIntoOpl(existingOpl, namespace) {
935
- let opl = existingOpl.replace(/\s*$/, "\n");
936
- if (!oplDefinesClass(opl, "User")) {
937
- opl += "\nclass User implements Namespace {}\n";
938
- }
939
- opl +=
940
- `\nclass ${namespace} implements Namespace {\n` +
941
- " related: {\n" +
942
- " use: User[]\n" +
943
- " }\n" +
944
- "}\n";
945
- return opl;
946
- }
947
- function printPermissionModelHelp(namespace) {
948
- // When the namespace is a valid OPL class name, show the exact snippet to
949
- // paste into the model — that's the concrete "how" behind "define it". The
950
- // 'use' relation is typed `User[]`, so `User` is a compile-time dependency:
951
- // include it too, since an existing model may not already define it.
952
- if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(namespace)) {
953
- ui.warnDetail(`Add the '${namespace}' namespace to your Ory Permissions model. Its 'use'`);
954
- ui.warnDetail("relation is typed `User[]`, so `User` must be defined too:");
955
- console.warn("");
956
- console.warn(ui.style.gray(" class User implements Namespace {}"));
957
- console.warn("");
958
- console.warn(ui.style.gray(` class ${namespace} implements Namespace {`));
959
- console.warn(ui.style.gray(" related: { use: User[] }"));
960
- console.warn(ui.style.gray(" }"));
961
- console.warn("");
962
- ui.warnDetail("(drop the `User` class if your model already defines it), then re-run");
963
- ui.warnDetail("install with --reconfigure to grant. You're in observe mode, so tools");
964
- ui.warnDetail("keep working meanwhile.");
965
- return;
966
- }
967
- ui.warnDetail(`Define the '${namespace}' namespace in Ory Permissions, then re-run install`);
968
- ui.warnDetail("with --reconfigure to grant. You're in observe mode, so tools keep working.");
969
- }
970
- // ─── ory CLI helpers ──────────────────────────────────────────────────────
971
- function oryCliPresent(runner) {
972
- return runner.exec(["version"]).status === 0;
973
- }
974
- /**
975
- * The `ory` binary isn't on `PATH`. Offer (prompted, default yes) to install it
976
- * automatically so the Ory Network path doesn't dead-end for a from-scratch
977
- * user. On accept, install `@ory/cli` into a plugin-managed dir and return a
978
- * runner bound to it; on decline or failure, return null so the caller falls
979
- * back to the manual instructions. Never throws.
980
- */
981
- async function maybeInstallOryCli(ctx, prompt) {
982
- ui.heading("Ory CLI");
983
- ui.info("Connecting to Ory Network needs the Ory CLI, which isn't installed yet.");
984
- ui.blank();
985
- const answer = await prompt(ui.promptLine("Install it now (via npm, no system changes)?", { hint: "[Y/n]" }));
986
- if (isNo(answer))
987
- return null;
988
- ui.step(`Installing ${exports.ORY_CLI_NPM_SPEC} — this can take a moment...`);
989
- const install = ctx.deps.installOryCliFn ?? installOryCli;
990
- let runner;
991
- try {
992
- runner = await install();
993
- }
994
- catch {
995
- runner = null;
996
- }
997
- if (!runner) {
998
- ui.warning("Could not install the Ory CLI automatically.");
999
- return null;
1000
- }
1001
- ui.success("Ory CLI installed.");
1002
- return runner;
1003
- }
1004
- function listProjects(runner, workspaceId) {
1005
- const r = runner.exec([
1006
- "list",
1007
- "projects",
1008
- "--workspace",
1009
- workspaceId,
1010
- "--format",
1011
- "json",
1012
- ]);
1013
- if (r.status !== 0)
1014
- return null;
1015
- return parseList(r.stdout, "projects");
1016
- }
1017
- async function maybeCreateWorkspace(runner, prompt, harness) {
1018
- ui.heading("Your Ory Network account has no workspaces yet.");
1019
- ui.blank();
1020
- const answer = await prompt(ui.promptLine("Create a new workspace now?", { hint: "[Y/n]" }));
1021
- if (isNo(answer))
1022
- return null;
1023
- const defaultName = `${harness}-workspace`;
1024
- const nameInput = await prompt(ui.promptLine("Workspace name", { hint: `[${defaultName}]` }));
1025
- const name = nameInput && nameInput.length > 0 ? nameInput : defaultName;
1026
- const r = runner.exec([
1027
- "create",
1028
- "workspace",
1029
- "--name",
1030
- name,
1031
- "--format",
1032
- "json",
1033
- "--yes",
1034
- ]);
1035
- if (r.status !== 0) {
1036
- ui.warning(`Could not create the workspace: ${r.stderr.trim() || "unknown error"}`);
1037
- return null;
1038
- }
1039
- const workspace = parseObject(r.stdout);
1040
- if (!workspace || !workspace.id)
1041
- return null;
1042
- ui.success(`Created workspace "${workspace.name ?? name}".`);
1043
- return workspace;
1044
- }
1045
- async function maybeCreateProject(runner, prompt, workspace, harness) {
1046
- const wsLabel = workspace.name ?? workspace.id;
1047
- ui.heading(`Workspace "${wsLabel}" has no projects yet.`);
1048
- ui.blank();
1049
- const answer = await prompt(ui.promptLine("Create a new project now?", { hint: "[Y/n]" }));
1050
- if (isNo(answer))
1051
- return null;
1052
- return createProjectInteractive(runner, prompt, workspace, harness);
1053
- }
1054
- /**
1055
- * Choose the project to wire up when the workspace already has at least one.
1056
- * Lists the existing projects for selection and — while the workspace is still
1057
- * under the Ory Network Developer-plan cap of two projects — offers a "create a
1058
- * new project" option, so an account with a single project can dedicate a fresh
1059
- * one to the agent plugin instead of being forced onto the existing project.
1060
- * Returns null if the user cancels.
1061
- */
1062
- async function selectOrCreateProject(runner, prompt, workspace, harness, projects) {
1063
- // Developer plans cap a workspace at two projects; only offer to create while
1064
- // there's still headroom.
1065
- const PROJECT_CAP = 2;
1066
- const canCreate = projects.length < PROJECT_CAP;
1067
- ui.heading("Select a project:");
1068
- ui.blank();
1069
- const entries = projects.map((p, i) => ({
1070
- key: String(i + 1),
1071
- label: `${p.name ?? p.slug ?? p.id}${p.slug ? ` (${p.slug})` : ""}`,
1072
- }));
1073
- if (canCreate) {
1074
- entries.push({
1075
- key: String(projects.length + 1),
1076
- label: "Create a new project for the agent plugin",
1077
- });
1078
- }
1079
- ui.menu(entries);
1080
- ui.blank();
1081
- for (let attempt = 0; attempt < 3; attempt++) {
1082
- const answer = await prompt(ui.promptLine("Enter a number", { hint: `[1-${entries.length}]` }));
1083
- if (answer === null || answer.trim() === "")
1084
- return null;
1085
- const n = Number.parseInt(answer.trim(), 10);
1086
- if (Number.isInteger(n) && n >= 1 && n <= projects.length) {
1087
- return projects[n - 1];
1088
- }
1089
- if (canCreate && n === projects.length + 1) {
1090
- return createProjectInteractive(runner, prompt, workspace, harness);
1091
- }
1092
- ui.warning(`"${answer}" is not a valid choice.`);
1093
- }
1094
- return null;
1095
- }
1096
- /**
1097
- * Prompt for a project name (defaulting to `<harness>-agent`) and create it in
1098
- * the given workspace. Shared by the empty-workspace path and the "create a new
1099
- * project" menu option. Returns null on failure.
1100
- */
1101
- async function createProjectInteractive(runner, prompt, workspace, harness) {
1102
- const defaultName = `${harness}-agent`;
1103
- const nameInput = await prompt(ui.promptLine("New project name", { hint: `[${defaultName}]` }));
1104
- const name = nameInput && nameInput.length > 0 ? nameInput : defaultName;
1105
- const r = runner.exec([
1106
- "create",
1107
- "project",
1108
- "--name",
1109
- name,
1110
- "--workspace",
1111
- workspace.id,
1112
- "--format",
1113
- "json",
1114
- "--yes",
1115
- ]);
1116
- if (r.status !== 0) {
1117
- ui.warning(`Could not create the project: ${r.stderr.trim() || "unknown error"}`);
1118
- return null;
1119
- }
1120
- const project = parseObject(r.stdout);
1121
- if (!project || !project.id)
1122
- return null;
1123
- ui.success(`Created project "${project.name ?? name}".`);
1124
- return project;
1125
- }
1126
- /** JSON-Patch path (relative to the oauth2 service config) that toggles DCR. */
1127
- const DCR_ENABLED_PATH = "/oidc/dynamic_client_registration/enabled";
1128
- /**
1129
- * Read the project's current DCR state via `ory get oauth2-config`.
1130
- * Returns `true`/`false` when the value is known, or `null` when it can't be
1131
- * determined (CLI error, unparseable output) so the caller falls open and
1132
- * prompts anyway rather than assuming a state.
1133
- */
1134
- function dcrEnabled(runner, projectId) {
1135
- const r = runner.exec([
1136
- "get",
1137
- "oauth2-config",
1138
- "--project",
1139
- projectId,
1140
- "--format",
1141
- "json",
1142
- ]);
1143
- if (r.status !== 0)
1144
- return null;
1145
- const config = parseObject(r.stdout);
1146
- if (!config)
1147
- return null;
1148
- const enabled = config.oidc?.dynamic_client_registration?.enabled;
1149
- return typeof enabled === "boolean" ? enabled : null;
1150
- }
1151
- /**
1152
- * Enable OAuth2 Dynamic Client Registration on the project so the agent
1153
- * identity can self-register (RFC 7591) at runtime. First probes the project's
1154
- * current DCR state and skips silently when it's already enabled. Otherwise
1155
- * prompts (default yes) since it changes project-wide config; best-effort and
1156
- * fail-open — a decline or failure just prints the manual command and continues.
1157
- */
1158
- async function maybeEnableDcr(runner, prompt, projectId) {
1159
- if (dcrEnabled(runner, projectId) === true)
1160
- return;
1161
- ui.heading("Dynamic Client Registration");
1162
- ui.info("The agent identity registers itself via OAuth2 Dynamic Client Registration");
1163
- ui.hint("(DCR), which Ory Network projects disable by default.");
1164
- ui.blank();
1165
- const answer = await prompt(ui.promptLine("Enable Dynamic Client Registration on this project?", { hint: "[Y/n]" }));
1166
- const manual = `ory patch oauth2-config --project ${projectId} --replace '${DCR_ENABLED_PATH}=true'`;
1167
- if (isNo(answer)) {
1168
- ui.hint("Skipped. Enable it later with:");
1169
- ui.command(manual);
1170
- ui.hint("or supply static agent credentials (ORY_AGENT_CLIENT_ID + ORY_AGENT_CLIENT_SECRET).");
1171
- return;
1172
- }
1173
- const r = runner.exec([
1174
- "patch",
1175
- "oauth2-config",
1176
- "--project",
1177
- projectId,
1178
- "--replace",
1179
- `${DCR_ENABLED_PATH}=true`,
1180
- "--format",
1181
- "json",
1182
- "--yes",
1183
- ]);
1184
- if (r.status === 0) {
1185
- ui.success("Dynamic Client Registration enabled.");
1186
- }
1187
- else {
1188
- ui.warning(`Could not enable DCR automatically: ${r.stderr.trim() || "unknown error"}`);
1189
- ui.warnDetail(`Enable it later with: ${manual}`);
1190
- }
1191
- }
1192
- /** The `client_name` every plugin-provisioned public PKCE client carries. */
1193
- const OAUTH2_CLIENT_NAME = "ory-agent-plugin";
1194
- /**
1195
- * Look for an already-provisioned public PKCE client on the project so a
1196
- * re-run reuses it instead of minting a duplicate. Matches by the fixed
1197
- * `client_name` {@link createOAuth2Client} assigns. Best-effort: any CLI or
1198
- * parse failure returns null and the caller falls back to creating one.
1199
- */
1200
- function findExistingOAuth2Client(runner, projectId) {
1201
- const r = runner.exec([
1202
- "list",
1203
- "oauth2-clients",
1204
- "--project",
1205
- projectId,
1206
- "--format",
1207
- "json",
1208
- ]);
1209
- if (r.status !== 0)
1210
- return null;
1211
- const clients = parseList(r.stdout, "items");
1212
- if (!clients)
1213
- return null;
1214
- const match = clients.find((c) => c.client_name === OAUTH2_CLIENT_NAME && c.client_id);
1215
- return match?.client_id ?? null;
1216
- }
1217
- function createOAuth2Client(runner, projectId) {
1218
- const args = [
1219
- "create",
1220
- "oauth2-client",
1221
- "--project",
1222
- projectId,
1223
- "--name",
1224
- OAUTH2_CLIENT_NAME,
1225
- "--grant-type",
1226
- "authorization_code,refresh_token",
1227
- "--response-type",
1228
- "code",
1229
- "--scope",
1230
- "openid,offline_access",
1231
- "--token-endpoint-auth-method",
1232
- "none",
1233
- "--format",
1234
- "json",
1235
- "--yes",
1236
- ];
1237
- for (const uri of exports.LOOPBACK_REDIRECT_URIS) {
1238
- args.push("--redirect-uri", uri);
1239
- }
1240
- const r = runner.exec(args);
1241
- if (r.status !== 0)
1242
- return null;
1243
- const client = parseObject(r.stdout);
1244
- return client?.client_id ?? null;
1245
- }
1246
- // ─── parsing helpers ────────────────────────────────────────────────────────
1247
- /**
1248
- * Parse a JSON list from `ory --format json` output. Tolerates both a bare
1249
- * array and the `{ <key>: [...] }` envelope the Console list endpoints use.
1250
- */
1251
- function parseList(stdout, key) {
1252
- const parsed = safeJsonParse(stdout);
1253
- if (parsed === null)
1254
- return null;
1255
- if (Array.isArray(parsed))
1256
- return parsed;
1257
- if (typeof parsed === "object") {
1258
- const value = parsed[key];
1259
- if (Array.isArray(value))
1260
- return value;
1261
- }
1262
- return null;
1263
- }
1264
- function parseObject(stdout) {
1265
- const parsed = safeJsonParse(stdout);
1266
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1267
- return parsed;
1268
- }
1269
- return null;
1270
- }
1271
- function safeJsonParse(stdout) {
1272
- const trimmed = stdout.trim();
1273
- if (!trimmed)
1274
- return null;
1275
- try {
1276
- return JSON.parse(trimmed);
1277
- }
1278
- catch {
1279
- return null;
1280
- }
1281
- }
1282
- /**
1283
- * Derive the canonical Ory Network project SDK/API URL from a project **slug**.
1284
- * The subdomain is always the slug — never the project id. A project id (a
1285
- * UUID) reaching here means a slug and an id were confused upstream, which
1286
- * would build a URL that points nowhere, so we refuse it loudly rather than
1287
- * silently misconfigure the plugin.
1288
- */
1289
- function projectUrlFromSlug(slug) {
1290
- if ((0, config_js_1.looksLikeProjectId)(slug)) {
1291
- throw new Error(`Refusing to build an SDK URL from "${slug}": that is a project id (UUID), not a slug.`);
1292
- }
1293
- return `https://${slug}.projects.oryapis.com`;
1294
- }
1295
- // ─── prompt helpers ─────────────────────────────────────────────────────────
1296
- /**
1297
- * Present a numbered list and prompt for a selection. Auto-selects when there
1298
- * is exactly one item. Returns null if the user cancels or gives up.
1299
- */
1300
- async function selectFromList(prompt, label, items, render) {
1301
- if (items.length === 1) {
1302
- ui.info(`Using the only ${label}: ${ui.style.bold(render(items[0]))}`);
1303
- return items[0];
1304
- }
1305
- ui.heading(`Select a ${label}:`);
1306
- ui.blank();
1307
- ui.menu(items.map((item, i) => ({ key: String(i + 1), label: render(item) })));
1308
- ui.blank();
1309
- for (let attempt = 0; attempt < 3; attempt++) {
1310
- const answer = await prompt(ui.promptLine("Enter a number", { hint: `[1-${items.length}]` }));
1311
- if (answer === null || answer.trim() === "")
1312
- return null;
1313
- const n = Number.parseInt(answer.trim(), 10);
1314
- if (Number.isInteger(n) && n >= 1 && n <= items.length) {
1315
- return items[n - 1];
1316
- }
1317
- ui.warning(`"${answer}" is not a valid choice.`);
1318
- }
1319
- return null;
1320
- }
1321
- function isLocalChoice(answer) {
1322
- if (answer === null)
1323
- return false;
1324
- const v = answer.trim().toLowerCase();
1325
- return v === "2" || v === "local" || v === "local-stack" || v === "localstack";
1326
- }
1327
- function isAuditChoice(answer) {
1328
- if (answer === null)
1329
- return false;
1330
- const v = answer.trim().toLowerCase();
1331
- return v === "3" || v === "audit" || v === "audit-only";
1332
- }
1333
- function isNo(answer) {
1334
- if (answer === null)
1335
- return false;
1336
- const v = answer.trim().toLowerCase();
1337
- return v === "n" || v === "no";
1338
- }
1339
- // ─── banners ────────────────────────────────────────────────────────────────
1340
- function printOryCliMissing() {
1341
- ui.heading("The Ory CLI (`ory`) is required to connect to Ory Network but was not found.");
1342
- ui.hint("Install it, then re-run install with --reconfigure:");
1343
- ui.summary([
1344
- ["macOS/Linux (Homebrew)", "brew install ory/tap/cli"],
1345
- [
1346
- "macOS/Linux (script)",
1347
- "bash <(curl https://raw.githubusercontent.com/ory/meta/master/install.sh) -b . ory",
1348
- ],
1349
- ["Docs", "https://www.ory.com/docs/guides/cli/installation"],
1350
- ]);
1351
- }
1352
- function printManualSetup(binName) {
1353
- ui.heading("You can connect to Ory at any time:");
1354
- ui.blank();
1355
- ui.bullet("Connect to an Ory project (enables auth & permission checks):");
1356
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --project-url <URL> --oauth2-client-id <CLIENT_ID>`);
1357
- ui.blank();
1358
- ui.bullet("Or enable audit logging only (no auth or permission checks):");
1359
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --audit-only`);
1360
- ui.blank();
1361
- ui.hint(`Config is saved to ${(0, config_js_1.getConfigPath)()} and shared across all agent plugins.`);
1362
- }
1363
- function printManualOAuth2ClientHelp(binName, projectId) {
1364
- ui.heading("Create the public OAuth2 client by hand, then supply its id:");
1365
- ui.blank();
1366
- console.log(ui.style.gray(` ory create oauth2-client --project ${projectId} \\`));
1367
- console.log(ui.style.gray(` --name "ory-agent-plugin" \\`));
1368
- console.log(ui.style.gray(` --grant-type authorization_code,refresh_token \\`));
1369
- console.log(ui.style.gray(` --response-type code \\`));
1370
- console.log(ui.style.gray(` --scope openid,offline_access \\`));
1371
- console.log(ui.style.gray(` --token-endpoint-auth-method none \\`));
1372
- for (const uri of exports.LOOPBACK_REDIRECT_URIS) {
1373
- console.log(ui.style.gray(` --redirect-uri ${uri} \\`));
1374
- }
1375
- console.log(ui.style.gray(` --format json`));
1376
- ui.blank();
1377
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --oauth2-client-id <CLIENT_ID>`);
1378
- }
1379
- /** Ory Console API base. The `ory` CLI has no project-API-key command, so key
1380
- * provisioning talks to the Console API directly, authenticated with the CLI's
1381
- * stored Ory Network session. Override with `ORY_CONSOLE_API_URL`. */
1382
- const CONSOLE_API_URL = process.env.ORY_CONSOLE_API_URL?.trim() || "https://api.console.ory.sh";
1383
- /** Path to the session the `ory` CLI persists after `ory auth`. */
1384
- const ORY_CLI_SESSION_FILE = ".ory-cloud.json";
1385
- /**
1386
- * Read the Ory Network session bearer the `ory` CLI stores at
1387
- * `~/.ory-cloud.json`. Returns null when the file is absent, unparseable, has
1388
- * no token, or the token has expired. Never throws.
1389
- */
1390
- function readConsoleSessionToken() {
1391
- try {
1392
- const raw = fs.readFileSync(path.join(os.homedir(), ORY_CLI_SESSION_FILE), "utf-8");
1393
- const parsed = JSON.parse(raw);
1394
- const token = parsed.access_token?.access_token;
1395
- if (!token)
1396
- return null;
1397
- const expiry = parsed.access_token?.expiry;
1398
- if (expiry) {
1399
- const expMs = Date.parse(expiry);
1400
- if (Number.isFinite(expMs) && expMs <= Date.now())
1401
- return null;
1402
- }
1403
- return token;
1404
- }
1405
- catch {
1406
- return null;
1407
- }
1408
- }
1409
- /**
1410
- * Mint an Ory Network **project API key** via the Console API
1411
- * (`POST /projects/{id}/tokens`), authenticated with the `ory` CLI session.
1412
- * Returns the `ory_pat_…` value, or null on any failure (no session, non-2xx,
1413
- * missing value). Never throws — provisioning is best-effort.
1414
- *
1415
- * This is the one place the wizard reaches past the `ory` CLI to the Console
1416
- * API: the CLI (as of v1.3) exposes no project-API-key command, and Keto
1417
- * permission checks can't authenticate without one.
1418
- */
1419
- async function createProjectApiKeyViaConsole(args) {
1420
- const token = readConsoleSessionToken();
1421
- if (!token)
1422
- return null;
1423
- try {
1424
- const res = await fetch(`${CONSOLE_API_URL}/projects/${args.projectId}/tokens`, {
1425
- method: "POST",
1426
- headers: {
1427
- Authorization: `Bearer ${token}`,
1428
- "Content-Type": "application/json",
1429
- },
1430
- body: JSON.stringify({ name: args.name }),
1431
- });
1432
- if (!res.ok)
1433
- return null;
1434
- const body = (await res.json());
1435
- return typeof body.value === "string" && body.value.length > 0 ? body.value : null;
1436
- }
1437
- catch {
1438
- return null;
1439
- }
1440
- }
1441
- /**
1442
- * Best-effort check that an already-stored project API key still authenticates
1443
- * against Keto. Runs one permission check with the key: if it completes (any
1444
- * `allowed` value) the key is valid; an auth rejection (`session_inactive` /
1445
- * `forbidden` / `session_aal2_required`) means it was revoked or deleted, so we
1446
- * return false and the caller re-provisions. Transient failures (network /
1447
- * rate-limit / unknown) return true so we don't re-mint on a blip. Never throws.
1448
- */
1449
- async function apiKeyAuthenticates(apiKey, projectUrl) {
1450
- try {
1451
- // Direct construction (no fromEnv) seeds the key as both agent token and
1452
- // admin key and skips DCR, so the probe uses exactly this key. No trace
1453
- // file is configured, so nothing is written to disk.
1454
- const probe = new client_js_1.OryAgentClient({ projectUrl, apiKey, harness: "install-probe" });
1455
- await probe.checkPermission({
1456
- namespace: process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools",
1457
- object: "__ory_agent_key_probe__",
1458
- relation: "__ory_agent_key_probe__",
1459
- subjectId: "__ory_agent_key_probe__",
1460
- });
1461
- return true;
1462
- }
1463
- catch (err) {
1464
- const code = err?.code;
1465
- if (code === "session_inactive" || code === "forbidden" || code === "session_aal2_required") {
1466
- return false;
1467
- }
1468
- return true;
1469
- }
1470
- }
1471
- /**
1472
- * Manual fallback steps for the project API key, shown when auto-provisioning
1473
- * is declined or fails. The `configure --api-key` command is the stable part;
1474
- * the Console link is a convenience.
1475
- */
1476
- function printManualApiKeySteps(binName, projectId) {
1477
- ui.info("Create one in the Ory Console (Project settings → API keys):");
1478
- ui.command(`https://console.ory.sh/projects/${projectId}/settings`);
1479
- ui.info("then store it so permission checks authenticate with it:");
1480
- ui.command(`${(0, cli_invocation_js_1.oryNpx)(binName)} configure --api-key <ory_pat_…>`);
1481
- ui.hint("Observe mode works without it; enforce mode needs it to allow tools.");
1482
- }
1483
- /**
1484
- * Provision the project API key that runtime Keto permission checks
1485
- * authenticate with. The agent's DCR OAuth2 token can't — Ory Network rejects
1486
- * it — so `enforce` mode needs a project API key. Best-effort and skippable:
1487
- * offers to mint one via the Console API and persist it, and on decline / no
1488
- * TTY / any failure falls back to manual guidance. No-ops when a key is already
1489
- * configured (env or config file).
1490
- */
1491
- async function maybeProvisionProjectApiKey(binName, harness, projectId, projectUrl, prompt, createFn, validateFn) {
1492
- const resolved = (0, config_js_1.resolveConfig)();
1493
- if (resolved.apiKey) {
1494
- // An operator-set env key is authoritative — never touch it.
1495
- if (resolved.apiKeySource === "env")
1496
- return;
1497
- // A stored (config-file) key that no longer authenticates — revoked or
1498
- // deleted — must not wedge setup: validate it and re-provision if it's
1499
- // dead. Presence alone isn't enough (a dangling key still fails at runtime
1500
- // with session_inactive).
1501
- if (await validateFn(resolved.apiKey, projectUrl))
1502
- return;
1503
- ui.heading("Project API key");
1504
- ui.warning("The stored project API key is no longer valid (revoked or deleted).");
1505
- }
1506
- else {
1507
- ui.heading("Project API key");
1508
- ui.info("Runtime permission checks call Ory's Permission API, which authenticates");
1509
- ui.hint("with a project API key — the agent's OAuth2 token isn't accepted there.");
1510
- ui.hint("(Observe mode works without it; enforce mode needs it to allow tools.)");
1511
- }
1512
- ui.blank();
1513
- const answer = await prompt(ui.promptLine("Create a project API key now and store it?", { hint: "[Y/n]" }));
1514
- // `null` = no TTY / cancelled; treat as skip so we never mint without consent.
1515
- if (answer !== null && !isNo(answer)) {
1516
- ui.step("Creating a project API key via the Ory Console…");
1517
- let key = null;
1518
- try {
1519
- // Name each key uniquely (harness + host + timestamp) so keys are
1520
- // distinguishable in the Console and a re-provision never collides with
1521
- // an existing entry.
1522
- key = await createFn({ projectId, name: projectApiKeyName(harness) });
1523
- }
1524
- catch {
1525
- key = null;
1526
- }
1527
- if (key) {
1528
- (0, config_js_1.saveConfig)({ apiKey: key });
1529
- ui.success("Project API key created and stored — permission checks will use it.");
1530
- return;
1531
- }
1532
- ui.warning("Couldn't create a project API key automatically (Console session may be missing or expired).");
1533
- }
1534
- printManualApiKeySteps(binName, projectId);
1535
- }
1536
- /** Distinguishable, collision-free label for a provisioned project API key. */
1537
- function projectApiKeyName(harness) {
1538
- let host = "unknown-host";
1539
- try {
1540
- host = os.hostname();
1541
- }
1542
- catch {
1543
- /* keep fallback */
1544
- }
1545
- return `ory-agent-plugins ${harness} ${host} ${new Date().toISOString()}`;
1546
- }