@ory/argus 1.5.3 → 1.6.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/assets/skills/ory-build-agent/SKILL.md +4 -4
- package/assets/skills/permissions-onboarding/SKILL.md +8 -5
- package/dist/adapters.d.ts +1 -1
- package/dist/adapters.js +27 -30
- package/dist/build-info.json +4 -4
- package/dist/cli.js +1 -1
- package/dist/client.js +5 -0
- package/dist/event-reporter.js +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +14 -2
- package/dist/logger.js +1 -1
- package/dist/mcp.d.ts +79 -11
- package/dist/mcp.js +255 -22
- package/dist/opl.d.ts +11 -1
- package/dist/opl.js +19 -2
- package/dist/permissions-cli.d.ts +1 -1
- package/dist/permissions-cli.js +3 -3
- package/dist/permissions.d.ts +27 -2
- package/dist/permissions.js +55 -1
- package/dist/post-install.d.ts +1 -1
- package/dist/post-install.js +2 -2
- package/dist/testing.d.ts +41 -29
- package/dist/testing.js +34 -12
- package/dist/types.d.ts +13 -0
- package/package.json +4 -4
package/dist/mcp.js
CHANGED
|
@@ -4,15 +4,25 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Provides types, parsers, and permission check helpers for securing
|
|
6
6
|
* MCP tool calls across all harness plugins. Each harness represents
|
|
7
|
-
* MCP tools differently — parsers normalize them into McpToolIdentifier
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* MCP tools differently — parsers normalize them into McpToolIdentifier.
|
|
8
|
+
* Adapters pass that identifier to `gate`, which checks the McpServer and
|
|
9
|
+
* McpTool namespaces with the same block and mode semantics as native tools.
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.parseClaudeCodeMcpTool = parseClaudeCodeMcpTool;
|
|
13
13
|
exports.parseGeminiMcpTool = parseGeminiMcpTool;
|
|
14
|
+
exports.parseMcpToolWithSeparator = parseMcpToolWithSeparator;
|
|
14
15
|
exports.parseMcpToolGeneric = parseMcpToolGeneric;
|
|
16
|
+
exports.mcpFingerprint = mcpFingerprint;
|
|
17
|
+
exports.mcpServerFingerprints = mcpServerFingerprints;
|
|
18
|
+
exports.mcpServerHint = mcpServerHint;
|
|
19
|
+
exports.normalizeMcpServerName = normalizeMcpServerName;
|
|
20
|
+
exports.mcpToolObject = mcpToolObject;
|
|
21
|
+
exports.parseMcpToolName = parseMcpToolName;
|
|
22
|
+
exports.parseMcpToolArgs = parseMcpToolArgs;
|
|
15
23
|
exports.checkMcpPermission = checkMcpPermission;
|
|
24
|
+
const node_crypto_1 = require("node:crypto");
|
|
25
|
+
const opl_js_1 = require("./opl.js");
|
|
16
26
|
const read_credential_js_1 = require("./read-credential.js");
|
|
17
27
|
// ─── Parsers ───────────────────────────────────────────────────────
|
|
18
28
|
/**
|
|
@@ -37,16 +47,53 @@ function parseClaudeCodeMcpTool(toolName) {
|
|
|
37
47
|
}
|
|
38
48
|
/**
|
|
39
49
|
* Parse MCP context from Gemini CLI hook input.
|
|
40
|
-
*
|
|
50
|
+
*
|
|
51
|
+
* Gemini provides server info via the `mcp_context` field: the configured
|
|
52
|
+
* `server_name`, the server's own `tool_name` (the hook `tool_name` is the
|
|
53
|
+
* prefixed `mcp_<server>_<tool>` form), and how the server is launched or
|
|
54
|
+
* reached (`command`, `args`, `url`).
|
|
41
55
|
*/
|
|
42
56
|
function parseGeminiMcpTool(toolName, mcpContext) {
|
|
43
57
|
if (!mcpContext || typeof mcpContext.server_name !== "string")
|
|
44
58
|
return null;
|
|
59
|
+
const fingerprints = mcpServerFingerprints({
|
|
60
|
+
url: typeof mcpContext.url === "string" ? mcpContext.url : undefined,
|
|
61
|
+
command: typeof mcpContext.command === "string" ? mcpContext.command : undefined,
|
|
62
|
+
args: Array.isArray(mcpContext.args)
|
|
63
|
+
? mcpContext.args.filter((arg) => typeof arg === "string")
|
|
64
|
+
: undefined,
|
|
65
|
+
});
|
|
45
66
|
return {
|
|
46
67
|
serverName: mcpContext.server_name,
|
|
47
|
-
toolName,
|
|
68
|
+
toolName: typeof mcpContext.tool_name === "string" && mcpContext.tool_name !== "" ? mcpContext.tool_name : toolName,
|
|
69
|
+
...(fingerprints.length > 0 ? { fingerprints } : {}),
|
|
48
70
|
};
|
|
49
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Parse a `<server><separator><tool>` tool name, as Goose (`__`), OpenClaw
|
|
74
|
+
* (`__`), Cline (`__`), and OpenCode (`_`) build them. With `servers`, the
|
|
75
|
+
* longest configured server name that prefixes the tool name wins, so server
|
|
76
|
+
* names containing the separator split correctly and native tools without a
|
|
77
|
+
* server prefix return null. Without `servers`, the first separator splits.
|
|
78
|
+
*/
|
|
79
|
+
function parseMcpToolWithSeparator(toolName, separator, servers) {
|
|
80
|
+
if (servers) {
|
|
81
|
+
let match;
|
|
82
|
+
for (const server of servers) {
|
|
83
|
+
if (server !== "" &&
|
|
84
|
+
toolName.length > server.length + separator.length &&
|
|
85
|
+
toolName.startsWith(server + separator) &&
|
|
86
|
+
server.length > (match?.length ?? -1)) {
|
|
87
|
+
match = server;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return match === undefined ? null : { serverName: match, toolName: toolName.slice(match.length + separator.length) };
|
|
91
|
+
}
|
|
92
|
+
const index = toolName.indexOf(separator);
|
|
93
|
+
if (index <= 0 || index + separator.length >= toolName.length)
|
|
94
|
+
return null;
|
|
95
|
+
return { serverName: toolName.slice(0, index), toolName: toolName.slice(index + separator.length) };
|
|
96
|
+
}
|
|
50
97
|
/**
|
|
51
98
|
* Generic MCP tool parser for harnesses that use a `mcp:<server>:<tool>`
|
|
52
99
|
* convention. Returns null if the tool ID doesn't start with the prefix.
|
|
@@ -56,6 +103,8 @@ function parseMcpToolGeneric(toolId, mcpPrefix = "mcp:") {
|
|
|
56
103
|
return null;
|
|
57
104
|
const rest = toolId.slice(mcpPrefix.length);
|
|
58
105
|
const sepIdx = rest.indexOf(":");
|
|
106
|
+
if (sepIdx === 0 || rest === "")
|
|
107
|
+
return null;
|
|
59
108
|
if (sepIdx === -1) {
|
|
60
109
|
return { serverName: rest, toolName: "" };
|
|
61
110
|
}
|
|
@@ -64,40 +113,224 @@ function parseMcpToolGeneric(toolId, mcpPrefix = "mcp:") {
|
|
|
64
113
|
toolName: rest.slice(sepIdx + 1),
|
|
65
114
|
};
|
|
66
115
|
}
|
|
116
|
+
// ─── Fingerprints ──────────────────────────────────────────────────
|
|
117
|
+
const MAX_MCP_FINGERPRINTS = 4;
|
|
118
|
+
const PACKAGE_RUNNERS = new Set(["npx", "bunx", "uvx", "pipx", "deno"]);
|
|
119
|
+
const PACKAGE_RUNNER_SUBCOMMANDS = new Set(["dlx", "exec", "run", "x", "tool"]);
|
|
120
|
+
/** Runners whose package follows one of {@link PACKAGE_RUNNER_SUBCOMMANDS} (`deno run npm:@scope/mcp`). */
|
|
121
|
+
const SUBCOMMAND_RUNNERS = new Set(["pnpm", "yarn", "bun", "uv", "pipx", "deno"]);
|
|
122
|
+
const CONTAINER_RUNNERS = new Set(["docker", "podman"]);
|
|
123
|
+
/** Container global flags, before the subcommand, that take a separate value argument. */
|
|
124
|
+
const CONTAINER_GLOBAL_VALUE_FLAGS = new Set([
|
|
125
|
+
"-H", "--host", "-c", "--context", "--config", "-l", "--log-level", "--tlscacert", "--tlscert", "--tlskey",
|
|
126
|
+
"--connection", "--url", "--identity", "--root", "--runroot", "--storage-driver", "--cgroup-manager",
|
|
127
|
+
]);
|
|
128
|
+
/** Container `run` flags that take a separate value argument. Unlisted flags are treated as boolean. */
|
|
129
|
+
const CONTAINER_VALUE_FLAGS = new Set([
|
|
130
|
+
"-a", "--attach", "--annotation", "--add-host", "--blkio-weight", "--blkio-weight-device", "--cap-add", "--cap-drop",
|
|
131
|
+
"--cgroup-parent", "--cgroupns", "--cidfile", "-c", "--cpu-shares", "--cpu-period", "--cpu-quota", "--cpu-rt-period",
|
|
132
|
+
"--cpu-rt-runtime", "--cpus", "--cpuset-cpus", "--cpuset-mems", "--detach-keys", "--device", "--device-cgroup-rule",
|
|
133
|
+
"--device-read-bps", "--device-read-iops", "--device-write-bps", "--device-write-iops", "--dns", "--dns-option",
|
|
134
|
+
"--dns-search", "--domainname", "-e", "--env", "--env-file", "--entrypoint", "--expose", "--gpus", "--group-add",
|
|
135
|
+
"--health-cmd", "--health-interval", "--health-retries", "--health-start-interval", "--health-start-period",
|
|
136
|
+
"--health-timeout", "-h", "--hostname", "--ip", "--ip6", "--ipc", "--isolation", "--kernel-memory", "-l", "--label",
|
|
137
|
+
"--label-file", "--link", "--link-local-ip", "--log-driver", "--log-opt", "--mac-address", "-m", "--memory",
|
|
138
|
+
"--memory-reservation", "--memory-swap", "--memory-swappiness", "--mount", "--name", "--network", "--net",
|
|
139
|
+
"--network-alias", "--oom-score-adj", "-p", "--publish", "--pid", "--pids-limit", "--platform", "--pull",
|
|
140
|
+
"--restart", "--runtime", "--security-opt", "--shm-size", "--stop-signal", "--stop-timeout", "--storage-opt",
|
|
141
|
+
"--sysctl", "--tmpfs", "-u", "--user", "--ulimit", "--userns", "--uts", "-v", "--volume", "--volume-driver",
|
|
142
|
+
"--volumes-from", "-w", "--workdir", "--arch", "--os", "--variant", "--secret", "--hooks-dir", "--pod",
|
|
143
|
+
"--uidmap", "--gidmap", "--subuidname", "--subgidname", "--userns-uid-map", "--userns-gid-map",
|
|
144
|
+
]);
|
|
145
|
+
/**
|
|
146
|
+
* The hash Agent Security matches against cataloged server identifiers: the
|
|
147
|
+
* first 16 hex characters of the SHA-256 of the trimmed, lowercased identifier.
|
|
148
|
+
*/
|
|
149
|
+
function mcpFingerprint(identifier) {
|
|
150
|
+
return (0, node_crypto_1.createHash)("sha256").update(identifier.trim().toLowerCase()).digest("hex").slice(0, 16);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Hash the identifiers that recognize an MCP server whatever it is named: the
|
|
154
|
+
* remote URL host, the package a runner launches (`npx -y @playwright/mcp@1`
|
|
155
|
+
* yields `@playwright/mcp`), or the container image (`ghcr.io/github/github-mcp-server`).
|
|
156
|
+
* Only hashes leave the process; paths, arguments, and credentials are never sent.
|
|
157
|
+
*/
|
|
158
|
+
function mcpServerFingerprints(source) {
|
|
159
|
+
const identifiers = new Set();
|
|
160
|
+
const host = urlHost(source.url);
|
|
161
|
+
if (host)
|
|
162
|
+
identifiers.add(host);
|
|
163
|
+
const words = [...(source.command ? source.command.trim().split(/\s+/) : []), ...(source.args ?? [])].filter((word) => word !== "");
|
|
164
|
+
const launched = launchedIdentifier(words);
|
|
165
|
+
if (launched)
|
|
166
|
+
identifiers.add(launched);
|
|
167
|
+
return [...identifiers].slice(0, MAX_MCP_FINGERPRINTS).map(mcpFingerprint);
|
|
168
|
+
}
|
|
169
|
+
function urlHost(value) {
|
|
170
|
+
if (!value)
|
|
171
|
+
return undefined;
|
|
172
|
+
try {
|
|
173
|
+
return new URL(value).hostname.toLowerCase() || undefined;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function launchedIdentifier(words) {
|
|
180
|
+
const [command, ...rest] = words;
|
|
181
|
+
if (!command)
|
|
182
|
+
return undefined;
|
|
183
|
+
const runner = command.split(/[\\/]/).pop().toLowerCase().replace(/\.(exe|cmd)$/, "");
|
|
184
|
+
if (CONTAINER_RUNNERS.has(runner)) {
|
|
185
|
+
const runIndex = containerRunIndex(rest);
|
|
186
|
+
if (runIndex < 0)
|
|
187
|
+
return undefined;
|
|
188
|
+
for (let index = runIndex + 1; index < rest.length; index++) {
|
|
189
|
+
const word = rest[index];
|
|
190
|
+
if (word.startsWith("-")) {
|
|
191
|
+
if (!word.includes("=") && CONTAINER_VALUE_FLAGS.has(word))
|
|
192
|
+
index++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
return containerImage(word);
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
let args = rest;
|
|
200
|
+
if (SUBCOMMAND_RUNNERS.has(runner)) {
|
|
201
|
+
while (args[0] && PACKAGE_RUNNER_SUBCOMMANDS.has(args[0]))
|
|
202
|
+
args = args.slice(1);
|
|
203
|
+
}
|
|
204
|
+
else if (!PACKAGE_RUNNERS.has(runner)) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
for (let index = 0; index < args.length; index++) {
|
|
208
|
+
const word = args[index];
|
|
209
|
+
if (word === "-p" || word === "--package" || word === "--from") {
|
|
210
|
+
const value = args[index + 1];
|
|
211
|
+
return value ? packageName(value) : undefined;
|
|
212
|
+
}
|
|
213
|
+
if (word.startsWith("-"))
|
|
214
|
+
continue;
|
|
215
|
+
return packageName(word);
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The index of the `run` subcommand in a container command's arguments, after
|
|
221
|
+
* any global flags (`docker --context remote run …`, `podman container run …`),
|
|
222
|
+
* or -1 when the command runs nothing.
|
|
223
|
+
*/
|
|
224
|
+
function containerRunIndex(args) {
|
|
225
|
+
for (let index = 0; index < args.length; index++) {
|
|
226
|
+
const word = args[index];
|
|
227
|
+
if (word.startsWith("-")) {
|
|
228
|
+
if (!word.includes("=") && CONTAINER_GLOBAL_VALUE_FLAGS.has(word))
|
|
229
|
+
index++;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (word === "container" && args[index + 1] === "run")
|
|
233
|
+
return index + 1;
|
|
234
|
+
return word === "run" ? index : -1;
|
|
235
|
+
}
|
|
236
|
+
return -1;
|
|
237
|
+
}
|
|
238
|
+
function packageName(spec) {
|
|
239
|
+
let name = spec.replace(/^npm:|^jsr:/, "");
|
|
240
|
+
const versionAt = name.lastIndexOf("@");
|
|
241
|
+
if (versionAt > 0)
|
|
242
|
+
name = name.slice(0, versionAt);
|
|
243
|
+
name = name.replace(/\[.*\]$/, "").replace(/[=<>~!].*$/, "");
|
|
244
|
+
if (name === "" || name.includes("://") || name.startsWith(".") || name.startsWith("/"))
|
|
245
|
+
return undefined;
|
|
246
|
+
return name.toLowerCase();
|
|
247
|
+
}
|
|
248
|
+
function containerImage(reference) {
|
|
249
|
+
let image = reference.toLowerCase().split("@")[0];
|
|
250
|
+
const lastSlash = image.lastIndexOf("/");
|
|
251
|
+
const tag = image.indexOf(":", lastSlash + 1);
|
|
252
|
+
if (tag >= 0)
|
|
253
|
+
image = image.slice(0, tag);
|
|
254
|
+
image = image.replace(/^(docker\.io|index\.docker\.io)\//, "").replace(/^library\//, "");
|
|
255
|
+
return image || undefined;
|
|
256
|
+
}
|
|
257
|
+
/** The `mcp` hint Agent Security resolves an MCP check's server from. */
|
|
258
|
+
function mcpServerHint(mcpTool) {
|
|
259
|
+
return {
|
|
260
|
+
server: mcpTool.serverName,
|
|
261
|
+
...(mcpTool.toolName ? { tool: mcpTool.toolName } : {}),
|
|
262
|
+
...(mcpTool.fingerprints?.length ? { fingerprints: mcpTool.fingerprints.slice(0, MAX_MCP_FINGERPRINTS) } : {}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
// ─── Normalization ─────────────────────────────────────────────────
|
|
266
|
+
/**
|
|
267
|
+
* Normalize a configured MCP server name to the reported `McpServer` object:
|
|
268
|
+
* trimmed and lowercased. Agent Security resolves it, with the check's `mcp`
|
|
269
|
+
* hint, to the catalog server it enforces.
|
|
270
|
+
*/
|
|
271
|
+
function normalizeMcpServerName(serverName) {
|
|
272
|
+
return serverName.trim().toLowerCase();
|
|
273
|
+
}
|
|
274
|
+
/** The `McpTool` object for one call: `<server>:<tool>`. */
|
|
275
|
+
function mcpToolObject(serverName, toolName) {
|
|
276
|
+
return `${normalizeMcpServerName(serverName)}:${toolName}`;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Parse a harness MCP tool name in either the `mcp__<server>__<tool>` form
|
|
280
|
+
* (Claude Code, Claude Agent SDK, Codex) or the `mcp:<server>:<tool>` form
|
|
281
|
+
* (OpenCode, OpenClaw, and older Codex builds).
|
|
282
|
+
*/
|
|
283
|
+
function parseMcpToolName(toolName) {
|
|
284
|
+
return parseClaudeCodeMcpTool(toolName) ?? parseMcpToolGeneric(toolName);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Parse the arguments of a generic "call an MCP tool" harness tool, such as
|
|
288
|
+
* Cline's `use_mcp_tool` (`{ server_name, tool_name, arguments }`). Returns null
|
|
289
|
+
* unless both a server and a tool name are present.
|
|
290
|
+
*/
|
|
291
|
+
function parseMcpToolArgs(toolArgs) {
|
|
292
|
+
if (!toolArgs || typeof toolArgs !== "object")
|
|
293
|
+
return null;
|
|
294
|
+
const args = toolArgs;
|
|
295
|
+
const serverName = [args.server_name, args.serverName, args.ServerName].find((value) => typeof value === "string" && value.trim() !== "");
|
|
296
|
+
const toolName = [args.tool_name, args.toolName, args.ToolName].find((value) => typeof value === "string" && value.trim() !== "");
|
|
297
|
+
return serverName && toolName ? { serverName, toolName } : null;
|
|
298
|
+
}
|
|
67
299
|
// ─── Permission Check ──────────────────────────────────────────────
|
|
68
300
|
/**
|
|
69
|
-
* Check MCP
|
|
301
|
+
* Check the user's MCP `use` permits without machine-principal blocks or mode.
|
|
70
302
|
*
|
|
71
|
-
* Server check:
|
|
72
|
-
*
|
|
303
|
+
* Server check: `McpServer:<server>#use`. Tool check, when the tool name is
|
|
304
|
+
* known and `checkToolLevel` is not false: `McpTool:<server>:<tool>#use`. The
|
|
305
|
+
* server name is normalized with {@link normalizeMcpServerName}.
|
|
73
306
|
*
|
|
74
|
-
*
|
|
307
|
+
* Harness adapters should pass `mcpTool` to `gate` / `decideTool` instead, which
|
|
308
|
+
* also applies Agent, Sub-Agent, and Project blocks and the permission mode.
|
|
75
309
|
* Throws on API errors — callers handle fail-open behavior.
|
|
76
310
|
*/
|
|
77
311
|
async function checkMcpPermission(client, mcpTool, options) {
|
|
78
|
-
const serverNamespace = options.serverNamespace ??
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
const checkToolLevel = options.checkToolLevel ??
|
|
83
|
-
process.env.ORY_MCP_TOOL_LEVEL_CHECKS === "true";
|
|
312
|
+
const serverNamespace = options.serverNamespace ?? opl_js_1.MCP_SERVER_NAMESPACE;
|
|
313
|
+
const toolNamespace = options.toolNamespace ?? opl_js_1.MCP_TOOL_NAMESPACE;
|
|
314
|
+
const checkToolLevel = options.checkToolLevel ?? true;
|
|
315
|
+
const serverName = normalizeMcpServerName(mcpTool.serverName);
|
|
84
316
|
// MCP tool calls arrive on the same subprocess lifecycle events as any other
|
|
85
317
|
// tool, so this read needs a credential resolved for exactly the reasons
|
|
86
318
|
// `gateToolCall` does (#242).
|
|
87
319
|
await (0, read_credential_js_1.ensureReadCredential)(client);
|
|
320
|
+
const mcp = mcpServerHint(mcpTool);
|
|
88
321
|
const serverCheck = {
|
|
89
322
|
namespace: serverNamespace,
|
|
90
|
-
object:
|
|
91
|
-
relation:
|
|
323
|
+
object: serverName,
|
|
324
|
+
relation: opl_js_1.PERMIT_USE,
|
|
92
325
|
...options.subject,
|
|
326
|
+
mcp,
|
|
93
327
|
};
|
|
94
328
|
const activityAttributes = {
|
|
95
329
|
mcpServer: mcpTool.serverName,
|
|
96
330
|
...(mcpTool.toolName ? { mcpTool: mcpTool.toolName } : {}),
|
|
97
331
|
...options.activityAttributes,
|
|
98
332
|
};
|
|
99
|
-
if (!checkToolLevel
|
|
100
|
-
// Server-only check
|
|
333
|
+
if (!checkToolLevel) {
|
|
101
334
|
const result = await client.checkPermission(serverCheck, { activityAttributes });
|
|
102
335
|
return {
|
|
103
336
|
serverAllowed: result.allowed,
|
|
@@ -107,12 +340,12 @@ async function checkMcpPermission(client, mcpTool, options) {
|
|
|
107
340
|
mcpTool,
|
|
108
341
|
};
|
|
109
342
|
}
|
|
110
|
-
// Batch check: server + tool
|
|
111
343
|
const toolCheck = {
|
|
112
344
|
namespace: toolNamespace,
|
|
113
|
-
object:
|
|
114
|
-
relation:
|
|
345
|
+
object: mcpToolObject(serverName, mcpTool.toolName ?? ""),
|
|
346
|
+
relation: opl_js_1.PERMIT_USE,
|
|
115
347
|
...options.subject,
|
|
348
|
+
mcp,
|
|
116
349
|
};
|
|
117
350
|
const batchResult = await client.batchCheckPermissions([serverCheck, toolCheck], { activityAttributes });
|
|
118
351
|
const serverAllowed = batchResult.results[0]?.allowed ?? false;
|
package/dist/opl.d.ts
CHANGED
|
@@ -86,6 +86,16 @@ export declare const DEFAULT_NAMESPACE = "AgentTool";
|
|
|
86
86
|
* `resolveShellCommandNamespace()` in tool-catalog.ts.
|
|
87
87
|
*/
|
|
88
88
|
export declare const DEFAULT_SHELL_NAMESPACE = "ShellTool";
|
|
89
|
+
/**
|
|
90
|
+
* Namespaces for MCP calls. A call is checked as `McpServer:<server>#use` and
|
|
91
|
+
* `McpTool:<server>:<tool>#use`, where `<server>` is the lowercased configured
|
|
92
|
+
* server name. Same default-allow / explicit-block model as the native tool
|
|
93
|
+
* namespaces; Agent Security projects per-server Admin, Write, Read, and No
|
|
94
|
+
* access levels into them. Not operator-configurable: the server catalog owns
|
|
95
|
+
* the objects.
|
|
96
|
+
*/
|
|
97
|
+
export declare const MCP_SERVER_NAMESPACE = "McpServer";
|
|
98
|
+
export declare const MCP_TOOL_NAMESPACE = "McpTool";
|
|
89
99
|
/**
|
|
90
100
|
* Namespace for delegation nodes — the current-state graph of who delegated to
|
|
91
101
|
* whom (`Delegation:agent:<join>`, `Delegation:subagent:<join>|<type>`). Its own
|
|
@@ -232,7 +242,7 @@ export declare function renderPrincipalClass(name: string): string;
|
|
|
232
242
|
/**
|
|
233
243
|
* Render a native tool-namespace class body: the `blockedSubjects` relation and
|
|
234
244
|
* the default-allow `use` permit Argus checks. The single definition is used for
|
|
235
|
-
*
|
|
245
|
+
* AgentTool, ShellTool, McpServer, and McpTool.
|
|
236
246
|
*/
|
|
237
247
|
export declare function renderToolNamespaceClass(namespace?: string): string;
|
|
238
248
|
/**
|
package/dist/opl.js
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* grant relation is the plural `users` and the permit the singular verb `use`.
|
|
36
36
|
*/
|
|
37
37
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
38
|
-
exports.NODE_REFERENCE_NAMESPACES = exports.PRINCIPAL_NAMESPACES = exports.OPL_IMPORT_SYMBOLS = exports.OPL_IMPORT_MODULE = exports.PERMIT_USE = exports.PROJECT_POSTURE_OBJECT = exports.PERMISSION_MODE_OBJECT = exports.PERMISSION_MODE_NAMESPACE = exports.RELATION_OPTED_OUT_SUBJECTS = exports.AUTO_ENROLL_OBJECT = exports.ENROLLMENT_NAMESPACE = exports.RELATION_MEMBERS = exports.SUBAGENT_TYPE_NAMESPACE = exports.AGENT_TYPE_NAMESPACE = exports.AGENT_ROLE_NAMESPACE = exports.SUBAGENT_NODE_NAMESPACE = exports.AGENT_NODE_NAMESPACE = exports.DELEGATION_NAMESPACE = exports.DEFAULT_SHELL_NAMESPACE = exports.DEFAULT_NAMESPACE = exports.RELATION_ENFORCED_SUBJECTS = exports.RELATION_CREDENTIALS = exports.RELATION_BLOCKED_SUBJECTS = exports.RELATION_DELEGATES = exports.RELATION_USERS = void 0;
|
|
38
|
+
exports.NODE_REFERENCE_NAMESPACES = exports.PRINCIPAL_NAMESPACES = exports.OPL_IMPORT_SYMBOLS = exports.OPL_IMPORT_MODULE = exports.PERMIT_USE = exports.PROJECT_POSTURE_OBJECT = exports.PERMISSION_MODE_OBJECT = exports.PERMISSION_MODE_NAMESPACE = exports.RELATION_OPTED_OUT_SUBJECTS = exports.AUTO_ENROLL_OBJECT = exports.ENROLLMENT_NAMESPACE = exports.RELATION_MEMBERS = exports.SUBAGENT_TYPE_NAMESPACE = exports.AGENT_TYPE_NAMESPACE = exports.AGENT_ROLE_NAMESPACE = exports.SUBAGENT_NODE_NAMESPACE = exports.AGENT_NODE_NAMESPACE = exports.DELEGATION_NAMESPACE = exports.MCP_TOOL_NAMESPACE = exports.MCP_SERVER_NAMESPACE = exports.DEFAULT_SHELL_NAMESPACE = exports.DEFAULT_NAMESPACE = exports.RELATION_ENFORCED_SUBJECTS = exports.RELATION_CREDENTIALS = exports.RELATION_BLOCKED_SUBJECTS = exports.RELATION_DELEGATES = exports.RELATION_USERS = void 0;
|
|
39
39
|
exports.renderOplImport = renderOplImport;
|
|
40
40
|
exports.renderPrincipalClass = renderPrincipalClass;
|
|
41
41
|
exports.renderToolNamespaceClass = renderToolNamespaceClass;
|
|
@@ -99,6 +99,16 @@ exports.DEFAULT_NAMESPACE = "AgentTool";
|
|
|
99
99
|
* `resolveShellCommandNamespace()` in tool-catalog.ts.
|
|
100
100
|
*/
|
|
101
101
|
exports.DEFAULT_SHELL_NAMESPACE = "ShellTool";
|
|
102
|
+
/**
|
|
103
|
+
* Namespaces for MCP calls. A call is checked as `McpServer:<server>#use` and
|
|
104
|
+
* `McpTool:<server>:<tool>#use`, where `<server>` is the lowercased configured
|
|
105
|
+
* server name. Same default-allow / explicit-block model as the native tool
|
|
106
|
+
* namespaces; Agent Security projects per-server Admin, Write, Read, and No
|
|
107
|
+
* access levels into them. Not operator-configurable: the server catalog owns
|
|
108
|
+
* the objects.
|
|
109
|
+
*/
|
|
110
|
+
exports.MCP_SERVER_NAMESPACE = "McpServer";
|
|
111
|
+
exports.MCP_TOOL_NAMESPACE = "McpTool";
|
|
102
112
|
/**
|
|
103
113
|
* Namespace for delegation nodes — the current-state graph of who delegated to
|
|
104
114
|
* whom (`Delegation:agent:<join>`, `Delegation:subagent:<join>|<type>`). Its own
|
|
@@ -259,7 +269,7 @@ function renderPrincipalClass(name) {
|
|
|
259
269
|
/**
|
|
260
270
|
* Render a native tool-namespace class body: the `blockedSubjects` relation and
|
|
261
271
|
* the default-allow `use` permit Argus checks. The single definition is used for
|
|
262
|
-
*
|
|
272
|
+
* AgentTool, ShellTool, McpServer, and McpTool.
|
|
263
273
|
*/
|
|
264
274
|
function renderToolNamespaceClass(namespace = exports.DEFAULT_NAMESPACE) {
|
|
265
275
|
return `class ${namespace} implements Namespace {
|
|
@@ -406,6 +416,13 @@ function buildAgentSecurityOpl(namespace = exports.DEFAULT_NAMESPACE, shellNames
|
|
|
406
416
|
"// default-allow / explicit-block model as the tool namespace above.",
|
|
407
417
|
renderToolNamespaceClass(shellNamespace),
|
|
408
418
|
"",
|
|
419
|
+
`// MCP calls: \`${exports.MCP_SERVER_NAMESPACE}:<server>\` and \`${exports.MCP_TOOL_NAMESPACE}:<server>:<tool>\`. Agent`,
|
|
420
|
+
"// Security links a scope to per-server tier anchors to apply Admin, Write,",
|
|
421
|
+
"// Read, or No access. Same default-allow / explicit-block model.",
|
|
422
|
+
renderToolNamespaceClass(exports.MCP_SERVER_NAMESPACE),
|
|
423
|
+
"",
|
|
424
|
+
renderToolNamespaceClass(exports.MCP_TOOL_NAMESPACE),
|
|
425
|
+
"",
|
|
409
426
|
"// The shared role whose members can be blocked as a group.",
|
|
410
427
|
renderAgentRoleNamespaceClass(),
|
|
411
428
|
"",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* Everything that *writes* to the project is provisioned in the **Ory Console**
|
|
13
13
|
* (Agent Security), not from the plugin: the permission model, explicit native
|
|
14
|
-
* blocks, MCP
|
|
14
|
+
* blocks, MCP server access, and the observe/enforce posture. That is what lets a
|
|
15
15
|
* plugin be installed with nothing but a project URL — the public OAuth2 client
|
|
16
16
|
* id has a reserved default, with no workspace privilege, project API key, or admin write path. The
|
|
17
17
|
* plugin is a read-only client of the project's permission state.
|
package/dist/permissions-cli.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Everything that *writes* to the project is provisioned in the **Ory Console**
|
|
14
14
|
* (Agent Security), not from the plugin: the permission model, explicit native
|
|
15
|
-
* blocks, MCP
|
|
15
|
+
* blocks, MCP server access, and the observe/enforce posture. That is what lets a
|
|
16
16
|
* plugin be installed with nothing but a project URL — the public OAuth2 client
|
|
17
17
|
* id has a reserved default, with no workspace privilege, project API key, or admin write path. The
|
|
18
18
|
* plugin is a read-only client of the project's permission state.
|
|
@@ -78,7 +78,7 @@ function printPermissionsHelp(binName) {
|
|
|
78
78
|
console.log(" observe — Log an audit event and allow the tool through. The default.");
|
|
79
79
|
console.log(" enforce — Block the tool. The production posture.");
|
|
80
80
|
console.log("");
|
|
81
|
-
console.log("The plugin only ever *reads* permissions. Native tool blocks, MCP
|
|
81
|
+
console.log("The plugin only ever *reads* permissions. Native tool blocks, MCP server access,");
|
|
82
82
|
console.log("the permission model, and the observe/enforce posture are all managed in");
|
|
83
83
|
console.log("the Ory Console (Agent Security) by someone with access to the project.");
|
|
84
84
|
console.log("");
|
|
@@ -97,7 +97,7 @@ function printProvisioningMovedToConsole(binName, attempted) {
|
|
|
97
97
|
console.error("Permissions are provisioned in the Ory Console (Agent Security):");
|
|
98
98
|
console.error(" - the permission model applied to the project");
|
|
99
99
|
console.error(" - explicit native-tool blocks for a user, agent, or group");
|
|
100
|
-
console.error(" - MCP
|
|
100
|
+
console.error(" - MCP server access");
|
|
101
101
|
console.error(" - the observe / enforce posture, project-wide or per principal");
|
|
102
102
|
console.error("");
|
|
103
103
|
console.error("The plugin reads that state and enforces it. To see what it resolves:");
|
package/dist/permissions.d.ts
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* opencode `throw OryDenialError`, etc.) without re-implementing the
|
|
28
28
|
* mode logic.
|
|
29
29
|
*/
|
|
30
|
+
import { type McpToolIdentifier } from "./mcp.js";
|
|
30
31
|
import type { OryAgentClient } from "./client.js";
|
|
31
32
|
import { type PermissionMode } from "./config.js";
|
|
32
33
|
import type { OryError, OryErrorCode, PermissionCheck, PermissionResult } from "./types.js";
|
|
@@ -34,8 +35,8 @@ import type { OryError, OryErrorCode, PermissionCheck, PermissionResult } from "
|
|
|
34
35
|
* Why a permission check came back denied, once the block-aware OPL schema is
|
|
35
36
|
* in play:
|
|
36
37
|
*
|
|
37
|
-
* - `not_granted` — compatibility for grant-based checks outside native
|
|
38
|
-
* AgentTool
|
|
38
|
+
* - `not_granted` — compatibility for grant-based checks outside the native
|
|
39
|
+
* AgentTool, ShellTool, McpServer, and McpTool policy.
|
|
39
40
|
* - `explicit_block` — a native `use` permit denied because a
|
|
40
41
|
* `blockedSubjects` relation matched.
|
|
41
42
|
*
|
|
@@ -272,6 +273,30 @@ export interface GateToolCallArgs {
|
|
|
272
273
|
* semantics; the new `interactive` kind means "do nothing else."
|
|
273
274
|
*/
|
|
274
275
|
export declare function gateToolCall(client: OryAgentClient, args: GateToolCallArgs): Promise<ToolGateOutcome>;
|
|
276
|
+
export interface GateMcpToolCallArgs {
|
|
277
|
+
harness: string;
|
|
278
|
+
/** The harness-side tool name, kept for activity and denial context. */
|
|
279
|
+
toolName: string;
|
|
280
|
+
mcpTool: McpToolIdentifier;
|
|
281
|
+
/** The user subject checked for `use`, built with `resolveUserSubject`. */
|
|
282
|
+
subject: Pick<PermissionCheck, "subjectId" | "subjectSet">;
|
|
283
|
+
activityAttributes?: Record<string, unknown>;
|
|
284
|
+
modeOverride?: PermissionMode;
|
|
285
|
+
principals?: PrincipalBlockSubjects;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Gate one MCP tool call against the Agent Security MCP namespaces.
|
|
289
|
+
*
|
|
290
|
+
* The call is checked twice with the native tool model: `McpServer:<server>`
|
|
291
|
+
* (No access blocks every tool on the server, cataloged or not) and
|
|
292
|
+
* `McpTool:<server>:<tool>` (Read and Write levels block tool tiers). Each check
|
|
293
|
+
* covers the user's `use` permit and every machine-principal block, so an MCP
|
|
294
|
+
* level set on the Project, an Identity, an Agent, or a Sub-Agent applies here.
|
|
295
|
+
* The server name is normalized to its lowercased form, matching the server
|
|
296
|
+
* catalog. The most restrictive outcome wins.
|
|
297
|
+
*/
|
|
298
|
+
export declare function gateMcpToolCall(client: OryAgentClient, args: GateMcpToolCallArgs): Promise<PermissionDecision>;
|
|
299
|
+
export declare function mostRestrictiveDecision(decisions: PermissionDecision[]): PermissionDecision;
|
|
275
300
|
/**
|
|
276
301
|
* Gate a shell tool by decomposing its command into the program/builtin words
|
|
277
302
|
* that will run and checking each as `ShellTool:<word>#use`, on top of the
|
package/dist/permissions.js
CHANGED
|
@@ -32,9 +32,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
32
32
|
exports.resolveCheckRelation = resolveCheckRelation;
|
|
33
33
|
exports.applyPermissionMode = applyPermissionMode;
|
|
34
34
|
exports.gateToolCall = gateToolCall;
|
|
35
|
+
exports.gateMcpToolCall = gateMcpToolCall;
|
|
36
|
+
exports.mostRestrictiveDecision = mostRestrictiveDecision;
|
|
35
37
|
exports.decomposeAndCheck = decomposeAndCheck;
|
|
36
38
|
exports.checkAndDecide = checkAndDecide;
|
|
37
39
|
const bash_parser_js_1 = require("./bash-parser.js");
|
|
40
|
+
const mcp_js_1 = require("./mcp.js");
|
|
38
41
|
const config_js_1 = require("./config.js");
|
|
39
42
|
const permission_mode_js_1 = require("./permission-mode.js");
|
|
40
43
|
const read_credential_js_1 = require("./read-credential.js");
|
|
@@ -284,6 +287,7 @@ async function batchNonShellGate(client, args) {
|
|
|
284
287
|
object: args.check.object,
|
|
285
288
|
relation: opl_js_1.RELATION_BLOCKED_SUBJECTS,
|
|
286
289
|
...ref,
|
|
290
|
+
...(args.check.mcp ? { mcp: args.check.mcp } : {}),
|
|
287
291
|
})),
|
|
288
292
|
];
|
|
289
293
|
try {
|
|
@@ -317,11 +321,59 @@ async function batchNonShellGate(client, args) {
|
|
|
317
321
|
const mode = args.modeOverride ?? (await (0, permission_mode_js_1.warmPermissionModeCache)(client, modeSubject, { additionalSubjects })).mode;
|
|
318
322
|
const [decision, blocked] = await Promise.all([
|
|
319
323
|
checkAndDecide(client, args.check, { activityAttributes: args.activityAttributes, modeOverride: mode }),
|
|
320
|
-
findBlockedPrincipal(client, {
|
|
324
|
+
findBlockedPrincipal(client, {
|
|
325
|
+
namespace: args.check.namespace,
|
|
326
|
+
object: args.check.object,
|
|
327
|
+
principals: args.principals,
|
|
328
|
+
mcp: args.check.mcp,
|
|
329
|
+
}),
|
|
321
330
|
]);
|
|
322
331
|
return applyBlockedDecision(client, args, mode, decision, blocked);
|
|
323
332
|
}
|
|
324
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Gate one MCP tool call against the Agent Security MCP namespaces.
|
|
336
|
+
*
|
|
337
|
+
* The call is checked twice with the native tool model: `McpServer:<server>`
|
|
338
|
+
* (No access blocks every tool on the server, cataloged or not) and
|
|
339
|
+
* `McpTool:<server>:<tool>` (Read and Write levels block tool tiers). Each check
|
|
340
|
+
* covers the user's `use` permit and every machine-principal block, so an MCP
|
|
341
|
+
* level set on the Project, an Identity, an Agent, or a Sub-Agent applies here.
|
|
342
|
+
* The server name is normalized to its lowercased form, matching the server
|
|
343
|
+
* catalog. The most restrictive outcome wins.
|
|
344
|
+
*/
|
|
345
|
+
async function gateMcpToolCall(client, args) {
|
|
346
|
+
await (0, read_credential_js_1.ensureReadCredential)(client, { harness: args.harness });
|
|
347
|
+
const serverName = (0, mcp_js_1.normalizeMcpServerName)(args.mcpTool.serverName);
|
|
348
|
+
const mcp = (0, mcp_js_1.mcpServerHint)(args.mcpTool);
|
|
349
|
+
// The tool is checked even when the harness reports no tool name: Agent
|
|
350
|
+
// Security checks an unnamed tool as Admin, so Read and Write still deny it.
|
|
351
|
+
// A resource read has no tool and is checked on the server alone.
|
|
352
|
+
const objects = [
|
|
353
|
+
{ namespace: opl_js_1.MCP_SERVER_NAMESPACE, object: serverName },
|
|
354
|
+
...(args.mcpTool.resource
|
|
355
|
+
? []
|
|
356
|
+
: [{ namespace: opl_js_1.MCP_TOOL_NAMESPACE, object: (0, mcp_js_1.mcpToolObject)(serverName, args.mcpTool.toolName ?? "") }]),
|
|
357
|
+
];
|
|
358
|
+
const decisions = await Promise.all(objects.map(({ namespace, object }) => batchNonShellGate(client, {
|
|
359
|
+
harness: args.harness,
|
|
360
|
+
toolName: args.toolName,
|
|
361
|
+
check: { namespace, object, relation: opl_js_1.PERMIT_USE, ...args.subject, mcp },
|
|
362
|
+
activityAttributes: args.activityAttributes,
|
|
363
|
+
modeOverride: args.modeOverride,
|
|
364
|
+
principals: args.principals,
|
|
365
|
+
})));
|
|
366
|
+
return mostRestrictiveDecision(decisions);
|
|
367
|
+
}
|
|
368
|
+
const DECISION_SEVERITY = {
|
|
369
|
+
allow: 0,
|
|
370
|
+
fail_open: 1,
|
|
371
|
+
observe: 2,
|
|
372
|
+
deny: 3,
|
|
373
|
+
};
|
|
374
|
+
function mostRestrictiveDecision(decisions) {
|
|
375
|
+
return decisions.reduce((current, next) => DECISION_SEVERITY[next.kind] > DECISION_SEVERITY[current.kind] ? next : current);
|
|
376
|
+
}
|
|
325
377
|
function applyBlockedDecision(client, args, mode, decision, blocked) {
|
|
326
378
|
if (!blocked)
|
|
327
379
|
return decision;
|
|
@@ -812,6 +864,7 @@ async function findBlockedPrincipal(client, args) {
|
|
|
812
864
|
namespace: args.namespace,
|
|
813
865
|
objects: [args.object],
|
|
814
866
|
principals: args.principals,
|
|
867
|
+
mcp: args.mcp,
|
|
815
868
|
})).get(args.object);
|
|
816
869
|
}
|
|
817
870
|
async function findBlockedPrincipals(client, args) {
|
|
@@ -843,6 +896,7 @@ async function findBlockedPrincipals(client, args) {
|
|
|
843
896
|
object,
|
|
844
897
|
relation: opl_js_1.RELATION_BLOCKED_SUBJECTS,
|
|
845
898
|
...ref,
|
|
899
|
+
...(args.mcp ? { mcp: args.mcp } : {}),
|
|
846
900
|
}))), { activityAttributes: { source: "principal_block", scope } });
|
|
847
901
|
for (const [i, result] of batch.results.entries()) {
|
|
848
902
|
const object = args.objects[Math.floor(i / candidates.length)];
|
package/dist/post-install.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* plugin logs activity locally and gates nothing. Nothing else changes either way.
|
|
17
17
|
*
|
|
18
18
|
* Everything *else* an Ory project needs to serve Agent Security — the login
|
|
19
|
-
* OAuth2 client, the permission model, native-tool blocks, MCP
|
|
19
|
+
* OAuth2 client, the permission model, native-tool blocks, MCP server access,
|
|
20
20
|
* and the observe/enforce posture — is provisioned in the **Ory Console**
|
|
21
21
|
* (Agent Security), by someone with access to the project. The plugin never
|
|
22
22
|
* writes project configuration, which is why installing it needs only a
|
package/dist/post-install.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* plugin logs activity locally and gates nothing. Nothing else changes either way.
|
|
18
18
|
*
|
|
19
19
|
* Everything *else* an Ory project needs to serve Agent Security — the login
|
|
20
|
-
* OAuth2 client, the permission model, native-tool blocks, MCP
|
|
20
|
+
* OAuth2 client, the permission model, native-tool blocks, MCP server access,
|
|
21
21
|
* and the observe/enforce posture — is provisioned in the **Ory Console**
|
|
22
22
|
* (Agent Security), by someone with access to the project. The plugin never
|
|
23
23
|
* writes project configuration, which is why installing it needs only a
|
|
@@ -112,7 +112,7 @@ async function runPostInstall(binName, _harness, args = []) {
|
|
|
112
112
|
if (security.connected) {
|
|
113
113
|
ui.success("Ory Agent Security is connected.");
|
|
114
114
|
ui.hint("Sign-in and permission checks run on the next session.");
|
|
115
|
-
ui.hint("Native-tool blocks, MCP
|
|
115
|
+
ui.hint("Native-tool blocks, MCP server access, and the observe/enforce posture are managed in the Ory Console (Agent Security).");
|
|
116
116
|
ui.hint(`Inspect what this machine resolves with \`${binName} permissions status\`.`);
|
|
117
117
|
}
|
|
118
118
|
else {
|