@ory/argus 1.2.2 → 1.2.3

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.
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "repo": "ory-agent-plugins",
3
- "commit": "fc1ff6ae66afbfb17f4190927e734abe3df8d241",
4
- "commitShort": "fc1ff6a",
3
+ "commit": "bce419a80533136b9877d3cd33e0a03e1e585f52",
4
+ "commitShort": "bce419a",
5
5
  "branch": "main",
6
- "commitDate": "2026-09-09T14:04:39-07:00",
6
+ "commitDate": "2026-09-11T13:08:35-07:00",
7
7
  "dirty": false,
8
- "builtAt": "2026-09-09T21:08:18.388Z"
8
+ "builtAt": "2026-09-11T20:11:58.706Z"
9
9
  }
@@ -205,7 +205,12 @@ function runHarnessContractSuite(adapter) {
205
205
  const observeEvents = (0, testing_js_1.getActivityEvents)(client, "permission.observe_deny");
206
206
  const blockObservedEvents = (0, testing_js_1.getActivityEvents)(client, "permission.block_observed");
207
207
  (0, vitest_1.expect)(observeEvents).toHaveLength(0);
208
- (0, vitest_1.expect)(blockObservedEvents).toHaveLength(check === "denied" && mode === "observe" ? 1 : 0);
208
+ if (check === "denied" && mode === "observe") {
209
+ (0, vitest_1.expect)(blockObservedEvents.length).toBeGreaterThanOrEqual(1);
210
+ }
211
+ else {
212
+ (0, vitest_1.expect)(blockObservedEvents).toHaveLength(0);
213
+ }
209
214
  });
210
215
  (0, vitest_1.it)("records tool.invoke when an interactive tool proceeds", async () => {
211
216
  process.env.ORY_INTERACTIVE_TOOLS = adapter.tool;
@@ -61,8 +61,19 @@ export interface AdditionalModeSubject {
61
61
  subject: ModeSubject;
62
62
  scope: "agent" | "subagent";
63
63
  }
64
+ export interface PermissionModeBatchPlan {
65
+ checks: PermissionCheck[];
66
+ resolve(results: Array<{
67
+ allowed: boolean;
68
+ error?: string;
69
+ }>): ResolvedPermissionMode;
70
+ }
64
71
  /** Drop all in-memory caches. Test-only (reassigns the WeakMap). */
65
72
  export declare function resetPermissionModeCache(): void;
73
+ export declare function preparePermissionModeBatch(client: OryAgentClient, subject: ModeSubject, opts?: {
74
+ now?: number;
75
+ additionalSubjects?: AdditionalModeSubject[];
76
+ }): PermissionModeBatchPlan;
66
77
  /**
67
78
  * Resolve the current permission mode for `subject`. Never throws — a Keto
68
79
  * error falls back to the persistent cache, then `observe`. See the module
@@ -49,6 +49,7 @@
49
49
  */
50
50
  Object.defineProperty(exports, "__esModule", { value: true });
51
51
  exports.resetPermissionModeCache = resetPermissionModeCache;
52
+ exports.preparePermissionModeBatch = preparePermissionModeBatch;
52
53
  exports.resolvePermissionMode = resolvePermissionMode;
53
54
  exports.warmPermissionModeCache = warmPermissionModeCache;
54
55
  const config_js_1 = require("./config.js");
@@ -123,6 +124,139 @@ function projectPostureSubject(namespace) {
123
124
  function resetPermissionModeCache() {
124
125
  clientMemo = new WeakMap();
125
126
  }
127
+ function preparePermissionModeBatch(client, subject, opts = {}) {
128
+ const now = opts.now ?? Date.now();
129
+ const projectUrl = (0, config_js_1.resolveConfig)().projectUrl;
130
+ const namespace = resolveNamespace();
131
+ const subjects = [
132
+ { subject, scope: "principal", key: subjectKey(subject) },
133
+ ...(opts.additionalSubjects ?? []).map((item) => ({ ...item, key: subjectKey(item.subject) })),
134
+ ].filter((item, index, all) => all.findIndex((candidate) => candidate.key === item.key) === index);
135
+ const memo = memoFor(client);
136
+ const fresh = (key) => {
137
+ const entry = memo.get(key);
138
+ if (entry && now - entry.fetchedAt < ttlMs())
139
+ return entry;
140
+ memo.delete(key);
141
+ return undefined;
142
+ };
143
+ const cachedProject = fresh(PROJECT_SCOPE_KEY);
144
+ const cachedSubjects = subjects.map((item) => fresh(item.key));
145
+ if (cachedProject?.mode === "enforce" || (cachedProject && cachedSubjects.every(Boolean))) {
146
+ const enforcing = cachedProject.mode === "enforce"
147
+ ? cachedProject
148
+ : cachedSubjects.find((entry) => entry?.mode === "enforce");
149
+ const resolved = {
150
+ mode: enforcing?.mode ?? "observe",
151
+ source: enforcing?.source ?? cachedProject.source,
152
+ };
153
+ return { checks: [], resolve: () => resolved };
154
+ }
155
+ const pending = [
156
+ ...(!cachedProject ? [{ scope: "project", key: PROJECT_SCOPE_KEY, subject: projectPostureSubject(namespace) }] : []),
157
+ ...subjects.flatMap((item, index) => cachedSubjects[index] ? [] : [item]),
158
+ ];
159
+ const checks = pending.map((item) => ({
160
+ namespace,
161
+ object: opl_js_1.PERMISSION_MODE_OBJECT,
162
+ relation: opl_js_1.RELATION_ENFORCED_SUBJECTS,
163
+ ...item.subject,
164
+ }));
165
+ return {
166
+ checks,
167
+ resolve(results) {
168
+ const consultedKeys = [];
169
+ let sawError = false;
170
+ const read = (key, cached) => {
171
+ consultedKeys.push(key);
172
+ if (cached)
173
+ return cached;
174
+ const index = pending.findIndex((item) => item.key === key);
175
+ const result = results[index];
176
+ if (!result || result.error) {
177
+ sawError = true;
178
+ return undefined;
179
+ }
180
+ const entry = {
181
+ mode: result.allowed ? "enforce" : "observe",
182
+ source: "server",
183
+ fetchedAt: now,
184
+ };
185
+ memo.set(key, entry);
186
+ return entry;
187
+ };
188
+ const ordered = [
189
+ { key: PROJECT_SCOPE_KEY, scope: "project", cached: cachedProject },
190
+ ...subjects.map((item, index) => ({ ...item, cached: cachedSubjects[index] })),
191
+ ];
192
+ for (const item of ordered) {
193
+ const entry = read(item.key, item.cached);
194
+ if (entry?.mode === "enforce") {
195
+ client.logger.activity("permission.mode_resolve", "ok", {
196
+ attributes: { permissionMode: "enforce", enforceGranted: true, scope: item.scope, source: "server" },
197
+ });
198
+ persistPermissionModeMemo(client, projectUrl, now, consultedKeys);
199
+ return { mode: "enforce", source: "server" };
200
+ }
201
+ }
202
+ if (consultedKeys.some((key) => memo.get(key)?.source === "server")) {
203
+ persistPermissionModeMemo(client, projectUrl, now, consultedKeys);
204
+ }
205
+ if (sawError)
206
+ return fallbackPermissionMode(projectUrl, subjects.map((item) => item.key));
207
+ client.logger.activity("permission.mode_resolve", "ok", {
208
+ attributes: { permissionMode: "observe", enforceGranted: false, scope: "principal", source: "server" },
209
+ });
210
+ return { mode: "observe", source: "server" };
211
+ },
212
+ };
213
+ }
214
+ function fallbackPermissionMode(projectUrl, subjectKeys) {
215
+ const cache = (0, config_js_1.resolveConfig)().permissionModeCache;
216
+ const scoped = cache && cache.projectUrl === projectUrl ? cache.scopes : undefined;
217
+ const project = scoped?.[PROJECT_SCOPE_KEY];
218
+ const subjectEntries = subjectKeys.map((key) => scoped?.[key]);
219
+ const scopedMode = project?.mode === "enforce" || subjectEntries.some((entry) => entry?.mode === "enforce")
220
+ ? "enforce"
221
+ : project && subjectEntries.every(Boolean)
222
+ ? "observe"
223
+ : undefined;
224
+ const legacyMode = cache && !cache.scopes && (!cache.projectUrl || cache.projectUrl === projectUrl)
225
+ ? cache.mode
226
+ : undefined;
227
+ const mode = scopedMode ?? (legacyMode === "enforce" ? "enforce" : undefined);
228
+ return mode ? { mode, source: "cache" } : { mode: "observe", source: "default" };
229
+ }
230
+ function persistPermissionModeMemo(client, projectUrl, now, requestedKeys) {
231
+ const updates = {};
232
+ for (const key of new Set(requestedKeys)) {
233
+ const entry = memoFor(client).get(key);
234
+ if (entry?.source === "server")
235
+ updates[key] = { mode: entry.mode, fetchedAt: entry.fetchedAt };
236
+ }
237
+ try {
238
+ (0, config_js_1.mutateConfig)((config) => {
239
+ const current = config.permissionModeCache;
240
+ const scopes = { ...(current && current.projectUrl === projectUrl ? current.scopes ?? {} : {}) };
241
+ const projectMode = updates[PROJECT_SCOPE_KEY]?.mode ?? scopes[PROJECT_SCOPE_KEY]?.mode ?? "observe";
242
+ let changed = current?.mode !== projectMode || current?.projectUrl !== projectUrl;
243
+ for (const [key, entry] of Object.entries(updates)) {
244
+ const refreshed = { mode: entry.mode, fetchedAt: now };
245
+ if (scopes[key]?.mode === refreshed.mode && scopes[key]?.fetchedAt === refreshed.fetchedAt)
246
+ continue;
247
+ scopes[key] = refreshed;
248
+ changed = true;
249
+ }
250
+ if (!changed)
251
+ return undefined;
252
+ const boundedScopes = Object.fromEntries(Object.entries(scopes).sort(([, left], [, right]) => right.fetchedAt - left.fetchedAt).slice(0, 256));
253
+ return { ...config, permissionModeCache: { mode: projectMode, fetchedAt: now, projectUrl, scopes: boundedScopes } };
254
+ });
255
+ }
256
+ catch {
257
+ // Persistence is best-effort; a later process can read the server again.
258
+ }
259
+ }
126
260
  /**
127
261
  * Resolve the current permission mode for `subject`. Never throws — a Keto
128
262
  * error falls back to the persistent cache, then `observe`. See the module
@@ -266,42 +400,7 @@ async function warmPermissionModeCache(client, subject, opts = {}) {
266
400
  });
267
401
  if (resolved.source === "server") {
268
402
  const requestedKeys = [PROJECT_SCOPE_KEY, subjectKey(subject), ...(opts.additionalSubjects ?? []).map((item) => subjectKey(item.subject))];
269
- const updates = {};
270
- for (const key of new Set(requestedKeys)) {
271
- const entry = memoFor(client).get(key);
272
- if (entry?.source === "server")
273
- updates[key] = { mode: entry.mode, fetchedAt: entry.fetchedAt };
274
- }
275
- try {
276
- (0, config_js_1.mutateConfig)((config) => {
277
- const current = config.permissionModeCache;
278
- const scopes = { ...(current && current.projectUrl === projectUrl ? current.scopes ?? {} : {}) };
279
- const projectMode = updates[PROJECT_SCOPE_KEY]?.mode
280
- ?? scopes[PROJECT_SCOPE_KEY]?.mode
281
- ?? "observe";
282
- let changed = current?.mode !== projectMode || current?.projectUrl !== projectUrl;
283
- for (const [key, entry] of Object.entries(updates)) {
284
- const refreshed = { mode: entry.mode, fetchedAt: now };
285
- if (scopes[key]?.mode === refreshed.mode && scopes[key]?.fetchedAt === refreshed.fetchedAt)
286
- continue;
287
- scopes[key] = refreshed;
288
- changed = true;
289
- }
290
- if (!changed)
291
- return undefined;
292
- const boundedScopes = Object.fromEntries(Object.entries(scopes)
293
- .sort(([, left], [, right]) => right.fetchedAt - left.fetchedAt)
294
- .slice(0, 256));
295
- return {
296
- ...config,
297
- permissionModeCache: { mode: projectMode, fetchedAt: now, projectUrl, scopes: boundedScopes },
298
- };
299
- });
300
- }
301
- catch {
302
- // Best-effort — a write failure just means the next process re-reads
303
- // from the server (or starts at observe until it can).
304
- }
403
+ persistPermissionModeMemo(client, projectUrl, now, requestedKeys);
305
404
  }
306
405
  return resolved;
307
406
  }
@@ -220,6 +220,9 @@ async function gateToolCall(client, args) {
220
220
  // every read 401s: `session_inactive` denies every tool under `enforce`, and
221
221
  // under `observe` stops checking anything at all (#242).
222
222
  await (0, read_credential_js_1.ensureReadCredential)(client, { harness: args.harness });
223
+ if (!(args.shellCommand && (0, tool_catalog_js_1.isShellTool)(args.harness, args.toolName))) {
224
+ return batchNonShellGate(client, args);
225
+ }
223
226
  // Resolve the mode ONCE for this gate — a per-principal Keto permission read
224
227
  // (server → cache → observe), cached with a short TTL. Threaded downstream as
225
228
  // `modeOverride` so the whole gate (top-level check + every decomposed shell
@@ -240,16 +243,7 @@ async function gateToolCall(client, args) {
240
243
  // than before it, so the extra read costs no serial latency, and let a block
241
244
  // override an allow afterwards.
242
245
  const [decision, blocked] = await Promise.all([
243
- args.shellCommand && (0, tool_catalog_js_1.isShellTool)(args.harness, args.toolName)
244
- ? // Shell tools (issue #76): decompose the command into per-word sub-checks
245
- // in addition to the top-level tool check. Only when the harness marks
246
- // this a shell tool AND the raw command was threaded through — otherwise
247
- // fall back to the plain single check (never breaks).
248
- decomposeAndCheck(client, { ...args, modeOverride: mode })
249
- : checkAndDecide(client, args.check, {
250
- activityAttributes: args.activityAttributes,
251
- modeOverride: mode,
252
- }),
246
+ decomposeAndCheck(client, { ...args, modeOverride: mode }),
253
247
  findBlockedPrincipal(client, {
254
248
  namespace: args.check.namespace,
255
249
  object: args.check.object,
@@ -273,6 +267,77 @@ async function gateToolCall(client, args) {
273
267
  }
274
268
  return applyPrincipalBlock(client, { args, mode, decision, blocked });
275
269
  }
270
+ async function batchNonShellGate(client, args) {
271
+ const additionalSubjects = [
272
+ { scope: "agent", subject: (0, subject_js_1.resolveAgentSubject)(client) },
273
+ { scope: "subagent", subject: (0, subject_js_1.resolveSubAgentSubject)(args.principals?.subAgentClientId) },
274
+ ].filter((item) => item.subject !== undefined);
275
+ const modeSubject = { subjectId: args.check.subjectId, subjectSet: args.check.subjectSet };
276
+ const modePlan = args.modeOverride ? undefined : (0, permission_mode_js_1.preparePermissionModeBatch)(client, modeSubject, { additionalSubjects });
277
+ const candidates = principalBlockCandidates(client, args.principals);
278
+ const effectiveCheck = { ...args.check, relation: resolveCheckRelation(args.check.relation) };
279
+ const checks = [
280
+ ...(modePlan?.checks ?? []),
281
+ effectiveCheck,
282
+ ...candidates.map(({ ref }) => ({
283
+ namespace: args.check.namespace,
284
+ object: args.check.object,
285
+ relation: opl_js_1.RELATION_BLOCKED_SUBJECTS,
286
+ ...ref,
287
+ })),
288
+ ];
289
+ try {
290
+ const batch = await client.batchCheckPermissions(checks, {
291
+ activityAttributes: { ...args.activityAttributes, source: "tool_gate" },
292
+ });
293
+ const modeCount = modePlan?.checks.length ?? 0;
294
+ const mode = args.modeOverride ?? modePlan.resolve(batch.results.slice(0, modeCount)).mode;
295
+ const toolResult = batch.results[modeCount];
296
+ if (!toolResult || toolResult.error)
297
+ throw new Error(toolResult?.error ?? "missing tool result");
298
+ const result = { allowed: toolResult.allowed, checkedAt: batch.checkedAt, check: args.check };
299
+ const decision = await decidePermissionResult(client, result, effectiveCheck, {
300
+ activityAttributes: args.activityAttributes,
301
+ modeOverride: mode,
302
+ });
303
+ let blocked;
304
+ for (const [index, candidate] of candidates.entries()) {
305
+ const block = batch.results[modeCount + 1 + index];
306
+ if (block?.allowed && !block.error) {
307
+ blocked = {
308
+ subject: candidate.level === "project" ? "project" : (0, subject_js_1.subjectLabel)(candidate.ref),
309
+ level: candidate.level,
310
+ };
311
+ break;
312
+ }
313
+ }
314
+ return applyBlockedDecision(client, args, mode, decision, blocked);
315
+ }
316
+ catch {
317
+ const mode = args.modeOverride ?? (await (0, permission_mode_js_1.warmPermissionModeCache)(client, modeSubject, { additionalSubjects })).mode;
318
+ const [decision, blocked] = await Promise.all([
319
+ checkAndDecide(client, args.check, { activityAttributes: args.activityAttributes, modeOverride: mode }),
320
+ findBlockedPrincipal(client, { namespace: args.check.namespace, object: args.check.object, principals: args.principals }),
321
+ ]);
322
+ return applyBlockedDecision(client, args, mode, decision, blocked);
323
+ }
324
+ }
325
+ function applyBlockedDecision(client, args, mode, decision, blocked) {
326
+ if (!blocked)
327
+ return decision;
328
+ if (decision.kind === "deny") {
329
+ return {
330
+ ...decision,
331
+ activityAttributes: {
332
+ ...decision.activityAttributes,
333
+ blockReason: "explicit_block",
334
+ blockedPrincipal: blocked.subject,
335
+ blockedPrincipalLevel: blocked.level,
336
+ },
337
+ };
338
+ }
339
+ return applyPrincipalBlock(client, { args, mode, decision, blocked });
340
+ }
276
341
  /**
277
342
  * Turn a machine-principal block into the gate's outcome, respecting
278
343
  * `permissionMode` exactly as any other deny does: `enforce` blocks, `observe`
@@ -610,6 +675,10 @@ async function checkAndDecide(client, check, opts = {}) {
610
675
  // Report the decision against the caller's original check (relation `use`),
611
676
  // not the internal permit rewrite, so callers see the coordinates they passed.
612
677
  result = { ...result, check };
678
+ return decidePermissionResult(client, result, effectiveCheck, opts);
679
+ }
680
+ async function decidePermissionResult(client, result, effectiveCheck, opts) {
681
+ const check = result.check;
613
682
  // A deny from the native `use` permit is necessarily an explicit block.
614
683
  // Retain the diagnostic probe only for other grant-based public checks.
615
684
  const blockReason = result.allowed
@@ -640,6 +709,24 @@ const PROJECT_BLOCK_SUBJECT = {
640
709
  relation: opl_js_1.RELATION_ENFORCED_SUBJECTS,
641
710
  },
642
711
  };
712
+ function principalBlockCandidates(client, principals) {
713
+ const p = principals ?? {};
714
+ return [
715
+ { level: "project", ref: PROJECT_BLOCK_SUBJECT },
716
+ { level: "agent", ref: (0, subject_js_1.resolveAgentSubject)(client) },
717
+ { level: "agent_session", ref: (0, subject_js_1.resolveAgentSessionSubject)(client, p.sessionId) },
718
+ { level: "subagent", ref: (0, subject_js_1.resolveSubAgentSubject)(p.subAgentClientId) },
719
+ {
720
+ level: "subagent_spawn",
721
+ ref: p.subAgentType ? (0, subject_js_1.resolveSubAgentSpawnSubject)(client, {
722
+ subAgentClientId: p.subAgentClientId,
723
+ subAgentType: p.subAgentType,
724
+ perSpawnId: p.perSpawnId,
725
+ sessionId: p.sessionId,
726
+ }) : undefined,
727
+ },
728
+ ].filter((candidate) => candidate.ref !== undefined);
729
+ }
643
730
  /**
644
731
  * Check whether the project or any acting machine principal is explicitly blocked from this
645
732
  * tool, and name the one that is.
package/dist/testing.d.ts CHANGED
@@ -241,13 +241,21 @@ export declare function stubOAuth2Inactive(client: OryAgentClient): import("vite
241
241
  */
242
242
  export declare function setTestPermissionMode(mode: PermissionMode): void;
243
243
  /** Stub checkPermission to allow. */
244
- export declare function stubPermissionAllowed(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
244
+ export declare function stubPermissionAllowed(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable> & {
245
+ batch: import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
246
+ };
245
247
  /** Stub checkPermission to deny (the tool check; the mode check still answers `currentTestMode`). */
246
- export declare function stubPermissionDenied(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
248
+ export declare function stubPermissionDenied(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable> & {
249
+ batch: import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
250
+ };
247
251
  /** Stub checkPermission to throw a network error. */
248
- export declare function stubPermissionNetworkError(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
252
+ export declare function stubPermissionNetworkError(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable> & {
253
+ batch: import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
254
+ };
249
255
  /** Stub checkPermission to throw a rate-limit error. */
250
- export declare function stubPermissionRateLimited(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
256
+ export declare function stubPermissionRateLimited(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable> & {
257
+ batch: import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
258
+ };
251
259
  /** Stub both checkPermission (server allow) and batchCheckPermission (both allow). */
252
260
  export declare function stubMcpAllowed(client: OryAgentClient): import("vitest").Mock<import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Procedure | import(".pnpm/@vitest+spy@4.1.7/node_modules/@vitest/spy", { with: { "resolution-mode": "import" } }).Constructable>;
253
261
  /** Stub checkPermission to deny (server-only MCP check). */
package/dist/testing.js CHANGED
@@ -454,7 +454,7 @@ function ensurePermissionCredential(client) {
454
454
  /** Stub checkPermission to allow. */
455
455
  function stubPermissionAllowed(client) {
456
456
  ensurePermissionCredential(client);
457
- stubApi(client, "permission", "batchCheckPermission", (request) => {
457
+ const batch = stubApi(client, "permission", "batchCheckPermission", (request) => {
458
458
  const tuples = request.batchCheckPermissionBody?.tuples ?? [];
459
459
  return Promise.resolve({
460
460
  data: {
@@ -466,22 +466,38 @@ function stubPermissionAllowed(client) {
466
466
  },
467
467
  });
468
468
  });
469
- return stubApi(client, "permission", "checkPermission", checkPermissionResponder(exports.PERMISSION_ALLOWED));
469
+ const check = stubApi(client, "permission", "checkPermission", checkPermissionResponder(exports.PERMISSION_ALLOWED));
470
+ return Object.assign(check, { batch });
470
471
  }
471
472
  /** Stub checkPermission to deny (the tool check; the mode check still answers `currentTestMode`). */
472
473
  function stubPermissionDenied(client) {
473
474
  ensurePermissionCredential(client);
474
- return stubApi(client, "permission", "checkPermission", checkPermissionResponder(exports.PERMISSION_DENIED));
475
+ const batch = stubApi(client, "permission", "batchCheckPermission", (request) => {
476
+ const tuples = request.batchCheckPermissionBody?.tuples ?? [];
477
+ return Promise.resolve({
478
+ data: {
479
+ results: tuples.map((tuple) => ({
480
+ allowed: tuple.relation === "enforcedSubjects" && currentTestMode === "enforce",
481
+ })),
482
+ },
483
+ });
484
+ });
485
+ const check = stubApi(client, "permission", "checkPermission", checkPermissionResponder(exports.PERMISSION_DENIED));
486
+ return Object.assign(check, { batch });
475
487
  }
476
488
  /** Stub checkPermission to throw a network error. */
477
489
  function stubPermissionNetworkError(client) {
478
490
  ensurePermissionCredential(client);
479
- return stubApi(client, "permission", "checkPermission", () => Promise.reject(makeNetworkError()));
491
+ const batch = stubApi(client, "permission", "batchCheckPermission", () => Promise.reject(makeNetworkError()));
492
+ const check = stubApi(client, "permission", "checkPermission", () => Promise.reject(makeNetworkError()));
493
+ return Object.assign(check, { batch });
480
494
  }
481
495
  /** Stub checkPermission to throw a rate-limit error. */
482
496
  function stubPermissionRateLimited(client) {
483
497
  ensurePermissionCredential(client);
484
- return stubApi(client, "permission", "checkPermission", () => Promise.reject(makeRateLimitError()));
498
+ const batch = stubApi(client, "permission", "batchCheckPermission", () => Promise.reject(makeRateLimitError()));
499
+ const check = stubApi(client, "permission", "checkPermission", () => Promise.reject(makeRateLimitError()));
500
+ return Object.assign(check, { batch });
485
501
  }
486
502
  // ─── MCP Stub Presets ─────────────────────────────────────────────
487
503
  /** Stub both checkPermission (server allow) and batchCheckPermission (both allow). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
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",