@ory/codex 0.1.3 → 0.2.1

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/README.md CHANGED
@@ -109,6 +109,8 @@ The plugin is **fail-open** on its own infrastructure failures (network errors,
109
109
 
110
110
  ### Enable enforcement
111
111
 
112
+ After install the plugin runs in **observe mode**: every tool call is checked against Ory Permissions, but a deny is recorded as a `permission.observe_deny` audit span and the tool runs anyway. This lets you see what *would* be blocked before turning on hard blocking.
113
+
112
114
  1. **Turn on the user gate.** In your shell:
113
115
 
114
116
  ```bash
@@ -117,19 +119,29 @@ The plugin is **fail-open** on its own infrastructure failures (network errors,
117
119
 
118
120
  The next Codex session opens a browser for PKCE login. Subsequent sessions reuse the persisted token until it expires.
119
121
 
120
- 2. **Grant yourself permission to use a tool.** Use the Ory MCP server from inside Codex (*"grant me invoke on the Bash tool"*) or the CLI directly:
122
+ 2. **Bootstrap tuples for the built-in tools.** One idempotent command grants the current user `use` on every tool Codex ships with (shell, apply_patch, …):
123
+
124
+ ```bash
125
+ npx -y -p @ory/codex ory-codex permissions bootstrap
126
+ ```
127
+
128
+ If a user identity is already cached at install time, the installer runs this for you automatically — re-run after adding tools, switching subjects, or changing the namespace.
129
+
130
+ 3. **Check coverage.** `permissions status` probes every tool in the harness's catalog and prints allowed / denied per tool:
121
131
 
122
132
  ```bash
123
- ory create relationship \
124
- --namespace AgentTools \
125
- --object Bash \
126
- --relation invoke \
127
- --subject-id <your-user-subject-id>
133
+ npx -y -p @ory/codex ory-codex permissions status
128
134
  ```
129
135
 
130
- Your subject id is printed at the start of every Codex session when `ORY_AGENT_DEBUG=true`.
136
+ Add tuples for any MCP server tools or custom commands by hand, or via the Ory MCP server from inside Codex (*"grant me use on the shell tool"*).
137
+
138
+ 4. **Promote to enforce.** Once the observe-mode logs look right, switch over:
139
+
140
+ ```bash
141
+ npx -y -p @ory/codex ory-codex permissions enforce
142
+ ```
131
143
 
132
- 3. **See a denial.** Pick a tool you didn't grant (or remove the tuple) and ask Codex to use it. The hook blocks the call and Codex shows the denial reason. The decision is recorded as a `tool.block` trace span.
144
+ Denies now block the tool call; Codex shows the denial reason and the decision is recorded as a `tool.block` trace span with `blocked: true`. Switch back any time with `permissions observe`.
133
145
 
134
146
  ## CLI reference
135
147
 
@@ -137,6 +149,7 @@ The plugin is **fail-open** on its own infrastructure failures (network errors,
137
149
  npx -y -p @ory/codex ory-codex install | uninstall [--global]
138
150
  npx -y -p @ory/codex ory-codex configure [--project-url <url>] [--api-key <key>] [--audit-only]
139
151
  npx -y -p @ory/codex ory-codex agent <status|unregister> Manage the agent's OAuth2 identity
152
+ npx -y -p @ory/codex ory-codex permissions <status|bootstrap|observe|enforce>
140
153
  npx -y -p @ory/codex ory-codex local <up|down|status|seed|logs|env|configure|reset>
141
154
  npx -y -p @ory/codex ory-codex status
142
155
  ```
@@ -144,7 +157,8 @@ npx -y -p @ory/codex ory-codex status
144
157
  Highlights:
145
158
 
146
159
  - `agent status` — show the current persisted DCR identity for the agent.
147
- - `configure --audit-only` — record decisions without blocking; useful for a phased rollout.
160
+ - `permissions observe` / `permissions enforce` — switch between "log denies, allow through" (the install default) and "block denies." `permissions bootstrap` writes `use` tuples for the harness's built-in tools so the promotion path doesn't require hand-writing relationships.
161
+ - `configure --audit-only` — kill switch that disables Ory entirely (no auth, no permission checks; only audit logging of tool invocations). For phased rollouts, prefer `permissions observe` over `--audit-only`.
148
162
  - `local seed` / `local env` — reseed the test user, or print env vars for pointing other tools at the local stack.
149
163
 
150
164
  ## Troubleshooting
package/dist/cli/main.js CHANGED
@@ -53,6 +53,10 @@ function main() {
53
53
  switch (command) {
54
54
  case "install":
55
55
  install(args);
56
+ postInstallPermissions("ory-codex", "codex").then(() => process.exit(0), (err) => {
57
+ console.error(err.message ?? err);
58
+ process.exit(1);
59
+ });
56
60
  break;
57
61
  case "uninstall":
58
62
  uninstall(args);
@@ -66,6 +70,12 @@ function main() {
66
70
  process.exit(1);
67
71
  });
68
72
  break;
73
+ case "permissions":
74
+ (0, argus_1.runPermissionsCommand)("ory-codex", "codex", args).then((code) => process.exit(code), (err) => {
75
+ console.error(err.message ?? err);
76
+ process.exit(1);
77
+ });
78
+ break;
69
79
  case "setup":
70
80
  require("./setup.js");
71
81
  break;
@@ -105,6 +115,12 @@ function manualSetup(isGlobal) {
105
115
  process.argv = ["node", "setup.js", ...setupArgs];
106
116
  require("./setup.js");
107
117
  }
118
+ async function postInstallPermissions(binName, harness) {
119
+ const bootstrapped = await (0, argus_1.maybeAutoBootstrap)(binName, harness);
120
+ (0, argus_1.printPermissionsOnboardingHelp)(binName, harness, {
121
+ bootstrappedAutomatically: bootstrapped,
122
+ });
123
+ }
108
124
  function status() {
109
125
  console.log("Ory Agent Plugin Status (Codex)");
110
126
  console.log("================================");
@@ -129,6 +145,7 @@ Commands:
129
145
  install [--global] Install the Ory hooks into Codex configuration
130
146
  uninstall Remove the Ory hooks from Codex configuration
131
147
  configure Set or view Ory project URL and API key
148
+ permissions <cmd> Manage permission mode and tool permissions (status, bootstrap, observe, enforce)
132
149
  setup [--global] Write hooks directly to config (fallback)
133
150
  status Show plugin status and configuration
134
151
  local <cmd> Manage local Ory dev environment (up, down, status, seed, ...)
package/dist/handlers.js CHANGED
@@ -180,40 +180,65 @@ async function handlePreToolUse(input, client) {
180
180
  subject,
181
181
  spanAttributes: { toolName },
182
182
  });
183
- if (!mcpResult.allowed) {
183
+ const mcpAttrs = {
184
+ toolName,
185
+ mcpServer: mcpTool.serverName,
186
+ mcpTool: mcpTool.toolName,
187
+ ...inputSummary,
188
+ };
189
+ const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
190
+ object: mcpTool.serverName,
191
+ relation: "use",
192
+ subjectId,
193
+ spanAttributes: mcpAttrs,
194
+ });
195
+ if (decision.kind === "allow") {
196
+ client.tracer.record("tool.invoke", "ok", { attributes: mcpAttrs });
197
+ return {};
198
+ }
199
+ if (decision.kind === "observe") {
184
200
  client.tracer.record("tool.block", "denied", {
185
- attributes: { toolName, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName, ...inputSummary, ...(0, argus_1.alertAttributes)(true) },
201
+ attributes: { ...mcpAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
186
202
  });
187
- return {
188
- decision: "block",
189
- reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }),
190
- };
203
+ client.tracer.record("tool.invoke", "ok", {
204
+ attributes: { ...mcpAttrs, allowed: false, observed: true },
205
+ });
206
+ return {};
191
207
  }
192
- client.tracer.record("tool.invoke", "ok", {
193
- attributes: { toolName, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName, ...inputSummary },
194
- });
195
- return {};
196
- }
197
- const namespace = resolveNamespace();
198
- const result = await client.checkPermission({
199
- namespace,
200
- object: toolName,
201
- relation: "use",
202
- ...subject,
203
- }, { spanAttributes: { toolName } });
204
- if (!result.allowed) {
205
208
  client.tracer.record("tool.block", "denied", {
206
- attributes: { toolName, ...inputSummary, allowed: result.allowed, ...(0, argus_1.alertAttributes)(true) },
209
+ attributes: { ...mcpAttrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
207
210
  });
208
211
  return {
209
212
  decision: "block",
210
- reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }),
213
+ reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }),
211
214
  };
212
215
  }
213
- client.tracer.record("tool.invoke", "ok", {
214
- attributes: { toolName, ...inputSummary, allowed: result.allowed },
216
+ const namespace = resolveNamespace();
217
+ const decision = await (0, argus_1.checkAndDecide)(client, { namespace, object: toolName, relation: "use", ...subject }, { spanAttributes: { toolName } });
218
+ if (decision.kind === "fail_open") {
219
+ return handlePermissionError(decision.error, toolName, client);
220
+ }
221
+ const attrs = { toolName, ...inputSummary };
222
+ if (decision.kind === "allow") {
223
+ client.tracer.record("tool.invoke", "ok", { attributes: { ...attrs, allowed: true } });
224
+ return {};
225
+ }
226
+ if (decision.kind === "observe") {
227
+ client.tracer.record("tool.block", "denied", {
228
+ attributes: { ...attrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
229
+ });
230
+ client.tracer.record("tool.invoke", "ok", {
231
+ attributes: { ...attrs, allowed: false, observed: true },
232
+ });
233
+ return {};
234
+ }
235
+ client.tracer.record("tool.block", "denied", {
236
+ attributes: { ...attrs, allowed: false, ...(0, argus_1.alertAttributes)(true) },
215
237
  });
216
- return {};
238
+ return {
239
+ decision: "block",
240
+ reason: (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }),
241
+ };
217
242
  }
218
243
  catch (err) {
219
244
  return handlePermissionError(err, toolName, client);
@@ -271,20 +296,34 @@ async function handlePermissionRequest(input, client) {
271
296
  subject,
272
297
  spanAttributes: { toolName },
273
298
  });
274
- return permissionRequestDecision(mcpResult.allowed ? "allow" : "deny", mcpResult.allowed
275
- ? "Ory MCP server permission granted"
276
- : (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }));
299
+ const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
300
+ object: mcpTool.serverName,
301
+ relation: "use",
302
+ subjectId,
303
+ spanAttributes: { toolName, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName },
304
+ });
305
+ if (decision.kind === "deny") {
306
+ return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, mcp: mcpTool }));
307
+ }
308
+ return permissionRequestDecision("allow", decision.kind === "observe"
309
+ ? "Ory observe mode — denied by policy but allowed through"
310
+ : "Ory MCP server permission granted");
277
311
  }
278
312
  const namespace = resolveNamespace();
279
- const result = await client.checkPermission({
280
- namespace,
281
- object: toolName,
282
- relation: "use",
283
- ...subject,
284
- }, { spanAttributes: { toolName } });
285
- return permissionRequestDecision(result.allowed ? "allow" : "deny", result.allowed
286
- ? "Ory permission granted"
287
- : (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }));
313
+ const decision = await (0, argus_1.checkAndDecide)(client, { namespace, object: toolName, relation: "use", ...subject }, { spanAttributes: { toolName } });
314
+ if (decision.kind === "fail_open") {
315
+ const code = decision.error.code;
316
+ const reason = code === "network_error" || code === "rate_limited"
317
+ ? `Ory unavailable (${code}), falling back to user prompt`
318
+ : `Ory permission check failed: ${decision.error.message}`;
319
+ return permissionRequestFallback(reason);
320
+ }
321
+ if (decision.kind === "deny") {
322
+ return permissionRequestDecision("deny", (0, argus_1.formatDenialMessage)({ tool: toolName, subjectId, namespace }));
323
+ }
324
+ return permissionRequestDecision("allow", decision.kind === "observe"
325
+ ? "Ory observe mode — denied by policy but allowed through"
326
+ : "Ory permission granted");
288
327
  }
289
328
  catch (err) {
290
329
  const oryErr = err;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/codex",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Ory plugin for Codex: scaffolding skills, a local Ory instance, and authentication, authorization, and audit for every tool call",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ory.com",
@@ -67,7 +67,7 @@
67
67
  ".codex-plugin"
68
68
  ],
69
69
  "dependencies": {
70
- "@ory/argus": "0.1.3"
70
+ "@ory/argus": "0.2.1"
71
71
  },
72
72
  "engines": {
73
73
  "node": ">=24"