@ory/opencode 0.3.0 → 0.4.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
@@ -13,26 +13,27 @@ You don't need an Ory account or any prior Ory experience to start.
13
13
 
14
14
  ## Install
15
15
 
16
- Add the plugin to your `opencode.json`:
16
+ Run the installer from your project directory:
17
17
 
18
- ```json
19
- { "plugin": ["@ory/opencode"] }
18
+ ```bash
19
+ npx -y -p @ory/opencode ory-opencode install
20
20
  ```
21
21
 
22
- OpenCode fetches the plugin from npm on next launch.
22
+ This writes the plugin into `opencode.json`, registers the Ory MCP server, and drops the Ory skill catalog into `.opencode/skills/` and the local-stack slash commands into `.opencode/commands/ory/` — all in one step. Run `npx -y -p @ory/opencode ory-opencode uninstall` to reverse it.
23
23
 
24
24
  <details>
25
- <summary>Alternative install paths</summary>
25
+ <summary>Alternative install: edit opencode.json by hand</summary>
26
26
 
27
- The Ory installer registers the plugin **and** the Ory MCP server in one step, with no prior `npm install` required:
27
+ If you want only the plugin (no MCP server, no skills, no slash commands), add it directly to `opencode.json`:
28
28
 
29
- ```bash
30
- npx @ory/opencode install # writes opencode.json + MCP config
31
- npx @ory/opencode install --project-dir <path>
32
- npx @ory/opencode uninstall
29
+ ```json
30
+ {
31
+ "$schema": "https://opencode.ai/config.json",
32
+ "plugin": ["@ory/opencode"]
33
+ }
33
34
  ```
34
35
 
35
- Either flow drops the Ory skill catalog into `.opencode/skills/` and the local-stack commands into `.opencode/commands/ory/`.
36
+ OpenCode fetches the plugin from npm on next launch. The Quickstart below will not work via this path — `/ory:local-up` and the scaffolding skills only land on disk when you run the installer.
36
37
 
37
38
  </details>
38
39
 
@@ -54,6 +55,14 @@ From any project where you'd like Ory authentication, inside OpenCode:
54
55
 
55
56
  3. **Sign in.** Start your app, visit the login page OpenCode added, and sign in with the seeded credentials. You now have a real Ory session backed by a real Ory stack — locally, offline, with zero configuration.
56
57
 
58
+ 4. **Turn on Ory login for the OpenCode session itself.** *(Optional but recommended.)* Out of the box the plugin only governs your *app*. To also attach an Ory identity to *OpenCode's* session — so every tool call is attributed to you, not a fallback `session:<id>` subject — set:
59
+
60
+ ```bash
61
+ export ORY_AUTH_GATE=1
62
+ ```
63
+
64
+ Then restart OpenCode. On next session start, OpenCode opens an Ory login in your browser; sign in with the same seeded credentials from step 1. This is what makes `permissions enforce` (see [Agent security](#agent-security)) deny on the right identity later. (Note: OpenCode's session-start primitive can't hard-block, so the gate runs in advisory mode — auth still happens and the audit trail is correct, but the session always proceeds.)
65
+
57
66
  That's the full Ory DX path. Stop here if you're just evaluating the plugin. Continue to [Agent security](#agent-security) when you're ready to enforce.
58
67
 
59
68
  ## What's included
@@ -168,6 +177,7 @@ Highlights:
168
177
  - **`/ory:local-up` fails.** Make sure Docker is running and ports `3000`, `4000`, `4100`, and `16686` are free.
169
178
  - **PKCE login loops.** Clear persisted state with `npx -y -p @ory/opencode ory-opencode agent unregister` and retry.
170
179
  - **`npx` fetches an old version.** Force a fresh fetch: `npx -y -p @ory/opencode@latest ory-opencode …`.
180
+ - **`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`.
171
181
  - **Need more signal.** Set `ORY_AGENT_DEBUG=true` and `ORY_AGENT_LOG_FILE=/tmp/ory.log` to capture structured logs.
172
182
 
173
183
  ## Links
package/dist/cli/main.js CHANGED
@@ -75,7 +75,10 @@ function main() {
75
75
  });
76
76
  break;
77
77
  case "status":
78
- status(args);
78
+ status(args).then(() => process.exit(0), (err) => {
79
+ console.error(err.message ?? err);
80
+ process.exit(1);
81
+ });
79
82
  break;
80
83
  case "local":
81
84
  (0, argus_1.runLocalCommand)("ory-opencode", args).catch((err) => {
@@ -111,13 +114,21 @@ const PLUGIN_MODULE = "@ory/opencode";
111
114
  const MCP_SERVER_NAME = "ory";
112
115
  const MCP_SERVER_COMMAND = [
113
116
  "npx",
117
+ "-y",
114
118
  "--package=@ory/mcp-server",
115
119
  "ory-mcp-server",
116
120
  ];
121
+ const OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
117
122
  function install(args) {
118
123
  const projectDir = parseProjectDir(args);
119
124
  const configPath = path.join(projectDir, "opencode.json");
125
+ const fileExists = fs.existsSync(configPath);
120
126
  const config = (0, argus_1.readJsonFile)(configPath);
127
+ // Set $schema for editor autocomplete on fresh configs only — don't
128
+ // overwrite a user's existing schema reference.
129
+ if (!fileExists && !config.$schema) {
130
+ config.$schema = OPENCODE_SCHEMA_URL;
131
+ }
121
132
  const plugin = (config.plugin ?? []).filter((p) => (typeof p === "string" ? p : p[0]) !== PLUGIN_MODULE);
122
133
  plugin.push(PLUGIN_MODULE);
123
134
  config.plugin = plugin;
@@ -158,63 +169,70 @@ function uninstall(args) {
158
169
  else {
159
170
  config.mcp = mcp;
160
171
  }
161
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
172
+ // If the file is now effectively empty (we created it, only $schema left
173
+ // from our own install) delete it instead of leaving `{}` behind. Keep
174
+ // it if there's any user-authored content.
175
+ const remainingKeys = Object.keys(config).filter((k) => k !== "$schema");
176
+ if (remainingKeys.length === 0) {
177
+ fs.unlinkSync(configPath);
178
+ console.log(`Removed Ory plugin and MCP server; opencode.json was empty after cleanup and has been deleted.`);
179
+ }
180
+ else {
181
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
182
+ console.log(`Ory plugin and MCP server removed from ${configPath}`);
183
+ }
162
184
  (0, assets_js_1.uninstallOpencodeOryAssets)(projectDir);
163
- console.log(`Ory plugin and MCP server removed from ${configPath}`);
164
185
  }
165
- function status(args) {
186
+ async function status(args) {
166
187
  const projectDir = parseProjectDir(args);
167
188
  const configPath = path.join(projectDir, "opencode.json");
168
- console.log("Ory Agent Plugin Status (OpenCode)");
169
- console.log("===================================");
170
- console.log("");
171
- // OpenCode-specific: check plugin registration
172
- const config = (0, argus_1.readJsonFile)(configPath);
173
- const moduleName = "@ory/opencode";
174
- const plugin = (config.plugin ?? []);
175
- const oryPlugin = plugin.find((p) => (typeof p === "string" ? p : p[0]) === moduleName);
176
- console.log("Configuration:");
177
- console.log(` Config file: ${configPath}`);
178
- console.log(` Plugin registered: ${oryPlugin ? "yes" : "NO"}`);
179
- if (oryPlugin) {
180
- console.log(` Module: ${typeof oryPlugin === "string" ? oryPlugin : oryPlugin[0]}`);
181
- }
182
- // Shared Ory config
183
- console.log("");
184
- console.log("Ory Configuration:");
185
- const resolved = (0, argus_1.resolveConfig)();
186
- const oryConfigPath = (0, argus_1.getConfigPath)();
187
- console.log(` Config file: ${oryConfigPath}`);
188
- console.log(` Project URL: ${resolved.projectUrl ?? "NOT SET"} [source: ${resolved.projectUrlSource}]`);
189
- console.log(` API Key: ${resolved.apiKey ? "(set)" : "NOT SET"} [source: ${resolved.apiKeySource}]`);
190
- (0, argus_1.printEnvironment)();
191
- (0, argus_1.printLogTail)();
189
+ await (0, argus_1.runStatusCommand)("ory-opencode", "opencode", {
190
+ title: "OpenCode",
191
+ printPluginSection: () => {
192
+ const config = (0, argus_1.readJsonFile)(configPath);
193
+ const moduleName = "@ory/opencode";
194
+ const plugin = (config.plugin ?? []);
195
+ const oryPlugin = plugin.find((p) => (typeof p === "string" ? p : p[0]) === moduleName);
196
+ console.log("Hooks & plugin:");
197
+ console.log(` Config file: ${configPath}`);
198
+ console.log(` Plugin registered: ${oryPlugin ? "yes" : "NO"}`);
199
+ if (oryPlugin) {
200
+ console.log(` Module: ${typeof oryPlugin === "string" ? oryPlugin : oryPlugin[0]}`);
201
+ }
202
+ },
203
+ });
192
204
  }
193
205
  function help() {
194
206
  console.log(`
195
207
  ory-opencode — Ory plugin for OpenCode
196
208
 
197
209
  Usage:
198
- npx ory-opencode <command> [options]
210
+ npx -y -p @ory/opencode ory-opencode <command> [options]
199
211
 
200
212
  Commands:
201
- install [--project-dir <path>] Register the Ory plugin in opencode.json
202
- uninstall [--project-dir <path>] Remove the Ory plugin from opencode.json
213
+ install [--project-dir <path>] Register the Ory plugin and MCP server in opencode.json
214
+ uninstall [--project-dir <path>] Remove the Ory plugin and MCP server from opencode.json
203
215
  configure Set or view Ory project URL and API key
204
- permissions <cmd> Manage permission mode and tool permissions (status, bootstrap, observe, enforce)
216
+ agent <status|unregister> Manage the agent's OAuth2 (DCR) identity
217
+ permissions <cmd> Manage permission mode and tool permissions
218
+ (status, bootstrap, observe, enforce)
219
+ local <cmd> Manage local Ory dev environment
220
+ (up, down, status, seed, logs, env, configure, reset)
205
221
  status [--project-dir <path>] Show plugin configuration and status
206
- local <cmd> Manage local Ory dev environment (up, down, status, seed, ...)
207
222
 
208
- After installing, the plugin hooks into these OpenCode lifecycle events:
209
- session.created Verify agent session via Ory Identities
223
+ After installing, the plugin registers these OpenCode hooks:
224
+ config Log resolved Ory config at startup
225
+ chat.message Trace user prompts per turn
226
+ permission.ask Authorize actions via Ory Permissions
210
227
  tool.execute.before Check tool permissions via Ory Permissions
211
228
  tool.execute.after Audit log tool executions
212
- permission.ask Authorize actions via Ory Permissions
213
229
 
214
230
  Examples:
215
- npx ory-opencode install # Register in current directory
216
- npx ory-opencode status # Check configuration
217
- npx ory-opencode uninstall # Remove plugin
231
+ npx -y -p @ory/opencode ory-opencode install
232
+ npx -y -p @ory/opencode ory-opencode configure --project-url https://<id>.projects.oryapis.com --api-key ory_pat_...
233
+ npx -y -p @ory/opencode ory-opencode permissions bootstrap
234
+ npx -y -p @ory/opencode ory-opencode local up
235
+ npx -y -p @ory/opencode ory-opencode status
218
236
  `);
219
237
  }
220
238
  main();
package/dist/cli/setup.js CHANGED
@@ -51,9 +51,11 @@ const PLUGIN_MODULE = "@ory/opencode";
51
51
  const MCP_SERVER_NAME = "ory";
52
52
  const MCP_SERVER_COMMAND = [
53
53
  "npx",
54
+ "-y",
54
55
  "--package=@ory/mcp-server",
55
56
  "ory-mcp-server",
56
57
  ];
58
+ const OPENCODE_SCHEMA_URL = "https://opencode.ai/config.json";
57
59
  // OpenCode's config schema uses `mcp` (singular) with a `local` connector
58
60
  // shape `{ type: "local", command: [...] }`, not the generic `mcpServers`
59
61
  // helper from core.
@@ -108,22 +110,34 @@ function main() {
108
110
  return;
109
111
  }
110
112
  const configPath = getConfigPath(args);
113
+ const fileExists = fs.existsSync(configPath);
111
114
  const existing = (0, argus_1.readJsonFile)(configPath);
112
115
  if (args.uninstall) {
113
- if (!fs.existsSync(configPath)) {
116
+ if (!fileExists) {
114
117
  console.log("No opencode.json found. Nothing to uninstall.");
115
118
  return;
116
119
  }
117
120
  let updated = removePlugin(existing);
118
121
  updated = removeOpencodeMcp(updated);
119
- (0, argus_1.writeJsonFile)(configPath, updated);
122
+ const remainingKeys = Object.keys(updated).filter((k) => k !== "$schema");
123
+ if (remainingKeys.length === 0) {
124
+ fs.unlinkSync(configPath);
125
+ console.log(`Removed Ory plugin and MCP server; opencode.json was empty after cleanup and has been deleted.`);
126
+ }
127
+ else {
128
+ (0, argus_1.writeJsonFile)(configPath, updated);
129
+ console.log(`Removed Ory plugin and MCP server from ${configPath}`);
130
+ }
120
131
  (0, assets_js_1.uninstallOpencodeOryAssets)(args.projectDir);
121
- console.log(`Removed Ory plugin and MCP server from ${configPath}`);
122
132
  console.log(`Removed Ory skills and commands from ${path.join(args.projectDir, ".opencode")}`);
123
133
  return;
124
134
  }
125
135
  let merged = mergePlugin(existing);
126
136
  merged = mergeOpencodeMcp(merged);
137
+ // Add $schema for editor autocomplete on fresh configs only.
138
+ if (!fileExists && !merged.$schema) {
139
+ merged = { $schema: OPENCODE_SCHEMA_URL, ...merged };
140
+ }
127
141
  (0, argus_1.writeJsonFile)(configPath, merged);
128
142
  (0, assets_js_1.installOpencodeOryAssets)(args.projectDir);
129
143
  console.log(`Ory plugin and MCP server configured in ${configPath}`);
package/dist/plugin.js CHANGED
@@ -31,7 +31,6 @@ function createOryPlugin(clientOrConfig, deps = {}) {
31
31
  return {
32
32
  config: createConfigHandler(client),
33
33
  "chat.message": createChatMessageHandler(client),
34
- "command.execute.before": createCommandBeforeHandler(client),
35
34
  "permission.ask": createPermissionHandler(client, options),
36
35
  "tool.execute.before": createToolBeforeHandler(client),
37
36
  "tool.execute.after": createToolAfterHandler(client),
@@ -78,27 +77,6 @@ function createChatMessageHandler(client) {
78
77
  });
79
78
  };
80
79
  }
81
- // ─── command.execute.before ────────────────────────────────────────
82
- function createCommandBeforeHandler(client) {
83
- return async (input, _output) => {
84
- if (input.sessionID) {
85
- client.tracer.setContext({
86
- traceId: (0, argus_1.deriveTraceId)(input.sessionID),
87
- sessionId: input.sessionID,
88
- });
89
- }
90
- client.logger.info("lifecycle.command_execute_before", {
91
- command: input.name,
92
- sessionID: input.sessionID,
93
- });
94
- client.tracer.record("hook.receive", "ok", {
95
- attributes: {
96
- event: "command.execute.before",
97
- command: input.name,
98
- },
99
- });
100
- };
101
- }
102
80
  // ─── Session verification on startup ────────────────────────────────
103
81
  async function verifyOnStartup(client, deps) {
104
82
  // Run the user auth gate. The OpenCode `server` factory has no block
@@ -185,38 +163,44 @@ async function verifyOnStartup(client, deps) {
185
163
  // ─── permission.ask ─────────────────────────────────────────────────
186
164
  function createPermissionHandler(client, _options) {
187
165
  return async (input, output) => {
166
+ // Canonical Permission uses `type`; older plugin versions read `toolID`.
167
+ // Coalesce so the handler works regardless of which the runtime populates.
168
+ const toolId = input.toolID ?? input.type ?? "unknown";
188
169
  client.logger.info("lifecycle.permission_ask", {
189
- toolID: input.toolID,
170
+ toolID: toolId,
190
171
  title: input.title,
191
172
  });
192
173
  // In audit-only mode, defer to user prompt
193
174
  if ((0, argus_1.resolveConfig)().auditOnly) {
194
175
  return;
195
176
  }
196
- const subject = (0, argus_1.resolveUserSubject)(client, "agent:opencode");
197
- const mcpTool = (0, argus_1.parseMcpToolGeneric)(input.toolID);
177
+ // Use the session as the subject fallback, matching tool.execute.before.
178
+ // Letting this default to the agent's own subject (the prior behavior)
179
+ // would silently match the agent's tuples instead of the user's.
180
+ const subject = (0, argus_1.resolveUserSubject)(client, input.sessionID ? `session:${input.sessionID}` : undefined);
181
+ const mcpTool = (0, argus_1.parseMcpToolGeneric)(toolId);
198
182
  try {
199
183
  if (mcpTool) {
200
184
  const mcpResult = await (0, argus_1.checkMcpPermission)(client, mcpTool, {
201
185
  subject,
202
- spanAttributes: { toolName: input.toolID },
186
+ spanAttributes: { toolName: toolId },
203
187
  });
204
188
  const decision = (0, argus_1.applyPermissionMode)(client, mcpResult.allowed, {
205
189
  object: mcpTool.serverName,
206
190
  relation: "use",
207
191
  subjectId: (0, argus_1.subjectLabel)(subject),
208
192
  ...("subjectSet" in subject ? { subjectSet: subject.subjectSet } : {}),
209
- spanAttributes: { toolName: input.toolID, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName },
193
+ spanAttributes: { toolName: toolId, mcpServer: mcpTool.serverName, mcpTool: mcpTool.toolName },
210
194
  });
211
195
  output.status = decision.kind === "deny" ? "deny" : "allow";
212
196
  return;
213
197
  }
214
- const decision = await (0, argus_1.checkAndDecide)(client, { namespace: resolveNamespace(), object: input.toolID, relation: "use", ...subject }, { spanAttributes: { toolName: input.toolID } });
198
+ const decision = await (0, argus_1.checkAndDecide)(client, { namespace: resolveNamespace(), object: toolId, relation: "use", ...subject }, { spanAttributes: { toolName: toolId } });
215
199
  if (decision.kind === "fail_open") {
216
200
  const code = decision.error.code;
217
201
  if (code === "network_error" || code === "rate_limited") {
218
202
  client.logger.warn("permission.fallback", {
219
- toolID: input.toolID,
203
+ toolID: toolId,
220
204
  code,
221
205
  });
222
206
  // Leave output.status as "ask" (default)
@@ -230,7 +214,7 @@ function createPermissionHandler(client, _options) {
230
214
  catch (err) {
231
215
  if (isOryError(err) && (err.code === "network_error" || err.code === "rate_limited")) {
232
216
  client.logger.warn("permission.fallback", {
233
- toolID: input.toolID,
217
+ toolID: toolId,
234
218
  code: err.code,
235
219
  });
236
220
  // Leave output.status as "ask" (default)
@@ -262,7 +246,7 @@ function createToolBeforeHandler(client) {
262
246
  });
263
247
  return;
264
248
  }
265
- const subject = (0, argus_1.resolveUserSubject)(client, input.sessionID ? `session:${input.sessionID}` : "agent:opencode");
249
+ const subject = (0, argus_1.resolveUserSubject)(client, input.sessionID ? `session:${input.sessionID}` : undefined);
266
250
  const subjectId = (0, argus_1.subjectLabel)(subject);
267
251
  const mcpTool = (0, argus_1.parseMcpToolGeneric)(input.tool);
268
252
  try {
@@ -379,15 +363,15 @@ function createToolAfterHandler(client) {
379
363
  sessionID: input.sessionID,
380
364
  callID: input.callID,
381
365
  });
382
- // Set trace context for this tool completion
383
366
  client.tracer.setContext({
384
367
  traceId: (0, argus_1.deriveTraceId)(input.sessionID),
385
368
  sessionId: input.sessionID,
386
369
  });
370
+ // OpenCode's canonical tool.execute.after input has no `args`,
371
+ // so input summary is unavailable here; output summary only.
387
372
  client.tracer.record("tool.complete", "ok", {
388
373
  attributes: {
389
374
  toolName: input.tool,
390
- ...(0, argus_1.summarizeToolInput)(input.tool, input.args),
391
375
  ...(0, argus_1.summarizeToolOutput)(input.tool, {
392
376
  title: output.title,
393
377
  output: output.output,
package/dist/types.d.ts CHANGED
@@ -25,10 +25,19 @@ export interface PluginModule {
25
25
  server: Plugin;
26
26
  }
27
27
  export interface Permission {
28
- toolID: string;
28
+ id?: string;
29
+ type?: string;
30
+ pattern?: string | Array<string>;
31
+ sessionID?: string;
32
+ messageID?: string;
33
+ callID?: string;
29
34
  title: string;
30
- description: string;
31
35
  metadata: Record<string, unknown>;
36
+ time?: {
37
+ created: number;
38
+ };
39
+ toolID?: string;
40
+ description?: string;
32
41
  }
33
42
  export interface ChatMessageInput {
34
43
  sessionID: string;
@@ -39,11 +48,6 @@ export interface ChatMessageInput {
39
48
  modelID?: string;
40
49
  };
41
50
  }
42
- export interface CommandExecuteBeforeInput {
43
- name: string;
44
- sessionID?: string;
45
- args?: unknown;
46
- }
47
51
  export interface Hooks {
48
52
  event?: (input: {
49
53
  event: unknown;
@@ -52,8 +56,6 @@ export interface Hooks {
52
56
  config?: (input: Record<string, unknown>) => Promise<void>;
53
57
  /** Fires on every user message. Observer only — useful for per-turn audit. */
54
58
  "chat.message"?: (input: ChatMessageInput, output: Record<string, unknown>) => Promise<void>;
55
- /** Fires before a slash command runs. Observer only. */
56
- "command.execute.before"?: (input: CommandExecuteBeforeInput, output: Record<string, unknown>) => Promise<void>;
57
59
  "permission.ask"?: (input: Permission, output: {
58
60
  status: "ask" | "deny" | "allow";
59
61
  }) => Promise<void>;
@@ -68,7 +70,6 @@ export interface Hooks {
68
70
  tool: string;
69
71
  sessionID: string;
70
72
  callID: string;
71
- args: unknown;
72
73
  }, output: {
73
74
  title: string;
74
75
  output: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ory/opencode",
3
- "version": "0.3.0",
3
+ "version": "0.4.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",
@@ -54,7 +54,6 @@
54
54
  }
55
55
  },
56
56
  "bin": {
57
- "opencode": "dist/cli/main.js",
58
57
  "ory-opencode": "dist/cli/main.js",
59
58
  "ory-opencode-setup": "dist/cli/setup.js"
60
59
  },
@@ -64,10 +63,10 @@
64
63
  "!dist/**/*.tsbuildinfo"
65
64
  ],
66
65
  "dependencies": {
67
- "@ory/argus": "0.3.0"
66
+ "@ory/argus": "0.4.0"
68
67
  },
69
68
  "engines": {
70
- "node": ">=24"
69
+ "node": ">=22"
71
70
  },
72
71
  "scripts": {
73
72
  "build": "tsc",