@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/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 +21 -0
- package/dist/contract-suite.js +2 -2
- package/dist/denial.d.ts +3 -1
- package/dist/denial.js +6 -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/permission-mode.d.ts +2 -2
- package/dist/permission-mode.js +5 -5
- package/dist/permissions-cli.d.ts +1 -1
- package/dist/permissions-cli.js +3 -3
- package/dist/permissions.d.ts +36 -12
- package/dist/permissions.js +105 -19
- 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 +14 -2
- 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
|
"",
|
|
@@ -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. **`
|
|
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 `
|
|
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
|
*/
|
package/dist/permission-mode.js
CHANGED
|
@@ -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. **`
|
|
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: "
|
|
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 `
|
|
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
|
|
381
|
+
note: "mode check failed and no cached value; defaulting to enforce",
|
|
382
382
|
});
|
|
383
|
-
return { mode: "
|
|
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
|
|
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
|
@@ -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
|
-
* -
|
|
17
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
284
|
-
*
|
|
285
|
-
*
|
|
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
|
|
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>;
|