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 +77 -0
- package/dist/config.d.ts +24 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +77 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/own-model.d.ts +9 -0
- package/dist/own-model.d.ts.map +1 -0
- package/dist/own-model.js +31 -0
- package/dist/own-model.js.map +1 -0
- package/package.json +41 -0
- package/src/config.ts +101 -0
- package/src/index.ts +86 -0
- package/src/own-model.ts +37 -0
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.
|
package/dist/config.d.ts
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"
|
package/src/own-model.ts
ADDED
|
@@ -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
|
+
}
|