@ory/argus 0.14.0 → 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.
- package/README.md +31 -46
- package/assets/commands/temporal-up.md +1 -1
- package/assets/skills/auth-setup/SKILL.md +1 -1
- package/assets/skills/local-dev/SKILL.md +17 -7
- package/assets/skills/ory-build-agent/SKILL.md +39 -91
- package/assets/skills/ory-e2b-sandbox/SKILL.md +18 -17
- package/assets/skills/ory-temporal-worker/SKILL.md +31 -33
- package/assets/skills/permissions-onboarding/SKILL.md +131 -104
- package/dist/adapters.d.ts +88 -32
- package/dist/adapters.js +443 -164
- package/dist/agent-auth.d.ts +226 -67
- package/dist/agent-auth.js +951 -205
- package/dist/auth-store.d.ts +37 -2
- package/dist/auth-store.js +37 -3
- package/dist/auth.d.ts +33 -4
- package/dist/auth.js +163 -20
- package/dist/bash-parser.d.ts +98 -0
- package/dist/bash-parser.js +396 -0
- package/dist/branding.d.ts +77 -16
- package/dist/branding.js +93 -23
- package/dist/build-info.json +4 -4
- package/dist/cli-invocation.d.ts +1 -1
- package/dist/cli-invocation.js +2 -1
- package/dist/cli.d.ts +20 -29
- package/dist/cli.js +271 -239
- package/dist/client.d.ts +175 -138
- package/dist/client.js +672 -391
- package/dist/config.d.ts +217 -54
- package/dist/config.js +461 -42
- package/dist/context.d.ts +10 -0
- package/dist/context.js +21 -0
- package/dist/contract-suite.d.ts +3 -5
- package/dist/contract-suite.js +75 -47
- package/dist/denial.d.ts +36 -3
- package/dist/denial.js +79 -10
- package/dist/event-reporter.d.ts +77 -0
- package/dist/event-reporter.js +776 -0
- package/dist/external-registrations-main.d.ts +10 -0
- package/dist/external-registrations-main.js +38 -0
- package/dist/external-registrations.d.ts +79 -0
- package/dist/external-registrations.js +188 -0
- package/dist/help-cli.d.ts +39 -0
- package/dist/help-cli.js +55 -0
- package/dist/hook-timeout.d.ts +64 -0
- package/dist/hook-timeout.js +88 -0
- package/dist/index.d.ts +28 -23
- package/dist/index.js +173 -58
- package/dist/lifecycle.d.ts +3 -3
- package/dist/lifecycle.js +38 -6
- package/dist/local/cli.js +11 -6
- package/dist/local/configs.d.ts +74 -18
- package/dist/local/configs.js +291 -84
- package/dist/local/health.js +7 -7
- package/dist/local/index.d.ts +2 -2
- package/dist/local/index.js +24 -10
- package/dist/local/manager.d.ts +20 -1
- package/dist/local/manager.js +159 -36
- package/dist/local/ports.d.ts +158 -0
- package/dist/local/ports.js +443 -0
- package/dist/local/seed.d.ts +13 -5
- package/dist/local/seed.js +62 -38
- package/dist/logger.d.ts +54 -25
- package/dist/logger.js +329 -63
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +10 -5
- package/dist/mirror-bootstrap.d.ts +48 -0
- package/dist/mirror-bootstrap.js +254 -0
- package/dist/opl.d.ts +289 -0
- package/dist/opl.js +446 -0
- package/dist/permission-mode.d.ts +87 -0
- package/dist/permission-mode.js +307 -0
- package/dist/permissions-cli.d.ts +13 -49
- package/dist/permissions-cli.js +154 -348
- package/dist/permissions.d.ts +148 -38
- package/dist/permissions.js +591 -45
- package/dist/post-install.d.ts +33 -0
- package/dist/post-install.js +127 -0
- package/dist/read-credential.d.ts +65 -0
- package/dist/read-credential.js +86 -0
- package/dist/registry/cli.js +5 -2
- package/dist/registry/config.d.ts +0 -17
- package/dist/registry/config.js +0 -23
- package/dist/registry/index.d.ts +1 -1
- package/dist/registry/index.js +2 -2
- package/dist/registry/manager.d.ts +4 -21
- package/dist/registry/manager.js +21 -37
- package/dist/runtime-credential.d.ts +140 -0
- package/dist/runtime-credential.js +572 -0
- package/dist/runtime.d.ts +408 -0
- package/dist/runtime.js +748 -0
- package/dist/setup.d.ts +23 -47
- package/dist/setup.js +59 -116
- package/dist/skills.js +0 -7
- package/dist/status-cli.d.ts +27 -11
- package/dist/status-cli.js +121 -115
- package/dist/status-data.d.ts +116 -17
- package/dist/status-data.js +121 -38
- package/dist/subject.d.ts +126 -20
- package/dist/subject.js +215 -30
- package/dist/testing.d.ts +74 -38
- package/dist/testing.js +185 -68
- package/dist/tool-catalog.d.ts +53 -11
- package/dist/tool-catalog.js +164 -13
- package/dist/tool-metadata.d.ts +7 -6
- package/dist/tool-metadata.js +6 -5
- package/dist/types.d.ts +11 -1
- package/dist/uninstall.d.ts +71 -34
- package/dist/uninstall.js +217 -85
- package/dist/user-login.d.ts +9 -10
- package/dist/user-login.js +56 -75
- package/dist/watch-cli.d.ts +6 -0
- package/dist/watch-cli.js +217 -0
- package/package.json +4 -27
- package/assets/commands/dashboard.md +0 -34
- package/dist/dashboard-cli.d.ts +0 -8
- package/dist/dashboard-cli.js +0 -70
- package/dist/dev.d.ts +0 -103
- package/dist/dev.js +0 -583
- package/dist/interactive-setup.d.ts +0 -286
- package/dist/interactive-setup.js +0 -1734
- package/dist/local/jaeger-main.d.ts +0 -13
- package/dist/local/jaeger-main.js +0 -85
- package/dist/local/jaeger.d.ts +0 -50
- package/dist/local/jaeger.js +0 -162
- package/dist/otel/exporter.d.ts +0 -17
- package/dist/otel/exporter.js +0 -12
- package/dist/otel/index.d.ts +0 -2
- package/dist/otel/index.js +0 -8
- package/dist/otel/otlp.d.ts +0 -103
- package/dist/otel/otlp.js +0 -385
- package/dist/project-api-key.d.ts +0 -69
- package/dist/project-api-key.js +0 -147
- package/dist/setup-actions.d.ts +0 -232
- package/dist/setup-actions.js +0 -507
- package/dist/tracer.d.ts +0 -190
- package/dist/tracer.js +0 -481
- package/dist/watch-sandbox.d.ts +0 -9
- package/dist/watch-sandbox.js +0 -81
- package/dist/web/api.d.ts +0 -33
- package/dist/web/api.js +0 -294
- package/dist/web/launch.d.ts +0 -11
- package/dist/web/launch.js +0 -96
- package/dist/web/server.d.ts +0 -20
- package/dist/web/server.js +0 -233
- package/dist/web/types.d.ts +0 -65
- package/dist/web/types.js +0 -2
- package/dist/webapp/assets/index-Wucl4SZs.css +0 -1
- package/dist/webapp/assets/index-m-GtEdq0.js +0 -49
- package/dist/webapp/favicon.ico +0 -0
- 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
|
+
}
|
package/dist/branding.d.ts
CHANGED
|
@@ -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)
|
|
11
|
-
* **stable,
|
|
12
|
-
* resource instead of duplicating it:
|
|
13
|
-
*
|
|
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
|
|
26
|
-
*
|
|
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".
|
|
28
60
|
*/
|
|
29
|
-
export declare const USER_LOGIN_CLIENT_NAME = "Ory Agent Security \u00B7 user login (
|
|
61
|
+
export declare const USER_LOGIN_CLIENT_NAME = "Ory Agent Security \u00B7 shared user login (all plugins)";
|
|
30
62
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
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 = "
|
|
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
|
|
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
|