@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/permissions.js
CHANGED
|
@@ -14,9 +14,8 @@
|
|
|
14
14
|
* tool through.
|
|
15
15
|
* - `enforce` — return `{ kind: "deny", … }` so the caller blocks.
|
|
16
16
|
* 3. On a thrown {@link OryError}, inspects the classified code:
|
|
17
|
-
* -
|
|
18
|
-
*
|
|
19
|
-
* Callers should log and allow.
|
|
17
|
+
* - Network, rate-limit and server failures deny in enforce mode;
|
|
18
|
+
* observe mode remains permissive.
|
|
20
19
|
* - Auth-rejected check calls (`forbidden`, `session_inactive`,
|
|
21
20
|
* `session_aal2_required` — the check itself was rejected, so
|
|
22
21
|
* its result cannot be trusted) — in `enforce` mode returns
|
|
@@ -32,9 +31,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
32
31
|
exports.resolveCheckRelation = resolveCheckRelation;
|
|
33
32
|
exports.applyPermissionMode = applyPermissionMode;
|
|
34
33
|
exports.gateToolCall = gateToolCall;
|
|
34
|
+
exports.gateMcpToolCall = gateMcpToolCall;
|
|
35
|
+
exports.mostRestrictiveDecision = mostRestrictiveDecision;
|
|
35
36
|
exports.decomposeAndCheck = decomposeAndCheck;
|
|
36
37
|
exports.checkAndDecide = checkAndDecide;
|
|
37
38
|
const bash_parser_js_1 = require("./bash-parser.js");
|
|
39
|
+
const mcp_js_1 = require("./mcp.js");
|
|
38
40
|
const config_js_1 = require("./config.js");
|
|
39
41
|
const permission_mode_js_1 = require("./permission-mode.js");
|
|
40
42
|
const read_credential_js_1 = require("./read-credential.js");
|
|
@@ -54,8 +56,7 @@ function resolveCheckRelation(relation) {
|
|
|
54
56
|
* authentication/authorization reasons — an expired or misconfigured
|
|
55
57
|
* agent credential, an inactive session, or a missing MFA step. The
|
|
56
58
|
* check result cannot be trusted, so `enforce` mode denies instead of
|
|
57
|
-
* failing open.
|
|
58
|
-
* `not_found`, `unknown`) keep the fail-open posture.
|
|
59
|
+
* failing open. Network and rate-limit failures are likewise untrusted.
|
|
59
60
|
*/
|
|
60
61
|
const CHECK_UNTRUSTED_CODES = new Set([
|
|
61
62
|
// The check reached Ory and was refused — an expired or misconfigured agent
|
|
@@ -69,7 +70,27 @@ const CHECK_UNTRUSTED_CODES = new Set([
|
|
|
69
70
|
// cert problem silently downgraded an `enforce` project to `observe`, and
|
|
70
71
|
// breaking TLS was sufficient to disable all tool gating (#218).
|
|
71
72
|
"tls_error",
|
|
73
|
+
"network_error",
|
|
74
|
+
"rate_limited",
|
|
72
75
|
]);
|
|
76
|
+
function isUntrustedCheckError(error) {
|
|
77
|
+
return !!error && (CHECK_UNTRUSTED_CODES.has(error.code) || (error.status !== undefined && error.status >= 500));
|
|
78
|
+
}
|
|
79
|
+
function isUnavailableCheckError(error) {
|
|
80
|
+
return error.code === "network_error" || error.code === "rate_limited" || (error.status !== undefined && error.status >= 500);
|
|
81
|
+
}
|
|
82
|
+
function rejectedCheckDecision(check, error, mode) {
|
|
83
|
+
const attrs = buildDecisionAttributes(mode, check.subjectId, check.subjectSet);
|
|
84
|
+
if (mode === "enforce") {
|
|
85
|
+
return {
|
|
86
|
+
kind: "deny",
|
|
87
|
+
result: { allowed: false, checkedAt: new Date().toISOString(), check },
|
|
88
|
+
mode: "enforce",
|
|
89
|
+
activityAttributes: { ...attrs, checkRejected: error.code },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { kind: "fail_open", error, mode, activityAttributes: attrs };
|
|
93
|
+
}
|
|
73
94
|
/**
|
|
74
95
|
* Max distinct command words checked per shell command (issue #76). A command
|
|
75
96
|
* with more than this is pathological (no legitimate command runs 100+ distinct
|
|
@@ -248,10 +269,13 @@ async function gateToolCall(client, args) {
|
|
|
248
269
|
namespace: args.check.namespace,
|
|
249
270
|
object: args.check.object,
|
|
250
271
|
principals: args.principals,
|
|
251
|
-
|
|
272
|
+
strict: mode === "enforce",
|
|
273
|
+
}).catch((error) => ({ error })),
|
|
252
274
|
]);
|
|
253
275
|
if (!blocked)
|
|
254
276
|
return decision;
|
|
277
|
+
if ("error" in blocked)
|
|
278
|
+
return rejectedCheckDecision(args.check, blocked.error, mode);
|
|
255
279
|
// Already denied on the user's own check — nothing to add to the outcome, but
|
|
256
280
|
// the block is worth recording as the stronger reason it would not have run.
|
|
257
281
|
if (decision.kind === "deny") {
|
|
@@ -284,6 +308,7 @@ async function batchNonShellGate(client, args) {
|
|
|
284
308
|
object: args.check.object,
|
|
285
309
|
relation: opl_js_1.RELATION_BLOCKED_SUBJECTS,
|
|
286
310
|
...ref,
|
|
311
|
+
...(args.check.mcp ? { mcp: args.check.mcp } : {}),
|
|
287
312
|
})),
|
|
288
313
|
];
|
|
289
314
|
try {
|
|
@@ -313,15 +338,70 @@ async function batchNonShellGate(client, args) {
|
|
|
313
338
|
}
|
|
314
339
|
return applyBlockedDecision(client, args, mode, decision, blocked);
|
|
315
340
|
}
|
|
316
|
-
catch {
|
|
341
|
+
catch (err) {
|
|
317
342
|
const mode = args.modeOverride ?? (await (0, permission_mode_js_1.warmPermissionModeCache)(client, modeSubject, { additionalSubjects })).mode;
|
|
343
|
+
const error = err?.status !== undefined || err?.code === "network_error" || err?.code === "rate_limited"
|
|
344
|
+
? err : client.classifyError(err);
|
|
345
|
+
if (isUnavailableCheckError(error))
|
|
346
|
+
return rejectedCheckDecision(args.check, error, mode);
|
|
318
347
|
const [decision, blocked] = await Promise.all([
|
|
319
348
|
checkAndDecide(client, args.check, { activityAttributes: args.activityAttributes, modeOverride: mode }),
|
|
320
|
-
findBlockedPrincipal(client, {
|
|
349
|
+
findBlockedPrincipal(client, {
|
|
350
|
+
namespace: args.check.namespace,
|
|
351
|
+
object: args.check.object,
|
|
352
|
+
principals: args.principals,
|
|
353
|
+
mcp: args.check.mcp,
|
|
354
|
+
strict: mode === "enforce",
|
|
355
|
+
}).catch((error) => ({ error })),
|
|
321
356
|
]);
|
|
357
|
+
if (blocked && "error" in blocked)
|
|
358
|
+
return rejectedCheckDecision(args.check, blocked.error, mode);
|
|
322
359
|
return applyBlockedDecision(client, args, mode, decision, blocked);
|
|
323
360
|
}
|
|
324
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Gate one MCP tool call against the Agent Security MCP namespaces.
|
|
364
|
+
*
|
|
365
|
+
* The call is checked twice with the native tool model: `McpServer:<server>`
|
|
366
|
+
* (No access blocks every tool on the server, cataloged or not) and
|
|
367
|
+
* `McpTool:<server>:<tool>` (Read and Write levels block tool tiers). Each check
|
|
368
|
+
* covers the user's `use` permit and every machine-principal block, so an MCP
|
|
369
|
+
* level set on the Project, an Identity, an Agent, or a Sub-Agent applies here.
|
|
370
|
+
* The server name is normalized to its lowercased form, matching the server
|
|
371
|
+
* catalog. The most restrictive outcome wins.
|
|
372
|
+
*/
|
|
373
|
+
async function gateMcpToolCall(client, args) {
|
|
374
|
+
await (0, read_credential_js_1.ensureReadCredential)(client, { harness: args.harness });
|
|
375
|
+
const serverName = (0, mcp_js_1.normalizeMcpServerName)(args.mcpTool.serverName);
|
|
376
|
+
const mcp = (0, mcp_js_1.mcpServerHint)(args.mcpTool);
|
|
377
|
+
// The tool is checked even when the harness reports no tool name: Agent
|
|
378
|
+
// Security checks an unnamed tool as Admin, so Read and Write still deny it.
|
|
379
|
+
// A resource read has no tool and is checked on the server alone.
|
|
380
|
+
const objects = [
|
|
381
|
+
{ namespace: opl_js_1.MCP_SERVER_NAMESPACE, object: serverName },
|
|
382
|
+
...(args.mcpTool.resource
|
|
383
|
+
? []
|
|
384
|
+
: [{ namespace: opl_js_1.MCP_TOOL_NAMESPACE, object: (0, mcp_js_1.mcpToolObject)(serverName, args.mcpTool.toolName ?? "") }]),
|
|
385
|
+
];
|
|
386
|
+
const decisions = await Promise.all(objects.map(({ namespace, object }) => batchNonShellGate(client, {
|
|
387
|
+
harness: args.harness,
|
|
388
|
+
toolName: args.toolName,
|
|
389
|
+
check: { namespace, object, relation: opl_js_1.PERMIT_USE, ...args.subject, mcp },
|
|
390
|
+
activityAttributes: args.activityAttributes,
|
|
391
|
+
modeOverride: args.modeOverride,
|
|
392
|
+
principals: args.principals,
|
|
393
|
+
})));
|
|
394
|
+
return mostRestrictiveDecision(decisions);
|
|
395
|
+
}
|
|
396
|
+
const DECISION_SEVERITY = {
|
|
397
|
+
allow: 0,
|
|
398
|
+
fail_open: 1,
|
|
399
|
+
observe: 2,
|
|
400
|
+
deny: 3,
|
|
401
|
+
};
|
|
402
|
+
function mostRestrictiveDecision(decisions) {
|
|
403
|
+
return decisions.reduce((current, next) => DECISION_SEVERITY[next.kind] > DECISION_SEVERITY[current.kind] ? next : current);
|
|
404
|
+
}
|
|
325
405
|
function applyBlockedDecision(client, args, mode, decision, blocked) {
|
|
326
406
|
if (!blocked)
|
|
327
407
|
return decision;
|
|
@@ -389,9 +469,9 @@ function applyPrincipalBlock(client, ctx) {
|
|
|
389
469
|
* Aggregation: **any deny → deny** (naming the offending word); else any observe
|
|
390
470
|
* → observe (pass through, per-word `permission.observe_deny` events recorded);
|
|
391
471
|
* else allow. A too-complex / unparseable command is **fail-closed** in enforce
|
|
392
|
-
* (deny + alert) and logged-then-passed in observe. A parser-unavailable
|
|
393
|
-
*
|
|
394
|
-
*
|
|
472
|
+
* (deny + alert) and logged-then-passed in observe. A parser-unavailable
|
|
473
|
+
* condition falls back to the top-level decision. Network, rate-limit, and
|
|
474
|
+
* server failures in word checks deny in enforce mode.
|
|
395
475
|
*/
|
|
396
476
|
async function decomposeAndCheck(client, args) {
|
|
397
477
|
// The word checks do not depend on the top-level result until aggregation, so
|
|
@@ -411,7 +491,7 @@ async function decomposeAndCheck(client, args) {
|
|
|
411
491
|
|| parsed.words.length > MAX_SHELL_WORDS
|
|
412
492
|
|| (parsed.tooComplex && expectedMode === "enforce")
|
|
413
493
|
? undefined
|
|
414
|
-
: fetchShellWordResults(client, args, namespace, parsed.words, subject);
|
|
494
|
+
: fetchShellWordResults(client, { ...args, modeOverride: expectedMode }, namespace, parsed.words, subject);
|
|
415
495
|
const [top, wordFetch] = await Promise.all([topPromise, wordsPromise]);
|
|
416
496
|
// Top-level Bash denied (enforce) or unreachable → no point decomposing.
|
|
417
497
|
if (top.kind === "deny" || top.kind === "fail_open")
|
|
@@ -486,10 +566,10 @@ async function decomposeAndCheck(client, args) {
|
|
|
486
566
|
let sawObserve = false;
|
|
487
567
|
let matchedBlocks = new Map();
|
|
488
568
|
if (words.length > 0) {
|
|
489
|
-
const fetched = wordFetch ?? await fetchShellWordResults(client, args, namespace, words, subject);
|
|
569
|
+
const fetched = wordFetch ?? await fetchShellWordResults(client, { ...args, modeOverride: mode }, namespace, words, subject);
|
|
490
570
|
if (!fetched.ok) {
|
|
491
571
|
const oryErr = fetched.error;
|
|
492
|
-
if (mode === "enforce" &&
|
|
572
|
+
if (mode === "enforce" && isUntrustedCheckError(oryErr)) {
|
|
493
573
|
return {
|
|
494
574
|
kind: "deny",
|
|
495
575
|
result: { allowed: false, checkedAt: new Date().toISOString(), check: args.check },
|
|
@@ -670,6 +750,7 @@ async function fetchShellWordResults(client, args, namespace, words, subject) {
|
|
|
670
750
|
namespace,
|
|
671
751
|
objects: chunk,
|
|
672
752
|
principals: args.principals,
|
|
753
|
+
strict: args.modeOverride === "enforce",
|
|
673
754
|
}),
|
|
674
755
|
]);
|
|
675
756
|
return { ok: true, results: batch.results, blocked };
|
|
@@ -694,8 +775,8 @@ async function fetchShellWordResults(client, args, namespace, words, subject) {
|
|
|
694
775
|
/**
|
|
695
776
|
* Run a permission check and resolve the configured mode against the
|
|
696
777
|
* result. Never throws — infrastructure errors surface as a typed
|
|
697
|
-
* `{ kind: "fail_open" }` decision, and
|
|
698
|
-
* enforce mode as `{ kind: "deny" }` (see
|
|
778
|
+
* `{ kind: "fail_open" }` decision in observe mode, and untrusted or unavailable
|
|
779
|
+
* check calls in enforce mode as `{ kind: "deny" }` (see
|
|
699
780
|
* {@link CHECK_UNTRUSTED_CODES}).
|
|
700
781
|
*/
|
|
701
782
|
async function checkAndDecide(client, check, opts = {}) {
|
|
@@ -716,9 +797,8 @@ async function checkAndDecide(client, check, opts = {}) {
|
|
|
716
797
|
}
|
|
717
798
|
catch (err) {
|
|
718
799
|
const oryErr = err;
|
|
719
|
-
if (mode === "enforce" &&
|
|
720
|
-
// The
|
|
721
|
-
// result cannot be trusted, so enforce mode must not fail open.
|
|
800
|
+
if (mode === "enforce" && isUntrustedCheckError(oryErr)) {
|
|
801
|
+
// The result cannot be trusted, so enforce mode must not fail open.
|
|
722
802
|
client.logger.warn("permission.check_rejected", {
|
|
723
803
|
namespace: check.namespace,
|
|
724
804
|
object: check.object,
|
|
@@ -812,6 +892,8 @@ async function findBlockedPrincipal(client, args) {
|
|
|
812
892
|
namespace: args.namespace,
|
|
813
893
|
objects: [args.object],
|
|
814
894
|
principals: args.principals,
|
|
895
|
+
mcp: args.mcp,
|
|
896
|
+
strict: args.strict,
|
|
815
897
|
})).get(args.object);
|
|
816
898
|
}
|
|
817
899
|
async function findBlockedPrincipals(client, args) {
|
|
@@ -843,6 +925,7 @@ async function findBlockedPrincipals(client, args) {
|
|
|
843
925
|
object,
|
|
844
926
|
relation: opl_js_1.RELATION_BLOCKED_SUBJECTS,
|
|
845
927
|
...ref,
|
|
928
|
+
...(args.mcp ? { mcp: args.mcp } : {}),
|
|
846
929
|
}))), { activityAttributes: { source: "principal_block", scope } });
|
|
847
930
|
for (const [i, result] of batch.results.entries()) {
|
|
848
931
|
const object = args.objects[Math.floor(i / candidates.length)];
|
|
@@ -856,6 +939,9 @@ async function findBlockedPrincipals(client, args) {
|
|
|
856
939
|
}
|
|
857
940
|
}
|
|
858
941
|
catch (err) {
|
|
942
|
+
const classified = err?.code ? err : client.classifyError(err);
|
|
943
|
+
if (args.strict && isUntrustedCheckError(classified))
|
|
944
|
+
throw classified;
|
|
859
945
|
client.logger.debug("permission.principal_block.failed", {
|
|
860
946
|
scope,
|
|
861
947
|
note: `${scope} principal block read failed; falling open for that scope`,
|
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 {
|
package/dist/testing.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* common assertion patterns so that harness plugin tests focus on
|
|
6
6
|
* harness-specific behavior rather than duplicating boilerplate.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
8
|
+
import { type Mock } from "vitest";
|
|
9
9
|
import { OryAgentClient } from "./client.js";
|
|
10
10
|
import type { ActivityEntry } from "./logger.js";
|
|
11
11
|
import type { PermissionMode } from "./config.js";
|
|
@@ -13,8 +13,8 @@ export { runHarnessContractSuite, type HarnessContractAdapter, type ContractCont
|
|
|
13
13
|
export interface SdkLazySessionContractAdapter {
|
|
14
14
|
name: string;
|
|
15
15
|
harness: string;
|
|
16
|
-
createSessionStarterMock:
|
|
17
|
-
ensureSessionMock:
|
|
16
|
+
createSessionStarterMock: Mock;
|
|
17
|
+
ensureSessionMock: Mock;
|
|
18
18
|
createSubject(client: OryAgentClient, projectUrl?: string): {
|
|
19
19
|
invoke(index: number): Promise<unknown>;
|
|
20
20
|
};
|
|
@@ -40,7 +40,7 @@ export declare function createMockClient(overrides?: Partial<ConstructorParamete
|
|
|
40
40
|
export declare function spyRecordDelegation(client: OryAgentClient, result?: {
|
|
41
41
|
nodeId: string;
|
|
42
42
|
delegationChain: string[];
|
|
43
|
-
}):
|
|
43
|
+
}): Mock<(input: import("./client.js").RecordDelegationInput, options?: {
|
|
44
44
|
fetchImpl?: typeof fetch;
|
|
45
45
|
activityAttributes?: Record<string, unknown>;
|
|
46
46
|
agentToken?: string;
|
|
@@ -52,7 +52,7 @@ export declare function spyRecordDelegation(client: OryAgentClient, result?: {
|
|
|
52
52
|
* Stub an internal API instance method on the client.
|
|
53
53
|
* Returns a vi.fn mock so callers can assert on calls.
|
|
54
54
|
*/
|
|
55
|
-
export declare function stubApi<K extends "frontend" | "oauth2" | "permission">(client: OryAgentClient, api: K, method: string, impl: (...args: unknown[]) => unknown):
|
|
55
|
+
export declare function stubApi<K extends "frontend" | "oauth2" | "permission">(client: OryAgentClient, api: K, method: string, impl: (...args: unknown[]) => unknown): Mock;
|
|
56
56
|
/** Successful session verification response (wraps Ory API shape). */
|
|
57
57
|
export declare const MOCK_SESSION_RESPONSE: {
|
|
58
58
|
data: {
|
|
@@ -76,7 +76,6 @@ export declare const MOCK_SESSION_RESPONSE: {
|
|
|
76
76
|
/** Inactive session (same shape, active: false). */
|
|
77
77
|
export declare const MOCK_INACTIVE_SESSION_RESPONSE: {
|
|
78
78
|
data: {
|
|
79
|
-
active: boolean;
|
|
80
79
|
id: string;
|
|
81
80
|
authenticated_at: string;
|
|
82
81
|
expires_at: string;
|
|
@@ -91,6 +90,7 @@ export declare const MOCK_INACTIVE_SESSION_RESPONSE: {
|
|
|
91
90
|
email: string;
|
|
92
91
|
};
|
|
93
92
|
};
|
|
93
|
+
active: boolean;
|
|
94
94
|
};
|
|
95
95
|
};
|
|
96
96
|
/** Successful OAuth2 token introspection response. */
|
|
@@ -238,19 +238,19 @@ export declare function disconnectSecurity(which?: "neither" | "no-client-id" |
|
|
|
238
238
|
*/
|
|
239
239
|
export declare function clearOryEnv(): void;
|
|
240
240
|
/** Stub verifySession to succeed. */
|
|
241
|
-
export declare function stubSessionSuccess(client: OryAgentClient):
|
|
241
|
+
export declare function stubSessionSuccess(client: OryAgentClient): Mock;
|
|
242
242
|
/** Stub verifySession to return inactive. */
|
|
243
|
-
export declare function stubSessionInactive(client: OryAgentClient):
|
|
243
|
+
export declare function stubSessionInactive(client: OryAgentClient): Mock;
|
|
244
244
|
/** Stub verifySession to throw a network error. */
|
|
245
|
-
export declare function stubSessionNetworkError(client: OryAgentClient):
|
|
245
|
+
export declare function stubSessionNetworkError(client: OryAgentClient): Mock;
|
|
246
246
|
/** Stub verifySession to throw MFA required. */
|
|
247
|
-
export declare function stubSessionMfaRequired(client: OryAgentClient):
|
|
247
|
+
export declare function stubSessionMfaRequired(client: OryAgentClient): Mock;
|
|
248
248
|
/** Stub verifySession to throw session_inactive. */
|
|
249
|
-
export declare function stubSessionExpired(client: OryAgentClient):
|
|
249
|
+
export declare function stubSessionExpired(client: OryAgentClient): Mock;
|
|
250
250
|
/** Stub introspectToken to succeed. */
|
|
251
|
-
export declare function stubOAuth2Success(client: OryAgentClient):
|
|
251
|
+
export declare function stubOAuth2Success(client: OryAgentClient): Mock;
|
|
252
252
|
/** Stub introspectToken to return inactive. */
|
|
253
|
-
export declare function stubOAuth2Inactive(client: OryAgentClient):
|
|
253
|
+
export declare function stubOAuth2Inactive(client: OryAgentClient): Mock;
|
|
254
254
|
/**
|
|
255
255
|
* Set the mode the server-read stub reports. Also clears the in-memory
|
|
256
256
|
* permission-mode cache so the new value takes effect immediately (tests run
|
|
@@ -258,26 +258,38 @@ export declare function stubOAuth2Inactive(client: OryAgentClient): import("vite
|
|
|
258
258
|
*/
|
|
259
259
|
export declare function setTestPermissionMode(mode: PermissionMode): void;
|
|
260
260
|
/** Stub checkPermission to allow. */
|
|
261
|
-
export declare function stubPermissionAllowed(client: OryAgentClient): import("vitest").
|
|
262
|
-
|
|
261
|
+
export declare function stubPermissionAllowed(client: OryAgentClient): import("vitest").MockInstance<import("../../../node_modules/vitest/dist/chunks/config.d.QBNYCgFU.js", { with: { "resolution-mode": "import" } }).v> & {
|
|
262
|
+
(...args: any[]): any;
|
|
263
|
+
new (...args: any[]): any;
|
|
264
|
+
} & {} & {
|
|
265
|
+
batch: Mock;
|
|
263
266
|
};
|
|
264
267
|
/** Stub checkPermission to deny (the tool check; the mode check still answers `currentTestMode`). */
|
|
265
|
-
export declare function stubPermissionDenied(client: OryAgentClient): import("vitest").
|
|
266
|
-
|
|
268
|
+
export declare function stubPermissionDenied(client: OryAgentClient): import("vitest").MockInstance<import("../../../node_modules/vitest/dist/chunks/config.d.QBNYCgFU.js", { with: { "resolution-mode": "import" } }).v> & {
|
|
269
|
+
(...args: any[]): any;
|
|
270
|
+
new (...args: any[]): any;
|
|
271
|
+
} & {} & {
|
|
272
|
+
batch: Mock;
|
|
267
273
|
};
|
|
268
274
|
/** Stub checkPermission to throw a network error. */
|
|
269
|
-
export declare function stubPermissionNetworkError(client: OryAgentClient): import("vitest").
|
|
270
|
-
|
|
275
|
+
export declare function stubPermissionNetworkError(client: OryAgentClient): import("vitest").MockInstance<import("../../../node_modules/vitest/dist/chunks/config.d.QBNYCgFU.js", { with: { "resolution-mode": "import" } }).v> & {
|
|
276
|
+
(...args: any[]): any;
|
|
277
|
+
new (...args: any[]): any;
|
|
278
|
+
} & {} & {
|
|
279
|
+
batch: Mock;
|
|
271
280
|
};
|
|
272
281
|
/** Stub checkPermission to throw a rate-limit error. */
|
|
273
|
-
export declare function stubPermissionRateLimited(client: OryAgentClient): import("vitest").
|
|
274
|
-
|
|
282
|
+
export declare function stubPermissionRateLimited(client: OryAgentClient): import("vitest").MockInstance<import("../../../node_modules/vitest/dist/chunks/config.d.QBNYCgFU.js", { with: { "resolution-mode": "import" } }).v> & {
|
|
283
|
+
(...args: any[]): any;
|
|
284
|
+
new (...args: any[]): any;
|
|
285
|
+
} & {} & {
|
|
286
|
+
batch: Mock;
|
|
275
287
|
};
|
|
276
|
-
/** Stub
|
|
277
|
-
export declare function stubMcpAllowed(client: OryAgentClient):
|
|
278
|
-
/** Stub
|
|
279
|
-
export declare function stubMcpServerDenied(client: OryAgentClient):
|
|
280
|
-
/** Stub
|
|
281
|
-
export declare function stubMcpToolDenied(client: OryAgentClient):
|
|
282
|
-
/** Stub
|
|
283
|
-
export declare function stubMcpNetworkError(client: OryAgentClient):
|
|
288
|
+
/** Stub MCP checks so the server and the tool are both allowed. */
|
|
289
|
+
export declare function stubMcpAllowed(client: OryAgentClient): Mock;
|
|
290
|
+
/** Stub MCP checks so the server is denied (No access). */
|
|
291
|
+
export declare function stubMcpServerDenied(client: OryAgentClient): Mock;
|
|
292
|
+
/** Stub MCP checks so the server is allowed but the tool is denied (its tier is blocked). */
|
|
293
|
+
export declare function stubMcpToolDenied(client: OryAgentClient): Mock;
|
|
294
|
+
/** Stub MCP checks to throw a network error (MCP fail-open). */
|
|
295
|
+
export declare function stubMcpNetworkError(client: OryAgentClient): Mock;
|
package/dist/testing.js
CHANGED
|
@@ -553,25 +553,47 @@ function stubPermissionRateLimited(client) {
|
|
|
553
553
|
return Object.assign(check, { batch });
|
|
554
554
|
}
|
|
555
555
|
// ─── MCP Stub Presets ─────────────────────────────────────────────
|
|
556
|
-
/**
|
|
557
|
-
|
|
556
|
+
/**
|
|
557
|
+
* Answer MCP gate checks tuple by tuple: the `McpServer` and `McpTool` use
|
|
558
|
+
* permits get the given answers, mode checks answer `currentTestMode`, block
|
|
559
|
+
* probes find no block, and any other use permit is allowed.
|
|
560
|
+
*/
|
|
561
|
+
function stubMcpDecision(client, answers) {
|
|
558
562
|
ensurePermissionCredential(client);
|
|
559
|
-
|
|
560
|
-
|
|
563
|
+
const answer = (tuple) => {
|
|
564
|
+
if (tuple.object === opl_js_1.PERMISSION_MODE_OBJECT && tuple.relation === opl_js_1.RELATION_ENFORCED_SUBJECTS) {
|
|
565
|
+
return currentTestMode === "enforce";
|
|
566
|
+
}
|
|
567
|
+
if (tuple.relation === "blockedSubjects")
|
|
568
|
+
return false;
|
|
569
|
+
if (tuple.namespace === "McpServer")
|
|
570
|
+
return answers.server;
|
|
571
|
+
if (tuple.namespace === "McpTool")
|
|
572
|
+
return answers.tool;
|
|
573
|
+
return true;
|
|
574
|
+
};
|
|
575
|
+
stubApi(client, "permission", "checkPermission", (params) => Promise.resolve(answer(params) ? exports.PERMISSION_ALLOWED : exports.PERMISSION_DENIED));
|
|
576
|
+
return stubApi(client, "permission", "batchCheckPermission", (request) => {
|
|
577
|
+
const tuples = request
|
|
578
|
+
.batchCheckPermissionBody?.tuples ?? [];
|
|
579
|
+
return Promise.resolve({ data: { results: tuples.map((tuple) => ({ allowed: answer(tuple) })) } });
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
/** Stub MCP checks so the server and the tool are both allowed. */
|
|
583
|
+
function stubMcpAllowed(client) {
|
|
584
|
+
return stubMcpDecision(client, { server: true, tool: true });
|
|
561
585
|
}
|
|
562
|
-
/** Stub
|
|
586
|
+
/** Stub MCP checks so the server is denied (No access). */
|
|
563
587
|
function stubMcpServerDenied(client) {
|
|
564
|
-
|
|
565
|
-
return stubApi(client, "permission", "checkPermission", checkPermissionResponder(exports.PERMISSION_DENIED));
|
|
588
|
+
return stubMcpDecision(client, { server: false, tool: true });
|
|
566
589
|
}
|
|
567
|
-
/** Stub
|
|
590
|
+
/** Stub MCP checks so the server is allowed but the tool is denied (its tier is blocked). */
|
|
568
591
|
function stubMcpToolDenied(client) {
|
|
569
|
-
|
|
570
|
-
stubApi(client, "permission", "checkPermission", checkPermissionResponder(exports.PERMISSION_ALLOWED));
|
|
571
|
-
return stubApi(client, "permission", "batchCheckPermission", () => Promise.resolve(exports.BATCH_SERVER_ALLOWED_TOOL_DENIED));
|
|
592
|
+
return stubMcpDecision(client, { server: true, tool: false });
|
|
572
593
|
}
|
|
573
|
-
/** Stub
|
|
594
|
+
/** Stub MCP checks to throw a network error (MCP fail-open). */
|
|
574
595
|
function stubMcpNetworkError(client) {
|
|
575
596
|
ensurePermissionCredential(client);
|
|
597
|
+
stubApi(client, "permission", "batchCheckPermission", () => Promise.reject(makeNetworkError()));
|
|
576
598
|
return stubApi(client, "permission", "checkPermission", () => Promise.reject(makeNetworkError()));
|
|
577
599
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -32,6 +32,19 @@ export interface PermissionCheck {
|
|
|
32
32
|
object: string;
|
|
33
33
|
relation: string;
|
|
34
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* The MCP server an `McpServer` or `McpTool` check targets, as the harness
|
|
37
|
+
* reported it. Agent Security resolves it to the catalog server it checks.
|
|
38
|
+
*/
|
|
39
|
+
mcp?: McpServerHint;
|
|
40
|
+
}
|
|
41
|
+
export interface McpServerHint {
|
|
42
|
+
/** The server name as the harness reports it. */
|
|
43
|
+
server: string;
|
|
44
|
+
/** The MCP tool name as the server defines it. */
|
|
45
|
+
tool?: string;
|
|
46
|
+
/** Hashed server identifiers from {@link mcpServerFingerprints}. */
|
|
47
|
+
fingerprints?: string[];
|
|
35
48
|
}
|
|
36
49
|
export interface PermissionResult {
|
|
37
50
|
allowed: boolean;
|
|
@@ -50,8 +63,7 @@ export type OryErrorCode = "session_inactive" | "session_aal2_required" | "forbi
|
|
|
50
63
|
/**
|
|
51
64
|
* TLS/certificate verification failed — an expired, self-signed, or otherwise
|
|
52
65
|
* untrusted certificate on the project host. Distinct from `network_error`
|
|
53
|
-
* because
|
|
54
|
-
* fails open, whereas a certificate failure means we cannot establish *who we
|
|
66
|
+
* because a certificate failure means we cannot establish *who we
|
|
55
67
|
* are talking to*, so the check result cannot be trusted at all. It is also
|
|
56
68
|
* precisely the failure an active network attacker produces, so under
|
|
57
69
|
* `enforce` it denies rather than passing the tool through.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/argus",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "Ory Argus: the core API for building authentication, authorization, and audit into AI agent harness plugins, extensions, and custom integrations",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://ory.com",
|
|
@@ -62,10 +62,10 @@
|
|
|
62
62
|
"!dist/**/*.tsbuildinfo"
|
|
63
63
|
],
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@ory/client": "^1.22.
|
|
65
|
+
"@ory/client": "^1.22.66",
|
|
66
66
|
"@vscode/tree-sitter-wasm": "^0.3.1",
|
|
67
|
-
"open": "^11.0.
|
|
68
|
-
"web-tree-sitter": "^0.
|
|
67
|
+
"open": "^11.0.4",
|
|
68
|
+
"web-tree-sitter": "^0.27.1"
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
71
71
|
"node": ">=22"
|