@ory/argus 1.5.3 → 1.7.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/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
- * and checkMcpPermission runs server-level (and optionally tool-level)
9
- * Ory permission checks.
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
- * Gemini provides server info via the `mcp_context` field.
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 server-level (and optionally tool-level) permissions.
301
+ * Check the user's MCP `use` permits without machine-principal blocks or mode.
70
302
  *
71
- * Server check: namespace=mcp_servers, object=<serverName>, relation=use
72
- * Tool check: namespace=mcp_tools, object=<server>:<tool>, relation=invoke
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
- * The subject is whatever the caller resolved (SubjectSet or direct ID).
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
- (process.env.ORY_MCP_SERVER_NAMESPACE ?? "mcp_servers");
80
- const toolNamespace = options.toolNamespace ??
81
- (process.env.ORY_MCP_TOOL_NAMESPACE ?? "mcp_tools");
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: mcpTool.serverName,
91
- relation: "users",
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 || !mcpTool.toolName) {
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: `${mcpTool.serverName}:${mcpTool.toolName}`,
114
- relation: "invoke",
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
- * both AgentTool and ShellTool; MCP namespaces remain grant-based elsewhere.
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
- * both AgentTool and ShellTool; MCP namespaces remain grant-based elsewhere.
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
  "",
@@ -39,7 +39,7 @@
39
39
  * 3. **Persistent cache** — when the server is unreachable, fall back to the
40
40
  * last value read from the server, persisted in `config.json`
41
41
  * (`source: "cache"`).
42
- * 4. **`observe`** — nothing was ever cached (`source: "default"`).
42
+ * 4. **`enforce`** — a failed read and no applicable cache (`source: "default"`).
43
43
  *
44
44
  * The persistent cache is written by {@link warmPermissionModeCache} at session
45
45
  * start and when a gate first discovers a new acting-machine scope. It only
@@ -76,7 +76,7 @@ export declare function preparePermissionModeBatch(client: OryAgentClient, subje
76
76
  }): PermissionModeBatchPlan;
77
77
  /**
78
78
  * Resolve the current permission mode for `subject`. Never throws — a Keto
79
- * error falls back to the persistent cache, then `observe`. See the module
79
+ * error falls back to the persistent cache, then `enforce`. See the module
80
80
  * doc for the precedence. Does **not** persist (that is
81
81
  * {@link warmPermissionModeCache}'s job); `opts.now` is injectable for tests.
82
82
  */
@@ -40,7 +40,7 @@
40
40
  * 3. **Persistent cache** — when the server is unreachable, fall back to the
41
41
  * last value read from the server, persisted in `config.json`
42
42
  * (`source: "cache"`).
43
- * 4. **`observe`** — nothing was ever cached (`source: "default"`).
43
+ * 4. **`enforce`** — a failed read and no applicable cache (`source: "default"`).
44
44
  *
45
45
  * The persistent cache is written by {@link warmPermissionModeCache} at session
46
46
  * start and when a gate first discovers a new acting-machine scope. It only
@@ -225,7 +225,7 @@ function fallbackPermissionMode(projectUrl, subjectKeys) {
225
225
  ? cache.mode
226
226
  : undefined;
227
227
  const mode = scopedMode ?? (legacyMode === "enforce" ? "enforce" : undefined);
228
- return mode ? { mode, source: "cache" } : { mode: "observe", source: "default" };
228
+ return mode ? { mode, source: "cache" } : { mode: "enforce", source: "default" };
229
229
  }
230
230
  function persistPermissionModeMemo(client, projectUrl, now, requestedKeys) {
231
231
  const updates = {};
@@ -259,7 +259,7 @@ function persistPermissionModeMemo(client, projectUrl, now, requestedKeys) {
259
259
  }
260
260
  /**
261
261
  * Resolve the current permission mode for `subject`. Never throws — a Keto
262
- * error falls back to the persistent cache, then `observe`. See the module
262
+ * error falls back to the persistent cache, then `enforce`. See the module
263
263
  * doc for the precedence. Does **not** persist (that is
264
264
  * {@link warmPermissionModeCache}'s job); `opts.now` is injectable for tests.
265
265
  */
@@ -378,9 +378,9 @@ async function resolvePermissionMode(client, subject, opts = {}) {
378
378
  }
379
379
  client.logger.debug("permission.mode_default", {
380
380
  code: oryErr?.code,
381
- note: "mode check failed and no cached value; defaulting to observe",
381
+ note: "mode check failed and no cached value; defaulting to enforce",
382
382
  });
383
- return { mode: "observe", source: "default" };
383
+ return { mode: "enforce", source: "default" };
384
384
  }
385
385
  }
386
386
  /**
@@ -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 grants, and the observe/enforce posture. That is what lets a
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.
@@ -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 grants, and the observe/enforce posture. That is what lets a
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 grants,");
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 grants");
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:");
@@ -13,9 +13,8 @@
13
13
  * tool through.
14
14
  * - `enforce` — return `{ kind: "deny", … }` so the caller blocks.
15
15
  * 3. On a thrown {@link OryError}, inspects the classified code:
16
- * - Infrastructure errors (`network_error`, `rate_limited`,
17
- * `not_found`, `unknown`) — returns `{ kind: "fail_open", … }`.
18
- * Callers should log and allow.
16
+ * - Network, rate-limit and server failures deny in enforce mode;
17
+ * observe mode remains permissive.
19
18
  * - Auth-rejected check calls (`forbidden`, `session_inactive`,
20
19
  * `session_aal2_required` — the check itself was rejected, so
21
20
  * its result cannot be trusted) — in `enforce` mode returns
@@ -27,6 +26,7 @@
27
26
  * opencode `throw OryDenialError`, etc.) without re-implementing the
28
27
  * mode logic.
29
28
  */
29
+ import { type McpToolIdentifier } from "./mcp.js";
30
30
  import type { OryAgentClient } from "./client.js";
31
31
  import { type PermissionMode } from "./config.js";
32
32
  import type { OryError, OryErrorCode, PermissionCheck, PermissionResult } from "./types.js";
@@ -34,14 +34,14 @@ import type { OryError, OryErrorCode, PermissionCheck, PermissionResult } from "
34
34
  * Why a permission check came back denied, once the block-aware OPL schema is
35
35
  * in play:
36
36
  *
37
- * - `not_granted` — compatibility for grant-based checks outside native
38
- * AgentTool/ShellTool policy (notably MCP).
37
+ * - `not_granted` — compatibility for grant-based checks outside the native
38
+ * AgentTool, ShellTool, McpServer, and McpTool policy.
39
39
  * - `explicit_block` — a native `use` permit denied because a
40
40
  * `blockedSubjects` relation matched.
41
41
  *
42
42
  * Threaded onto the decision's activity attributes and into the observe-mode
43
43
  * audit event so "explicitly blocked" is distinguishable from "never granted"
44
- * in activity logs, mirroring how `checkRejected` distinguishes an auth-rejected
44
+ * in activity logs, mirroring how `checkRejected` distinguishes a failed
45
45
  * deny.
46
46
  */
47
47
  export type BlockReason = "not_granted" | "explicit_block";
@@ -65,7 +65,7 @@ export interface DecisionActivityAttributes {
65
65
  /** SubjectSet rendered as `<namespace>:<object>#<relation>` when used. */
66
66
  subjectSet?: string;
67
67
  /**
68
- * Set when the check call itself was auth-rejected and enforce mode
68
+ * Set when the check call itself failed and enforce mode
69
69
  * turned that into a deny — the classified {@link OryErrorCode} that
70
70
  * caused it. Makes the "denied because the check failed" case
71
71
  * distinguishable from a normal Keto deny in the audit trail.
@@ -272,6 +272,30 @@ export interface GateToolCallArgs {
272
272
  * semantics; the new `interactive` kind means "do nothing else."
273
273
  */
274
274
  export declare function gateToolCall(client: OryAgentClient, args: GateToolCallArgs): Promise<ToolGateOutcome>;
275
+ export interface GateMcpToolCallArgs {
276
+ harness: string;
277
+ /** The harness-side tool name, kept for activity and denial context. */
278
+ toolName: string;
279
+ mcpTool: McpToolIdentifier;
280
+ /** The user subject checked for `use`, built with `resolveUserSubject`. */
281
+ subject: Pick<PermissionCheck, "subjectId" | "subjectSet">;
282
+ activityAttributes?: Record<string, unknown>;
283
+ modeOverride?: PermissionMode;
284
+ principals?: PrincipalBlockSubjects;
285
+ }
286
+ /**
287
+ * Gate one MCP tool call against the Agent Security MCP namespaces.
288
+ *
289
+ * The call is checked twice with the native tool model: `McpServer:<server>`
290
+ * (No access blocks every tool on the server, cataloged or not) and
291
+ * `McpTool:<server>:<tool>` (Read and Write levels block tool tiers). Each check
292
+ * covers the user's `use` permit and every machine-principal block, so an MCP
293
+ * level set on the Project, an Identity, an Agent, or a Sub-Agent applies here.
294
+ * The server name is normalized to its lowercased form, matching the server
295
+ * catalog. The most restrictive outcome wins.
296
+ */
297
+ export declare function gateMcpToolCall(client: OryAgentClient, args: GateMcpToolCallArgs): Promise<PermissionDecision>;
298
+ export declare function mostRestrictiveDecision(decisions: PermissionDecision[]): PermissionDecision;
275
299
  /**
276
300
  * Gate a shell tool by decomposing its command into the program/builtin words
277
301
  * that will run and checking each as `ShellTool:<word>#use`, on top of the
@@ -280,16 +304,16 @@ export declare function gateToolCall(client: OryAgentClient, args: GateToolCallA
280
304
  * Aggregation: **any deny → deny** (naming the offending word); else any observe
281
305
  * → observe (pass through, per-word `permission.observe_deny` events recorded);
282
306
  * else allow. A too-complex / unparseable command is **fail-closed** in enforce
283
- * (deny + alert) and logged-then-passed in observe. A parser-unavailable or
284
- * infrastructure error is **fail-open** (top-level decision only) — availability
285
- * must not hinge on the parser.
307
+ * (deny + alert) and logged-then-passed in observe. A parser-unavailable
308
+ * condition falls back to the top-level decision. Network, rate-limit, and
309
+ * server failures in word checks deny in enforce mode.
286
310
  */
287
311
  export declare function decomposeAndCheck(client: OryAgentClient, args: GateToolCallArgs): Promise<PermissionDecision>;
288
312
  /**
289
313
  * Run a permission check and resolve the configured mode against the
290
314
  * result. Never throws — infrastructure errors surface as a typed
291
- * `{ kind: "fail_open" }` decision, and auth-rejected check calls in
292
- * enforce mode as `{ kind: "deny" }` (see
315
+ * `{ kind: "fail_open" }` decision in observe mode, and untrusted or unavailable
316
+ * check calls in enforce mode as `{ kind: "deny" }` (see
293
317
  * {@link CHECK_UNTRUSTED_CODES}).
294
318
  */
295
319
  export declare function checkAndDecide(client: OryAgentClient, check: PermissionCheck, opts?: CheckAndDecideOptions): Promise<PermissionDecision>;