@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,396 @@
1
+ "use strict";
2
+ /**
3
+ * Shell-command decomposition for fine-grained permission checks.
4
+ *
5
+ * The permission gate authorizes on *tool name* only, so a denied surface
6
+ * (`WebFetch`, `Read`, an MCP write) is trivially reached through
7
+ * `bash -c "curl …"`. This module parses a shell command with the
8
+ * `tree-sitter-bash` grammar (via `web-tree-sitter`) into a **structured
9
+ * command model** — the shared foundation for the permission work:
10
+ *
11
+ * - #76 (this): check each command word as `ShellTool:<word>#use`.
12
+ * Derived from `ParsedShell.commands.map(c => c.name)`.
13
+ * - #77 (argument scoping): per-binary extractors read `ShellCommand.args`.
14
+ * - #78 (combinatory dataflow): edges are built from `ShellCommand.pipeline`
15
+ * grouping and `ShellCommand.redirects`.
16
+ * - #79 (env escalation): `ShellCommand.assignments` and redirect targets.
17
+ *
18
+ * Building the model once (one parse, one AST walk) keeps every follow-on
19
+ * additive instead of re-walking the tree.
20
+ *
21
+ * **Why a grammar parser, not a regex/tokenizer.** A hand-rolled tokenizer is
22
+ * vulnerable to *parser-differential attacks* — where the checker reads a
23
+ * command differently than the shell that will run it. tree-sitter-bash gives
24
+ * us the shell's own view of the command.
25
+ *
26
+ * **Failure postures (two, deliberately opposite):**
27
+ * - `tooComplex` — parsed but unresolvable (dynamic command name, parse
28
+ * error, dynamic `sh -c` target). Security signal: enforce denies.
29
+ * - `parserUnavailable` — the runtime/grammar could not load. Availability
30
+ * signal: the gate falls open. A broken build must never block shell use.
31
+ *
32
+ * **Known static-analysis limits (checks for them are #77/#78/#79):** command
33
+ * names built at runtime, programs invoked as interpreter arguments
34
+ * (`python -c "…"`, `find -exec`), and dynamic dataflow. The backstop is that
35
+ * the decode/exec primitives themselves (`sh`, `bash`, `eval`, `base64`, …) are
36
+ * ordinary checked words.
37
+ */
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.extractShellCommands = extractShellCommands;
40
+ exports.extractBashCommandWords = extractBashCommandWords;
41
+ /** Literal leaf node types whose `.text` is a real (non-expanded) string. */
42
+ const LITERAL_LEAF_TYPES = new Set(["word", "string_content", "raw_string"]);
43
+ /**
44
+ * Wrapper commands whose *argument* is another command to run. Each wrapper is
45
+ * itself a command; we additionally synthesize the command it wraps.
46
+ */
47
+ const WRAPPERS = new Set([
48
+ "sudo", "doas", "env", "nohup", "setsid", "command", "exec", "nice",
49
+ "ionice", "chrt", "stdbuf", "time", "timeout", "xargs",
50
+ ]);
51
+ /** Wrappers that take a numeric positional operand before the wrapped command. */
52
+ const OPERAND_WRAPPERS = new Set(["timeout", "nice", "ionice", "chrt"]);
53
+ /** A numeric operand (`5`, `5s`, `1.5`) consumed by an {@link OPERAND_WRAPPERS}. */
54
+ const NUMERIC_OPERAND = /^\d+(\.\d+)?[smhdSMHD]?$/;
55
+ /** Shells whose `-c "<script>"` argument is re-parsed as a nested command. */
56
+ const SHELL_DASH_C = new Set(["sh", "bash", "dash", "zsh", "ksh", "ash"]);
57
+ /** Cap re-parse recursion (`sh -c "sh -c …"`) so a pathological input can't loop. */
58
+ const MAX_REPARSE_DEPTH = 6;
59
+ /**
60
+ * Cap AST-walk recursion depth. A deeply nested command (`$( $( … ) )` thousands
61
+ * deep) would otherwise overflow the stack — and a thrown error would fail the
62
+ * gate OPEN. Past this depth we stop descending and mark the command
63
+ * `tooComplex` (fail-closed in enforce). Well above any real command; the
64
+ * grammar itself flags such inputs with ERROR nodes long before this.
65
+ */
66
+ const MAX_WALK_DEPTH = 400;
67
+ let parserPromise = null;
68
+ /**
69
+ * Lazily initialize a singleton tree-sitter parser bound to the bash grammar.
70
+ * Returns `null` (never throws) if the WASM runtime or grammar cannot be loaded.
71
+ */
72
+ async function getBashParser() {
73
+ if (!parserPromise) {
74
+ parserPromise = (async () => {
75
+ try {
76
+ const { Parser, Language } = await import("web-tree-sitter");
77
+ await Parser.init();
78
+ // GRAMMAR PARITY: this WASM bundles tree-sitter-bash 0.25.x. The Python
79
+ // core pins the same grammar minor (see ory-argus/pyproject.toml) so the
80
+ // two parse identically; the shared bash-parser-fixtures.json is the CI
81
+ // guard. Bump both sides together.
82
+ const wasmPath = require.resolve("@vscode/tree-sitter-wasm/wasm/tree-sitter-bash.wasm");
83
+ const bash = await Language.load(wasmPath);
84
+ const parser = new Parser();
85
+ parser.setLanguage(bash);
86
+ return parser;
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ })();
92
+ }
93
+ return parserPromise;
94
+ }
95
+ /** Strip a directory prefix: `/usr/bin/curl` → `curl`, `curl` → `curl`. */
96
+ function basename(word) {
97
+ const slash = word.lastIndexOf("/");
98
+ return slash >= 0 ? word.slice(slash + 1) : word;
99
+ }
100
+ /** Strip a single matched pair of surrounding quotes, if present. */
101
+ function stripQuotes(text) {
102
+ if (text.length >= 2) {
103
+ const first = text[0];
104
+ if ((first === '"' || first === "'") && text[text.length - 1] === first) {
105
+ return text.slice(1, -1);
106
+ }
107
+ }
108
+ return text;
109
+ }
110
+ /** The last named leaf under a node (deepest, right-most). */
111
+ function lastLeaf(node) {
112
+ let cur = node;
113
+ while (cur.namedChildCount > 0) {
114
+ const next = cur.namedChild(cur.namedChildCount - 1);
115
+ if (!next)
116
+ break;
117
+ cur = next;
118
+ }
119
+ return cur;
120
+ }
121
+ /** Literal string value of a node, or `null` when any part is an expansion. */
122
+ function literalText(node) {
123
+ switch (node.type) {
124
+ case "word":
125
+ case "number":
126
+ return node.text;
127
+ case "raw_string":
128
+ return stripQuotes(node.text);
129
+ case "string": {
130
+ let out = "";
131
+ for (let i = 0; i < node.namedChildCount; i++) {
132
+ const child = node.namedChild(i);
133
+ if (!child)
134
+ continue;
135
+ if (child.type === "string_content")
136
+ out += child.text;
137
+ else
138
+ return null; // an expansion / command substitution inside
139
+ }
140
+ return out;
141
+ }
142
+ default:
143
+ return null;
144
+ }
145
+ }
146
+ /**
147
+ * Resolve a `command_name` node to a program word, or `null` when the name is
148
+ * dynamic (`$X`, `$(…)`, or a path whose final segment is an expansion).
149
+ */
150
+ function resolveCommandName(nameNode) {
151
+ const child = nameNode.namedChild(0);
152
+ if (!child)
153
+ return basename(nameNode.text);
154
+ switch (child.type) {
155
+ case "word":
156
+ return basename(child.text);
157
+ case "raw_string":
158
+ return basename(stripQuotes(child.text));
159
+ case "simple_expansion":
160
+ case "expansion":
161
+ case "command_substitution":
162
+ case "process_substitution":
163
+ return null;
164
+ case "string":
165
+ case "concatenation": {
166
+ const leaf = lastLeaf(child);
167
+ if (LITERAL_LEAF_TYPES.has(leaf.type))
168
+ return basename(leaf.text);
169
+ return null;
170
+ }
171
+ default:
172
+ return basename(child.text);
173
+ }
174
+ }
175
+ function argNodes(cmd) {
176
+ return cmd.childrenForFieldName("argument").filter((n) => n !== null);
177
+ }
178
+ function collectAssignments(cmd) {
179
+ const out = [];
180
+ for (const child of cmd.namedChildren) {
181
+ if (child.type !== "variable_assignment")
182
+ continue;
183
+ const nameNode = child.childForFieldName("name");
184
+ const valueNode = child.childForFieldName("value");
185
+ if (!nameNode)
186
+ continue;
187
+ out.push({
188
+ name: nameNode.text,
189
+ value: valueNode ? literalText(valueNode) : "",
190
+ });
191
+ }
192
+ return out;
193
+ }
194
+ function collectRedirects(cmd) {
195
+ const parent = cmd.parent;
196
+ if (!parent || parent.type !== "redirected_statement")
197
+ return [];
198
+ const out = [];
199
+ for (const child of parent.namedChildren) {
200
+ if (child.type !== "file_redirect" && child.type !== "heredoc_redirect")
201
+ continue;
202
+ const dest = child.childForFieldName("destination");
203
+ // Operator token contains the redirect direction.
204
+ const opText = child.text;
205
+ const direction = opText.includes(">") ? "write" : "read";
206
+ const opChild = child.child(0);
207
+ out.push({
208
+ operator: opChild ? opChild.text : opText,
209
+ target: dest ? literalText(dest) ?? dest.text : null,
210
+ direction,
211
+ });
212
+ }
213
+ return out;
214
+ }
215
+ /**
216
+ * Descend a wrapper's argument list to the command it runs, synthesizing a
217
+ * {@link ShellCommand} for that command (and recursing through nested wrappers,
218
+ * e.g. `sudo env A=1 curl`).
219
+ */
220
+ function descendWrapper(wrapper, args, pipeline, state) {
221
+ const skipsNumeric = OPERAND_WRAPPERS.has(wrapper);
222
+ for (let i = 0; i < args.length; i++) {
223
+ const txt = literalText(args[i]);
224
+ if (txt === null) {
225
+ state.tooComplex = true;
226
+ return;
227
+ }
228
+ if (txt.startsWith("-"))
229
+ continue;
230
+ if (/^[A-Za-z_]\w*=/.test(txt))
231
+ continue;
232
+ if (skipsNumeric && NUMERIC_OPERAND.test(txt))
233
+ continue;
234
+ const wrapped = basename(txt);
235
+ if (wrapped.length === 0)
236
+ return;
237
+ const rest = args.slice(i + 1);
238
+ state.commands.push({
239
+ name: wrapped,
240
+ args: rest.map((a) => a.text),
241
+ assignments: [],
242
+ redirects: [],
243
+ pipeline,
244
+ });
245
+ if (WRAPPERS.has(wrapped))
246
+ descendWrapper(wrapped, rest, pipeline, state);
247
+ return;
248
+ }
249
+ }
250
+ /** Recursively walk the tree, building the command model. */
251
+ function walk(node, pipeline, parser, state, depth, treeDepth) {
252
+ // Bound recursion so a maliciously deep AST can't overflow the stack (which
253
+ // would throw and fail the gate open). Stop and fail closed instead.
254
+ if (treeDepth > MAX_WALK_DEPTH) {
255
+ state.tooComplex = true;
256
+ return;
257
+ }
258
+ if (node.type === "pipeline") {
259
+ const id = state.pipelineCounter++;
260
+ for (const child of node.children)
261
+ walk(child, id, parser, state, depth, treeDepth + 1);
262
+ return;
263
+ }
264
+ if (node.type === "command") {
265
+ const nameNode = node.childForFieldName("name");
266
+ if (!nameNode) {
267
+ // Assignment-only command (`FOO=bar`) — no program word. Left for #79.
268
+ for (const child of node.children)
269
+ walk(child, -1, parser, state, depth, treeDepth + 1);
270
+ return;
271
+ }
272
+ const pid = pipeline >= 0 ? pipeline : state.pipelineCounter++;
273
+ const name = resolveCommandName(nameNode);
274
+ if (name === null)
275
+ state.tooComplex = true;
276
+ const args = argNodes(node);
277
+ state.commands.push({
278
+ name,
279
+ args: args.map((a) => a.text),
280
+ assignments: collectAssignments(node),
281
+ redirects: collectRedirects(node),
282
+ pipeline: pid,
283
+ });
284
+ if (name && WRAPPERS.has(name))
285
+ descendWrapper(name, args, pid, state);
286
+ if (name && SHELL_DASH_C.has(name)) {
287
+ const flagIdx = args.findIndex((a) => {
288
+ const t = literalText(a);
289
+ return t !== null && t.startsWith("-") && t.includes("c");
290
+ });
291
+ if (flagIdx >= 0) {
292
+ const target = args[flagIdx + 1];
293
+ if (!target)
294
+ state.tooComplex = true;
295
+ else {
296
+ const script = literalText(target);
297
+ if (script === null)
298
+ state.tooComplex = true;
299
+ else
300
+ reparse(script, parser, state, depth + 1);
301
+ }
302
+ }
303
+ }
304
+ if (name === "eval") {
305
+ const parts = [];
306
+ let dynamic = false;
307
+ for (const a of args) {
308
+ const t = literalText(a);
309
+ if (t === null)
310
+ dynamic = true;
311
+ else
312
+ parts.push(t);
313
+ }
314
+ if (dynamic || parts.length === 0)
315
+ state.tooComplex = true;
316
+ else
317
+ reparse(parts.join(" "), parser, state, depth + 1);
318
+ }
319
+ // Descend into arguments for nested command substitutions ($(curl …)).
320
+ for (const child of node.children)
321
+ walk(child, -1, parser, state, depth, treeDepth + 1);
322
+ return;
323
+ }
324
+ for (const child of node.children) {
325
+ walk(child, pipeline >= 0 ? pipeline : -1, parser, state, depth, treeDepth + 1);
326
+ }
327
+ }
328
+ /** Parse a nested script string and fold its commands into `state`. */
329
+ function reparse(script, parser, state, depth) {
330
+ if (depth > MAX_REPARSE_DEPTH || script.trim().length === 0)
331
+ return;
332
+ const tree = parser.parse(script);
333
+ if (!tree) {
334
+ state.tooComplex = true;
335
+ return;
336
+ }
337
+ if (tree.rootNode.hasError)
338
+ state.tooComplex = true;
339
+ walk(tree.rootNode, -1, parser, state, depth, 0);
340
+ }
341
+ /**
342
+ * Decompose a shell command into a {@link ParsedShell} model. Never throws.
343
+ * This is the shared foundation; see {@link extractBashCommandWords} for the
344
+ * flat word list the #76 gate uses.
345
+ */
346
+ async function extractShellCommands(command) {
347
+ if (!command || command.trim().length === 0) {
348
+ return { commands: [], tooComplex: false };
349
+ }
350
+ const parser = await getBashParser();
351
+ if (!parser) {
352
+ return { commands: [], tooComplex: false, parserUnavailable: true };
353
+ }
354
+ let tree;
355
+ try {
356
+ tree = parser.parse(command);
357
+ }
358
+ catch {
359
+ return { commands: [], tooComplex: false, parserUnavailable: true };
360
+ }
361
+ if (!tree)
362
+ return { commands: [], tooComplex: true };
363
+ const state = { commands: [], tooComplex: false, pipelineCounter: 0 };
364
+ if (tree.rootNode.hasError)
365
+ state.tooComplex = true;
366
+ // The walk must never throw out of this function — an uncaught error would
367
+ // fail the gate OPEN. Any internal failure (e.g. an unexpected node shape or
368
+ // a stack limit) becomes a fail-CLOSED `tooComplex` result instead.
369
+ try {
370
+ walk(tree.rootNode, -1, parser, state, 0, 0);
371
+ }
372
+ catch {
373
+ return { commands: state.commands, tooComplex: true };
374
+ }
375
+ return { commands: state.commands, tooComplex: state.tooComplex };
376
+ }
377
+ /**
378
+ * Flat view of {@link extractShellCommands} for the #76 gate: the deduped set
379
+ * of command/builtin words the command will execute. Never throws. See
380
+ * {@link BashParseResult} for the two failure postures.
381
+ */
382
+ async function extractBashCommandWords(command) {
383
+ const parsed = await extractShellCommands(command);
384
+ if (parsed.parserUnavailable) {
385
+ return { words: [], tooComplex: false, parserUnavailable: true };
386
+ }
387
+ const words = [];
388
+ const seen = new Set();
389
+ for (const cmd of parsed.commands) {
390
+ if (cmd.name === null || seen.has(cmd.name))
391
+ continue;
392
+ seen.add(cmd.name);
393
+ words.push(cmd.name);
394
+ }
395
+ return { words, tooComplex: parsed.tooComplex };
396
+ }
@@ -7,10 +7,13 @@
7
7
  * The policy has two tiers, because many users install these plugins against
8
8
  * the **same** Ory project:
9
9
  *
10
- * - **Shared** resources (one per project, reused by every user/harness) get a
11
- * **stable, identity-free** name so all installs find and reuse the one
12
- * resource instead of duplicating it: {@link USER_LOGIN_CLIENT_NAME} and the
13
- * permission namespace ({@link PERMISSION_NAMESPACE}).
10
+ * - **Shared** resources (one per project, reused by every user/harness) are
11
+ * addressed by a **stable, reserved identifier** so all installs find and
12
+ * reuse the one resource instead of duplicating it: the login client's
13
+ * {@link USER_LOGIN_CLIENT_ID} and the permission namespace
14
+ * ({@link PERMISSION_NAMESPACE}). An identifier, not a display name — see
15
+ * {@link USER_LOGIN_CLIENT_ID} for why the distinction is a security
16
+ * property and not a preference.
14
17
  * - **Per-installer** resources (many per project by design — confidential
15
18
  * per-machine DCR credentials, write-once API keys) get an **attributable**
16
19
  * name carrying *who* and *where*, so a shared project's resource list stays
@@ -22,36 +25,94 @@
22
25
  /** Human-readable umbrella name for everything this product creates. */
23
26
  export declare const PRODUCT_NAME = "Ory Agent Security";
24
27
  /**
25
- * The public PKCE client the human's browser sign-in uses. It is a *public*
26
- * client (no secret), so every user on a project shares the one client — the
27
- * installer dedupes by this exact name. Stable and identity-free on purpose.
28
+ * The **reserved client id** of the shared user-login OAuth2 client the one
29
+ * thing that identifies it. Every provisioning path (Ory Console setup, the
30
+ * local-dev Hydra seed) creates the client *at this id*, and every lookup
31
+ * addresses it by this id, so all of them converge on one client.
32
+ *
33
+ * It is an id rather than a name for a security reason. A project set up for
34
+ * Agent Security has public dynamic client registration enabled, and Hydra's DCR
35
+ * endpoint places no restriction on `client_name` — so anyone can register a
36
+ * client carrying {@link USER_LOGIN_CLIENT_NAME} verbatim with their own
37
+ * `redirect_uris`, and a name-based lookup can resolve to it (Hydra lists
38
+ * clients ordered by id, so re-registering until the assigned UUID sorts first
39
+ * is roughly a coin flip). Whoever then installs a plugin would complete a PKCE
40
+ * login — `offline_access` included — against the attacker's client. A
41
+ * `client_id` is not forgeable the same way: Hydra assigns a fresh UUID to a
42
+ * dynamically registered client and refuses a caller-chosen one, so only a
43
+ * caller holding the project's admin credentials can claim this id.
44
+ *
45
+ * There is deliberately **no name-based fallback** anywhere: a fallback reopens
46
+ * exactly that hole.
47
+ */
48
+ export declare const USER_LOGIN_CLIENT_ID = "ory-agent-security-login";
49
+ /**
50
+ * The **display name** of the shared user-login client — a human-readable label
51
+ * for the Ory Console's client list, and nothing more. It is a *public* client
52
+ * (no secret) that every user on a project shares across every harness plugin,
53
+ * and the name says so plainly; no "PKCE" in it, since how it authenticates is
54
+ * an implementation detail, not what it is.
55
+ *
56
+ * Never resolve the client by this string. It is attacker-supplyable (see
57
+ * {@link USER_LOGIN_CLIENT_ID}); {@link USER_LOGIN_CLIENT_ID} is what dedupes
58
+ * installs onto one client, and {@link isUserLoginClient} is the only accepted
59
+ * test for "is this the login client".
28
60
  */
29
- export declare const USER_LOGIN_CLIENT_NAME = "Ory Agent Security \u00B7 user login (PKCE)";
61
+ export declare const USER_LOGIN_CLIENT_NAME = "Ory Agent Security \u00B7 shared user login (all plugins)";
30
62
  /**
31
- * Names an older installer may have given the shared user-login client. The
32
- * reuse probe matches these too, so renaming the client never orphans one
33
- * created by a previous install (which would then mint a duplicate).
63
+ * Whether a client is *the* shared user-login client, decided **only** by its
64
+ * id. A client carrying {@link USER_LOGIN_CLIENT_NAME} under any other id is
65
+ * some other client possibly one an attacker registered and is rejected.
34
66
  */
35
- export declare const LEGACY_USER_LOGIN_CLIENT_NAMES: string[];
36
- /** All names the shared user-login client may carry, newest first. */
37
- export declare const USER_LOGIN_CLIENT_NAMES: string[];
67
+ export declare function isUserLoginClient(client: {
68
+ clientId?: string;
69
+ clientName?: string;
70
+ }): boolean;
71
+ /**
72
+ * The single source of truth for the interactive user-login OAuth2 scopes.
73
+ * Every place that registers the client (`ory create/update oauth2-client`,
74
+ * the local-dev Hydra seed, the manual-setup help) and the PKCE flow that
75
+ * requests them must agree, or the flow fails with `invalid_scope` (or the
76
+ * requested scope is silently dropped). `openid`/`offline_access` are the
77
+ * baseline; `profile`/`email` populate the id_token's name claims so the
78
+ * signed-in user can be shown by name.
79
+ */
80
+ export declare const USER_LOGIN_BASE_SCOPES: readonly ["openid", "offline_access"];
81
+ export declare const USER_LOGIN_SCOPES: readonly string[];
82
+ /** Space-delimited form for an OAuth2 `authorize` request / Hydra client `scope`. */
83
+ export declare const USER_LOGIN_SCOPE_OAUTH: string;
84
+ /** Comma-delimited form for the `ory … oauth2-client --scope` CLI flag. */
85
+ export declare const USER_LOGIN_SCOPE_CLI: string;
38
86
  /** Default name for a project the installer creates. Harness-agnostic, since a
39
87
  * team typically points several harnesses at one shared project. */
40
88
  export declare const DEFAULT_PROJECT_NAME = "Ory Agent Security";
41
89
  /** The shared permission-model namespace (an OPL class name, so it must stay a
42
90
  * bare identifier — no spaces/branding — and stable across installs). */
43
- export declare const PERMISSION_NAMESPACE = "AgentTools";
91
+ export declare const PERMISSION_NAMESPACE = "AgentTool";
44
92
  /**
45
93
  * The client_name for an agent's (or sub-agent's) OAuth2 identity registered
46
94
  * via DCR. These are confidential, per-machine credentials — many per shared
47
95
  * project by design — so the name is attributable: the harness it belongs to
48
- * and the host it was registered from (plus the sub-agent type, when it is one).
96
+ * and the host it was registered from, plus a short session label and the
97
+ * sub-agent type when applicable.
49
98
  */
50
99
  export declare function agentClientName(opts: {
51
100
  harness?: string;
52
101
  host: string;
53
102
  subAgentType?: string;
103
+ sessionId?: string;
54
104
  }): string;
105
+ /**
106
+ * A short, stable label for a session id, for **display only** — `status`
107
+ * output and log lines. Ids are usually UUIDs; the leading characters separate
108
+ * them well enough for a human scanning a list, and the full id is always
109
+ * recoverable from the delegation record and activity events.
110
+ *
111
+ * Never use this to build a permission subject. A derived session subject must
112
+ * carry the full session id (`Agent:<clientId>|<session>`) so it matches what an
113
+ * admin writes in the Console after reading the same id off the delegation graph.
114
+ */
115
+ export declare function shortSessionLabel(sessionId: string): string;
55
116
  /**
56
117
  * The name for a project API key minted for runtime permission checks. Ory only
57
118
  * returns a key's secret at creation, so a key can't be reused across machines