@ory/argus 1.0.1 → 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.
@@ -361,7 +361,8 @@ PKCE flow, permission tuples, and activity events are all visible:
361
361
  `jq` — you
362
362
  should see `user.auth` → `agent.auth` → `permission.check` →
363
363
  `tool.complete` for every call.
364
- 5. Promote to enforce once the tools are granted. The posture is a
364
+ 5. Native tools are allowed unless explicitly blocked. Promote to enforce once
365
+ the block policy is ready. The posture is a
365
366
  permission on the Ory project, read on every session — an admin sets it
366
367
  in the Ory Console (Agent Security); see the resolved value with
367
368
  `... permissions` via any harness CLI (same shared config file).
@@ -376,9 +377,9 @@ For full env-var coverage (including the user/agent split,
376
377
  - It does not generate the agent. The user owns the agent loop, tool
377
378
  catalog, and deployment shape. This skill only drops `@ory/argus` into
378
379
  whatever they already have.
379
- - It does not grant permissions. Those are provisioned in the Ory Console
380
- (Agent Security); `@ory/argus` only reads them. Locally, the dev stack's
381
- seed step grants the catalog for you.
380
+ - It does not write permissions. Native-tool blocks and MCP grants are
381
+ provisioned in the Ory Console (Agent Security); `@ory/argus` only reads
382
+ them. The local seed writes no baseline native-tool grants.
382
383
  - It does not adapt one of the existing harness plugins (`@ory/claude-code`,
383
384
  `@ory/codex`, `@ory/gemini-cli`, `@ory/openclaw`, `@ory/opencode`). Those
384
385
  are for users running those harnesses — not building a custom agent.
@@ -244,19 +244,19 @@ The Workflow itself produces no Ory activity — only its Activities do.
244
244
 
245
245
  ## Step 6 — Promotion from observe to enforce
246
246
 
247
- The worker starts in `observe` mode by default: denies pass through but each is
248
- recorded as a `permission.observe_deny` activity event. Once the user has confirmed
249
- the deny set is what they expect, an admin promotes the posture to `enforce` in
247
+ The worker starts in `observe` mode by default: explicit blocks pass through but
248
+ each is recorded as a `permission.block_observed` activity event. Once the user
249
+ has confirmed the block set is what they expect, an admin promotes the posture to `enforce` in
250
250
  the Ory Console (Agent Security). It is a permission on the project, read on
251
251
  every session, so the worker picks it up on its next run with no redeploy.
252
252
 
253
- On a hosted Ory project, grant `use` on each Activity name (`send_email`, …) in
254
- the Ory Console (Agent Security) before promoting the posture there — otherwise
255
- enforcement blocks every Activity. See {{REF_PERMISSIONS_ONBOARDING}}.
253
+ Activities are allowed by default. Add `blockedSubjects` relations for Activity
254
+ names that must not run, then review them before promoting the posture. See
255
+ {{REF_PERMISSIONS_ONBOARDING}}.
256
256
 
257
- To exercise the deny path locally, kick the Workflow with an Activity name the
258
- local stack never granted, and watch it fail with the `Ory denied use of …`
259
- error in the Temporal Web UI.
257
+ To exercise the deny path locally, add a `blockedSubjects` relation for an
258
+ Activity name, kick the Workflow, and watch the explicit-block error in the
259
+ Temporal Web UI.
260
260
 
261
261
  ## Step 7 — Beyond the dev server
262
262
 
@@ -1,195 +1,119 @@
1
1
  ---
2
2
  name: ory-permissions-onboarding
3
- description: Onboard a fresh install onto Ory Permissions for AI agent tool calls. Use when the user has just installed the Ory plugin and wants per-tool authorization enforced without first getting blocked by missing permissions. Covers connecting the plugin, reading coverage in observe mode, and what has to happen in the Ory Console (grants, blocks, enforce posture).
3
+ description: Onboard native agent-tool authorization with default allow and explicit deny overrides. Covers connecting the plugin, reviewing allowed and blocked tools, writing blocks in the Ory Console, and promoting the project from observe to enforce. MCP and PermissionMode remain grant-based.
4
4
  ---
5
5
 
6
- # Onboard onto Ory Permissions for Agent Tool Calls
6
+ # Onboard Ory Permissions for Agent Tool Calls
7
7
 
8
- You are helping a user move a freshly-installed Ory agent plugin from
9
- "disconnected" to "permissions are enforcing", without the common
10
- first-run failure mode of getting every tool call blocked because the
11
- project has no grants yet.
8
+ Help the user connect Agent Security and roll out explicit native-tool blocks
9
+ without treating missing grants as denials.
12
10
 
13
- **The plugin only reads permissions. It never writes them.** Everything
14
- that provisions the project the permission model, per-tool grants,
15
- explicit blocks, and the observe/enforce posture is done in the **Ory
16
- Console** (Agent Security) by someone with access to the project. That
17
- split is deliberate: it means installing and connecting a plugin needs
18
- nothing but project connection details and a public OAuth2 client id, with no
19
- workspace privilege and no project API key.
11
+ **The plugin only reads permissions. It never writes them.** The permission
12
+ model, native-tool blocks, MCP grants, and observe/enforce posture are managed
13
+ in the **Ory Console** (Agent Security).
20
14
 
21
- Two things govern behavior:
15
+ Native policy is simple:
22
16
 
23
- - **Whether Agent Security is connected** it runs when a project URL,
24
- canonical Agent Security origin, and public OAuth2 client id resolve. Existing
25
- configurations fall back to the project URL when the canonical origin is
26
- unset. Not connected means no login and no permission checks, with
27
- tool calls recorded locally. Skills, commands, the local dev stack, and
28
- the MCP server work either way — they don't need a project at all.
29
- - **`permissionMode`** — `observe` or `enforce`, **read from the Ory
30
- project** on every session, and only meaningful once connected.
31
- `observe` (the default) runs every check, records denials, and lets the
32
- tool proceed. `enforce` blocks on deny. There is no local override — an
33
- admin sets it in the Console.
17
+ - `AgentTool` and `ShellTool` have no `users` relation.
18
+ - Their `use` permit is `!blockedSubjects.includes(subject)`.
19
+ - No matching block means allowed; a matching block means denied.
20
+ - `observe` logs the block and proceeds; `enforce` vetoes the call.
21
+ - MCP permissions and `PermissionMode#enforcedSubjects` remain grant-based.
34
22
 
35
- The journey: **install connect → observe → grant in the Console →
36
- enforce in the Console.**
23
+ ## 1. Connect Agent Security
37
24
 
38
- ## Step 1: Connect the plugin to the project
39
-
40
- A fresh install has the developer-experience half working and Agent
41
- Security not connected. Copy the connection details shown in the Ory Console
42
- under **Agent Security**:
25
+ Copy the connection details shown in the Ory Console under **Agent Security**:
43
26
 
44
27
  ```sh
45
28
  {{NPX}} configure --project-url <URL> --agent-security-url <URL>
46
29
  ```
47
30
 
48
- - **project URL** — the project's SDK URL (`https://<slug>.projects.oryapis.com`).
49
- - **Agent Security URL** — the canonical broker origin (`https://agents.console.ory.com` in production).
50
- - **OAuth2 client id** — the project's public login client, provisioned
51
- in the Console. The per-session user login needs it to complete its
52
- PKCE browser flow.
53
-
54
- Then confirm what the plugin resolved:
31
+ Then inspect the resolved configuration:
55
32
 
56
33
  ```sh
57
34
  {{NPX}} status
58
35
  ```
59
36
 
60
- What you want to see:
61
-
62
- - **Project URL**: a real Ory URL or your local dev gateway (not "NOT SET").
63
- - **OAuth2 Client**: the project's public login client (not "NOT SET").
64
- - **Agent Security**: connected. If it says "not connected", it names the
65
- unresolved connection value.
66
-
67
- If the user doesn't have those values, they need them from whoever
68
- administers the project. Don't try to create the client from here — the
69
- plugin has no privilege to, by design.
37
+ Confirm that Agent Security is connected and the public OAuth2 login client is
38
+ resolved. The plugin needs no workspace privilege or project API key.
70
39
 
71
- ## Step 2: Look at the current permission posture
40
+ ## 2. Review Native Tool Access
72
41
 
73
42
  ```sh
74
43
  {{NPX}} permissions
75
44
  ```
76
45
 
77
- This prints:
46
+ The report shows:
78
47
 
79
- - The live **permission mode** (`observe` or `enforce`) and whether it
80
- came from the project, a cached value, or the default.
81
- - The **subject** the user identity permissions are checked against.
82
- - For each tool in this harness's built-in catalog, **allowed / denied /
83
- blocked / errored**, from a real check against the project right now.
84
- - A one-line coverage summary for decomposed shell command words.
48
+ - The live permission mode and its source.
49
+ - The user subject being checked.
50
+ - Each built-in native tool as `allowed`, `blocked`, or `errored`.
51
+ - A compact allowed/blocked summary for decomposed shell command words.
85
52
 
86
- On a freshly-connected project you will usually see every tool reported
87
- as **denied** no grants exist for this user yet. In observe mode that
88
- blocks nobody: tool calls run through, and you are seeing what *would*
89
- be blocked under enforce.
53
+ A fresh project normally reports native tools as **allowed**. There is no grant
54
+ coverage step and the local seed writes no baseline `AgentTool` or `ShellTool`
55
+ grants.
90
56
 
91
- If status prints `Mode: enforce` while tools are denied, tool calls
92
- **are** being blocked right now. The fix is in the Console (grant the
93
- tools, or move the project back to observe) — not from the CLI.
57
+ ## 3. Add Explicit Blocks in the Console
94
58
 
95
- ## Step 3: Run a real session in observe mode
59
+ To block a native tool, write a `blockedSubjects` relation in the Ory Console:
96
60
 
97
- Observe mode is the discovery phase. Use the harness normally, then look
98
- at what was recorded:
99
-
100
- ```sh
101
- {{NPX}} status # summary + recent activity
102
- {{NPX}} watch # live event, decision, and error view
61
+ ```text
62
+ namespace: AgentTool
63
+ object: <tool name> e.g. WebFetch
64
+ relation: blockedSubjects
65
+ subject: User:<id>
103
66
  ```
104
67
 
105
- Launch the harness with `ORY_AGENT_DEBUG=true` to include verbose diagnostics in
106
- the watched log and stderr. Denials appear as `permission.observe_deny` activity events (and
107
- `permission.block_observed` for an explicit block), with the tool name
108
- and the subject. That list is exactly what has to be granted before
109
- enforce is safe.
110
-
111
- ## Step 4: Grant the tools in the Ory Console
68
+ To block a command word reached through a shell tool:
112
69
 
113
- In the Ory Console → **Agent Security**, grant the subject `use` on the
114
- tools it needs. The grant shape the runtime checks is:
115
-
116
- ```
117
- namespace: AgentTool (or whatever ORY_PERMISSION_NAMESPACE is set to)
118
- object: <tool name> (e.g. Read, Bash, WebFetch)
119
- relation: use
120
- subject: User:<id> (a SubjectSet — namespace `User`, object the user id)
70
+ ```text
71
+ namespace: ShellTool
72
+ object: <command word> e.g. curl
73
+ relation: blockedSubjects
74
+ subject: User:<id>
121
75
  ```
122
76
 
123
- Notes worth passing on to the user:
124
-
125
- - The subject is a **SubjectSet** (`User:<id>`), which is what the
126
- Console's *Add relationship* dialog emits and exactly what the runtime
127
- check uses. A bare subject id will not match.
128
- - Granting a **group** and putting users in it scales better than
129
- per-user grants; the check follows subject-set expansion.
130
- - **Shell command words** are authorized separately in the `ShellTool`
131
- namespace (`ShellTool:curl#use`, …) because a denied surface is
132
- otherwise reachable through `bash -c "curl …"`.
133
- - **MCP server tools** are discovered dynamically per session, so they
134
- are not in the built-in catalog and need granting as they come into
135
- scope.
136
-
137
- Re-run `{{NPX}} permissions` afterwards to confirm coverage.
77
+ Blocks can also target the project, an agent credential, an agent session, a
78
+ sub-agent credential, or a sub-agent spawn. Group/SubjectSet expansion is
79
+ supported. A native deny is always an explicit block, never a missing grant.
138
80
 
139
- ## Step 5: Promote to enforce in the Console
81
+ MCP is intentionally different: continue granting MCP server/tool access with
82
+ its existing `users`/`use` and `invoke` relations.
140
83
 
141
- Once the tools the user actually needs report `allowed`, an admin flips
142
- the posture in the Console (Agent Security). The mode is a permission on
143
- the project, so it can be set **project-wide** — covering principals
144
- that don't exist yet — or **per principal** as an exception.
84
+ ## 4. Observe Before Enforcing
145
85
 
146
- Every session reads the live mode, so the change takes effect without
147
- reinstalling or reconfiguring anything. Confirm it landed:
86
+ While the project is in `observe`, run normal sessions and inspect:
148
87
 
149
88
  ```sh
150
89
  {{NPX}} permissions
90
+ {{NPX}} watch
151
91
  ```
152
92
 
153
- ## Step 6: Verify a real session under enforce
93
+ Matching blocks emit `permission.block_observed` but the tool still runs. Review
94
+ those events, then promote the project or selected principals to `enforce` in
95
+ the Ory Console. Every gate reads the project-controlled posture.
154
96
 
155
- Start a normal agent session and confirm:
97
+ ## 5. Verify Enforcement
156
98
 
157
- - **Allowed tools** invoke without complaint.
158
- - **Denied tools** are blocked with a clear "Ory: permission denied"
159
- message naming the tool (or the shell command word).
160
- - The activity log shows `permission.check` events per tool call and
161
- `tool.block` events for denials.
99
+ Under `enforce`:
162
100
 
163
- If a tool is unexpectedly blocked, run `{{NPX}} permissions` to
164
- see whether it is `denied` (no grant) or `blocked` (an explicit
165
- deny-override), then fix it in the Console and re-test.
101
+ - Tools without a matching block run normally.
102
+ - Explicitly blocked tools produce a clear "explicitly blocked" message.
103
+ - A blocked `ShellTool` word blocks the outer shell command and names the word.
104
+ - Infrastructure failures retain the core fail-open behavior.
166
105
 
167
- ## Explicit blocks (deny-override)
168
-
169
- Granting is additive. To express the opposite — a subject is **blocked**
170
- from a tool, beating any grant, including one inherited through a group —
171
- an admin writes a `blockedSubjects` relation in the Console. Ory evaluates it
172
- natively (the `use` permit is "granted AND not blocked"), so it holds
173
- no matter how the grant was reached.
174
-
175
- A block behaves like any other deny: under **observe** it is recorded (a
176
- `permission.block_observed` activity event) but the tool still runs; under
177
- **enforce** it blocks. `permissions` marks blocked tools so they
178
- are distinguishable from a plain missing grant at a glance.
106
+ If a tool is unexpectedly blocked, use `{{NPX}} permissions` to identify the
107
+ block and remove or narrow its `blockedSubjects` relation in the Console.
179
108
 
180
109
  ## Reference
181
110
 
182
111
  | Command | Effect |
183
112
  |---|---|
184
- | `{{NPX}} configure --project-url <URL> --agent-security-url <URL>` | Connect the plugin to the canonical Agent Security broker. |
185
- | `{{NPX}} configure --disconnect` | Turn sign-in and permission checks back off. Skills, commands, the local stack, and activity logging are unaffected. |
186
- | `{{NPX}} permissions` | Print the live mode + per-tool allowed/denied/blocked breakdown. Read-only. |
187
- | `{{NPX}} status` | Configuration, identities, permission coverage, recent activity. |
188
-
189
- Done in the **Ory Console** (Agent Security), not from the CLI: applying
190
- the permission model, granting `use` on tools, writing explicit blocks,
191
- and setting the observe/enforce posture.
192
-
193
- For deeper background on the authentication side of the flow (which
194
- identity is the subject, how the user gate resolves it), see
195
- {{REF_AUTH_SETUP}} and {{REF_LOGIN_FLOW}}.
113
+ | `{{NPX}} configure --project-url <URL> --agent-security-url <URL>` | Connect Agent Security. |
114
+ | `{{NPX}} configure --disconnect` | Disconnect sign-in and permission checks. |
115
+ | `{{NPX}} permissions` | Print live mode plus native allowed/blocked state. Read-only. |
116
+ | `{{NPX}} status` | Print configuration, identities, native tool access, and recent activity. |
117
+
118
+ For authentication and subject-resolution details, see {{REF_AUTH_SETUP}} and
119
+ {{REF_LOGIN_FLOW}}.
package/dist/adapters.js CHANGED
@@ -366,7 +366,7 @@ async function decideTool(client, opts) {
366
366
  const label = (0, subject_js_1.subjectLabel)(subjectRef);
367
367
  const canBlock = opts.canBlock ?? true;
368
368
  const activityAttrs = gateActivityAttributes(opts);
369
- const check = { namespace, object: opts.toolName, relation: "users", ...subjectRef };
369
+ const check = { namespace, object: opts.toolName, relation: "use", ...subjectRef };
370
370
  if (!(0, config_js_1.isSecurityConnected)()) {
371
371
  const outcome = {
372
372
  kind: "not_connected",
@@ -172,6 +172,16 @@ function resolveCommandName(nameNode) {
172
172
  return basename(child.text);
173
173
  }
174
174
  }
175
+ function commandNameIsAmbiguous(nameNode, name) {
176
+ const raw = nameNode.text;
177
+ if (!/[\\'"$]/.test(raw))
178
+ return false;
179
+ if (!name)
180
+ return true;
181
+ if (raw === `"${name}"` || raw === `'${name}'`)
182
+ return false;
183
+ return !raw.endsWith(`/${name}"`) && !raw.endsWith(`/${name}'`);
184
+ }
175
185
  function argNodes(cmd) {
176
186
  return cmd.childrenForFieldName("argument").filter((n) => n !== null);
177
187
  }
@@ -271,7 +281,7 @@ function walk(node, pipeline, parser, state, depth, treeDepth) {
271
281
  }
272
282
  const pid = pipeline >= 0 ? pipeline : state.pipelineCounter++;
273
283
  const name = resolveCommandName(nameNode);
274
- if (name === null)
284
+ if (name === null || commandNameIsAmbiguous(nameNode, name))
275
285
  state.tooComplex = true;
276
286
  const args = argNodes(node);
277
287
  state.commands.push({
@@ -361,6 +371,8 @@ async function extractShellCommands(command) {
361
371
  if (!tree)
362
372
  return { commands: [], tooComplex: true };
363
373
  const state = { commands: [], tooComplex: false, pipelineCounter: 0 };
374
+ if (command.includes("\\\n"))
375
+ state.tooComplex = true;
364
376
  if (tree.rootNode.hasError)
365
377
  state.tooComplex = true;
366
378
  // The walk must never throw out of this function — an uncaught error would
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "repo": "ory-agent-plugins",
3
- "commit": "f803b3fe7be2cd6918652a082a7a4a3eb4ddab53",
4
- "commitShort": "f803b3f",
3
+ "commit": "fc87fd73772d79007d1072c605ede76de42792ce",
4
+ "commitShort": "fc87fd7",
5
5
  "branch": "main",
6
- "commitDate": "2026-08-30T11:50:10-07:00",
6
+ "commitDate": "2026-08-30T17:10:01-07:00",
7
7
  "dirty": false,
8
- "builtAt": "2026-08-30T18:53:36.809Z"
8
+ "builtAt": "2026-08-31T00:13:34.583Z"
9
9
  }
package/dist/cli.js CHANGED
@@ -125,7 +125,7 @@ function runConfigureCommand(binName, args) {
125
125
  console.log(`User subject ns: ${resolved.userSubjectNamespace ?? "User (default)"} [${resolved.userSubjectNamespaceSource}]`);
126
126
  console.log("");
127
127
  if (resolved.security.connected) {
128
- console.log("Sign-in and permission checks are running. Tool grants, explicit blocks,");
128
+ console.log("Sign-in and permission checks are running. Native-tool blocks, MCP grants,");
129
129
  console.log("and the observe/enforce posture are managed in the Ory Console (Agent Security).");
130
130
  console.log("");
131
131
  console.log("To disconnect (skills, commands, the local stack, and activity logging keep working):");
@@ -200,9 +200,12 @@ function runHarnessContractSuite(adapter) {
200
200
  else {
201
201
  (0, vitest_1.expect)(blockEvents).toHaveLength(0);
202
202
  }
203
- // Observe-mode denies also emit the audit event; enforce must not.
203
+ // Native tools are default-allow, so a denied use check is an
204
+ // explicit block and receives the block-specific observe event.
204
205
  const observeEvents = (0, testing_js_1.getActivityEvents)(client, "permission.observe_deny");
205
- (0, vitest_1.expect)(observeEvents).toHaveLength(check === "denied" && mode === "observe" ? 1 : 0);
206
+ const blockObservedEvents = (0, testing_js_1.getActivityEvents)(client, "permission.block_observed");
207
+ (0, vitest_1.expect)(observeEvents).toHaveLength(0);
208
+ (0, vitest_1.expect)(blockObservedEvents).toHaveLength(check === "denied" && mode === "observe" ? 1 : 0);
206
209
  });
207
210
  (0, vitest_1.it)("records tool.invoke when an interactive tool proceeds", async () => {
208
211
  process.env.ORY_INTERACTIVE_TOOLS = adapter.tool;
@@ -849,7 +849,7 @@ function printSeedResult(result) {
849
849
  console.log(` User password: ${result.user.password}`);
850
850
  console.log(` User OAuth2: ${result.user.client.clientId} (PKCE)`);
851
851
  console.log("");
852
- console.log(` Permissions: ${result.permissions.tuples} entries in '${result.permissions.namespace}' for ${result.permissions.subject}`);
852
+ console.log(` Native tool grants: none (default allow; explicit blocks only)`);
853
853
  console.log("");
854
854
  console.log("To use with any Ory agent plugin, set these environment variables:");
855
855
  console.log("");
@@ -53,7 +53,7 @@ export interface SeedResult {
53
53
  /** OAuth2 authorization_code+PKCE client for interactive logins. */
54
54
  client: SeededOAuth2Client;
55
55
  };
56
- /** Permission tuples written against the user's subject. */
56
+ /** Native baseline permission tuples written against the user's subject (always zero). */
57
57
  permissions: {
58
58
  namespace: string;
59
59
  subject: string;
@@ -27,14 +27,11 @@ exports.USER_SUBJECT_NAMESPACE = exports.USER_CLIENT_ID = void 0;
27
27
  exports.seedLocalEnvironment = seedLocalEnvironment;
28
28
  const auth_js_1 = require("../auth.js");
29
29
  const branding_js_1 = require("../branding.js");
30
- const tool_catalog_js_1 = require("../tool-catalog.js");
31
30
  const subject_js_1 = require("../subject.js");
32
- const opl_js_1 = require("../opl.js");
33
31
  const configs_js_1 = require("./configs.js");
34
32
  const ports_js_1 = require("./ports.js");
35
33
  const health_js_1 = require("./health.js");
36
34
  const KRATOS_ADMIN = (0, ports_js_1.hostUrl)(configs_js_1.LOCAL_STACK_HOST, configs_js_1.KRATOS_ADMIN_PORT);
37
- const KETO_WRITE = (0, ports_js_1.hostUrl)(configs_js_1.LOCAL_STACK_HOST, configs_js_1.KETO_WRITE_PORT);
38
35
  const HYDRA_ADMIN = (0, ports_js_1.hostUrl)(configs_js_1.LOCAL_STACK_HOST, configs_js_1.HYDRA_ADMIN_PORT);
39
36
  const USER_EMAIL = "user@ory-local.dev";
40
37
  const USER_PASSWORD = "ory-user-local-dev-password!";
@@ -49,11 +46,6 @@ const USER_PASSWORD = "ory-user-local-dev-password!";
49
46
  * dragging the value out of the `local up` banner.
50
47
  */
51
48
  exports.USER_CLIENT_ID = branding_js_1.USER_LOGIN_CLIENT_ID;
52
- /**
53
- * Tools the explicit local-stack command seeds tuples for. Sourced from the
54
- * shared catalog so one seed covers every harness's built-in tool set.
55
- */
56
- const COMMON_TOOLS = tool_catalog_js_1.ALL_TOOLS;
57
49
  async function jsonFetch(url, opts = {}) {
58
50
  const res = await fetch(url, {
59
51
  ...opts,
@@ -94,44 +86,6 @@ async function seedIdentity(email, password, name) {
94
86
  * install config all address the user under the same namespace.
95
87
  */
96
88
  exports.USER_SUBJECT_NAMESPACE = subject_js_1.DEFAULT_USER_SUBJECT_NAMESPACE;
97
- /**
98
- * Write permission tuples granting the user identity access to all
99
- * common tools. The subject is written as a SubjectSet
100
- * `User:<userId>` (no relation) so the Console UI's *Add relationship*
101
- * dialog — which only emits SubjectSet subjects — can recreate the same
102
- * tuple by hand during the runbook walkthrough.
103
- */
104
- async function seedPermissions(userId, namespace, objects = COMMON_TOOLS) {
105
- let created = 0;
106
- for (const object of objects) {
107
- try {
108
- await jsonFetch(`${KETO_WRITE}/admin/relation-tuples`, {
109
- method: "PUT",
110
- body: JSON.stringify({
111
- namespace,
112
- object,
113
- relation: opl_js_1.RELATION_USERS,
114
- subject_set: {
115
- namespace: exports.USER_SUBJECT_NAMESPACE,
116
- object: userId,
117
- relation: "",
118
- },
119
- }),
120
- });
121
- created++;
122
- }
123
- catch (err) {
124
- const msg = err instanceof Error ? err.message : String(err);
125
- if (!msg.includes("409")) {
126
- process.stderr.write(` Warning: failed to create permission for ${object}: ${msg}\n`);
127
- }
128
- else {
129
- created++;
130
- }
131
- }
132
- }
133
- return created;
134
- }
135
89
  /**
136
90
  * Delete a Hydra OAuth2 client if it exists, then create it at a known
137
91
  * client_id (with a known secret, for a confidential client). We always
@@ -209,22 +163,10 @@ async function seedLocalEnvironment(namespace = "AgentTool") {
209
163
  process.stderr.write(" Creating user identity...\n");
210
164
  const userIdentity = await seedIdentity(USER_EMAIL, USER_PASSWORD, "Local User");
211
165
  process.stderr.write(` User: ${userIdentity.id} (${userIdentity.email})\n`);
212
- // 2. Permission tuples written against the user's identity since the
213
- // user is the subject of permission checks in the plugin handlers.
214
- // Subject is a SubjectSet `User:<id>` so the Console UI can rewrite
215
- // the same tuple shape by hand during the runbook demo.
166
+ // 2. Native tools are allowed by default. Seed no AgentTool or ShellTool
167
+ // baseline grants; local behavior must match hosted default-allow policy.
216
168
  const subjectLabel = `${exports.USER_SUBJECT_NAMESPACE}:${userIdentity.id}`;
217
- process.stderr.write(` Creating permissions for ${COMMON_TOOLS.length} tools...\n`);
218
- const tupleCount = await seedPermissions(userIdentity.id, namespace);
219
- process.stderr.write(` Permissions: ${tupleCount} entries in '${namespace}' for ${subjectLabel}\n`);
220
- // Shell-command sub-tools (issue #76): grant every catalog word in the
221
- // ShellTool namespace so a launched harness can run `bash -c "curl …"`
222
- // out of the box, and the enforce demo (revoke `ShellTool:curl#users`,
223
- // watch it block) works without hand-writing tuples first.
224
- const shellWords = (0, tool_catalog_js_1.getShellCommandCatalog)();
225
- const shellNamespace = (0, tool_catalog_js_1.resolveShellCommandNamespace)();
226
- const shellCount = await seedPermissions(userIdentity.id, shellNamespace, shellWords);
227
- process.stderr.write(` Shell commands: ${shellCount} entries in '${shellNamespace}' for ${subjectLabel}\n`);
169
+ process.stderr.write(" Native tools: allowed unless explicitly blocked (no baseline grants)\n");
228
170
  // 3. User OAuth2 client (PKCE) — pre-registered so the user gate's
229
171
  // PKCE flow has somewhere to authenticate against. The agent's
230
172
  // OAuth2 client is intentionally NOT pre-registered; the harness
@@ -253,6 +195,6 @@ async function seedLocalEnvironment(namespace = "AgentTool") {
253
195
  }
254
196
  return {
255
197
  user: { identity: userIdentity, password: USER_PASSWORD, client: userClient },
256
- permissions: { namespace, subject: subjectLabel, tuples: tupleCount },
198
+ permissions: { namespace, subject: subjectLabel, tuples: 0 },
257
199
  };
258
200
  }
package/dist/opl.d.ts CHANGED
@@ -1,19 +1,14 @@
1
1
  /**
2
2
  * Ory Permission Language (OPL) schema for the Agent Security permission model.
3
3
  *
4
- * Argus relies on more than relation-tuple *existence*: a subject may be granted
5
- * a tool and separately **blocked** from it, and the block must win. Keto
6
- * permission is otherwise purely existence-based (no path ⇒ no access, any path
7
- * ⇒ access), so the deny-override is modeled natively in OPL as a permit:
4
+ * Native agent and shell tools are allowed by default and may be explicitly
5
+ * blocked. The deny is modeled natively in OPL as a permit:
8
6
  *
9
- * use = users.includes(subject) && !blockedSubjects.includes(subject)
7
+ * use = !blockedSubjects.includes(subject)
10
8
  *
11
- * Keto evaluates the permit in a single check, so a `blockedSubjects` relation
12
- * beats a `users` grant no matter how the grant was reached (direct subject or
13
- * subject-set expansion). Argus always checks the **`use` permit** (see
14
- * `resolveCheckRelation`), so this model must be applied to the project for
15
- * checks to resolve; with nothing blocked, `use` resolves identically to a bare
16
- * `users` existence check, so grants alone allow.
9
+ * Argus always checks the **`use` permit**, so a missing block allows and a
10
+ * matching `blockedSubjects` relation denies. This model must be applied to the
11
+ * project for checks to resolve.
17
12
  *
18
13
  * ## Naming conventions
19
14
  *
@@ -194,8 +189,7 @@ export declare const PERMISSION_MODE_OBJECT = "mode";
194
189
  */
195
190
  export declare const PROJECT_POSTURE_OBJECT = "project";
196
191
  /**
197
- * The permit Argus checks: `users && !blockedSubjects`, evaluated natively by
198
- * Keto. A verb, as Keto's own `view` is — it names the question the gate asks.
192
+ * The native-tool permit Argus checks: `!blockedSubjects`, evaluated by Keto.
199
193
  */
200
194
  export declare const PERMIT_USE = "use";
201
195
  /**
@@ -236,9 +230,9 @@ export declare function renderOplImport(): string;
236
230
  /** Render a principal-namespace class (an empty `implements Namespace`). */
237
231
  export declare function renderPrincipalClass(name: string): string;
238
232
  /**
239
- * Render a tool-namespace class body: the `users` grant, the `blockedSubjects`
240
- * deny-override, and the `use` permit (`users && !blockedSubjects`) Argus
241
- * checks. The single definition of that class, used for both tool namespaces.
233
+ * Render a native tool-namespace class body: the `blockedSubjects` relation and
234
+ * the default-allow `use` permit Argus checks. The single definition is used for
235
+ * both AgentTool and ShellTool; MCP namespaces remain grant-based elsewhere.
242
236
  */
243
237
  export declare function renderToolNamespaceClass(namespace?: string): string;
244
238
  /**
@@ -248,8 +242,7 @@ export declare function renderToolNamespaceClass(namespace?: string): string;
248
242
  */
249
243
  export declare function renderDelegationNamespaceClass(namespace?: string): string;
250
244
  /**
251
- * Render the role class: the members of a shared role a project grants tools to.
252
- * One class, one relation — a role is a group of subjects and nothing else.
245
+ * Render the role class: a reusable group whose members can be blocked together.
253
246
  */
254
247
  export declare function renderAgentRoleNamespaceClass(namespace?: string): string;
255
248
  /** Render a harness/sub-agent type group whose members inherit posture. */
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
- * Argus relies on more than relation-tuple *existence*: a subject may be granted
6
- * a tool and separately **blocked** from it, and the block must win. Keto
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 = users.includes(subject) && !blockedSubjects.includes(subject)
8
+ * use = !blockedSubjects.includes(subject)
11
9
  *
12
- * Keto evaluates the permit in a single check, so a `blockedSubjects` relation
13
- * beats a `users` grant no matter how the grant was reached (direct subject or
14
- * subject-set expansion). Argus always checks the **`use` permit** (see
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: `users && !blockedSubjects`, evaluated natively by
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 `users` grant, the `blockedSubjects`
267
- * deny-override, and the `use` permit (`users && !blockedSubjects`) Argus
268
- * checks. The single definition of that class, used for both tool namespaces.
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 grantable in their own right: \`${subject_js_1.AGENT_NAMESPACE}:<clientId>\` is
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 grant may also name the shared role, so one membership covers every
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
- // Deny-override: any matching \`${exports.RELATION_BLOCKED_SUBJECTS}\` relation beats a
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: the members of a shared role a project grants tools to.
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 grant names
326
- // \`(${namespace}:<role>#${exports.RELATION_MEMBERS})\` as its subject, so adding a member grants
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.RELATION_USERS}\` is the set of subjects`,
412
- `// granted the tool; \`${exports.RELATION_BLOCKED_SUBJECTS}\` is an explicit deny-override. The`,
413
- `// \`${exports.PERMIT_USE}\` permit is the effective decision Argus checks: granted AND not`,
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
- "// grant / block / use model as the tool namespace above.",
406
+ "// default-allow / explicit-block model as the tool namespace above.",
421
407
  renderToolNamespaceClass(shellNamespace),
422
408
  "",
423
- "// The shared role a project grants tools to, so an admin adds a member",
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, the per-tool
14
- * grants, explicit blocks, and the observe/enforce posture. That is what lets a
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.
@@ -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, the per-tool
15
- * grants, explicit blocks, and the observe/enforce posture. That is what lets a
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. Tool grants, explicit blocks,");
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(" - per-tool grants for a user, agent, or group");
101
- console.error(" - explicit blocks (a deny that beats any grant)");
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 coverage to report. The rest of the plugin is unaffected.");
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
- : r.allowed
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, ${denied} denied, ${blocked} blocked, ${errored} errored.`);
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 shellDenied = 0;
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
- shellDenied = shellResults.filter((r) => r.error === undefined && !r.allowed).length;
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, ${shellDenied} denied, ` +
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 that overrides any grant.");
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 effective `use` permit, exactly as the runtime gate does.
203
- const relation = (0, permissions_js_1.resolveCheckRelation)("users");
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
- let blocked = false;
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;
@@ -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` — the subject simply has no `use` grant on the object
38
- * (the legacy "deny by absence" case).
39
- * - `explicit_block` — a `blocked` relation matched the subject, so the
40
- * `access` permit's deny-override fired even if a `use` grant exists.
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
- * Map the logical grant relation a caller passes (`use`) onto the relation or
50
- * permit Argus actually checks. A `use` check always routes through the
51
- * block-aware `access` permit (`use && !blocked`), so an explicit block is
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
  /**
@@ -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
- * Map the logical grant relation a caller passes (`use`) onto the relation or
46
- * permit Argus actually checks. A `use` check always routes through the
47
- * block-aware `access` permit (`use && !blocked`), so an explicit block is
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
- // Route each word through the block-aware `access` permit (like the
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.RELATION_USERS,
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
- // Disambiguate an explicit block from a plain missing grant on the first
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 = primaryBlock
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.RELATION_USERS, ...subject },
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
- // Route a `use` grant check through the block-aware `access` permit so
592
- // explicit blocks are always respected.
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
- // On a deny, disambiguate "explicitly blocked" from "never granted" so the
628
- // audit trail and denial messaging can tell them apart. Best-effort.
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
- : await probeBlockReason(client, check);
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,
@@ -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, the per-tool grants, explicit blocks,
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
@@ -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, the per-tool grants, explicit blocks,
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("Tool grants, explicit blocks, and the observe/enforce posture are managed in the Ory Console (Agent Security).");
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/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 permission coverage:");
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:");
@@ -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
- * coverage of the harness's built-in tool catalog.
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
@@ -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
- * coverage of the harness's built-in tool catalog.
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, denied, errored } = probe;
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 && denied === 0) {
215
- console.log(` Coverage: probe failed (${probe.errorCode ?? "unknown"})`);
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
- ? `fully granted (${allowed}/${total} built-in tools)`
219
+ ? `all allowed (${allowed}/${total} built-in tools)`
220
220
  : allowed === 0
221
- ? `no grants (0/${total} built-in tools)`
222
- : `partial (${allowed}/${total} built-in tools allowed)`;
223
- console.log(` Coverage: ${status}`);
224
- if (denied > 0) {
225
- console.log(" grant the missing tools in the Ory Console (Agent Security)");
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 / Coverage lines for every path that can't probe. */
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(` Coverage: ${coverage}`);
235
+ console.log(` Tool access: ${coverage}`);
236
236
  }
237
237
  function isUserTokenUsable() {
238
238
  const tokens = (0, auth_store_js_1.loadTokens)();
@@ -16,6 +16,8 @@ import { type StatusSystemSection } from "./status-system.js";
16
16
  export interface PermissionsCoverage {
17
17
  total: number;
18
18
  allowed: number;
19
+ blocked: number;
20
+ /** @deprecated Native false results are explicit blocks; use `blocked`. */
19
21
  denied: number;
20
22
  errored: number;
21
23
  errorCode?: string;
@@ -32,14 +34,13 @@ export interface PermissionsCoverage {
32
34
  }
33
35
  /**
34
36
  * Probe the harness's built-in tool catalog against Ory, returning how many
35
- * tools the resolved user subject is allowed / denied / errored on, plus the
37
+ * tools the resolved user subject is allowed / blocked / errored on, plus the
36
38
  * live permission mode and the subject that was checked. Returns undefined when
37
39
  * a probe can't run (no project URL, empty catalog, no user identity). Never
38
40
  * throws.
39
41
  *
40
- * Checks route through {@link resolveCheckRelation}, i.e. the `access` permit
41
- * the same relation the runtime gate evaluates so a tool carrying an explicit
42
- * block is reported as denied here too, exactly as a tool call would be.
42
+ * Checks use the same default-allow `use` permit as the runtime gate. A false
43
+ * result therefore means an explicit block, not missing grant coverage.
43
44
  */
44
45
  export declare function probePermissionsCoverage(harness: string): Promise<PermissionsCoverage | undefined>;
45
46
  export interface StatusConfigSection {
@@ -141,7 +142,9 @@ export interface StatusSubAgentSection {
141
142
  }
142
143
  export type PermissionsCoverageStatus =
143
144
  /** Agent Security isn't connected — no project URL and/or OAuth2 client id. */
144
- "not_connected" | "no_catalog" | "no_user" | "fully_granted" | "partial" | "no_grants" | "probe_failed";
145
+ "not_connected" | "no_catalog" | "no_user" | "fully_allowed" | "partially_blocked" | "fully_blocked"
146
+ /** @deprecated Pre-default-allow status names retained for API compatibility. */
147
+ | "fully_granted" | "partial" | "no_grants" | "probe_failed";
145
148
  export interface StatusPermissionsSection {
146
149
  mode: "observe" | "enforce";
147
150
  /** Where `mode` came from: a live project read, the local cache, or the default. */
@@ -50,7 +50,6 @@ exports.collectStatusReport = collectStatusReport;
50
50
  */
51
51
  const config_js_1 = require("./config.js");
52
52
  const permission_mode_js_1 = require("./permission-mode.js");
53
- const permissions_js_1 = require("./permissions.js");
54
53
  const auth_store_js_1 = require("./auth-store.js");
55
54
  const agent_auth_js_1 = require("./agent-auth.js");
56
55
  const subject_js_1 = require("./subject.js");
@@ -69,14 +68,13 @@ function resolveNamespace() {
69
68
  }
70
69
  /**
71
70
  * Probe the harness's built-in tool catalog against Ory, returning how many
72
- * tools the resolved user subject is allowed / denied / errored on, plus the
71
+ * tools the resolved user subject is allowed / blocked / errored on, plus the
73
72
  * live permission mode and the subject that was checked. Returns undefined when
74
73
  * a probe can't run (no project URL, empty catalog, no user identity). Never
75
74
  * throws.
76
75
  *
77
- * Checks route through {@link resolveCheckRelation}, i.e. the `access` permit
78
- * the same relation the runtime gate evaluates so a tool carrying an explicit
79
- * block is reported as denied here too, exactly as a tool call would be.
76
+ * Checks use the same default-allow `use` permit as the runtime gate. A false
77
+ * result therefore means an explicit block, not missing grant coverage.
80
78
  */
81
79
  async function probePermissionsCoverage(harness) {
82
80
  const resolved = (0, config_js_1.resolveConfig)();
@@ -106,10 +104,11 @@ async function probePermissionsCoverage(harness) {
106
104
  additionalSubjects: agentSubject ? [{ scope: "agent", subject: agentSubject }] : [],
107
105
  });
108
106
  const namespace = resolveNamespace();
109
- const relation = (0, permissions_js_1.resolveCheckRelation)("users");
107
+ const relation = opl_js_1.PERMIT_USE;
110
108
  const result = {
111
109
  total: catalog.length,
112
110
  allowed: 0,
111
+ blocked: 0,
113
112
  denied: 0,
114
113
  errored: 0,
115
114
  mode: live.mode,
@@ -124,8 +123,10 @@ async function probePermissionsCoverage(harness) {
124
123
  });
125
124
  if (probe.allowed)
126
125
  result.allowed++;
127
- else
126
+ else {
127
+ result.blocked++;
128
128
  result.denied++;
129
+ }
129
130
  }
130
131
  catch (err) {
131
132
  const ory = err;
@@ -274,14 +275,14 @@ async function collectPermissions(harness) {
274
275
  return { ...base, coverageStatus: "no_user" };
275
276
  // The probe read the mode live; prefer it over the cached config value.
276
277
  const withMode = { ...base, mode: coverage.mode, modeSource: coverage.modeSource };
277
- if (coverage.errored > 0 && coverage.allowed === 0 && coverage.denied === 0) {
278
+ if (coverage.errored > 0 && coverage.allowed === 0 && coverage.blocked === 0) {
278
279
  return { ...withMode, coverageStatus: "probe_failed", coverage };
279
280
  }
280
281
  const status = coverage.allowed === coverage.total
281
- ? "fully_granted"
282
+ ? "fully_allowed"
282
283
  : coverage.allowed === 0
283
- ? "no_grants"
284
- : "partial";
284
+ ? "fully_blocked"
285
+ : "partially_blocked";
285
286
  return { ...withMode, coverageStatus: status, coverage };
286
287
  }
287
288
  function isUserTokenUsable() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/argus",
3
- "version": "1.0.1",
3
+ "version": "1.1.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",