@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
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Shell-command decomposition for fine-grained permission checks.
3
+ *
4
+ * The permission gate authorizes on *tool name* only, so a denied surface
5
+ * (`WebFetch`, `Read`, an MCP write) is trivially reached through
6
+ * `bash -c "curl …"`. This module parses a shell command with the
7
+ * `tree-sitter-bash` grammar (via `web-tree-sitter`) into a **structured
8
+ * command model** — the shared foundation for the permission work:
9
+ *
10
+ * - #76 (this): check each command word as `ShellTool:<word>#use`.
11
+ * Derived from `ParsedShell.commands.map(c => c.name)`.
12
+ * - #77 (argument scoping): per-binary extractors read `ShellCommand.args`.
13
+ * - #78 (combinatory dataflow): edges are built from `ShellCommand.pipeline`
14
+ * grouping and `ShellCommand.redirects`.
15
+ * - #79 (env escalation): `ShellCommand.assignments` and redirect targets.
16
+ *
17
+ * Building the model once (one parse, one AST walk) keeps every follow-on
18
+ * additive instead of re-walking the tree.
19
+ *
20
+ * **Why a grammar parser, not a regex/tokenizer.** A hand-rolled tokenizer is
21
+ * vulnerable to *parser-differential attacks* — where the checker reads a
22
+ * command differently than the shell that will run it. tree-sitter-bash gives
23
+ * us the shell's own view of the command.
24
+ *
25
+ * **Failure postures (two, deliberately opposite):**
26
+ * - `tooComplex` — parsed but unresolvable (dynamic command name, parse
27
+ * error, dynamic `sh -c` target). Security signal: enforce denies.
28
+ * - `parserUnavailable` — the runtime/grammar could not load. Availability
29
+ * signal: the gate falls open. A broken build must never block shell use.
30
+ *
31
+ * **Known static-analysis limits (checks for them are #77/#78/#79):** command
32
+ * names built at runtime, programs invoked as interpreter arguments
33
+ * (`python -c "…"`, `find -exec`), and dynamic dataflow. The backstop is that
34
+ * the decode/exec primitives themselves (`sh`, `bash`, `eval`, `base64`, …) are
35
+ * ordinary checked words.
36
+ */
37
+ /** A redirection attached to a command (`> file`, `< file`, `2>err`, `>/dev/tcp/…`). */
38
+ export interface ShellRedirect {
39
+ /** Raw operator text (`>`, `>>`, `<`, `2>`, `&>`, …). */
40
+ operator: string;
41
+ /** Literal redirect target (path / fd / device), or `null` when dynamic. */
42
+ target: string | null;
43
+ /** `read` for `<`/`<<`/`<<<`, `write` for `>`/`>>`/`&>`. */
44
+ direction: "read" | "write";
45
+ }
46
+ /** A NAME=value assignment (an `env`-style prefix on a command). */
47
+ export interface ShellAssignment {
48
+ name: string;
49
+ /** Literal value, or `null` when it contains an expansion. */
50
+ value: string | null;
51
+ }
52
+ /** One effective command invocation extracted from a shell command line. */
53
+ export interface ShellCommand {
54
+ /**
55
+ * Resolved program/builtin word, normalized to its basename. `null` when the
56
+ * command name is dynamic/unresolvable (`$X`, `$(…)`), which also flips
57
+ * {@link ParsedShell.tooComplex}.
58
+ */
59
+ name: string | null;
60
+ /** Raw text of each argument token, in order (for #77 argument scoping). */
61
+ args: string[];
62
+ /** `NAME=value` assignment prefixes on this command (for #79 env scoping). */
63
+ assignments: ShellAssignment[];
64
+ /** Redirects on this command / its enclosing statement (for #78/#79). */
65
+ redirects: ShellRedirect[];
66
+ /**
67
+ * Pipeline group id. Commands joined by `|` share an id, so #78 can form the
68
+ * pipe edges between them. Standalone commands each get a distinct id.
69
+ */
70
+ pipeline: number;
71
+ }
72
+ /** Structured result of decomposing one shell command line. */
73
+ export interface ParsedShell {
74
+ /** Every effective command invocation (including wrapper- and `sh -c`-nested). */
75
+ commands: ShellCommand[];
76
+ /** Something couldn't be resolved statically (dynamic name / parse error). */
77
+ tooComplex: boolean;
78
+ /** The tree-sitter runtime/grammar failed to load — fail OPEN, don't block. */
79
+ parserUnavailable?: boolean;
80
+ }
81
+ /** Back-compat flat view used by the #76 gate: the deduped command words. */
82
+ export interface BashParseResult {
83
+ words: string[];
84
+ tooComplex: boolean;
85
+ parserUnavailable?: boolean;
86
+ }
87
+ /**
88
+ * Decompose a shell command into a {@link ParsedShell} model. Never throws.
89
+ * This is the shared foundation; see {@link extractBashCommandWords} for the
90
+ * flat word list the #76 gate uses.
91
+ */
92
+ export declare function extractShellCommands(command: string): Promise<ParsedShell>;
93
+ /**
94
+ * Flat view of {@link extractShellCommands} for the #76 gate: the deduped set
95
+ * of command/builtin words the command will execute. Never throws. See
96
+ * {@link BashParseResult} for the two failure postures.
97
+ */
98
+ export declare function extractBashCommandWords(command: string): Promise<BashParseResult>;
@@ -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
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * The umbrella brand and the names for every resource the installer creates in
3
+ * (or for) an Ory project. Centralized here so every install path — the web
4
+ * wizard, the TTY wizard, and the agent-identity gates — produces the same,
5
+ * self-describing names, and so the naming policy lives in one place.
6
+ *
7
+ * The policy has two tiers, because many users install these plugins against
8
+ * the **same** Ory project:
9
+ *
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.
17
+ * - **Per-installer** resources (many per project by design — confidential
18
+ * per-machine DCR credentials, write-once API keys) get an **attributable**
19
+ * name carrying *who* and *where*, so a shared project's resource list stays
20
+ * legible and prunable: {@link agentClientName}, {@link projectApiKeyName}.
21
+ *
22
+ * Everything shares the {@link PRODUCT_NAME} prefix so the plugin's resources
23
+ * group together and are instantly recognizable in a shared Ory Console.
24
+ */
25
+ /** Human-readable umbrella name for everything this product creates. */
26
+ export declare const PRODUCT_NAME = "Ory Agent Security";
27
+ /**
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".
60
+ */
61
+ export declare const USER_LOGIN_CLIENT_NAME = "Ory Agent Security \u00B7 shared user login (all plugins)";
62
+ /**
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.
66
+ */
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;
86
+ /** Default name for a project the installer creates. Harness-agnostic, since a
87
+ * team typically points several harnesses at one shared project. */
88
+ export declare const DEFAULT_PROJECT_NAME = "Ory Agent Security";
89
+ /** The shared permission-model namespace (an OPL class name, so it must stay a
90
+ * bare identifier — no spaces/branding — and stable across installs). */
91
+ export declare const PERMISSION_NAMESPACE = "AgentTool";
92
+ /**
93
+ * The client_name for an agent's (or sub-agent's) OAuth2 identity registered
94
+ * via DCR. These are confidential, per-machine credentials — many per shared
95
+ * project by design — so the name is attributable: the harness it belongs to
96
+ * and the host it was registered from, plus a short session label and the
97
+ * sub-agent type when applicable.
98
+ */
99
+ export declare function agentClientName(opts: {
100
+ harness?: string;
101
+ host: string;
102
+ subAgentType?: string;
103
+ sessionId?: string;
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;
116
+ /**
117
+ * The name for a project API key minted for runtime permission checks. Ory only
118
+ * returns a key's secret at creation, so a key can't be reused across machines
119
+ * — each installer that mints one gets its own. The name therefore carries the
120
+ * signed-in *user* (the axis that matters on a shared project) plus the host, so
121
+ * a team admin can tell whose key is whose and prune stale ones. No timestamp:
122
+ * the Ory Console's key listing already shows a "Date Added" column. `user`
123
+ * falls back to the host alone when it isn't resolvable (e.g. login skipped).
124
+ */
125
+ export declare function projectApiKeyName(opts: {
126
+ host: string;
127
+ user?: string;
128
+ }): string;