openguardrails-instrumentation-opencode 0.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,77 @@
1
+ # openguardrails-instrumentation-opencode
2
+
3
+ Guard an [opencode](https://github.com/anomalyco/opencode) agent's tool calls
4
+ through the [OpenGuardrails (OGR)](https://www.npmjs.com/package/@openguardrails/core)
5
+ protocol — the TS counterpart of `openguardrails-instrumentation-hermes`.
6
+
7
+ The agent configures **its own guardrails**: plain **text + regex** rules (no
8
+ model required), and optionally **its own model** as an LLM judge. Enforced as a
9
+ pure opencode plugin — **no core changes, no fork**.
10
+
11
+ ```bash
12
+ npm install openguardrails-instrumentation-opencode
13
+ ```
14
+
15
+ ## How it works
16
+
17
+ opencode fires `tool.execute.before` for every tool, before it runs. This plugin
18
+ turns the call into an OGR `GuardEvent`, runs it through a `Runtime` built from
19
+ your policy, and enforces the `Verdict`:
20
+
21
+ | OGR decision | opencode behavior |
22
+ | --- | --- |
23
+ | `allow` / `modify` / `redact` | proceed |
24
+ | `block` | throw → the agent sees a tool error and must find a safer path |
25
+ | `require_approval` | throw → asks you to re-run intentionally or relax the policy |
26
+
27
+ It is a **restrict-only** guard: it can stop a would-run tool call, never loosen
28
+ one. (opencode's own `permission` rules still apply first.)
29
+
30
+ ## Enable
31
+
32
+ In your opencode config:
33
+
34
+ ```jsonc
35
+ {
36
+ "plugin": ["openguardrails-instrumentation-opencode"]
37
+ }
38
+ ```
39
+
40
+ ## Configure your guardrails
41
+
42
+ Drop an OGR policy at **`.opencode/guardrails.json`** (the agent can write/edit
43
+ this itself), or pass it inline as plugin options. A sensible default ships in
44
+ the package (`curl|bash`, `rm -rf /`, credential-file access, `| sudo`).
45
+
46
+ ```json
47
+ {
48
+ "composition": { "security.*": { "strategy": "deny-wins", "on_all_failed": "block" } },
49
+ "config_rules": {
50
+ "command_rules": [
51
+ { "id": "no-prod-deploy", "regex": "deploy\\s+--env\\s+prod",
52
+ "category": "security.malicious_command", "decision": "require_approval",
53
+ "score": 0.9, "why": "production deploys need explicit human approval" }
54
+ ]
55
+ }
56
+ }
57
+ ```
58
+
59
+ ### Use your own model as the judge
60
+
61
+ ```json
62
+ {
63
+ "config_rules": { "command_rules": [] },
64
+ "judge": { "baseURL": "https://api.openai.com/v1", "model": "gpt-4o-mini", "apiKey": "sk-..." }
65
+ }
66
+ ```
67
+
68
+ Any OpenAI-compatible chat endpoint works — point it at the same model your agent
69
+ uses, or a dedicated guard model. The judge weighs provenance and returns an OGR
70
+ verdict; the deterministic text/regex rules remain the baseline.
71
+
72
+ ## Status
73
+
74
+ `v0.1`. Pure plugin via `tool.execute.before`. A first-class "ask the human"
75
+ (`require_approval` as an interactive prompt) and transcript-based provenance
76
+ tainting are tracked follow-ups; today `require_approval` is enforced as a
77
+ deny-with-guidance.
@@ -0,0 +1,24 @@
1
+ import type { Policy } from "@openguardrails/core";
2
+ /** "Use your own model as the guardrail" — any OpenAI-compatible chat endpoint. */
3
+ export interface JudgeConfig {
4
+ baseURL: string;
5
+ model: string;
6
+ apiKey?: string;
7
+ headers?: Record<string, string>;
8
+ }
9
+ export interface GuardrailsOptions {
10
+ /** Inline OGR policy (overrides the file + default). */
11
+ policy?: Policy;
12
+ /** Path to a guardrails policy file (defaults to .opencode/guardrails.json). */
13
+ policyPath?: string;
14
+ /** Enable the LLM-judge detector backed by your own model. */
15
+ judge?: JudgeConfig;
16
+ }
17
+ /** Default text/regex guardrails — deterministic, no model required. */
18
+ export declare const DEFAULT_POLICY: Policy;
19
+ export interface ResolvedConfig {
20
+ policy: Policy;
21
+ judge?: JudgeConfig;
22
+ }
23
+ export declare function loadGuardrailsConfig(directory: string, options?: GuardrailsOptions): ResolvedConfig;
24
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAElD,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CACjC;AAED,MAAM,WAAW,iBAAiB;IAChC,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,WAAW,CAAA;CACpB;AAED,wEAAwE;AACxE,eAAO,MAAM,cAAc,EAAE,MA0C5B,CAAA;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,WAAW,CAAA;CACpB;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,iBAAiB,GAAG,cAAc,CAenG"}
package/dist/config.js ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Guardrails configuration for the opencode integration.
3
+ *
4
+ * The agent configures its OWN guardrails — text + regex rules (no model
5
+ * needed), and optionally its own model as an LLM judge. Config resolution:
6
+ *
7
+ * 1. a sensible default policy (below)
8
+ * 2. `.opencode/guardrails.json` in the project (agent-editable — this is how
9
+ * an agent gives itself guardrails)
10
+ * 3. plugin `options` passed in opencode config (highest precedence)
11
+ *
12
+ * The policy IS an OGR policy.json (composition + config_rules), so the same
13
+ * file format works across every OGR integration.
14
+ */
15
+ import { readFileSync, existsSync } from "node:fs";
16
+ import { join } from "node:path";
17
+ /** Default text/regex guardrails — deterministic, no model required. */
18
+ export const DEFAULT_POLICY = {
19
+ composition: {
20
+ "security.*": { strategy: "deny-wins", on_all_failed: "block" },
21
+ default: { strategy: "deny-wins" },
22
+ },
23
+ config_rules: {
24
+ secret_env_markers: ["SECRET", "TOKEN", "KEY", "PASSWORD", "AWS_", "PRIVATE", "CREDENTIAL"],
25
+ command_rules: [
26
+ {
27
+ id: "pipe-to-shell",
28
+ regex: "(curl|wget)\\b.*\\|\\s*(ba)?sh",
29
+ category: "security.malicious_command",
30
+ decision: "require_approval",
31
+ score: 0.85,
32
+ why: "remote script fetched and piped directly into a shell",
33
+ },
34
+ {
35
+ id: "rm-rf-root",
36
+ regex: "rm\\s+-rf\\s+/(\\s|$)",
37
+ category: "security.malicious_command",
38
+ decision: "block",
39
+ score: 1.0,
40
+ why: "destructive recursive delete of the filesystem root",
41
+ },
42
+ {
43
+ id: "secret-file-access",
44
+ regex: "(\\.env\\b|/\\.aws/credentials|/\\.ssh/id_|/\\.ssh/|auth\\.json|\\.netrc)",
45
+ category: "security.secret_leak",
46
+ decision: "block",
47
+ score: 0.9,
48
+ why: "command references a credential file — independent of the reader",
49
+ },
50
+ {
51
+ id: "pipe-to-sudo",
52
+ regex: "\\|\\s*sudo\\b",
53
+ category: "security.privilege_escalation",
54
+ decision: "require_approval",
55
+ score: 0.7,
56
+ why: "output piped into sudo",
57
+ },
58
+ ],
59
+ },
60
+ };
61
+ export function loadGuardrailsConfig(directory, options) {
62
+ let policy = DEFAULT_POLICY;
63
+ const path = options?.policyPath ?? join(directory, ".opencode", "guardrails.json");
64
+ if (existsSync(path)) {
65
+ try {
66
+ policy = JSON.parse(readFileSync(path, "utf8"));
67
+ }
68
+ catch {
69
+ // malformed file → keep the safe default rather than failing open silently
70
+ }
71
+ }
72
+ if (options?.policy)
73
+ policy = options.policy;
74
+ const judge = options?.judge ?? policy["judge"];
75
+ return { policy, judge };
76
+ }
77
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAA;AAoBhC,wEAAwE;AACxE,MAAM,CAAC,MAAM,cAAc,GAAW;IACpC,WAAW,EAAE;QACX,YAAY,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE;QAC/D,OAAO,EAAE,EAAE,QAAQ,EAAE,WAAW,EAAE;KACnC;IACD,YAAY,EAAE;QACZ,kBAAkB,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,YAAY,CAAC;QAC3F,aAAa,EAAE;YACb;gBACE,EAAE,EAAE,eAAe;gBACnB,KAAK,EAAE,gCAAgC;gBACvC,QAAQ,EAAE,4BAA4B;gBACtC,QAAQ,EAAE,kBAAkB;gBAC5B,KAAK,EAAE,IAAI;gBACX,GAAG,EAAE,uDAAuD;aAC7D;YACD;gBACE,EAAE,EAAE,YAAY;gBAChB,KAAK,EAAE,uBAAuB;gBAC9B,QAAQ,EAAE,4BAA4B;gBACtC,QAAQ,EAAE,OAAO;gBACjB,KAAK,EAAE,GAAG;gBACV,GAAG,EAAE,qDAAqD;aAC3D;YACD;gBACE,EAAE,EAAE,oBAAoB;gBACxB,KAAK,EAAE,2EAA2E;gBAClF,QAAQ,EAAE,sBAAsB;gBAChC,QAAQ,EAAE,OAAO;gBACjB,KAAK,EAAE,GAAG;gBACV,GAAG,EAAE,kEAAkE;aACxE;YACD;gBACE,EAAE,EAAE,cAAc;gBAClB,KAAK,EAAE,gBAAgB;gBACvB,QAAQ,EAAE,+BAA+B;gBACzC,QAAQ,EAAE,kBAAkB;gBAC5B,KAAK,EAAE,GAAG;gBACV,GAAG,EAAE,wBAAwB;aAC9B;SACF;KACF;CACF,CAAA;AAOD,MAAM,UAAU,oBAAoB,CAAC,SAAiB,EAAE,OAA2B;IACjF,IAAI,MAAM,GAAW,cAAc,CAAA;IAEnC,MAAM,IAAI,GAAG,OAAO,EAAE,UAAU,IAAI,IAAI,CAAC,SAAS,EAAE,WAAW,EAAE,iBAAiB,CAAC,CAAA;IACnF,IAAI,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QACrB,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAW,CAAA;QAC3D,CAAC;QAAC,MAAM,CAAC;YACP,2EAA2E;QAC7E,CAAC;IACH,CAAC;IACD,IAAI,OAAO,EAAE,MAAM;QAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAA;IAE5C,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK,IAAK,MAAM,CAAC,OAAO,CAA6B,CAAA;IAC5E,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;AAC1B,CAAC"}
@@ -0,0 +1,23 @@
1
+ /**
2
+ * openguardrails-instrumentation-opencode
3
+ *
4
+ * An opencode plugin that guards an agent's tool calls through the OpenGuardrails
5
+ * (OGR) protocol — the TS counterpart of `openguardrails-instrumentation-hermes`.
6
+ *
7
+ * It hooks `tool.execute.before` (fired for every tool, before it runs), turns
8
+ * the call into an OGR `GuardEvent`, runs it through a `Runtime` built from the
9
+ * agent's own guardrails policy (text/regex rules, plus optionally its own model
10
+ * as an LLM judge), and enforces the `Verdict`:
11
+ *
12
+ * allow | modify | redact → proceed
13
+ * block | require_approval → throw (deny-and-continue: the agent sees a tool
14
+ * error and must find a safer path or get approval)
15
+ *
16
+ * No opencode core changes required. This is a "restrict-only" guard: it can stop
17
+ * a would-run tool call, never loosen one.
18
+ */
19
+ import type { Plugin } from "@opencode-ai/plugin";
20
+ export declare const OpenGuardrailsPlugin: Plugin;
21
+ export default OpenGuardrailsPlugin;
22
+ export { DEFAULT_POLICY, type GuardrailsOptions, type JudgeConfig } from "./config.js";
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,OAAO,KAAK,EAAE,MAAM,EAAS,MAAM,qBAAqB,CAAA;AAyBxD,eAAO,MAAM,oBAAoB,EAAE,MAuClC,CAAA;AAED,eAAe,oBAAoB,CAAA;AACnC,OAAO,EAAE,cAAc,EAAE,KAAK,iBAAiB,EAAE,KAAK,WAAW,EAAE,MAAM,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,51 @@
1
+ import { Runtime, ConfigRulesDetector, LLMJudgeDetector, } from "@openguardrails/core";
2
+ import { loadGuardrailsConfig } from "./config.js";
3
+ import { openAICompatibleBackend } from "./own-model.js";
4
+ let seq = 0;
5
+ function id(prefix) {
6
+ seq += 1;
7
+ const rand = globalThis.crypto?.randomUUID?.().slice(0, 8) ?? Math.random().toString(36).slice(2, 10);
8
+ return `${prefix}-${seq.toString(36)}-${rand}`;
9
+ }
10
+ function brief(v) {
11
+ const cats = v.categories.map((c) => `${c.id}(${c.score})`).join(", ");
12
+ const why = v.reasons.filter((r) => !r.startsWith("[")).join("; ");
13
+ return [cats, why].filter(Boolean).join(" — ") || v.decision;
14
+ }
15
+ export const OpenGuardrailsPlugin = async ({ directory }, options) => {
16
+ const { policy, judge } = loadGuardrailsConfig(directory, options);
17
+ const detectors = [new ConfigRulesDetector(policy.config_rules ?? {})];
18
+ if (judge)
19
+ detectors.push(new LLMJudgeDetector(openAICompatibleBackend(judge)));
20
+ const runtime = new Runtime(detectors, policy);
21
+ const hooks = {
22
+ "tool.execute.before": async (input, output) => {
23
+ const ev = {
24
+ kind: "tool_call",
25
+ observationPoint: "agent_hook",
26
+ subject: { agent_id: "opencode", agent_type: "opencode", session_id: input.sessionID },
27
+ payload: { name: input.tool, arguments: output.args },
28
+ eventId: id("evt"),
29
+ guardId: id("ga"),
30
+ timestamp: new Date().toISOString(),
31
+ sessionId: input.sessionID,
32
+ // v0.1: principal is trusted. Transcript-based tainting (web/mcp results
33
+ // → untrusted provenance) is a follow-up via the opencode session API.
34
+ provenance: [{ source: "user", trust: "trusted" }],
35
+ };
36
+ const verdict = await runtime.evaluate(ev);
37
+ if (verdict.decision === "block") {
38
+ throw new Error(`[OpenGuardrails] blocked this ${input.tool} call: ${brief(verdict)}`);
39
+ }
40
+ if (verdict.decision === "require_approval") {
41
+ throw new Error(`[OpenGuardrails] this ${input.tool} call needs your explicit approval: ${brief(verdict)}. ` +
42
+ `Re-run only if you intend this, or relax .opencode/guardrails.json.`);
43
+ }
44
+ // allow | modify | redact → proceed
45
+ },
46
+ };
47
+ return hooks;
48
+ };
49
+ export default OpenGuardrailsPlugin;
50
+ export { DEFAULT_POLICY } from "./config.js";
51
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * "Use your own model as the guardrail" — an OGR LLMBackend that calls any
3
+ * OpenAI-compatible chat-completions endpoint. Point it at the same model the
4
+ * agent already uses, a cheaper sibling, or a dedicated guard model.
5
+ */
6
+ import type { LLMBackend } from "@openguardrails/core";
7
+ import type { JudgeConfig } from "./config.js";
8
+ export declare function openAICompatibleBackend(cfg: JudgeConfig): LLMBackend;
9
+ //# sourceMappingURL=own-model.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"own-model.d.ts","sourceRoot":"","sources":["../src/own-model.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAE9C,wBAAgB,uBAAuB,CAAC,GAAG,EAAE,WAAW,GAAG,UAAU,CA4BpE"}
@@ -0,0 +1,31 @@
1
+ export function openAICompatibleBackend(cfg) {
2
+ const url = cfg.baseURL.replace(/\/+$/, "") + "/chat/completions";
3
+ return {
4
+ name: `own-model:${cfg.model}`,
5
+ async complete(system, user) {
6
+ const res = await fetch(url, {
7
+ method: "POST",
8
+ headers: {
9
+ "content-type": "application/json",
10
+ ...(cfg.apiKey ? { authorization: `Bearer ${cfg.apiKey}` } : {}),
11
+ ...(cfg.headers ?? {}),
12
+ },
13
+ body: JSON.stringify({
14
+ model: cfg.model,
15
+ temperature: 0,
16
+ messages: [
17
+ { role: "system", content: system },
18
+ { role: "user", content: user },
19
+ ],
20
+ }),
21
+ });
22
+ if (!res.ok)
23
+ throw new Error(`guard model returned ${res.status}`);
24
+ const data = (await res.json());
25
+ const text = data.choices?.[0]?.message?.content ?? "";
26
+ // Strip a ```json fence if the model wrapped its reply.
27
+ return text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim();
28
+ },
29
+ };
30
+ }
31
+ //# sourceMappingURL=own-model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"own-model.js","sourceRoot":"","sources":["../src/own-model.ts"],"names":[],"mappings":"AAQA,MAAM,UAAU,uBAAuB,CAAC,GAAgB;IACtD,MAAM,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,mBAAmB,CAAA;IACjE,OAAO;QACL,IAAI,EAAE,aAAa,GAAG,CAAC,KAAK,EAAE;QAC9B,KAAK,CAAC,QAAQ,CAAC,MAAc,EAAE,IAAY;YACzC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC3B,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE;oBACP,cAAc,EAAE,kBAAkB;oBAClC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,UAAU,GAAG,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;oBAChE,GAAG,CAAC,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;iBACvB;gBACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,KAAK,EAAE,GAAG,CAAC,KAAK;oBAChB,WAAW,EAAE,CAAC;oBACd,QAAQ,EAAE;wBACR,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,EAAE;wBACnC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE;qBAChC;iBACF,CAAC;aACH,CAAC,CAAA;YACF,IAAI,CAAC,GAAG,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,CAAC,MAAM,EAAE,CAAC,CAAA;YAClE,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4D,CAAA;YAC1F,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,CAAA;YACtD,wDAAwD;YACxD,OAAO,IAAI,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QAC7E,CAAC;KACF,CAAA;AACH,CAAC"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "openguardrails-instrumentation-opencode",
3
+ "version": "0.1.0",
4
+ "description": "Guard an opencode agent's tool calls through the OpenGuardrails (OGR) protocol — agent-configurable text/regex guardrails, or use your own model as the judge. No core changes.",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "author": "OpenGuardrails",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "main": "./dist/index.js",
15
+ "types": "./dist/index.d.ts",
16
+ "files": ["dist", "src"],
17
+ "scripts": {
18
+ "build": "tsc -b",
19
+ "clean": "tsc -b --clean"
20
+ },
21
+ "keywords": ["opencode", "plugin", "ai", "agent", "security", "guardrails", "ogr", "openguardrails"],
22
+ "dependencies": {
23
+ "@openguardrails/core": "^0.1.0"
24
+ },
25
+ "peerDependencies": {
26
+ "@opencode-ai/plugin": "*"
27
+ },
28
+ "devDependencies": {
29
+ "@opencode-ai/plugin": "latest",
30
+ "@types/node": "^22"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/openguardrails/openguardrails-js.git",
35
+ "directory": "packages/instrumentation-opencode"
36
+ },
37
+ "homepage": "https://openguardrails.com",
38
+ "publishConfig": {
39
+ "access": "public"
40
+ }
41
+ }
package/src/config.ts ADDED
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Guardrails configuration for the opencode integration.
3
+ *
4
+ * The agent configures its OWN guardrails — text + regex rules (no model
5
+ * needed), and optionally its own model as an LLM judge. Config resolution:
6
+ *
7
+ * 1. a sensible default policy (below)
8
+ * 2. `.opencode/guardrails.json` in the project (agent-editable — this is how
9
+ * an agent gives itself guardrails)
10
+ * 3. plugin `options` passed in opencode config (highest precedence)
11
+ *
12
+ * The policy IS an OGR policy.json (composition + config_rules), so the same
13
+ * file format works across every OGR integration.
14
+ */
15
+ import { readFileSync, existsSync } from "node:fs"
16
+ import { join } from "node:path"
17
+ import type { Policy } from "@openguardrails/core"
18
+
19
+ /** "Use your own model as the guardrail" — any OpenAI-compatible chat endpoint. */
20
+ export interface JudgeConfig {
21
+ baseURL: string
22
+ model: string
23
+ apiKey?: string
24
+ headers?: Record<string, string>
25
+ }
26
+
27
+ export interface GuardrailsOptions {
28
+ /** Inline OGR policy (overrides the file + default). */
29
+ policy?: Policy
30
+ /** Path to a guardrails policy file (defaults to .opencode/guardrails.json). */
31
+ policyPath?: string
32
+ /** Enable the LLM-judge detector backed by your own model. */
33
+ judge?: JudgeConfig
34
+ }
35
+
36
+ /** Default text/regex guardrails — deterministic, no model required. */
37
+ export const DEFAULT_POLICY: Policy = {
38
+ composition: {
39
+ "security.*": { strategy: "deny-wins", on_all_failed: "block" },
40
+ default: { strategy: "deny-wins" },
41
+ },
42
+ config_rules: {
43
+ secret_env_markers: ["SECRET", "TOKEN", "KEY", "PASSWORD", "AWS_", "PRIVATE", "CREDENTIAL"],
44
+ command_rules: [
45
+ {
46
+ id: "pipe-to-shell",
47
+ regex: "(curl|wget)\\b.*\\|\\s*(ba)?sh",
48
+ category: "security.malicious_command",
49
+ decision: "require_approval",
50
+ score: 0.85,
51
+ why: "remote script fetched and piped directly into a shell",
52
+ },
53
+ {
54
+ id: "rm-rf-root",
55
+ regex: "rm\\s+-rf\\s+/(\\s|$)",
56
+ category: "security.malicious_command",
57
+ decision: "block",
58
+ score: 1.0,
59
+ why: "destructive recursive delete of the filesystem root",
60
+ },
61
+ {
62
+ id: "secret-file-access",
63
+ regex: "(\\.env\\b|/\\.aws/credentials|/\\.ssh/id_|/\\.ssh/|auth\\.json|\\.netrc)",
64
+ category: "security.secret_leak",
65
+ decision: "block",
66
+ score: 0.9,
67
+ why: "command references a credential file — independent of the reader",
68
+ },
69
+ {
70
+ id: "pipe-to-sudo",
71
+ regex: "\\|\\s*sudo\\b",
72
+ category: "security.privilege_escalation",
73
+ decision: "require_approval",
74
+ score: 0.7,
75
+ why: "output piped into sudo",
76
+ },
77
+ ],
78
+ },
79
+ }
80
+
81
+ export interface ResolvedConfig {
82
+ policy: Policy
83
+ judge?: JudgeConfig
84
+ }
85
+
86
+ export function loadGuardrailsConfig(directory: string, options?: GuardrailsOptions): ResolvedConfig {
87
+ let policy: Policy = DEFAULT_POLICY
88
+
89
+ const path = options?.policyPath ?? join(directory, ".opencode", "guardrails.json")
90
+ if (existsSync(path)) {
91
+ try {
92
+ policy = JSON.parse(readFileSync(path, "utf8")) as Policy
93
+ } catch {
94
+ // malformed file → keep the safe default rather than failing open silently
95
+ }
96
+ }
97
+ if (options?.policy) policy = options.policy
98
+
99
+ const judge = options?.judge ?? (policy["judge"] as JudgeConfig | undefined)
100
+ return { policy, judge }
101
+ }
package/src/index.ts ADDED
@@ -0,0 +1,86 @@
1
+ /**
2
+ * openguardrails-instrumentation-opencode
3
+ *
4
+ * An opencode plugin that guards an agent's tool calls through the OpenGuardrails
5
+ * (OGR) protocol — the TS counterpart of `openguardrails-instrumentation-hermes`.
6
+ *
7
+ * It hooks `tool.execute.before` (fired for every tool, before it runs), turns
8
+ * the call into an OGR `GuardEvent`, runs it through a `Runtime` built from the
9
+ * agent's own guardrails policy (text/regex rules, plus optionally its own model
10
+ * as an LLM judge), and enforces the `Verdict`:
11
+ *
12
+ * allow | modify | redact → proceed
13
+ * block | require_approval → throw (deny-and-continue: the agent sees a tool
14
+ * error and must find a safer path or get approval)
15
+ *
16
+ * No opencode core changes required. This is a "restrict-only" guard: it can stop
17
+ * a would-run tool call, never loosen one.
18
+ */
19
+ import type { Plugin, Hooks } from "@opencode-ai/plugin"
20
+ import {
21
+ Runtime,
22
+ ConfigRulesDetector,
23
+ LLMJudgeDetector,
24
+ type Detector,
25
+ type GuardEvent,
26
+ type Verdict,
27
+ } from "@openguardrails/core"
28
+ import { loadGuardrailsConfig, type GuardrailsOptions } from "./config.js"
29
+ import { openAICompatibleBackend } from "./own-model.js"
30
+
31
+ let seq = 0
32
+ function id(prefix: string): string {
33
+ seq += 1
34
+ const rand = globalThis.crypto?.randomUUID?.().slice(0, 8) ?? Math.random().toString(36).slice(2, 10)
35
+ return `${prefix}-${seq.toString(36)}-${rand}`
36
+ }
37
+
38
+ function brief(v: Verdict): string {
39
+ const cats = v.categories.map((c) => `${c.id}(${c.score})`).join(", ")
40
+ const why = v.reasons.filter((r) => !r.startsWith("[")).join("; ")
41
+ return [cats, why].filter(Boolean).join(" — ") || v.decision
42
+ }
43
+
44
+ export const OpenGuardrailsPlugin: Plugin = async ({ directory }, options) => {
45
+ const { policy, judge } = loadGuardrailsConfig(directory, options as GuardrailsOptions | undefined)
46
+
47
+ const detectors: Detector[] = [new ConfigRulesDetector(policy.config_rules ?? {})]
48
+ if (judge) detectors.push(new LLMJudgeDetector(openAICompatibleBackend(judge)))
49
+ const runtime = new Runtime(detectors, policy)
50
+
51
+ const hooks: Hooks = {
52
+ "tool.execute.before": async (input, output) => {
53
+ const ev: GuardEvent = {
54
+ kind: "tool_call",
55
+ observationPoint: "agent_hook",
56
+ subject: { agent_id: "opencode", agent_type: "opencode", session_id: input.sessionID },
57
+ payload: { name: input.tool, arguments: output.args },
58
+ eventId: id("evt"),
59
+ guardId: id("ga"),
60
+ timestamp: new Date().toISOString(),
61
+ sessionId: input.sessionID,
62
+ // v0.1: principal is trusted. Transcript-based tainting (web/mcp results
63
+ // → untrusted provenance) is a follow-up via the opencode session API.
64
+ provenance: [{ source: "user", trust: "trusted" }],
65
+ }
66
+
67
+ const verdict = await runtime.evaluate(ev)
68
+
69
+ if (verdict.decision === "block") {
70
+ throw new Error(`[OpenGuardrails] blocked this ${input.tool} call: ${brief(verdict)}`)
71
+ }
72
+ if (verdict.decision === "require_approval") {
73
+ throw new Error(
74
+ `[OpenGuardrails] this ${input.tool} call needs your explicit approval: ${brief(verdict)}. ` +
75
+ `Re-run only if you intend this, or relax .opencode/guardrails.json.`,
76
+ )
77
+ }
78
+ // allow | modify | redact → proceed
79
+ },
80
+ }
81
+
82
+ return hooks
83
+ }
84
+
85
+ export default OpenGuardrailsPlugin
86
+ export { DEFAULT_POLICY, type GuardrailsOptions, type JudgeConfig } from "./config.js"
@@ -0,0 +1,37 @@
1
+ /**
2
+ * "Use your own model as the guardrail" — an OGR LLMBackend that calls any
3
+ * OpenAI-compatible chat-completions endpoint. Point it at the same model the
4
+ * agent already uses, a cheaper sibling, or a dedicated guard model.
5
+ */
6
+ import type { LLMBackend } from "@openguardrails/core"
7
+ import type { JudgeConfig } from "./config.js"
8
+
9
+ export function openAICompatibleBackend(cfg: JudgeConfig): LLMBackend {
10
+ const url = cfg.baseURL.replace(/\/+$/, "") + "/chat/completions"
11
+ return {
12
+ name: `own-model:${cfg.model}`,
13
+ async complete(system: string, user: string): Promise<string> {
14
+ const res = await fetch(url, {
15
+ method: "POST",
16
+ headers: {
17
+ "content-type": "application/json",
18
+ ...(cfg.apiKey ? { authorization: `Bearer ${cfg.apiKey}` } : {}),
19
+ ...(cfg.headers ?? {}),
20
+ },
21
+ body: JSON.stringify({
22
+ model: cfg.model,
23
+ temperature: 0,
24
+ messages: [
25
+ { role: "system", content: system },
26
+ { role: "user", content: user },
27
+ ],
28
+ }),
29
+ })
30
+ if (!res.ok) throw new Error(`guard model returned ${res.status}`)
31
+ const data = (await res.json()) as { choices?: Array<{ message?: { content?: string } }> }
32
+ const text = data.choices?.[0]?.message?.content ?? ""
33
+ // Strip a ```json fence if the model wrapped its reply.
34
+ return text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/i, "").trim()
35
+ },
36
+ }
37
+ }