orca-opencode-plugin 1.1.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 ADDED
@@ -0,0 +1,189 @@
1
+ # Orca OpenCode Plugin
2
+
3
+ OpenCode plugin wrapper for Orca runtime guardrails.
4
+
5
+ ## What this plugin does
6
+
7
+ This plugin adds Orca-native lifecycle hooks to OpenCode. It lets OpenCode call the Orca CLI for policy checks, audit logging, and runtime safety decisions without duplicating policy logic.
8
+
9
+ The plugin is a thin integration layer. The Orca CLI remains the source of truth for all policy decisions.
10
+
11
+ ## Prerequisites
12
+
13
+ - Orca CLI built and available in PATH (run `orca doctor` to verify)
14
+ - OpenCode host installed
15
+
16
+ Orca must be installed separately. The plugin does not bundle the Orca CLI.
17
+
18
+ ## Install from npm
19
+
20
+ Add to your `opencode.json`:
21
+
22
+ ```json
23
+ {
24
+ "$schema": "https://opencode.ai/config.json",
25
+ "plugin": ["orca-opencode-plugin"]
26
+ }
27
+ ```
28
+
29
+ Then install dependencies:
30
+
31
+ ```bash
32
+ npm install orca-opencode-plugin
33
+ ```
34
+
35
+ The strongest local protection remains running OpenCode through `orca run -- opencode`; the OpenCode plugin provides native hooks and guardrails inside OpenCode.
36
+
37
+ ## Install from local path
38
+
39
+ If you prefer to use the plugin directly from the Orca repository:
40
+
41
+ ### Project-local install
42
+
43
+ Copy or symlink this directory into your project:
44
+
45
+ ```bash
46
+ # From the Orca repo root
47
+ mkdir -p .opencode/plugins
48
+ cp integrations/opencode-plugin/orca.ts .opencode/plugins/orca.ts
49
+ ```
50
+
51
+ See `examples/project-plugin-path.md` for details.
52
+
53
+ ### Global install
54
+
55
+ Copy or symlink to the OpenCode global plugins directory:
56
+
57
+ ```bash
58
+ mkdir -p ~/.config/opencode/plugins
59
+ cp integrations/opencode-plugin/orca.ts ~/.config/opencode/plugins/orca.ts
60
+ ```
61
+
62
+ See `examples/global-plugin-path.md` for details.
63
+
64
+ ### Verify the plugin is recognized
65
+
66
+ ```bash
67
+ orca plugin doctor opencode
68
+ ```
69
+
70
+ ## Verify install
71
+
72
+ Run the Orca plugin doctor:
73
+
74
+ ```bash
75
+ orca plugin doctor opencode
76
+ ```
77
+
78
+ Expected output sections:
79
+ - Orca version
80
+ - Policy status (present/valid)
81
+ - Plugin directories (opencode: found)
82
+ - Host binaries (opencode: detected or not detected)
83
+
84
+ ## Hooks included
85
+
86
+ The plugin registers lifecycle hooks that call `orca hook opencode <event>`:
87
+
88
+ | Event | When it fires | Behavior |
89
+ |-------|---------------|----------|
90
+ | `session.created` | At the start of an OpenCode session | Informational (readiness log) |
91
+ | `tool.execute.before` | Before OpenCode invokes a tool | **Blocking** — Orca can prevent the tool call |
92
+ | `tool.execute.after` | After OpenCode finishes using a tool | Informational (audit only) |
93
+ | `permission.asked` | When OpenCode requests user permission | **Blocking** — Orca can deny the permission |
94
+ | `file.edited` | When a file is edited by OpenCode | Informational (audit only) |
95
+ | `command.executed` | When a shell command is executed | Informational (audit only) |
96
+ | `session.updated` | When the session state changes | Informational (audit only) |
97
+ | `session.idle` | When the session becomes idle | Informational (audit only) |
98
+ | `session.error` | When a session error occurs | Informational (audit only) |
99
+ | `shell.env` | When the shell environment is read | Informational (secrets redacted) |
100
+
101
+ ## How hooks call Orca
102
+
103
+ Each hook sends a JSON payload to `orca hook opencode <event>` via stdin and reads a JSON decision from stdout. The plugin preserves OpenCode's expected return values. Human-readable logs go to stderr.
104
+
105
+ Example payload for `tool.execute.before`:
106
+
107
+ ```json
108
+ {
109
+ "version": 1,
110
+ "host": "opencode",
111
+ "event": "PreToolUse",
112
+ "payload": {
113
+ "tool": "shell",
114
+ "command": "git status"
115
+ },
116
+ "session_id": "session-uuid",
117
+ "timestamp": "2026-01-01T00:00:00Z"
118
+ }
119
+ ```
120
+
121
+ Example response:
122
+
123
+ ```json
124
+ {
125
+ "version": 1,
126
+ "decision": "allow",
127
+ "risk": "low",
128
+ "category": "command",
129
+ "reason": "policy_allow",
130
+ "message": "Allowed by policy"
131
+ }
132
+ ```
133
+
134
+ If the decision is `block`, the plugin throws an error that prevents the tool from executing.
135
+
136
+ ## Run redteam
137
+
138
+ ```bash
139
+ orca redteam --ci
140
+ ```
141
+
142
+ ## Replay sessions
143
+
144
+ ```bash
145
+ orca replay --session last --verify
146
+ ```
147
+
148
+ ## Uninstall
149
+
150
+ Remove the plugin from your OpenCode configuration:
151
+
152
+ ```bash
153
+ # npm package
154
+ npm uninstall orca-opencode-plugin
155
+
156
+ # Project-local file
157
+ rm .opencode/plugins/orca.ts
158
+
159
+ # Global file
160
+ rm ~/.config/opencode/plugins/orca.ts
161
+ ```
162
+
163
+ This plugin does not mutate host configuration, so uninstalling is safe.
164
+
165
+ ## Known limitations
166
+
167
+ - Hooks are advisory for informational events; blocking hooks depend on OpenCode honoring thrown errors.
168
+ - The strongest protection remains `orca run -- opencode`.
169
+ - Plugin installation depends on OpenCode version and plugin loading mechanism.
170
+ - No telemetry is collected.
171
+ - Official npm publication is in progress; the package structure is ready for publication.
172
+
173
+ ## Security model
174
+
175
+ - This plugin calls the Orca CLI; it does not reimplement policy logic.
176
+ - No raw secrets are persisted in plugin files.
177
+ - Secrets are redacted from payloads before sending to Orca (keys matching `password`, `token`, `secret`, `api_key`, etc. are replaced with `[REDACTED]`).
178
+ - Hook return values remain valid for OpenCode parsing.
179
+ - Human logs go to stderr.
180
+ - CI mode never prompts.
181
+ - This plugin does not claim stronger enforcement than OpenCode hooks support.
182
+
183
+ ## No MCP server behavior
184
+
185
+ The OpenCode plugin does not add MCP server behavior or drone-specific plugin features.
186
+
187
+ ## Strongest protection warning
188
+
189
+ > The Orca OpenCode plugin adds lifecycle hooks for OpenCode. For the strongest local protection, run the OpenCode process itself through Orca with `orca run -- opencode`.
@@ -0,0 +1,24 @@
1
+ interface OpenCodeContext {
2
+ hooks: {
3
+ on: (event: string, handler: (data: unknown) => unknown | Promise<unknown>) => void;
4
+ };
5
+ shell?: {
6
+ $: (strings: TemplateStringsArray, ...values: unknown[]) => Promise<{
7
+ stdout: string;
8
+ stderr: string;
9
+ exitCode: number;
10
+ }>;
11
+ };
12
+ logger?: {
13
+ info: (msg: string) => void;
14
+ warn: (msg: string) => void;
15
+ error: (msg: string) => void;
16
+ };
17
+ session?: {
18
+ id?: string;
19
+ cwd?: string;
20
+ };
21
+ }
22
+ export default function orcaPlugin(context: OpenCodeContext): void;
23
+ export {};
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,UAAU,eAAe;IACvB,KAAK,EAAE;QACL,EAAE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;KACrF,CAAC;IACF,KAAK,CAAC,EAAE;QACN,CAAC,EAAE,CAAC,OAAO,EAAE,oBAAoB,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC3H,CAAC;IACF,MAAM,CAAC,EAAE;QACP,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAC5B,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAC5B,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;KAC9B,CAAC;IACF,OAAO,CAAC,EAAE;QACR,EAAE,CAAC,EAAE,MAAM,CAAC;QACZ,GAAG,CAAC,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAgID,MAAM,CAAC,OAAO,UAAU,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,IAAI,CA6FjE"}
package/dist/index.js ADDED
@@ -0,0 +1,179 @@
1
+ import { execSync } from 'child_process';
2
+ import { existsSync } from 'fs';
3
+ import { join, resolve } from 'path';
4
+ const SECRET_KEYS = [
5
+ 'password', 'token', 'secret', 'api_key', 'apikey', 'api_secret',
6
+ 'auth', 'authorization', 'bearer', 'private_key', 'access_token',
7
+ 'refresh_token', 'credential', 'passwd', 'pwd',
8
+ ];
9
+ function redactSecrets(data) {
10
+ if (data === null || data === undefined)
11
+ return data;
12
+ if (typeof data === 'string')
13
+ return data;
14
+ if (Array.isArray(data))
15
+ return data.map(redactSecrets);
16
+ if (typeof data !== 'object')
17
+ return data;
18
+ const result = {};
19
+ for (const [key, value] of Object.entries(data)) {
20
+ const lowerKey = key.toLowerCase();
21
+ if (SECRET_KEYS.some((s) => lowerKey.includes(s))) {
22
+ result[key] = '[REDACTED]';
23
+ }
24
+ else {
25
+ result[key] = redactSecrets(value);
26
+ }
27
+ }
28
+ return result;
29
+ }
30
+ function buildPayload(event, data, sessionId) {
31
+ return {
32
+ version: 1,
33
+ host: 'opencode',
34
+ event,
35
+ payload: redactSecrets(data),
36
+ session_id: sessionId,
37
+ timestamp: new Date().toISOString(),
38
+ };
39
+ }
40
+ function findOrca(cwd) {
41
+ try {
42
+ const which = execSync('which orca', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
43
+ const bin = which.trim();
44
+ if (bin)
45
+ return bin;
46
+ }
47
+ catch {
48
+ // orca not in PATH
49
+ }
50
+ const candidates = [
51
+ cwd ? join(cwd, 'zig-out', 'bin', 'orca') : null,
52
+ cwd ? join(cwd, '..', 'zig-out', 'bin', 'orca') : null,
53
+ cwd ? join(cwd, '..', '..', 'zig-out', 'bin', 'orca') : null,
54
+ resolve('zig-out', 'bin', 'orca'),
55
+ resolve('..', 'zig-out', 'bin', 'orca'),
56
+ resolve('..', '..', 'zig-out', 'bin', 'orca'),
57
+ ].filter((p) => p !== null);
58
+ for (const p of candidates) {
59
+ if (existsSync(p))
60
+ return p;
61
+ }
62
+ return null;
63
+ }
64
+ async function callOrca(orcaBin, event, data, sessionId, blocking, shell, logger) {
65
+ const payload = buildPayload(event, data, sessionId);
66
+ const payloadJson = JSON.stringify(payload);
67
+ let stdout = '';
68
+ try {
69
+ if (shell?.$) {
70
+ const result = await shell.$ `echo ${payloadJson} | ${orcaBin} hook opencode ${event}`;
71
+ stdout = result.stdout ?? '';
72
+ }
73
+ else {
74
+ stdout = execSync(`${orcaBin} hook opencode ${event}`, {
75
+ input: payloadJson,
76
+ encoding: 'utf-8',
77
+ timeout: blocking ? 15000 : 10000,
78
+ stdio: ['pipe', 'pipe', 'pipe'],
79
+ });
80
+ }
81
+ if (!stdout.trim()) {
82
+ return { decision: 'allow' };
83
+ }
84
+ return JSON.parse(stdout);
85
+ }
86
+ catch (err) {
87
+ const safeErr = redactSecrets({ message: err.message });
88
+ logger?.error?.(`[orca] Hook ${event} failed: ${safeErr.message}`);
89
+ return blocking
90
+ ? {
91
+ decision: 'block',
92
+ risk: 'high',
93
+ category: 'unknown',
94
+ reason: 'orca_hook_error',
95
+ message: 'Orca hook failed; blocking as a precaution.',
96
+ }
97
+ : {
98
+ decision: 'allow',
99
+ risk: 'unknown',
100
+ category: 'unknown',
101
+ reason: 'orca_hook_error',
102
+ message: 'Orca hook failed; allowing because this event is non-blocking.',
103
+ };
104
+ }
105
+ }
106
+ export default function orcaPlugin(context) {
107
+ const cwd = context.session?.cwd ?? process.cwd();
108
+ const sessionId = context.session?.id;
109
+ const orcaBin = findOrca(cwd);
110
+ const { shell, logger } = context;
111
+ if (!orcaBin) {
112
+ logger?.warn?.('[orca] Binary not found in PATH or typical build paths. ' +
113
+ 'Build with: zig build (produces ./zig-out/bin/orca)');
114
+ return;
115
+ }
116
+ logger?.info?.(`[orca] Plugin loaded. Binary: ${orcaBin}`);
117
+ context.hooks.on('session.created', async (session) => {
118
+ logger?.info?.('[orca] Plugin ready for session.');
119
+ await callOrca(orcaBin, 'SessionStart', { session_id: session?.id }, sessionId, false, shell, logger);
120
+ });
121
+ context.hooks.on('tool.execute.before', async (toolCall) => {
122
+ const response = await callOrca(orcaBin, 'PreToolUse', toolCall, sessionId, true, shell, logger);
123
+ if (response.decision === 'block') {
124
+ const msg = response.message || response.reason || 'Blocked by Orca policy';
125
+ logger?.error?.(`[orca] Blocked tool execution: ${msg}`);
126
+ throw new Error(`Orca blocked tool execution: ${msg}`);
127
+ }
128
+ if (response.decision === 'warn') {
129
+ logger?.warn?.(`[orca] Warning: ${response.message || response.reason}`);
130
+ }
131
+ return toolCall;
132
+ });
133
+ context.hooks.on('tool.execute.after', async (result) => {
134
+ await callOrca(orcaBin, 'PostToolUse', result, sessionId, false, shell, logger);
135
+ return result;
136
+ });
137
+ context.hooks.on('permission.asked', async (permission) => {
138
+ const response = await callOrca(orcaBin, 'PermissionRequest', permission, sessionId, true, shell, logger);
139
+ if (response.decision === 'block') {
140
+ const msg = response.message || response.reason || 'Blocked by Orca policy';
141
+ logger?.error?.(`[orca] Blocked permission: ${msg}`);
142
+ throw new Error(`Orca blocked permission request: ${msg}`);
143
+ }
144
+ if (response.decision === 'warn') {
145
+ logger?.warn?.(`[orca] Permission warning: ${response.message || response.reason}`);
146
+ }
147
+ return permission;
148
+ });
149
+ context.hooks.on('file.edited', async (edit) => {
150
+ await callOrca(orcaBin, 'FileEdit', edit, sessionId, false, shell, logger);
151
+ return edit;
152
+ });
153
+ context.hooks.on('command.executed', async (command) => {
154
+ await callOrca(orcaBin, 'CommandExecuted', command, sessionId, false, shell, logger);
155
+ return command;
156
+ });
157
+ context.hooks.on('session.updated', async (session) => {
158
+ await callOrca(orcaBin, 'SessionUpdate', session, sessionId, false, shell, logger);
159
+ return session;
160
+ });
161
+ context.hooks.on('session.idle', async (session) => {
162
+ await callOrca(orcaBin, 'SessionIdle', session, sessionId, false, shell, logger);
163
+ return session;
164
+ });
165
+ context.hooks.on('session.error', async (error) => {
166
+ const safeError = redactSecrets({
167
+ message: error?.message,
168
+ stack: error?.stack,
169
+ type: error?.type,
170
+ });
171
+ await callOrca(orcaBin, 'SessionError', safeError, sessionId, false, shell, logger);
172
+ return error;
173
+ });
174
+ context.hooks.on('shell.env', async (env) => {
175
+ const safeEnv = redactSecrets(env);
176
+ await callOrca(orcaBin, 'ShellEnv', safeEnv, sessionId, false, shell, logger);
177
+ return env;
178
+ });
179
+ }
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "orca-opencode-plugin",
3
+ "version": "1.1.0",
4
+ "description": "OpenCode plugin wrapper for Orca runtime guardrails.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist",
10
+ "README.md",
11
+ "package.json"
12
+ ],
13
+ "keywords": [
14
+ "opencode",
15
+ "plugin",
16
+ "orca",
17
+ "ai-agents",
18
+ "security",
19
+ "guardrails"
20
+ ],
21
+ "license": "Apache-2.0",
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ },
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "pack:dry-run": "npm pack --dry-run"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/chriskarani/orca.git"
32
+ },
33
+ "homepage": "https://github.com/chriskarani/orca#readme",
34
+ "bugs": {
35
+ "url": "https://github.com/chriskarani/orca/issues"
36
+ },
37
+ "devDependencies": {
38
+ "@types/node": "^25.6.2",
39
+ "typescript": "^6.0.3"
40
+ }
41
+ }