@ory/opencode 0.9.1 → 0.11.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 +4 -4
- package/dist/cli/assets.js +4 -1
- package/dist/plugin.js +65 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -122,7 +122,7 @@ Bundled and registered by the Ory installer. Exposes the Ory CLI and the Ory Net
|
|
|
122
122
|
/ory:temporal-up # start a local Temporal dev server (for ory-temporal-worker)
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
`local-up` runs a complete Ory on your laptop: the Ory APIs (Identities, OAuth2, Permissions) at `http://localhost:4000`, a login UI on `:3000
|
|
125
|
+
`local-up` runs a complete Ory on your laptop: the Ory APIs (Identities, OAuth2, Permissions) at `http://localhost:4000`, a login UI on `:4455` (not :3000, to avoid Next.js port conflicts), and Jaeger (the trace viewer) on `:16686`. A test user identity is seeded and its credentials are printed for you. Use it to:
|
|
126
126
|
|
|
127
127
|
- **Learn Ory hands-on** without signing up for a hosted project.
|
|
128
128
|
- **Prototype** flows (login, social, MFA, recovery, permissions) against a real Ory backend.
|
|
@@ -205,14 +205,14 @@ Headless / CI runs that already hold a session token can skip this entirely by s
|
|
|
205
205
|
Once the plugin is pointed at an Ory project (local or hosted), OpenCode's session and every tool call can be governed by Ory.
|
|
206
206
|
|
|
207
207
|
- **Authentication.** Two identities. The human at the keyboard (the **user**) authenticates interactively via Ory Identities when user login is enabled (`ORY_USER_LOGIN=true`, off by default — browser PKCE flow on first session, persisted token thereafter). The OpenCode process (the **agent**) gets its own OAuth2 identity, self-registered via [Dynamic Client Registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591) on first run.
|
|
208
|
-
- **Authorization.** When OpenCode prompts for permission to use a tool, the plugin checks [Ory Permissions](https://www.ory.com/docs/keto) (Zanzibar-style relations) against the user's subject
|
|
208
|
+
- **Authorization.** When OpenCode prompts for permission to use a tool, the plugin checks [Ory Permissions](https://www.ory.com/docs/keto) (Zanzibar-style relations) against the user's subject. In **enforce** mode a deny blocks the tool by throwing an `OryDenialError` (OpenCode surfaces the denial reason); in **observe** mode (default) the deny is recorded as a `permission.observe_deny` span and OpenCode's own permission prompt is left in place — the plugin only answers the prompt on an explicit Ory allow or an enforce-mode deny, never on an observe-mode deny or an infrastructure failure. A parallel pre-tool check is recorded as part of the audit trail. MCP tool calls additionally get a server-level check.
|
|
209
209
|
- **Audit.** Every decision (allow, deny, fallback) is recorded as a structured trace span: NDJSON file output and/or OTLP/HTTP export to Jaeger, Honeycomb, Grafana, and similar collectors. The user → agent delegation is written to Ory as a relation so *"agent X acting on behalf of user Y"* stays queryable after tokens expire.
|
|
210
210
|
|
|
211
211
|
The plugin is **fail-open** on its own infrastructure failures (network errors, rate limits, missing config), so enforcement is only as strong as your permission grants — grant explicit `use` on the tools each user should be able to run.
|
|
212
212
|
|
|
213
213
|
### Enable enforcement
|
|
214
214
|
|
|
215
|
-
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.
|
|
215
|
+
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 (and at OpenCode's permission prompt, the native ask is left for the user to answer). This lets you see what *would* be blocked before turning on hard blocking.
|
|
216
216
|
|
|
217
217
|
1. **Turn on user login.** It's off by default. In your shell:
|
|
218
218
|
|
|
@@ -269,7 +269,7 @@ Highlights:
|
|
|
269
269
|
|
|
270
270
|
## Troubleshooting
|
|
271
271
|
|
|
272
|
-
- **`/ory:local-up` fails.** Make sure Docker is running and ports `
|
|
272
|
+
- **`/ory:local-up` fails.** Make sure Docker is running and ports `4455` (login UI), `4000`, `4100`, and `16686` are free.
|
|
273
273
|
- **PKCE login loops.** Clear persisted state with `npx -y -p @ory/opencode ory-opencode agent unregister` and retry.
|
|
274
274
|
- **`npx` fetches an old version.** Force a fresh fetch: `npx -y -p @ory/opencode@latest ory-opencode …`.
|
|
275
275
|
- **`npm error code ENOVERSIONS` on install.** Your npm has a supply-chain freshness filter (`min-release-age`) that's hiding all `@ory/opencode` versions newer than its threshold. Either wait it out, or run the installer with `--min-release-age=0`: `npx -y --min-release-age=0 -p @ory/opencode ory-opencode install`.
|
package/dist/cli/assets.js
CHANGED
|
@@ -56,8 +56,11 @@ function opencodeRoot(projectDir) {
|
|
|
56
56
|
function installOpencodeOryAssets(projectDir) {
|
|
57
57
|
const root = opencodeRoot(projectDir);
|
|
58
58
|
fs.mkdirSync(root, { recursive: true });
|
|
59
|
+
// `.opencode/skills` is shared with user-authored skills, so never wipe
|
|
60
|
+
// the whole directory — surgically remove only the Ory-owned skill dirs
|
|
61
|
+
// (same list the uninstall path uses) before re-writing them fresh.
|
|
59
62
|
const skillsDir = path.join(root, "skills");
|
|
60
|
-
|
|
63
|
+
(0, argus_1.removeSkillDirs)(skillsDir, argus_1.ORY_SKILL_NAMES);
|
|
61
64
|
(0, argus_1.writeSkillTree)(skillsDir, (0, argus_1.renderOrySkills)("opencode", RENDER_OPTS));
|
|
62
65
|
const cmdDir = path.join(root, "commands", CMD_SUBDIR);
|
|
63
66
|
fs.rmSync(cmdDir, { recursive: true, force: true });
|
package/dist/plugin.js
CHANGED
|
@@ -95,6 +95,12 @@ async function verifyOnStartup(client, deps) {
|
|
|
95
95
|
// attaches the agent's bearer token to outgoing Ory API calls.
|
|
96
96
|
const agentGate = deps.agentGate ?? argus_1.ensureAgentIdentity;
|
|
97
97
|
await agentGate(client, { projectUrl: (0, argus_1.resolveConfig)().projectUrl, harness: "opencode" });
|
|
98
|
+
// Record the user→agent delegation so the audit trail captures that
|
|
99
|
+
// this user authorized this agent for this session. Best-effort:
|
|
100
|
+
// requires both principals to be populated, and any failure is logged
|
|
101
|
+
// and swallowed (fail-open — delegation tracking is for audit, not
|
|
102
|
+
// enforcement).
|
|
103
|
+
await recordUserDelegatesAgent(client);
|
|
98
104
|
if (decision.mode !== "disabled") {
|
|
99
105
|
return;
|
|
100
106
|
}
|
|
@@ -201,7 +207,7 @@ function createPermissionHandler(client, _options) {
|
|
|
201
207
|
...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
|
|
202
208
|
spanAttributes: { toolName: toolId, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName },
|
|
203
209
|
});
|
|
204
|
-
output
|
|
210
|
+
applyPermissionAskDecision(output, decision.kind);
|
|
205
211
|
return;
|
|
206
212
|
}
|
|
207
213
|
const outcome = await (0, argus_1.gateToolCall)(client, {
|
|
@@ -210,8 +216,10 @@ function createPermissionHandler(client, _options) {
|
|
|
210
216
|
check: { namespace: resolveNamespace(), object: toolId, relation: "use", ...subject },
|
|
211
217
|
spanAttributes: { toolName: toolId },
|
|
212
218
|
});
|
|
213
|
-
if (outcome.kind === "interactive") {
|
|
214
|
-
//
|
|
219
|
+
if (outcome.kind === "interactive" || outcome.kind === "audit_only") {
|
|
220
|
+
// Ory is disabled (audit-only kill switch) or this is a
|
|
221
|
+
// user-interaction primitive: leave output.status as "ask" (default)
|
|
222
|
+
// so the harness's native prompt reaches the user.
|
|
215
223
|
return;
|
|
216
224
|
}
|
|
217
225
|
const decision = outcome;
|
|
@@ -228,7 +236,7 @@ function createPermissionHandler(client, _options) {
|
|
|
228
236
|
output.status = "ask";
|
|
229
237
|
return;
|
|
230
238
|
}
|
|
231
|
-
output
|
|
239
|
+
applyPermissionAskDecision(output, decision.kind);
|
|
232
240
|
}
|
|
233
241
|
catch (err) {
|
|
234
242
|
if (isOryError(err) && (err.code === "network_error" || err.code === "rate_limited")) {
|
|
@@ -415,6 +423,59 @@ function createToolAfterHandler(client) {
|
|
|
415
423
|
function resolveNamespace() {
|
|
416
424
|
return process.env.ORY_PERMISSION_NAMESPACE ?? "AgentTools";
|
|
417
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Write the user→agent delegation tuple. Idempotent and fail-open:
|
|
428
|
+
* requires both principal subjects to be populated; any error (including
|
|
429
|
+
* an unconfigured projectUrl, which manifests as a network_error) is
|
|
430
|
+
* logged and swallowed. Purely audit-trail data — does not affect
|
|
431
|
+
* enforcement.
|
|
432
|
+
*/
|
|
433
|
+
async function recordUserDelegatesAgent(client) {
|
|
434
|
+
const user = client.userPrincipal.subject;
|
|
435
|
+
const agent = client.agentPrincipal.subject;
|
|
436
|
+
if (!user || !agent) {
|
|
437
|
+
client.logger.debug("delegation.skip", {
|
|
438
|
+
reason: "missing principal",
|
|
439
|
+
hasUser: !!user,
|
|
440
|
+
hasAgent: !!agent,
|
|
441
|
+
});
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
try {
|
|
445
|
+
await client.createRelationship({
|
|
446
|
+
namespace: resolveNamespace(),
|
|
447
|
+
object: `agent:${agent}`,
|
|
448
|
+
relation: "delegate",
|
|
449
|
+
subjectId: `user:${user}`,
|
|
450
|
+
}, { spanAttributes: { delegation: "user-to-agent" } });
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
const oryErr = err;
|
|
454
|
+
client.logger.warn("delegation.user_to_agent.failed", {
|
|
455
|
+
code: oryErr.code,
|
|
456
|
+
message: oryErr.message,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Map an Ory permission decision onto OpenCode's `permission.ask` output.
|
|
462
|
+
*
|
|
463
|
+
* Only explicit outcomes answer the prompt: `deny` → `"deny"`,
|
|
464
|
+
* `allow` → `"allow"`. An `observe`-mode deny leaves `output.status`
|
|
465
|
+
* untouched (OpenCode's default is `"ask"`), so the harness's native
|
|
466
|
+
* user prompt still appears — observe mode must never escalate a
|
|
467
|
+
* policy-denied tool into an auto-approval. The observe deny is already
|
|
468
|
+
* recorded as a `permission.observe_deny` audit span by the gate.
|
|
469
|
+
*/
|
|
470
|
+
function applyPermissionAskDecision(output, kind) {
|
|
471
|
+
if (kind === "deny") {
|
|
472
|
+
output.status = "deny";
|
|
473
|
+
}
|
|
474
|
+
else if (kind === "allow") {
|
|
475
|
+
output.status = "allow";
|
|
476
|
+
}
|
|
477
|
+
// kind === "observe": leave the harness's native prompt in place.
|
|
478
|
+
}
|
|
418
479
|
function isOryError(err) {
|
|
419
480
|
return (typeof err === "object" &&
|
|
420
481
|
err !== null &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ory/opencode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.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",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"!dist/**/*.tsbuildinfo"
|
|
64
64
|
],
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@ory/argus": "0.
|
|
66
|
+
"@ory/argus": "0.11.0"
|
|
67
67
|
},
|
|
68
68
|
"engines": {
|
|
69
69
|
"node": ">=22"
|