@ory/opencode 0.1.3 → 0.2.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/README.md CHANGED
@@ -111,6 +111,8 @@ The plugin is **fail-open** on its own infrastructure failures (network errors,
111
111
 
112
112
  ### Enable enforcement
113
113
 
114
+ 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.
115
+
114
116
  1. **Turn on the user gate.** In your shell:
115
117
 
116
118
  ```bash
@@ -119,19 +121,29 @@ The plugin is **fail-open** on its own infrastructure failures (network errors,
119
121
 
120
122
  The next OpenCode session refreshes or prompts for PKCE login. Subsequent sessions reuse the persisted token until it expires.
121
123
 
122
- 2. **Grant yourself permission to use a tool.** Use the Ory MCP server from inside OpenCode (*"grant me invoke on the Bash tool"*) or the CLI directly:
124
+ 2. **Bootstrap tuples for the built-in tools.** One idempotent command grants the current user `use` on every tool OpenCode ships with (read, write, edit, bash, …):
125
+
126
+ ```bash
127
+ npx -y -p @ory/opencode ory-opencode permissions bootstrap
128
+ ```
129
+
130
+ 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.
131
+
132
+ 3. **Check coverage.** `permissions status` probes every tool in the harness's catalog and prints allowed / denied per tool:
123
133
 
124
134
  ```bash
125
- ory create relationship \
126
- --namespace AgentTools \
127
- --object Bash \
128
- --relation invoke \
129
- --subject-id <your-user-subject-id>
135
+ npx -y -p @ory/opencode ory-opencode permissions status
130
136
  ```
131
137
 
132
- Your subject id is printed at the start of every OpenCode session when `ORY_AGENT_DEBUG=true`.
138
+ Add tuples for any MCP server tools or custom commands by hand, or via the Ory MCP server from inside OpenCode (*"grant me use on the bash tool"*).
139
+
140
+ 4. **Promote to enforce.** Once the observe-mode logs look right, switch over:
141
+
142
+ ```bash
143
+ npx -y -p @ory/opencode ory-opencode permissions enforce
144
+ ```
133
145
 
134
- 3. **See a denial.** Pick a tool you didn't grant (or remove the tuple) and ask OpenCode to use it. The hook blocks the call and OpenCode shows the denial reason. The decision is recorded as a `tool.block` trace span.
146
+ Denies now ask the harness to block via a thrown `OryDenialError`; OpenCode shows the denial reason and the decision is recorded as a `tool.block` trace span. Switch back any time with `permissions observe`.
135
147
 
136
148
  ## CLI reference
137
149
 
@@ -139,6 +151,7 @@ The plugin is **fail-open** on its own infrastructure failures (network errors,
139
151
  npx -y -p @ory/opencode ory-opencode install | uninstall [--project-dir <path>]
140
152
  npx -y -p @ory/opencode ory-opencode configure [--project-url <url>] [--api-key <key>] [--audit-only]
141
153
  npx -y -p @ory/opencode ory-opencode agent <status|unregister> Manage the agent's OAuth2 identity
154
+ npx -y -p @ory/opencode ory-opencode permissions <status|bootstrap|observe|enforce>
142
155
  npx -y -p @ory/opencode ory-opencode local <up|down|status|seed|logs|env|configure|reset>
143
156
  npx -y -p @ory/opencode ory-opencode status [--project-dir <path>]
144
157
  ```
@@ -146,7 +159,8 @@ npx -y -p @ory/opencode ory-opencode status [--projec
146
159
  Highlights:
147
160
 
148
161
  - `agent status` — show the current persisted DCR identity for the agent.
149
- - `configure --audit-only` — record decisions without blocking; useful for a phased rollout.
162
+ - `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.
163
+ - `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`.
150
164
  - `local seed` / `local env` — reseed the test user, or print env vars for pointing other tools at the local stack.
151
165
 
152
166
  ## Troubleshooting
package/dist/cli/main.js CHANGED
@@ -51,6 +51,10 @@ function main() {
51
51
  switch (command) {
52
52
  case "install":
53
53
  install(args);
54
+ postInstallPermissions("ory-opencode", "opencode").then(() => process.exit(0), (err) => {
55
+ console.error(err.message ?? err);
56
+ process.exit(1);
57
+ });
54
58
  break;
55
59
  case "uninstall":
56
60
  uninstall(args);
@@ -64,6 +68,12 @@ function main() {
64
68
  process.exit(1);
65
69
  });
66
70
  break;
71
+ case "permissions":
72
+ (0, argus_1.runPermissionsCommand)("ory-opencode", "opencode", args).then((code) => process.exit(code), (err) => {
73
+ console.error(err.message ?? err);
74
+ process.exit(1);
75
+ });
76
+ break;
67
77
  case "status":
68
78
  status(args);
69
79
  break;
@@ -85,6 +95,12 @@ function main() {
85
95
  process.exit(1);
86
96
  }
87
97
  }
98
+ async function postInstallPermissions(binName, harness) {
99
+ const bootstrapped = await (0, argus_1.maybeAutoBootstrap)(binName, harness);
100
+ (0, argus_1.printPermissionsOnboardingHelp)(binName, harness, {
101
+ bootstrappedAutomatically: bootstrapped,
102
+ });
103
+ }
88
104
  function parseProjectDir(args) {
89
105
  const idx = args.indexOf("--project-dir");
90
106
  if (idx !== -1 && args[idx + 1])
@@ -185,6 +201,7 @@ Commands:
185
201
  install [--project-dir <path>] Register the Ory plugin in opencode.json
186
202
  uninstall [--project-dir <path>] Remove the Ory plugin from opencode.json
187
203
  configure Set or view Ory project URL and API key
204
+ permissions <cmd> Manage permission mode and tool tuples (status, bootstrap, observe, enforce)
188
205
  status [--project-dir <path>] Show plugin configuration and status
189
206
  local <cmd> Manage local Ory dev environment (up, down, status, seed, ...)
190
207
 
package/dist/plugin.js CHANGED
@@ -201,16 +201,30 @@ function createPermissionHandler(client, _options) {
201
201
  subject,
202
202
  spanAttributes: { toolName: input.toolID },
203
203
  });
204
- output.status = mcpResult.allowed ? "allow" : "deny";
204
+ const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
205
+ object: mcpTool.serverName,
206
+ relation: "use",
207
+ subjectId: (0, argus_1.subjectLabel)(subject),
208
+ spanAttributes: { toolName: input.toolID, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName },
209
+ });
210
+ output.status = decision.kind === "deny" ? "deny" : "allow";
211
+ return;
212
+ }
213
+ const decision = await (0, argus_1.checkAndDecide)(client, { namespace: resolveNamespace(), object: input.toolID, relation: "use", ...subject }, { spanAttributes: { toolName: input.toolID } });
214
+ if (decision.kind === "fail_open") {
215
+ const code = decision.error.code;
216
+ if (code === "network_error" || code === "rate_limited") {
217
+ client.logger.warn("permission.fallback", {
218
+ toolID: input.toolID,
219
+ code,
220
+ });
221
+ // Leave output.status as "ask" (default)
222
+ return;
223
+ }
224
+ output.status = "ask";
205
225
  return;
206
226
  }
207
- const result = await client.checkPermission({
208
- namespace: resolveNamespace(),
209
- object: input.toolID,
210
- relation: "use",
211
- ...subject,
212
- }, { spanAttributes: { toolName: input.toolID } });
213
- output.status = result.allowed ? "allow" : "deny";
227
+ output.status = decision.kind === "deny" ? "deny" : "allow";
214
228
  }
215
229
  catch (err) {
216
230
  if (isOryError(err) && (err.code === "network_error" || err.code === "rate_limited")) {
@@ -256,63 +270,90 @@ function createToolBeforeHandler(client) {
256
270
  subject,
257
271
  spanAttributes: { toolName: input.tool },
258
272
  });
259
- if (!mcpResult.allowed) {
273
+ const mcpAttrs = {
274
+ toolName: input.tool,
275
+ mcpServer: mcpTool.serverName,
276
+ mcpTool: mcpTool.toolName,
277
+ ...inputSummary,
278
+ };
279
+ const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
280
+ object: mcpTool.serverName,
281
+ relation: "use",
282
+ subjectId,
283
+ spanAttributes: mcpAttrs,
284
+ });
285
+ if (decision.kind === "allow") {
286
+ client.tracer.record("tool.invoke", "ok", { attributes: mcpAttrs });
287
+ return;
288
+ }
289
+ if (decision.kind === "observe") {
260
290
  client.tracer.record("tool.block", "denied", {
261
- attributes: { toolName: input.tool, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName, ...inputSummary, ...(0, argus_1.alertAttributes)(false) },
262
- });
263
- const message = (0, argus_1.formatDenialMessage)({
264
- tool: input.tool,
265
- subjectId,
266
- mcp: mcpTool,
291
+ attributes: { ...mcpAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
267
292
  });
268
- client.logger.warn("tool.denied", {
269
- tool: input.tool,
270
- subjectId,
271
- message,
272
- });
273
- // OpenCode has no native block signal in tool.execute.before;
274
- // throwing aborts execution (documented .env-protection pattern)
275
- // and surfaces error.message back to the agent.
276
- throw new argus_1.OryDenialError({
277
- tool: input.tool,
278
- subjectId,
279
- mcp: mcpTool,
293
+ client.tracer.record("tool.invoke", "ok", {
294
+ attributes: { ...mcpAttrs, allowed: false, observed: true },
280
295
  });
296
+ return;
281
297
  }
282
- client.tracer.record("tool.invoke", "ok", {
283
- attributes: { toolName: input.tool, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName, ...inputSummary },
284
- });
285
- return;
286
- }
287
- const namespace = resolveNamespace();
288
- const result = await client.checkPermission({
289
- namespace,
290
- object: input.tool,
291
- relation: "use",
292
- ...subject,
293
- }, { spanAttributes: { toolName: input.tool } });
294
- if (!result.allowed) {
295
298
  client.tracer.record("tool.block", "denied", {
296
- attributes: { toolName: input.tool, ...inputSummary, allowed: result.allowed, ...(0, argus_1.alertAttributes)(false) },
299
+ attributes: { ...mcpAttrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
297
300
  });
298
301
  const message = (0, argus_1.formatDenialMessage)({
299
302
  tool: input.tool,
300
303
  subjectId,
301
- namespace,
304
+ mcp: mcpTool,
302
305
  });
303
306
  client.logger.warn("tool.denied", {
304
307
  tool: input.tool,
305
308
  subjectId,
306
309
  message,
307
310
  });
311
+ // OpenCode has no native block signal in tool.execute.before;
312
+ // throwing aborts execution (documented .env-protection pattern)
313
+ // and surfaces error.message back to the agent.
308
314
  throw new argus_1.OryDenialError({
309
315
  tool: input.tool,
310
316
  subjectId,
311
- namespace,
317
+ mcp: mcpTool,
312
318
  });
313
319
  }
314
- client.tracer.record("tool.invoke", "ok", {
315
- attributes: { toolName: input.tool, ...inputSummary, allowed: result.allowed },
320
+ const namespace = resolveNamespace();
321
+ const decision = await (0, argus_1.checkAndDecide)(client, { namespace, object: input.tool, relation: "use", ...subject }, { spanAttributes: { toolName: input.tool } });
322
+ if (decision.kind === "fail_open") {
323
+ handlePermissionError(decision.error, input.tool, client);
324
+ return;
325
+ }
326
+ const attrs = { toolName: input.tool, ...inputSummary };
327
+ if (decision.kind === "allow") {
328
+ client.tracer.record("tool.invoke", "ok", { attributes: { ...attrs, allowed: true } });
329
+ return;
330
+ }
331
+ if (decision.kind === "observe") {
332
+ client.tracer.record("tool.block", "denied", {
333
+ attributes: { ...attrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
334
+ });
335
+ client.tracer.record("tool.invoke", "ok", {
336
+ attributes: { ...attrs, allowed: false, observed: true },
337
+ });
338
+ return;
339
+ }
340
+ client.tracer.record("tool.block", "denied", {
341
+ attributes: { ...attrs, allowed: false, ...(0, argus_1.alertAttributes)(false) },
342
+ });
343
+ const message = (0, argus_1.formatDenialMessage)({
344
+ tool: input.tool,
345
+ subjectId,
346
+ namespace,
347
+ });
348
+ client.logger.warn("tool.denied", {
349
+ tool: input.tool,
350
+ subjectId,
351
+ message,
352
+ });
353
+ throw new argus_1.OryDenialError({
354
+ tool: input.tool,
355
+ subjectId,
356
+ namespace,
316
357
  });
317
358
  }
318
359
  catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/opencode",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Ory plugin for OpenCode: 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",
@@ -64,7 +64,7 @@
64
64
  "!dist/**/*.tsbuildinfo"
65
65
  ],
66
66
  "dependencies": {
67
- "@ory/argus": "0.1.3"
67
+ "@ory/argus": "0.2.0"
68
68
  },
69
69
  "engines": {
70
70
  "node": ">=24"