Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/guardrails/profile/opencode.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"$schema": "https://opencode.ai/config.json",
"instructions": ["AGENTS.md"],
"default_agent": "implement",
"plugin": ["./plugins/guardrail.ts", "./plugins/team.ts"],
"enabled_providers": [
"zai",
"zai-coding-plan",
Expand Down
73 changes: 73 additions & 0 deletions packages/opencode/test/scenario/guardrails.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,6 +1046,79 @@ test("guardrail delegation gates and quality hooks fire correctly", async () =>
})
}, 15000)

test("guardrail plugin loads from profile config and fires session.created", async () => {
await withProfile(async () => {
await using tmp = await tmpdir({ git: true })
const files = guard(tmp.path)

await Instance.provide({
directory: tmp.path,
fn: async () => {
// 1. Verify plugin is loaded from config
const plugins = await Plugin.list()
Comment on lines +1050 to +1058
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new integration test sets OPENCODE_CONFIG_DIR to the guardrails profile directory via withProfile(). Config will auto-discover and load plugins/*.{ts,js} from every config directory (see packages/opencode/src/config/config.ts:320-330), so the guardrail plugin may be loaded even if the opencode.json plugin field were missing. As a result, the test may not actually validate the PR’s fix (loading via the config plugin list). Consider adjusting this test to load the profile config via OPENCODE_CONFIG (a single file) or a temp config file that includes the plugin list but does not reside in a directory containing plugins/, so the only way the plugin can load is through cfg.plugin resolution.

Copilot uses AI. Check for mistakes.
const guardrailPlugin = plugins.find(
(item) => typeof item.event === "function" && typeof item["tool.execute.before"] === "function",
)
expect(guardrailPlugin).toBeDefined()

// 2. Verify config includes plugin field
const cfg = await Config.get()
expect(cfg.plugin).toBeDefined()
expect(cfg.plugin!.some((p: unknown) => typeof p === "string" && String(p).includes("guardrail"))).toBe(true)

// 3. Fire session.created and verify state file is written
await guardrailPlugin!.event!({
event: {
type: "session.created",
properties: { sessionID: "session_config_load_test" },
},
} as any)
await wait()

const state = await Bun.file(files.state).json()
expect(state.mode).toBe("enforced")
expect(state.active_tasks).toEqual({})
expect(state.active_task_count).toBe(0)
expect(state.llm_call_count).toBe(0)
expect(state.llm_calls_by_provider).toEqual({})
expect(state.session_providers).toEqual([])
expect(state.consecutive_failures).toBe(0)
expect(state.issue_verification_done).toBe(false)

// 4. Verify events.jsonl was written
const logText = await Bun.file(files.log).text()
expect(logText).toContain("session.created")
expect(logText).toContain("session_config_load_test")

// 5. Fire tool.execute.before for secret file — verify hard block
await expect(
Plugin.trigger(
"tool.execute.before",
{ tool: "read", sessionID: "session_config_load_test", callID: "call_secret" },
{ args: { filePath: path.join(tmp.path, ".env.production") } },
),
).rejects.toThrow("secret material")

// 6. Fire tool.execute.after for bash — verify state tracking
await Plugin.trigger(
"tool.execute.after",
{ tool: "bash", sessionID: "session_config_load_test", callID: "call_test", args: { command: "bun test" } },
{ title: "bash", output: "19 pass", metadata: { exitCode: 0 } },
)
const updatedState = await Bun.file(files.state).json()
expect(updatedState.tests_executed).toBe(true)

// 7. Verify shell.env hook exposes guardrail env vars
const envOut = { env: {} as Record<string, string> }
await guardrailPlugin!["shell.env"]!({ cwd: tmp.path } as any, envOut as any)
expect(envOut.env.OPENCODE_GUARDRAIL_MODE).toBe("enforced")
expect(envOut.env.OPENCODE_GUARDRAIL_ROOT).toBeDefined()
expect(envOut.env.OPENCODE_GUARDRAIL_STATE).toBeDefined()
},
})
})
}, 15000)

for (const replay of Object.values(replays)) {
it.live(`guardrail replay keeps ${replay.command} executable`, () =>
run(replay).pipe(
Expand Down
Loading