@ory/argus 1.6.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-info.json +4 -4
- package/dist/client.js +16 -0
- package/dist/contract-suite.js +2 -2
- package/dist/denial.d.ts +3 -1
- package/dist/denial.js +6 -0
- package/dist/permission-mode.d.ts +2 -2
- package/dist/permission-mode.js +5 -5
- package/dist/permissions.d.ts +9 -10
- package/dist/permissions.js +51 -19
- package/dist/types.d.ts +1 -2
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"repo": "ory-agent-plugins",
|
|
3
|
-
"commit": "
|
|
4
|
-
"commitShort": "
|
|
3
|
+
"commit": "11290e2d19d1b0cabd49b94f8eaab8c3eb6f5f8e",
|
|
4
|
+
"commitShort": "11290e2d",
|
|
5
5
|
"branch": "main",
|
|
6
|
-
"commitDate": "2026-10-
|
|
6
|
+
"commitDate": "2026-10-31T18:27:13-07:00",
|
|
7
7
|
"dirty": false,
|
|
8
|
-
"builtAt": "2026-
|
|
8
|
+
"builtAt": "2026-11-01T01:30:26.563Z"
|
|
9
9
|
}
|
package/dist/client.js
CHANGED
|
@@ -241,6 +241,9 @@ class OryAgentClient {
|
|
|
241
241
|
|| (result.error !== undefined && typeof result.error !== "string"))) {
|
|
242
242
|
throw httpError(403, "Agent Security returned an invalid permission result");
|
|
243
243
|
}
|
|
244
|
+
if (data.results.some((result) => result.error !== undefined)) {
|
|
245
|
+
throw httpError(503, "Agent Security could not complete a permission check");
|
|
246
|
+
}
|
|
244
247
|
results.push(...data.results);
|
|
245
248
|
headers = response.headers;
|
|
246
249
|
}
|
|
@@ -720,6 +723,9 @@ class OryAgentClient {
|
|
|
720
723
|
...(check.mcp ? { mcp: check.mcp } : {}),
|
|
721
724
|
});
|
|
722
725
|
const data = response.data;
|
|
726
|
+
if (data.error !== undefined && data.error !== null) {
|
|
727
|
+
throw httpError(503, "Agent Security could not complete a permission check");
|
|
728
|
+
}
|
|
723
729
|
const oryRequestId = extractOryRequestId(response.headers);
|
|
724
730
|
const result = {
|
|
725
731
|
allowed: data.allowed,
|
|
@@ -789,6 +795,9 @@ class OryAgentClient {
|
|
|
789
795
|
},
|
|
790
796
|
});
|
|
791
797
|
const data = response.data;
|
|
798
|
+
if (data.results.some((result) => result.error !== undefined && result.error !== null)) {
|
|
799
|
+
throw httpError(503, "Agent Security could not complete a permission check");
|
|
800
|
+
}
|
|
792
801
|
const oryRequestId = extractOryRequestId(response.headers);
|
|
793
802
|
const result = {
|
|
794
803
|
results: data.results.map((r) => ({
|
|
@@ -966,6 +975,9 @@ class OryAgentClient {
|
|
|
966
975
|
cause: err,
|
|
967
976
|
};
|
|
968
977
|
}
|
|
978
|
+
if (err instanceof Error && (err.name === "TimeoutError" || err.name === "AbortError")) {
|
|
979
|
+
return { code: "network_error", message: "Permission request timed out or was aborted", cause: err };
|
|
980
|
+
}
|
|
969
981
|
const socketCode = findSocketErrorCode(err);
|
|
970
982
|
if (socketCode) {
|
|
971
983
|
return {
|
|
@@ -1257,6 +1269,10 @@ const SOCKET_ERROR_CODES = new Set([
|
|
|
1257
1269
|
"ENETUNREACH",
|
|
1258
1270
|
"EPIPE",
|
|
1259
1271
|
"ECONNABORTED",
|
|
1272
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
1273
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
1274
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
1275
|
+
"UND_ERR_SOCKET",
|
|
1260
1276
|
]);
|
|
1261
1277
|
/**
|
|
1262
1278
|
* The socket-level error code on `err`, walking `cause` the same way
|
package/dist/contract-suite.js
CHANGED
|
@@ -167,8 +167,8 @@ function runHarnessContractSuite(adapter) {
|
|
|
167
167
|
{ check: "allowed", mode: "enforce", blocked: false, invoke: true, block: false },
|
|
168
168
|
{ check: "denied", mode: "enforce", blocked: toolCanBlock, invoke: false, block: true },
|
|
169
169
|
{ check: "denied", mode: "observe", blocked: false, invoke: true, block: true },
|
|
170
|
-
{ check: "network_error", mode: "enforce", blocked:
|
|
171
|
-
{ check: "rate_limited", mode: "enforce", blocked:
|
|
170
|
+
{ check: "network_error", mode: "enforce", blocked: toolCanBlock, invoke: false, block: true },
|
|
171
|
+
{ check: "rate_limited", mode: "enforce", blocked: toolCanBlock, invoke: false, block: true },
|
|
172
172
|
])("check=$check × $mode → blocked=$blocked", async ({ check, mode, blocked, invoke, block }) => {
|
|
173
173
|
const stubs = {
|
|
174
174
|
allowed: testing_js_1.stubPermissionAllowed,
|
package/dist/denial.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface DenialContext {
|
|
|
39
39
|
* `activityAttributes.blockReason === "explicit_block"`.
|
|
40
40
|
*/
|
|
41
41
|
blocked?: boolean;
|
|
42
|
+
/** The check failed before a trustworthy permission decision was available. */
|
|
43
|
+
checkRejected?: string;
|
|
42
44
|
}
|
|
43
45
|
/**
|
|
44
46
|
* Activity attributes that mark a denial as a security alert.
|
|
@@ -82,7 +84,7 @@ export declare function formatAlertSummary(ctx: DenialContext): string;
|
|
|
82
84
|
*/
|
|
83
85
|
export declare function shellDenialFields(decision: {
|
|
84
86
|
activityAttributes?: unknown;
|
|
85
|
-
}): Pick<DenialContext, "shellWord" | "shellDeniedWords" | "shellTooComplex" | "shellNamespace" | "blocked">;
|
|
87
|
+
}): Pick<DenialContext, "shellWord" | "shellDeniedWords" | "shellTooComplex" | "shellNamespace" | "blocked" | "checkRejected">;
|
|
86
88
|
/**
|
|
87
89
|
* Human-facing denial reason. Shown in agent UIs (Claude Code's `decision`
|
|
88
90
|
* reason, Gemini CLI's systemMessage, OpenClaw's blockReason, OpenCode's
|
package/dist/denial.js
CHANGED
|
@@ -70,6 +70,7 @@ function shellDenialFields(decision) {
|
|
|
70
70
|
shellTooComplex: sa.shellTooComplex,
|
|
71
71
|
shellNamespace: (0, tool_catalog_js_1.resolveShellCommandNamespace)(),
|
|
72
72
|
blocked: sa.blockReason === "explicit_block",
|
|
73
|
+
checkRejected: sa.checkRejected,
|
|
73
74
|
};
|
|
74
75
|
}
|
|
75
76
|
/**
|
|
@@ -78,6 +79,9 @@ function shellDenialFields(decision) {
|
|
|
78
79
|
* thrown error message).
|
|
79
80
|
*/
|
|
80
81
|
function formatDenialMessage(ctx) {
|
|
82
|
+
if (ctx.checkRejected) {
|
|
83
|
+
return `Ory: tool "${ctx.tool}" blocked because its permission check could not be completed (${ctx.checkRejected}). Check Agent Security availability and credentials, then retry.`;
|
|
84
|
+
}
|
|
81
85
|
if (ctx.shellTooComplex) {
|
|
82
86
|
return (`Ory: shell command blocked — it could not be safely parsed and is ` +
|
|
83
87
|
`denied in enforce mode. Subject "${ctx.subjectId}". ` +
|
|
@@ -127,6 +131,8 @@ function formatDenialMessage(ctx) {
|
|
|
127
131
|
* systemMessage which appears inline in the agent's TUI).
|
|
128
132
|
*/
|
|
129
133
|
function formatDenialSummary(ctx) {
|
|
134
|
+
if (ctx.checkRejected)
|
|
135
|
+
return `Ory: permission check unavailable (${ctx.checkRejected}); tool blocked.`;
|
|
130
136
|
if (ctx.shellTooComplex) {
|
|
131
137
|
return `Ory: shell command blocked (could not be safely parsed). ${ADMIN_HINT}`;
|
|
132
138
|
}
|
|
@@ -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
|
/**
|
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
|
|
@@ -42,7 +41,7 @@ import type { OryError, OryErrorCode, PermissionCheck, PermissionResult } from "
|
|
|
42
41
|
*
|
|
43
42
|
* Threaded onto the decision's activity attributes and into the observe-mode
|
|
44
43
|
* audit event so "explicitly blocked" is distinguishable from "never granted"
|
|
45
|
-
* in activity logs, mirroring how `checkRejected` distinguishes
|
|
44
|
+
* in activity logs, mirroring how `checkRejected` distinguishes a failed
|
|
46
45
|
* deny.
|
|
47
46
|
*/
|
|
48
47
|
export type BlockReason = "not_granted" | "explicit_block";
|
|
@@ -66,7 +65,7 @@ export interface DecisionActivityAttributes {
|
|
|
66
65
|
/** SubjectSet rendered as `<namespace>:<object>#<relation>` when used. */
|
|
67
66
|
subjectSet?: string;
|
|
68
67
|
/**
|
|
69
|
-
* Set when the check call itself
|
|
68
|
+
* Set when the check call itself failed and enforce mode
|
|
70
69
|
* turned that into a deny — the classified {@link OryErrorCode} that
|
|
71
70
|
* caused it. Makes the "denied because the check failed" case
|
|
72
71
|
* distinguishable from a normal Keto deny in the audit trail.
|
|
@@ -305,16 +304,16 @@ export declare function mostRestrictiveDecision(decisions: PermissionDecision[])
|
|
|
305
304
|
* Aggregation: **any deny → deny** (naming the offending word); else any observe
|
|
306
305
|
* → observe (pass through, per-word `permission.observe_deny` events recorded);
|
|
307
306
|
* else allow. A too-complex / unparseable command is **fail-closed** in enforce
|
|
308
|
-
* (deny + alert) and logged-then-passed in observe. A parser-unavailable
|
|
309
|
-
*
|
|
310
|
-
*
|
|
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.
|
|
311
310
|
*/
|
|
312
311
|
export declare function decomposeAndCheck(client: OryAgentClient, args: GateToolCallArgs): Promise<PermissionDecision>;
|
|
313
312
|
/**
|
|
314
313
|
* Run a permission check and resolve the configured mode against the
|
|
315
314
|
* result. Never throws — infrastructure errors surface as a typed
|
|
316
|
-
* `{ kind: "fail_open" }` decision, and
|
|
317
|
-
* 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
|
|
318
317
|
* {@link CHECK_UNTRUSTED_CODES}).
|
|
319
318
|
*/
|
|
320
319
|
export declare function checkAndDecide(client: OryAgentClient, check: PermissionCheck, opts?: CheckAndDecideOptions): Promise<PermissionDecision>;
|
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
|
|
@@ -57,8 +56,7 @@ function resolveCheckRelation(relation) {
|
|
|
57
56
|
* authentication/authorization reasons — an expired or misconfigured
|
|
58
57
|
* agent credential, an inactive session, or a missing MFA step. The
|
|
59
58
|
* check result cannot be trusted, so `enforce` mode denies instead of
|
|
60
|
-
* failing open.
|
|
61
|
-
* `not_found`, `unknown`) keep the fail-open posture.
|
|
59
|
+
* failing open. Network and rate-limit failures are likewise untrusted.
|
|
62
60
|
*/
|
|
63
61
|
const CHECK_UNTRUSTED_CODES = new Set([
|
|
64
62
|
// The check reached Ory and was refused — an expired or misconfigured agent
|
|
@@ -72,7 +70,27 @@ const CHECK_UNTRUSTED_CODES = new Set([
|
|
|
72
70
|
// cert problem silently downgraded an `enforce` project to `observe`, and
|
|
73
71
|
// breaking TLS was sufficient to disable all tool gating (#218).
|
|
74
72
|
"tls_error",
|
|
73
|
+
"network_error",
|
|
74
|
+
"rate_limited",
|
|
75
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
|
+
}
|
|
76
94
|
/**
|
|
77
95
|
* Max distinct command words checked per shell command (issue #76). A command
|
|
78
96
|
* with more than this is pathological (no legitimate command runs 100+ distinct
|
|
@@ -251,10 +269,13 @@ async function gateToolCall(client, args) {
|
|
|
251
269
|
namespace: args.check.namespace,
|
|
252
270
|
object: args.check.object,
|
|
253
271
|
principals: args.principals,
|
|
254
|
-
|
|
272
|
+
strict: mode === "enforce",
|
|
273
|
+
}).catch((error) => ({ error })),
|
|
255
274
|
]);
|
|
256
275
|
if (!blocked)
|
|
257
276
|
return decision;
|
|
277
|
+
if ("error" in blocked)
|
|
278
|
+
return rejectedCheckDecision(args.check, blocked.error, mode);
|
|
258
279
|
// Already denied on the user's own check — nothing to add to the outcome, but
|
|
259
280
|
// the block is worth recording as the stronger reason it would not have run.
|
|
260
281
|
if (decision.kind === "deny") {
|
|
@@ -317,8 +338,12 @@ async function batchNonShellGate(client, args) {
|
|
|
317
338
|
}
|
|
318
339
|
return applyBlockedDecision(client, args, mode, decision, blocked);
|
|
319
340
|
}
|
|
320
|
-
catch {
|
|
341
|
+
catch (err) {
|
|
321
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);
|
|
322
347
|
const [decision, blocked] = await Promise.all([
|
|
323
348
|
checkAndDecide(client, args.check, { activityAttributes: args.activityAttributes, modeOverride: mode }),
|
|
324
349
|
findBlockedPrincipal(client, {
|
|
@@ -326,8 +351,11 @@ async function batchNonShellGate(client, args) {
|
|
|
326
351
|
object: args.check.object,
|
|
327
352
|
principals: args.principals,
|
|
328
353
|
mcp: args.check.mcp,
|
|
329
|
-
|
|
354
|
+
strict: mode === "enforce",
|
|
355
|
+
}).catch((error) => ({ error })),
|
|
330
356
|
]);
|
|
357
|
+
if (blocked && "error" in blocked)
|
|
358
|
+
return rejectedCheckDecision(args.check, blocked.error, mode);
|
|
331
359
|
return applyBlockedDecision(client, args, mode, decision, blocked);
|
|
332
360
|
}
|
|
333
361
|
}
|
|
@@ -441,9 +469,9 @@ function applyPrincipalBlock(client, ctx) {
|
|
|
441
469
|
* Aggregation: **any deny → deny** (naming the offending word); else any observe
|
|
442
470
|
* → observe (pass through, per-word `permission.observe_deny` events recorded);
|
|
443
471
|
* else allow. A too-complex / unparseable command is **fail-closed** in enforce
|
|
444
|
-
* (deny + alert) and logged-then-passed in observe. A parser-unavailable
|
|
445
|
-
*
|
|
446
|
-
*
|
|
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.
|
|
447
475
|
*/
|
|
448
476
|
async function decomposeAndCheck(client, args) {
|
|
449
477
|
// The word checks do not depend on the top-level result until aggregation, so
|
|
@@ -463,7 +491,7 @@ async function decomposeAndCheck(client, args) {
|
|
|
463
491
|
|| parsed.words.length > MAX_SHELL_WORDS
|
|
464
492
|
|| (parsed.tooComplex && expectedMode === "enforce")
|
|
465
493
|
? undefined
|
|
466
|
-
: fetchShellWordResults(client, args, namespace, parsed.words, subject);
|
|
494
|
+
: fetchShellWordResults(client, { ...args, modeOverride: expectedMode }, namespace, parsed.words, subject);
|
|
467
495
|
const [top, wordFetch] = await Promise.all([topPromise, wordsPromise]);
|
|
468
496
|
// Top-level Bash denied (enforce) or unreachable → no point decomposing.
|
|
469
497
|
if (top.kind === "deny" || top.kind === "fail_open")
|
|
@@ -538,10 +566,10 @@ async function decomposeAndCheck(client, args) {
|
|
|
538
566
|
let sawObserve = false;
|
|
539
567
|
let matchedBlocks = new Map();
|
|
540
568
|
if (words.length > 0) {
|
|
541
|
-
const fetched = wordFetch ?? await fetchShellWordResults(client, args, namespace, words, subject);
|
|
569
|
+
const fetched = wordFetch ?? await fetchShellWordResults(client, { ...args, modeOverride: mode }, namespace, words, subject);
|
|
542
570
|
if (!fetched.ok) {
|
|
543
571
|
const oryErr = fetched.error;
|
|
544
|
-
if (mode === "enforce" &&
|
|
572
|
+
if (mode === "enforce" && isUntrustedCheckError(oryErr)) {
|
|
545
573
|
return {
|
|
546
574
|
kind: "deny",
|
|
547
575
|
result: { allowed: false, checkedAt: new Date().toISOString(), check: args.check },
|
|
@@ -722,6 +750,7 @@ async function fetchShellWordResults(client, args, namespace, words, subject) {
|
|
|
722
750
|
namespace,
|
|
723
751
|
objects: chunk,
|
|
724
752
|
principals: args.principals,
|
|
753
|
+
strict: args.modeOverride === "enforce",
|
|
725
754
|
}),
|
|
726
755
|
]);
|
|
727
756
|
return { ok: true, results: batch.results, blocked };
|
|
@@ -746,8 +775,8 @@ async function fetchShellWordResults(client, args, namespace, words, subject) {
|
|
|
746
775
|
/**
|
|
747
776
|
* Run a permission check and resolve the configured mode against the
|
|
748
777
|
* result. Never throws — infrastructure errors surface as a typed
|
|
749
|
-
* `{ kind: "fail_open" }` decision, and
|
|
750
|
-
* 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
|
|
751
780
|
* {@link CHECK_UNTRUSTED_CODES}).
|
|
752
781
|
*/
|
|
753
782
|
async function checkAndDecide(client, check, opts = {}) {
|
|
@@ -768,9 +797,8 @@ async function checkAndDecide(client, check, opts = {}) {
|
|
|
768
797
|
}
|
|
769
798
|
catch (err) {
|
|
770
799
|
const oryErr = err;
|
|
771
|
-
if (mode === "enforce" &&
|
|
772
|
-
// The
|
|
773
|
-
// 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.
|
|
774
802
|
client.logger.warn("permission.check_rejected", {
|
|
775
803
|
namespace: check.namespace,
|
|
776
804
|
object: check.object,
|
|
@@ -865,6 +893,7 @@ async function findBlockedPrincipal(client, args) {
|
|
|
865
893
|
objects: [args.object],
|
|
866
894
|
principals: args.principals,
|
|
867
895
|
mcp: args.mcp,
|
|
896
|
+
strict: args.strict,
|
|
868
897
|
})).get(args.object);
|
|
869
898
|
}
|
|
870
899
|
async function findBlockedPrincipals(client, args) {
|
|
@@ -910,6 +939,9 @@ async function findBlockedPrincipals(client, args) {
|
|
|
910
939
|
}
|
|
911
940
|
}
|
|
912
941
|
catch (err) {
|
|
942
|
+
const classified = err?.code ? err : client.classifyError(err);
|
|
943
|
+
if (args.strict && isUntrustedCheckError(classified))
|
|
944
|
+
throw classified;
|
|
913
945
|
client.logger.debug("permission.principal_block.failed", {
|
|
914
946
|
scope,
|
|
915
947
|
note: `${scope} principal block read failed; falling open for that scope`,
|
package/dist/types.d.ts
CHANGED
|
@@ -63,8 +63,7 @@ export type OryErrorCode = "session_inactive" | "session_aal2_required" | "forbi
|
|
|
63
63
|
/**
|
|
64
64
|
* TLS/certificate verification failed — an expired, self-signed, or otherwise
|
|
65
65
|
* untrusted certificate on the project host. Distinct from `network_error`
|
|
66
|
-
* because
|
|
67
|
-
* fails open, whereas a certificate failure means we cannot establish *who we
|
|
66
|
+
* because a certificate failure means we cannot establish *who we
|
|
68
67
|
* are talking to*, so the check result cannot be trusted at all. It is also
|
|
69
68
|
* precisely the failure an active network attacker produces, so under
|
|
70
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",
|