@ory/argus 1.0.0 → 1.1.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 +5 -4
- package/assets/skills/ory-temporal-worker/SKILL.md +9 -9
- package/assets/skills/permissions-onboarding/SKILL.md +66 -142
- package/dist/adapters.js +1 -1
- package/dist/bash-parser.js +13 -1
- package/dist/build-info.json +4 -4
- package/dist/cli.js +1 -1
- package/dist/contract-suite.js +5 -2
- package/dist/local/manager.js +1 -1
- package/dist/local/seed.d.ts +1 -1
- package/dist/local/seed.js +4 -62
- package/dist/opl.d.ts +11 -18
- package/dist/opl.js +21 -36
- package/dist/permissions-cli.d.ts +2 -2
- package/dist/permissions-cli.js +15 -32
- package/dist/permissions.d.ts +7 -10
- package/dist/permissions.js +16 -28
- package/dist/post-install.d.ts +1 -1
- package/dist/post-install.js +2 -2
- package/dist/runtime.d.ts +5 -0
- package/dist/runtime.js +78 -1
- package/dist/setup.js +1 -1
- package/dist/status-cli.d.ts +1 -1
- package/dist/status-cli.js +12 -12
- package/dist/status-data.d.ts +8 -5
- package/dist/status-data.js +12 -11
- package/package.json +1 -1
package/dist/opl.js
CHANGED
|
@@ -2,19 +2,14 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Ory Permission Language (OPL) schema for the Agent Security permission model.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* permission is otherwise purely existence-based (no path ⇒ no access, any path
|
|
8
|
-
* ⇒ access), so the deny-override is modeled natively in OPL as a permit:
|
|
5
|
+
* Native agent and shell tools are allowed by default and may be explicitly
|
|
6
|
+
* blocked. The deny is modeled natively in OPL as a permit:
|
|
9
7
|
*
|
|
10
|
-
* use =
|
|
8
|
+
* use = !blockedSubjects.includes(subject)
|
|
11
9
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `resolveCheckRelation`), so this model must be applied to the project for
|
|
16
|
-
* checks to resolve; with nothing blocked, `use` resolves identically to a bare
|
|
17
|
-
* `users` existence check, so grants alone allow.
|
|
10
|
+
* Argus always checks the **`use` permit**, so a missing block allows and a
|
|
11
|
+
* matching `blockedSubjects` relation denies. This model must be applied to the
|
|
12
|
+
* project for checks to resolve.
|
|
18
13
|
*
|
|
19
14
|
* ## Naming conventions
|
|
20
15
|
*
|
|
@@ -207,8 +202,7 @@ exports.PERMISSION_MODE_OBJECT = "mode";
|
|
|
207
202
|
*/
|
|
208
203
|
exports.PROJECT_POSTURE_OBJECT = "project";
|
|
209
204
|
/**
|
|
210
|
-
* The permit Argus checks:
|
|
211
|
-
* Keto. A verb, as Keto's own `view` is — it names the question the gate asks.
|
|
205
|
+
* The native-tool permit Argus checks: `!blockedSubjects`, evaluated by Keto.
|
|
212
206
|
*/
|
|
213
207
|
exports.PERMIT_USE = "use";
|
|
214
208
|
/** Conventional user namespace used for SubjectSet-addressed users (`User:<id>`). */
|
|
@@ -263,30 +257,25 @@ function renderPrincipalClass(name) {
|
|
|
263
257
|
return `class ${name} implements Namespace {}`;
|
|
264
258
|
}
|
|
265
259
|
/**
|
|
266
|
-
* Render a tool-namespace class body: the `
|
|
267
|
-
*
|
|
268
|
-
*
|
|
260
|
+
* Render a native tool-namespace class body: the `blockedSubjects` relation and
|
|
261
|
+
* the default-allow `use` permit Argus checks. The single definition is used for
|
|
262
|
+
* both AgentTool and ShellTool; MCP namespaces remain grant-based elsewhere.
|
|
269
263
|
*/
|
|
270
264
|
function renderToolNamespaceClass(namespace = exports.DEFAULT_NAMESPACE) {
|
|
271
265
|
return `class ${namespace} implements Namespace {
|
|
272
266
|
related: {
|
|
273
|
-
// Machine principals are
|
|
267
|
+
// Machine principals are blockable in their own right: \`${subject_js_1.AGENT_NAMESPACE}:<clientId>\` is
|
|
274
268
|
// one session's registered agent credential and \`${subject_js_1.SUBAGENT_NAMESPACE}:<clientId>\` one
|
|
275
269
|
// typed sub-agent credential in that session. Their narrower session- and spawn-scoped forms
|
|
276
270
|
// (\`${subject_js_1.AGENT_NAMESPACE}:<clientId>|<session>\`, \`${subject_js_1.SUBAGENT_NAMESPACE}:<clientId>|<session>|<type>\`) are
|
|
277
271
|
// members of the same namespaces, which is what lets a block name one run.
|
|
278
|
-
// A
|
|
279
|
-
// tool in every tool namespace: \`@(${exports.AGENT_ROLE_NAMESPACE}:<role>#${exports.RELATION_MEMBERS})\`.
|
|
280
|
-
${exports.RELATION_USERS}: (${USER_NAMESPACE} | ${subject_js_1.SESSION_NAMESPACE} | ${subject_js_1.AGENT_NAMESPACE} | ${subject_js_1.SUBAGENT_NAMESPACE} | SubjectSet<${exports.AGENT_ROLE_NAMESPACE}, "${exports.RELATION_MEMBERS}"> | SubjectSet<${namespace}, "${exports.RELATION_USERS}">)[]
|
|
272
|
+
// A block may also name the shared role's members.
|
|
281
273
|
${exports.RELATION_BLOCKED_SUBJECTS}: (${USER_NAMESPACE} | ${subject_js_1.SESSION_NAMESPACE} | ${subject_js_1.AGENT_NAMESPACE} | ${subject_js_1.SUBAGENT_NAMESPACE} | SubjectSet<${exports.AGENT_ROLE_NAMESPACE}, "${exports.RELATION_MEMBERS}"> | SubjectSet<${namespace}, "${exports.RELATION_BLOCKED_SUBJECTS}">)[]
|
|
282
274
|
}
|
|
283
275
|
|
|
284
276
|
permits = {
|
|
285
|
-
//
|
|
286
|
-
// \`${exports.RELATION_USERS}\` grant, regardless of how the grant was reached (direct or
|
|
287
|
-
// via a set).
|
|
277
|
+
// Native tools are allowed unless an explicit block matches.
|
|
288
278
|
${exports.PERMIT_USE}: (ctx: Context): boolean =>
|
|
289
|
-
this.related.${exports.RELATION_USERS}.includes(ctx.subject) &&
|
|
290
279
|
!this.related.${exports.RELATION_BLOCKED_SUBJECTS}.includes(ctx.subject),
|
|
291
280
|
}
|
|
292
281
|
}`;
|
|
@@ -316,15 +305,13 @@ function renderDelegationNamespaceClass(namespace = exports.DELEGATION_NAMESPACE
|
|
|
316
305
|
}`;
|
|
317
306
|
}
|
|
318
307
|
/**
|
|
319
|
-
* Render the role class:
|
|
320
|
-
* One class, one relation — a role is a group of subjects and nothing else.
|
|
308
|
+
* Render the role class: a reusable group whose members can be blocked together.
|
|
321
309
|
*/
|
|
322
310
|
function renderAgentRoleNamespaceClass(namespace = exports.AGENT_ROLE_NAMESPACE) {
|
|
323
311
|
return `class ${namespace} implements Namespace {
|
|
324
312
|
related: {
|
|
325
|
-
// The subjects that belong to the role. A tool
|
|
326
|
-
// \`(${namespace}:<role>#${exports.RELATION_MEMBERS})\`
|
|
327
|
-
// every tool the role holds, in either tool namespace, from one relation.
|
|
313
|
+
// The subjects that belong to the role. A native-tool block can name
|
|
314
|
+
// \`(${namespace}:<role>#${exports.RELATION_MEMBERS})\` to block the group.
|
|
328
315
|
${exports.RELATION_MEMBERS}: (${USER_NAMESPACE} | ${subject_js_1.AGENT_NAMESPACE} | ${subject_js_1.SUBAGENT_NAMESPACE} | SubjectSet<${namespace}, "${exports.RELATION_MEMBERS}">)[]
|
|
329
316
|
}
|
|
330
317
|
}`;
|
|
@@ -408,20 +395,18 @@ function buildAgentSecurityOpl(namespace = exports.DEFAULT_NAMESPACE, shellNames
|
|
|
408
395
|
"// that has no such relation, rather than storing a tuple that never matches.",
|
|
409
396
|
...exports.NODE_REFERENCE_NAMESPACES.map(renderPrincipalClass),
|
|
410
397
|
"",
|
|
411
|
-
`// Harness tool objects (\`${namespace}:Bash\`). \`${exports.
|
|
412
|
-
`//
|
|
413
|
-
|
|
414
|
-
"// blocked.",
|
|
398
|
+
`// Harness tool objects (\`${namespace}:Bash\`). \`${exports.RELATION_BLOCKED_SUBJECTS}\` is an explicit`,
|
|
399
|
+
`// deny. The \`${exports.PERMIT_USE}\` permit is the effective decision Argus checks: allowed`,
|
|
400
|
+
"// unless blocked.",
|
|
415
401
|
renderToolNamespaceClass(namespace),
|
|
416
402
|
"",
|
|
417
403
|
"// Shell command sub-tools (issue #76): each program/builtin word a shell",
|
|
418
404
|
`// command runs is checked as \`${shellNamespace}:<word>#${exports.PERMIT_USE}\`, so a blocked`,
|
|
419
405
|
'// binary (e.g. curl) can\'t be reached through `bash -c "curl …"`. Same',
|
|
420
|
-
"//
|
|
406
|
+
"// default-allow / explicit-block model as the tool namespace above.",
|
|
421
407
|
renderToolNamespaceClass(shellNamespace),
|
|
422
408
|
"",
|
|
423
|
-
"// The shared role
|
|
424
|
-
"// instead of re-granting every tool per identity.",
|
|
409
|
+
"// The shared role whose members can be blocked as a group.",
|
|
425
410
|
renderAgentRoleNamespaceClass(),
|
|
426
411
|
"",
|
|
427
412
|
"// Delegation nodes: the current-state graph of who delegated to whom, written",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* v0.14, and the report is the same.)
|
|
11
11
|
*
|
|
12
12
|
* Everything that *writes* to the project is provisioned in the **Ory Console**
|
|
13
|
-
* (Agent Security), not from the plugin: the permission model,
|
|
14
|
-
*
|
|
13
|
+
* (Agent Security), not from the plugin: the permission model, explicit native
|
|
14
|
+
* blocks, MCP grants, and the observe/enforce posture. That is what lets a
|
|
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
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
* v0.14, and the report is the same.)
|
|
12
12
|
*
|
|
13
13
|
* Everything that *writes* to the project is provisioned in the **Ory Console**
|
|
14
|
-
* (Agent Security), not from the plugin: the permission model,
|
|
15
|
-
*
|
|
14
|
+
* (Agent Security), not from the plugin: the permission model, explicit native
|
|
15
|
+
* blocks, MCP grants, and the observe/enforce posture. That is what lets a
|
|
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.
|
|
@@ -26,7 +26,6 @@ const logger_js_1 = require("./logger.js");
|
|
|
26
26
|
const agent_auth_js_1 = require("./agent-auth.js");
|
|
27
27
|
const tool_catalog_js_1 = require("./tool-catalog.js");
|
|
28
28
|
const subject_js_1 = require("./subject.js");
|
|
29
|
-
const permissions_js_1 = require("./permissions.js");
|
|
30
29
|
const opl_js_1 = require("./opl.js");
|
|
31
30
|
function resolveNamespace() {
|
|
32
31
|
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTool";
|
|
@@ -79,7 +78,7 @@ function printPermissionsHelp(binName) {
|
|
|
79
78
|
console.log(" observe — Log an audit event and allow the tool through. The default.");
|
|
80
79
|
console.log(" enforce — Block the tool. The production posture.");
|
|
81
80
|
console.log("");
|
|
82
|
-
console.log("The plugin only ever *reads* permissions.
|
|
81
|
+
console.log("The plugin only ever *reads* permissions. Native tool blocks, MCP grants,");
|
|
83
82
|
console.log("the permission model, and the observe/enforce posture are all managed in");
|
|
84
83
|
console.log("the Ory Console (Agent Security) by someone with access to the project.");
|
|
85
84
|
console.log("");
|
|
@@ -97,8 +96,8 @@ function printProvisioningMovedToConsole(binName, attempted) {
|
|
|
97
96
|
console.error("");
|
|
98
97
|
console.error("Permissions are provisioned in the Ory Console (Agent Security):");
|
|
99
98
|
console.error(" - the permission model applied to the project");
|
|
100
|
-
console.error(" -
|
|
101
|
-
console.error(" -
|
|
99
|
+
console.error(" - explicit native-tool blocks for a user, agent, or group");
|
|
100
|
+
console.error(" - MCP grants");
|
|
102
101
|
console.error(" - the observe / enforce posture, project-wide or per principal");
|
|
103
102
|
console.error("");
|
|
104
103
|
console.error("The plugin reads that state and enforces it. To see what it resolves:");
|
|
@@ -119,7 +118,7 @@ async function runPermissionsStatus(binName, harness) {
|
|
|
119
118
|
console.log("");
|
|
120
119
|
if (!resolved.security.connected) {
|
|
121
120
|
console.log("Ory Agent Security is not connected, so no permission checks run and there");
|
|
122
|
-
console.log("is no
|
|
121
|
+
console.log("is no native tool access to report. The rest of the plugin is unaffected.");
|
|
123
122
|
console.log("");
|
|
124
123
|
console.log(`Connect with: ${binName} configure --project-url <URL>`);
|
|
125
124
|
console.log("The project URL comes from the Ory Console (Agent Security).");
|
|
@@ -157,7 +156,6 @@ async function runPermissionsStatus(binName, harness) {
|
|
|
157
156
|
const results = await probeCatalog(client, catalog, namespace, subject);
|
|
158
157
|
const allowed = results.filter((r) => r.error === undefined && r.allowed).length;
|
|
159
158
|
const blocked = results.filter((r) => r.blocked).length;
|
|
160
|
-
const denied = results.filter((r) => r.error === undefined && !r.allowed && !r.blocked).length;
|
|
161
159
|
const errored = results.filter((r) => r.error !== undefined).length;
|
|
162
160
|
const widest = Math.max(...catalog.map((t) => t.length), 4);
|
|
163
161
|
console.log(` ${"Tool".padEnd(widest)} Status`);
|
|
@@ -167,53 +165,38 @@ async function runPermissionsStatus(binName, harness) {
|
|
|
167
165
|
? `error (${r.error})`
|
|
168
166
|
: r.blocked
|
|
169
167
|
? "blocked"
|
|
170
|
-
:
|
|
171
|
-
? "allowed"
|
|
172
|
-
: "denied";
|
|
168
|
+
: "allowed";
|
|
173
169
|
console.log(` ${r.tool.padEnd(widest)} ${status}`);
|
|
174
170
|
}
|
|
175
171
|
console.log("");
|
|
176
|
-
console.log(`Summary: ${allowed} allowed, ${
|
|
172
|
+
console.log(`Summary: ${allowed} allowed, ${blocked} blocked, ${errored} errored.`);
|
|
177
173
|
// Shell sub-tools (issue #76): a compact count rather than 100+ rows.
|
|
178
|
-
let
|
|
174
|
+
let shellBlocked = 0;
|
|
179
175
|
if ((0, tool_catalog_js_1.getShellToolCatalog)(harness).length > 0) {
|
|
180
176
|
const shellNamespace = (0, tool_catalog_js_1.resolveShellCommandNamespace)();
|
|
181
177
|
const shellWords = (0, tool_catalog_js_1.getShellCommandCatalog)();
|
|
182
178
|
const shellResults = await probeCatalog(client, shellWords, shellNamespace, subject);
|
|
183
179
|
const sAllowed = shellResults.filter((r) => r.allowed).length;
|
|
184
|
-
|
|
180
|
+
shellBlocked = shellResults.filter((r) => r.blocked).length;
|
|
185
181
|
const sErrored = shellResults.filter((r) => r.error !== undefined).length;
|
|
186
|
-
console.log(`Shell commands (${shellNamespace}): ${sAllowed} allowed, ${
|
|
182
|
+
console.log(`Shell commands (${shellNamespace}): ${sAllowed} allowed, ${shellBlocked} blocked, ` +
|
|
187
183
|
`${sErrored} errored of ${shellWords.length}.`);
|
|
188
184
|
}
|
|
189
|
-
if (denied > 0 || shellDenied > 0) {
|
|
190
|
-
console.log("");
|
|
191
|
-
console.log("Denied tools have no grant for this subject. Grant them in the Ory");
|
|
192
|
-
console.log("Console (Agent Security) — the plugin cannot write permissions.");
|
|
193
|
-
}
|
|
194
185
|
if (blocked > 0) {
|
|
195
186
|
console.log("");
|
|
196
|
-
console.log("'blocked' tools carry an explicit block
|
|
187
|
+
console.log("'blocked' tools carry an explicit native-tool block.");
|
|
197
188
|
console.log("Remove it in the Ory Console (Agent Security).");
|
|
198
189
|
}
|
|
199
190
|
return 0;
|
|
200
191
|
}
|
|
201
192
|
async function probeCatalog(client, catalog, namespace, subject) {
|
|
202
|
-
// Evaluate the
|
|
203
|
-
const relation =
|
|
193
|
+
// Evaluate the default-allow `use` permit, exactly as the runtime gate does.
|
|
194
|
+
const relation = opl_js_1.PERMIT_USE;
|
|
204
195
|
const rows = [];
|
|
205
196
|
for (const tool of catalog) {
|
|
206
197
|
try {
|
|
207
198
|
const result = await client.checkPermission({ namespace, object: tool, relation, ...subject }, { activityAttributes: { toolName: tool, source: "permissions_status" } });
|
|
208
|
-
|
|
209
|
-
if (!result.allowed) {
|
|
210
|
-
// Distinguish an explicit block from a plain missing grant.
|
|
211
|
-
blocked = await client
|
|
212
|
-
.checkPermission({ namespace, object: tool, relation: opl_js_1.RELATION_BLOCKED_SUBJECTS, ...subject }, { activityAttributes: { toolName: tool, source: "permissions_status_block" } })
|
|
213
|
-
.then((r) => r.allowed)
|
|
214
|
-
.catch(() => false);
|
|
215
|
-
}
|
|
216
|
-
rows.push({ tool, allowed: result.allowed, blocked });
|
|
199
|
+
rows.push({ tool, allowed: result.allowed, blocked: !result.allowed });
|
|
217
200
|
}
|
|
218
201
|
catch (err) {
|
|
219
202
|
const ory = err;
|
package/dist/permissions.d.ts
CHANGED
|
@@ -34,10 +34,10 @@ 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` —
|
|
38
|
-
*
|
|
39
|
-
* - `explicit_block` — a `
|
|
40
|
-
* `
|
|
37
|
+
* - `not_granted` — compatibility for grant-based checks outside native
|
|
38
|
+
* AgentTool/ShellTool policy (notably MCP).
|
|
39
|
+
* - `explicit_block` — a native `use` permit denied because a
|
|
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"
|
|
@@ -46,12 +46,9 @@ import type { OryError, OryErrorCode, PermissionCheck, PermissionResult } from "
|
|
|
46
46
|
*/
|
|
47
47
|
export type BlockReason = "not_granted" | "explicit_block";
|
|
48
48
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* always respected — the permission model must therefore be applied to the
|
|
53
|
-
* project (done in the Ory Console, Agent Security) for checks to resolve. Any
|
|
54
|
-
* other relation passes through unchanged.
|
|
49
|
+
* Compatibility shim for callers that still pass the former native `users`
|
|
50
|
+
* relation. Native adapters now pass `use` directly; mapping `users` preserves
|
|
51
|
+
* the public helper contract without reintroducing grant-based native checks.
|
|
55
52
|
*/
|
|
56
53
|
export declare function resolveCheckRelation(relation: string): string;
|
|
57
54
|
/**
|
package/dist/permissions.js
CHANGED
|
@@ -42,12 +42,9 @@ const opl_js_1 = require("./opl.js");
|
|
|
42
42
|
const subject_js_1 = require("./subject.js");
|
|
43
43
|
const tool_catalog_js_1 = require("./tool-catalog.js");
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* always respected — the permission model must therefore be applied to the
|
|
49
|
-
* project (done in the Ory Console, Agent Security) for checks to resolve. Any
|
|
50
|
-
* other relation passes through unchanged.
|
|
45
|
+
* Compatibility shim for callers that still pass the former native `users`
|
|
46
|
+
* relation. Native adapters now pass `use` directly; mapping `users` preserves
|
|
47
|
+
* the public helper contract without reintroducing grant-based native checks.
|
|
51
48
|
*/
|
|
52
49
|
function resolveCheckRelation(relation) {
|
|
53
50
|
return relation === opl_js_1.RELATION_USERS ? opl_js_1.PERMIT_USE : relation;
|
|
@@ -428,10 +425,7 @@ async function decomposeAndCheck(client, args) {
|
|
|
428
425
|
const results = [];
|
|
429
426
|
for (let start = 0; start < words.length; start += batchSize) {
|
|
430
427
|
const chunk = words.slice(start, start + batchSize);
|
|
431
|
-
|
|
432
|
-
// top-level check), so an explicit `ShellTool:<word>#blocked` overrides
|
|
433
|
-
// a `use` grant. With no block present, `access` == bare `use`.
|
|
434
|
-
const wordRelation = resolveCheckRelation("users");
|
|
428
|
+
const wordRelation = opl_js_1.PERMIT_USE;
|
|
435
429
|
const checks = chunk.map((word) => ({
|
|
436
430
|
namespace,
|
|
437
431
|
object: word,
|
|
@@ -482,7 +476,7 @@ async function decomposeAndCheck(client, args) {
|
|
|
482
476
|
const decision = applyPermissionMode(client, blocked ? false : allowed, {
|
|
483
477
|
namespace,
|
|
484
478
|
object: word,
|
|
485
|
-
relation: blocked ? opl_js_1.RELATION_BLOCKED_SUBJECTS : opl_js_1.
|
|
479
|
+
relation: blocked ? opl_js_1.RELATION_BLOCKED_SUBJECTS : opl_js_1.PERMIT_USE,
|
|
486
480
|
subjectId: blocked?.subject ?? subject.subjectId,
|
|
487
481
|
subjectSet: blocked ? undefined : subject.subjectSet,
|
|
488
482
|
activityAttributes: {
|
|
@@ -494,7 +488,7 @@ async function decomposeAndCheck(client, args) {
|
|
|
494
488
|
} : {}),
|
|
495
489
|
},
|
|
496
490
|
modeOverride: args.modeOverride,
|
|
497
|
-
blockReason: blocked ? "explicit_block" : undefined,
|
|
491
|
+
blockReason: blocked || !allowed ? "explicit_block" : undefined,
|
|
498
492
|
});
|
|
499
493
|
if (decision.kind === "deny") {
|
|
500
494
|
deniedWords.push(word);
|
|
@@ -510,17 +504,9 @@ async function decomposeAndCheck(client, args) {
|
|
|
510
504
|
const primaryBlock = primaryBlockedWord ? matchedBlocks.get(primaryBlockedWord) : undefined;
|
|
511
505
|
// Aggregate. Enforce: any denied word blocks. Observe: pass through.
|
|
512
506
|
if (deniedWords.length > 0) {
|
|
513
|
-
//
|
|
514
|
-
// denied word, so the deny event and denial message say "explicitly blocked".
|
|
507
|
+
// A native `use` deny can only be an explicit block.
|
|
515
508
|
const primaryWord = primaryBlockedWord ?? deniedWords[0];
|
|
516
|
-
const blockReason =
|
|
517
|
-
? "explicit_block"
|
|
518
|
-
: await probeBlockReason(client, {
|
|
519
|
-
namespace,
|
|
520
|
-
object: primaryWord,
|
|
521
|
-
relation: opl_js_1.RELATION_USERS,
|
|
522
|
-
...subject,
|
|
523
|
-
});
|
|
509
|
+
const blockReason = "explicit_block";
|
|
524
510
|
return {
|
|
525
511
|
kind: "deny",
|
|
526
512
|
result: {
|
|
@@ -533,7 +519,7 @@ async function decomposeAndCheck(client, args) {
|
|
|
533
519
|
relation: opl_js_1.RELATION_BLOCKED_SUBJECTS,
|
|
534
520
|
subjectId: primaryBlock.subject,
|
|
535
521
|
}
|
|
536
|
-
: { namespace, object: primaryWord, relation: opl_js_1.
|
|
522
|
+
: { namespace, object: primaryWord, relation: opl_js_1.PERMIT_USE, ...subject },
|
|
537
523
|
},
|
|
538
524
|
mode: "enforce",
|
|
539
525
|
activityAttributes: {
|
|
@@ -588,8 +574,8 @@ async function checkAndDecide(client, check, opts = {}) {
|
|
|
588
574
|
const resolved = (0, config_js_1.resolveConfig)();
|
|
589
575
|
const mode = opts.modeOverride ?? resolved.permissionMode;
|
|
590
576
|
const activityAttributes = buildDecisionAttributes(mode, check.subjectId, check.subjectSet);
|
|
591
|
-
//
|
|
592
|
-
//
|
|
577
|
+
// Native adapters pass `use`; `resolveCheckRelation` keeps older public
|
|
578
|
+
// callers that pass `users` on the same default-allow permit.
|
|
593
579
|
const effectiveCheck = {
|
|
594
580
|
...check,
|
|
595
581
|
relation: resolveCheckRelation(check.relation),
|
|
@@ -624,11 +610,13 @@ async function checkAndDecide(client, check, opts = {}) {
|
|
|
624
610
|
// Report the decision against the caller's original check (relation `use`),
|
|
625
611
|
// not the internal permit rewrite, so callers see the coordinates they passed.
|
|
626
612
|
result = { ...result, check };
|
|
627
|
-
//
|
|
628
|
-
//
|
|
613
|
+
// A deny from the native `use` permit is necessarily an explicit block.
|
|
614
|
+
// Retain the diagnostic probe only for other grant-based public checks.
|
|
629
615
|
const blockReason = result.allowed
|
|
630
616
|
? undefined
|
|
631
|
-
:
|
|
617
|
+
: effectiveCheck.relation === opl_js_1.PERMIT_USE
|
|
618
|
+
? "explicit_block"
|
|
619
|
+
: await probeBlockReason(client, check);
|
|
632
620
|
const inner = applyPermissionMode(client, result.allowed, {
|
|
633
621
|
namespace: check.namespace,
|
|
634
622
|
object: check.object,
|
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,
|
|
19
|
+
* OAuth2 client, the permission model, native-tool blocks, MCP grants,
|
|
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,
|
|
20
|
+
* OAuth2 client, the permission model, native-tool blocks, MCP grants,
|
|
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("
|
|
115
|
+
ui.hint("Native-tool blocks, MCP grants, 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/runtime.d.ts
CHANGED
|
@@ -345,6 +345,11 @@ export interface PreparedRuntime {
|
|
|
345
345
|
/** Store directories GC'd after rewiring — reported by `install`. */
|
|
346
346
|
prunedStores: string[];
|
|
347
347
|
}
|
|
348
|
+
/** Serialize installs because npm mutates and prunes the shared prefix in place. */
|
|
349
|
+
export declare function withRuntimeInstallLock<T>(action: () => T, options?: {
|
|
350
|
+
timeoutMs?: number;
|
|
351
|
+
pollMs?: number;
|
|
352
|
+
}): T;
|
|
348
353
|
/**
|
|
349
354
|
* One call per plugin install: resolve the runtime, write the shims, record
|
|
350
355
|
* the wiring, and GC stores nothing references any more.
|
package/dist/runtime.js
CHANGED
|
@@ -104,12 +104,14 @@ exports.removeRuntimeWiring = removeRuntimeWiring;
|
|
|
104
104
|
exports.pruneRuntimeStores = pruneRuntimeStores;
|
|
105
105
|
exports.resolveRuntimeForInstall = resolveRuntimeForInstall;
|
|
106
106
|
exports.requireHookCommand = requireHookCommand;
|
|
107
|
+
exports.withRuntimeInstallLock = withRuntimeInstallLock;
|
|
107
108
|
exports.wireRuntime = wireRuntime;
|
|
108
109
|
exports.checkRuntimeHealth = checkRuntimeHealth;
|
|
109
110
|
exports.describeRuntimeHealth = describeRuntimeHealth;
|
|
110
111
|
const fs = __importStar(require("node:fs"));
|
|
111
112
|
const path = __importStar(require("node:path"));
|
|
112
113
|
const node_child_process_1 = require("node:child_process");
|
|
114
|
+
const node_crypto_1 = require("node:crypto");
|
|
113
115
|
const config_js_1 = require("./config.js");
|
|
114
116
|
// ─── Paths ─────────────────────────────────────────────────────────
|
|
115
117
|
/** Root of the runtime store: `<dataDir>/runtime`. */
|
|
@@ -120,6 +122,9 @@ function getRuntimeRoot() {
|
|
|
120
122
|
function getRuntimeStoreDir(version) {
|
|
121
123
|
return path.join(getRuntimeRoot(), version);
|
|
122
124
|
}
|
|
125
|
+
function getRuntimeInstallLockPath() {
|
|
126
|
+
return path.join(getRuntimeRoot(), ".install.lock");
|
|
127
|
+
}
|
|
123
128
|
/** Directory holding the generated entry shims: `<dataDir>/bin`. */
|
|
124
129
|
function getShimDir() {
|
|
125
130
|
return path.join((0, config_js_1.getDataDir)(), "bin");
|
|
@@ -222,7 +227,16 @@ function materializeRuntime(opts) {
|
|
|
222
227
|
const storeDir = getRuntimeStoreDir(version);
|
|
223
228
|
fs.mkdirSync(storeDir, { recursive: true });
|
|
224
229
|
const extras = opts.extraPackages ?? [`${exports.MCP_SERVER_PACKAGE}@${version}`];
|
|
225
|
-
|
|
230
|
+
// npm --no-save prunes packages omitted from a later install into the same
|
|
231
|
+
// prefix. Reinstall every runtime already wired to this shared store so
|
|
232
|
+
// adding one harness cannot remove another harness's package.
|
|
233
|
+
const retained = Object.values(readRuntimeManifest().harnesses)
|
|
234
|
+
.filter((wiring) => wiring.storeDir &&
|
|
235
|
+
path.resolve(wiring.storeDir) === path.resolve(storeDir))
|
|
236
|
+
.map((wiring) => `${wiring.packageName}@${wiring.version}`);
|
|
237
|
+
const specs = [
|
|
238
|
+
...new Set([`${packageName}@${version}`, ...retained, ...extras]),
|
|
239
|
+
];
|
|
226
240
|
const installer = opts.installer ?? exports.defaultNpmInstaller;
|
|
227
241
|
const result = installer({ prefix: storeDir, specs });
|
|
228
242
|
if (!result.ok) {
|
|
@@ -648,6 +662,66 @@ function requireHookCommand(runtime) {
|
|
|
648
662
|
}
|
|
649
663
|
return runtime.hookCommand;
|
|
650
664
|
}
|
|
665
|
+
const RUNTIME_LOCK_TIMEOUT_MS = 5 * 60_000;
|
|
666
|
+
const RUNTIME_LOCK_POLL_MS = 50;
|
|
667
|
+
/** Serialize installs because npm mutates and prunes the shared prefix in place. */
|
|
668
|
+
function withRuntimeInstallLock(action, options = {}) {
|
|
669
|
+
const root = getRuntimeRoot();
|
|
670
|
+
const lockPath = getRuntimeInstallLockPath();
|
|
671
|
+
const deadline = Date.now() + (options.timeoutMs ?? RUNTIME_LOCK_TIMEOUT_MS);
|
|
672
|
+
fs.mkdirSync(root, { recursive: true });
|
|
673
|
+
let fd;
|
|
674
|
+
const token = `${process.pid}:${(0, node_crypto_1.randomUUID)()}`;
|
|
675
|
+
for (;;) {
|
|
676
|
+
try {
|
|
677
|
+
fd = fs.openSync(lockPath, "wx", 0o600);
|
|
678
|
+
fs.writeSync(fd, token);
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
catch (err) {
|
|
682
|
+
if (err.code !== "EEXIST")
|
|
683
|
+
throw err;
|
|
684
|
+
if (Date.now() > deadline) {
|
|
685
|
+
throw new Error(`Timed out waiting for the Ory runtime install lock at ${lockPath}. ` +
|
|
686
|
+
"Remove it only if no other plugin install is running.");
|
|
687
|
+
}
|
|
688
|
+
sleepRuntimeLock(options.pollMs ?? RUNTIME_LOCK_POLL_MS);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
return action();
|
|
693
|
+
}
|
|
694
|
+
finally {
|
|
695
|
+
try {
|
|
696
|
+
fs.closeSync(fd);
|
|
697
|
+
}
|
|
698
|
+
catch {
|
|
699
|
+
/* best-effort */
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
if (fs.readFileSync(lockPath, "utf8") === token)
|
|
703
|
+
fs.unlinkSync(lockPath);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
/* best-effort */
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
let runtimeLockSleepBuffer;
|
|
711
|
+
function sleepRuntimeLock(ms) {
|
|
712
|
+
if (runtimeLockSleepBuffer === undefined) {
|
|
713
|
+
try {
|
|
714
|
+
runtimeLockSleepBuffer = new Int32Array(new SharedArrayBuffer(4));
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
runtimeLockSleepBuffer = null;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
if (!runtimeLockSleepBuffer) {
|
|
721
|
+
throw new Error("This runtime cannot wait for another Ory plugin install to finish.");
|
|
722
|
+
}
|
|
723
|
+
Atomics.wait(runtimeLockSleepBuffer, 0, 0, ms);
|
|
724
|
+
}
|
|
651
725
|
/**
|
|
652
726
|
* One call per plugin install: resolve the runtime, write the shims, record
|
|
653
727
|
* the wiring, and GC stores nothing references any more.
|
|
@@ -656,6 +730,9 @@ function requireHookCommand(runtime) {
|
|
|
656
730
|
* harness is about to use is never a GC candidate.
|
|
657
731
|
*/
|
|
658
732
|
function wireRuntime(opts) {
|
|
733
|
+
return withRuntimeInstallLock(() => wireRuntimeUnlocked(opts));
|
|
734
|
+
}
|
|
735
|
+
function wireRuntimeUnlocked(opts) {
|
|
659
736
|
const target = resolveRuntimeForInstall({
|
|
660
737
|
packageName: opts.packageName,
|
|
661
738
|
packageRoot: opts.packageRoot,
|
package/dist/setup.js
CHANGED
|
@@ -441,7 +441,7 @@ function printConfiguredNextStepsNow(harnessName, uninstallCmd, opts) {
|
|
|
441
441
|
else {
|
|
442
442
|
ui.hint("It starts in observe mode: every tool call is checked, but nothing is blocked yet.");
|
|
443
443
|
ui.blank();
|
|
444
|
-
ui.bullet("1. See what's happening — config and
|
|
444
|
+
ui.bullet("1. See what's happening — config and native tool access:");
|
|
445
445
|
ui.command(`${npx} status`);
|
|
446
446
|
ui.blank();
|
|
447
447
|
ui.bullet("2. Turn on enforcement once the activity log looks right:");
|
package/dist/status-cli.d.ts
CHANGED
|
@@ -20,7 +20,7 @@ export declare function printAgentIdentitySection(harness: string): void;
|
|
|
20
20
|
export declare function printSubAgentIdentitiesSection(harness: string): void;
|
|
21
21
|
/**
|
|
22
22
|
* Render the "Permissions" block: the deny posture, the namespace, and the
|
|
23
|
-
*
|
|
23
|
+
* allowed/blocked state of the harness's built-in tool catalog.
|
|
24
24
|
*
|
|
25
25
|
* The network probe runs only when a project URL is configured *and* a user
|
|
26
26
|
* identity is available (cached PKCE tokens or `ORY_USER_SUBJECT_ID`). When it
|
package/dist/status-cli.js
CHANGED
|
@@ -161,7 +161,7 @@ function printSubAgentIdentitiesSection(harness) {
|
|
|
161
161
|
}
|
|
162
162
|
/**
|
|
163
163
|
* Render the "Permissions" block: the deny posture, the namespace, and the
|
|
164
|
-
*
|
|
164
|
+
* allowed/blocked state of the harness's built-in tool catalog.
|
|
165
165
|
*
|
|
166
166
|
* The network probe runs only when a project URL is configured *and* a user
|
|
167
167
|
* identity is available (cached PKCE tokens or `ORY_USER_SUBJECT_ID`). When it
|
|
@@ -207,32 +207,32 @@ async function printPermissionsSection(binName, harness) {
|
|
|
207
207
|
if (!probe) {
|
|
208
208
|
return printNoCoverage(cachedMode, namespace, "unavailable (no user identity could be resolved)");
|
|
209
209
|
}
|
|
210
|
-
const { total, allowed,
|
|
210
|
+
const { total, allowed, blocked, errored } = probe;
|
|
211
211
|
console.log(` Mode: ${probe.mode}${formatModeSuffix(probe.modeSource)}`);
|
|
212
212
|
console.log(` Namespace: ${namespace}`);
|
|
213
213
|
console.log(` Subject: ${probe.subject}`);
|
|
214
|
-
if (errored > 0 && allowed === 0 &&
|
|
215
|
-
console.log(`
|
|
214
|
+
if (errored > 0 && allowed === 0 && blocked === 0) {
|
|
215
|
+
console.log(` Tool access: probe failed (${probe.errorCode ?? "unknown"})`);
|
|
216
216
|
return;
|
|
217
217
|
}
|
|
218
218
|
const status = allowed === total
|
|
219
|
-
? `
|
|
219
|
+
? `all allowed (${allowed}/${total} built-in tools)`
|
|
220
220
|
: allowed === 0
|
|
221
|
-
? `
|
|
222
|
-
:
|
|
223
|
-
console.log(`
|
|
224
|
-
if (
|
|
225
|
-
console.log("
|
|
221
|
+
? `all blocked (${blocked}/${total} built-in tools)`
|
|
222
|
+
: `${allowed} allowed, ${blocked} blocked of ${total} built-in tools`;
|
|
223
|
+
console.log(` Tool access: ${status}`);
|
|
224
|
+
if (blocked > 0) {
|
|
225
|
+
console.log(" remove unwanted blocks in the Ory Console (Agent Security)");
|
|
226
226
|
}
|
|
227
227
|
else if (errored > 0) {
|
|
228
228
|
console.log(` ${errored} probe error(s) — run "${(0, cli_invocation_js_1.oryNpx)(binName)} permissions" for details`);
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
|
-
/** The Mode / Namespace /
|
|
231
|
+
/** The Mode / Namespace / Tool access lines for every path that can't probe. */
|
|
232
232
|
function printNoCoverage(mode, namespace, coverage) {
|
|
233
233
|
console.log(` Mode: ${mode}`);
|
|
234
234
|
console.log(` Namespace: ${namespace}`);
|
|
235
|
-
console.log(`
|
|
235
|
+
console.log(` Tool access: ${coverage}`);
|
|
236
236
|
}
|
|
237
237
|
function isUserTokenUsable() {
|
|
238
238
|
const tokens = (0, auth_store_js_1.loadTokens)();
|