@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 +23 -9
- package/dist/cli/main.js +17 -0
- package/dist/plugin.js +86 -45
- package/package.json +2 -2
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. **
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
- `
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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.
|
|
269
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
317
|
+
mcp: mcpTool,
|
|
312
318
|
});
|
|
313
319
|
}
|
|
314
|
-
|
|
315
|
-
|
|
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.
|
|
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.
|
|
67
|
+
"@ory/argus": "0.2.0"
|
|
68
68
|
},
|
|
69
69
|
"engines": {
|
|
70
70
|
"node": ">=24"
|