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
4 changes: 3 additions & 1 deletion docs/ai-guardrails/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ The main source set for this migration is:
- Claude Code official hooks and settings docs
- Anthropic skill guide PDF (`The Complete Guide to Building Skills for Claude`) and summary
- OpenCode rules, skills, commands, and plugins docs
- Z.AI OpenCode / Coding Plan docs

In this migration, references to the `BDF` document should be interpreted as Anthropic's PDF `The Complete Guide to Building Skills for Claude`, which is the skill-construction guide the source repository philosophy lines up with operationally.

Expand Down Expand Up @@ -137,7 +138,7 @@ The remaining work is intentionally split into two stages:
- `now required before MVP claim`: `#5`, `#6`, `#7`, and `#13`
- `later, after MVP floor`: `#14` and `#12`

The detailed rationale lives in `docs/ai-guardrails/mvp-readiness.md`. Future sessions should start there before expanding issue scope.
The detailed rationale lives in `docs/ai-guardrails/mvp-readiness.md`. The `#13` boundary and intentional deferrals are fixed in `docs/ai-guardrails/adr/006-plugin-hardening-floor.md`. Future sessions should start there before expanding issue scope.

## Tracking

Expand Down Expand Up @@ -173,6 +174,7 @@ When continuing this work in future sessions:
- MVP readiness: `docs/ai-guardrails/mvp-readiness.md`
- Migration inventory: `docs/ai-guardrails/migration/`
- Scenario tests: `packages/opencode/test/scenario/`
- Scripted replays: `packages/opencode/test/scenario/replay.ts` and `packages/opencode/test/scenario/harness.ts`
- Thin distribution package: `packages/guardrails/`

## Primary references
Expand Down
5 changes: 3 additions & 2 deletions docs/ai-guardrails/adr/002-provider-admission-lanes.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This follows the same philosophy imported from `claude-code-skills` epic `#130`

Adopt three admission lanes:

1. `zai` and `openai` are the standard confidential-code lane.
1. `zai`, `zai-coding-plan`, and `openai` are the standard confidential-code lane.
2. `openrouter` is admitted only as a separate evaluation lane.
3. OpenRouter-backed evaluation stays on an explicit `provider-eval` agent and command instead of widening the default implementation lane.

Expand All @@ -39,8 +39,9 @@ The policy is implemented in two layers:

### Standard lane

- admitted providers: `zai`, `openai`
- admitted providers: `zai`, `zai-coding-plan`, `openai`
- admitted models are pinned through provider allowlists
- `zai-coding-plan` is exposed as its own provider because Z.AI's official OpenCode guidance instructs Coding Plan subscribers to select `Z.AI Coding Plan` rather than overloading the general `Z.AI` provider
- preview, free, and non-approved variants are excluded by default

### Evaluation lane
Expand Down
45 changes: 45 additions & 0 deletions docs/ai-guardrails/adr/005-scripted-scenario-replays.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# ADR 005: Scripted Scenario Replays

- Status: Accepted
- Date: 2026-04-03

## Context

`#5` and `#6` added guarded commands, subagents, and provider lanes. The existing scenario suite proved config and plugin slices, but it still left an MVP gap: the guarded workflows were defined in files yet not replayed end to end.

Epic `#130` from the source Claude harness makes the requirement explicit: implemented behavior is not complete until it is shown to fire in the real runtime path.

The local runtime also needs a way to grow future release-sensitive checks without creating a deep OpenCode fork or relying on live third-party APIs inside tests.

## Decision

Adopt scripted scenario replays for guardrail workflow coverage:

- run scenario tests under `packages/opencode/test/scenario/`
- boot the packaged guardrail profile through the real config, command, agent, plugin, and session layers
- replace network LLM calls with a deterministic fake LLM server
- script expected model replies as replay steps so guarded workflows can be re-run exactly
- assert on runtime artifacts that matter to MVP claims: session messages, task tool output, provider routing, and guardrail state/log files

The replay layer is intentionally small. It is not a second runtime. It is a deterministic driver for the existing runtime path.

## Consequences

### Positive

- workflow commands are proven through the same session path users invoke
- provider-lane regressions can be caught without hitting live vendor APIs
- future issues can add replays for release gates, review freshness, or share/server restrictions without forking core runtime behavior

### Negative

- replay scripts must stay aligned with upstream session semantics
- fake LLM responses prove routing and workflow mechanics, not model quality

## Evidence

- OpenCode commands: https://opencode.ai/docs/commands
- OpenCode plugins: https://opencode.ai/docs/plugins
- OpenCode config: https://opencode.ai/docs/config
- Claude Code hooks guide: https://docs.anthropic.com/en/docs/claude-code/hooks-guide
- Anthropic skill guide PDF: https://resources.anthropic.com/hubfs/The-Complete-Guide-to-Building-Skill-for-Claude.pdf
70 changes: 70 additions & 0 deletions docs/ai-guardrails/adr/006-plugin-hardening-floor.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# ADR 006: Plugin Hardening Floor For MVP

## Status

Accepted

## Context

Issue `#13` exists because the first plugin MVP proved the OpenCode hook surface, but it did not yet migrate enough of the high-value fast-feedback guardrails to support an MVP claim.

The source philosophy from epic `#130`, the source README, the harness-engineering references, and the Claude skill guide all point to the same rule:

- mechanism before prose
- fastest reliable feedback layer first
- "implemented" is not "working" without runtime proof

That means the next plugin wave should add only the local-runtime behaviors that materially strengthen the guarded workflows now, while refusing to quietly absorb later operational hardening.

## Decision

The MVP floor for plugin hardening in this repo is:

1. protect runtime-owned and policy-protected files from local mutation
2. block obvious version-baseline regressions before edit or write completion
3. track source-read budget and block further source edits once the budget is exceeded
4. record fact-check freshness and review freshness as local runtime state
5. inject that state into `/review`, `/ship`, `/handoff`, and compaction carry-over
6. scenario-test the above behavior in `packages/opencode/test/scenario/guardrails.test.ts`

This floor is implemented in `packages/guardrails/profile/plugins/guardrail.ts`.

## Included In MVP Floor

These behaviors are part of the MVP claim:

- provider lane enforcement remains declarative and independent from plugin hardening
- protected runtime/config mutation is blocked at `tool.execute.before`
- version downgrade and `:latest` pin regressions are blocked at `tool.execute.before`
- source-read budget is tracked in plugin state and blocks further source edits once exceeded
- successful `read`, `webfetch`, Context7, selected CLI checks, `edit`, `write`, and `task` completion update local guardrail state
- `/review`, `/ship`, `/handoff`, and session compaction consume that state so guarded workflows can report stale or missing checks explicitly

## Explicit Deferrals

These items are intentionally not part of the MVP floor:

- authoritative merge/review freshness enforcement in GitHub or CI
- post-merge and deployment verification
- Claude-specific local hook deployment integrity
- broader structural reminders that need more repository-specific tuning
- a separate `post-lint-format` plugin clone when OpenCode already formats on `edit` and `write`
- stronger fact-check-before-edit or GitHub-write blocking until the workflow and source-of-truth state are better defined

Those items belong to later maturity work such as `#14`, not to the MVP floor.

## Consequences

- the thin distribution stays upstream-friendly because enforcement remains in the packaged profile/plugin layer
- the guarded workflows now have file-backed state rather than prompt-only expectations
- the repo has a written boundary for what `#13` must do now versus what later issues should carry

## Sources

- `docs/ai-guardrails/README.md`
- `docs/ai-guardrails/mvp-readiness.md`
- `docs/ai-guardrails/migration/claude-code-skills-inventory.md`
- `claude-code-skills` README
- `claude-code-skills` epic `#130`
- `claude-code-skills/docs/references/harness-engineering-best-practices-2026.md`
- Anthropic `The Complete Guide to Building Skills for Claude`
4 changes: 2 additions & 2 deletions packages/guardrails/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Current contents focus on the first thin-distribution slice:
- packaged custom config dir profile
- packaged plugin for runtime guardrail hooks
- guarded `implement` and `review` agents plus packaged `/implement`, `/review`, `/ship`, and `/handoff` workflow commands
- declarative provider admission policy for `zai`, `openai`, and the isolated OpenRouter evaluation lane
- declarative provider admission policy for `zai`, `zai-coding-plan`, `openai`, and the isolated OpenRouter evaluation lane
- scenario coverage for managed config precedence, project-local asset compatibility, plugin behavior, and workflow safety defaults

Planned next slices are tracked in the fork:
Expand All @@ -73,7 +73,7 @@ It respects an existing `OPENCODE_CONFIG_DIR` so project- or environment-specifi

The packaged profile defaults to the `implement` agent. Review and release-readiness work should run through the packaged `/review`, `/ship`, and `/handoff` commands so the workflow stays read-only at the gate layer.

Provider admission is also packaged here. Standard confidential-code work is admitted on the `zai` and `openai` lane. OpenRouter-backed candidates are available only through the dedicated `provider-eval` lane so evaluation traffic does not silently become the default implementation path.
Provider admission is also packaged here. Standard confidential-code work is admitted on the `zai`, `zai-coding-plan`, and `openai` lane. `zai-coding-plan` is kept as a separate provider because Z.AI's official OpenCode guide tells Coding Plan subscribers to select `Z.AI Coding Plan` explicitly. OpenRouter-backed candidates are available only through the dedicated `provider-eval` lane so evaluation traffic does not silently become the default implementation path.

## Managed deployment

Expand Down
35 changes: 29 additions & 6 deletions packages/guardrails/bin/opencode-guardrails
Original file line number Diff line number Diff line change
@@ -1,27 +1,50 @@
#!/usr/bin/env node

const child = require("child_process")
const fs = require("fs")
const path = require("path")
import { spawnSync } from "node:child_process"
import fs from "node:fs"
import path from "node:path"
import { fileURLToPath } from "node:url"
import { parseEnv } from "node:util"

function fail(msg) {
console.error(msg)
process.exit(1)
}

function bin() {
const file = require.resolve("opencode/package.json")
const file = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "opencode", "package.json")
const root = path.dirname(file)
const json = JSON.parse(fs.readFileSync(file, "utf8"))
const rel = typeof json.bin === "string" ? json.bin : json.bin?.opencode
if (!rel) fail("Failed to resolve opencode bin from dependency")
return path.resolve(root, rel)
}

const dir = path.resolve(__dirname, "..", "profile")
function env(dir) {
let cur = dir
for (;;) {
const file = path.join(cur, ".env")
if (fs.existsSync(file)) return file
const parent = path.dirname(cur)
if (parent === cur) return
cur = parent
}
}

function load(dir) {
const file = env(dir)
if (!file) return
const data = parseEnv(fs.readFileSync(file, "utf8").replace(/^\s*export\s+/gm, ""))
for (const [key, val] of Object.entries(data)) {
process.env[key] ??= val
}
}

const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "profile")
load(process.cwd())
process.env.OPENCODE_CONFIG_DIR ||= dir

const out = child.spawnSync(bin(), process.argv.slice(2), {
const out = spawnSync(bin(), process.argv.slice(2), {
stdio: "inherit",
env: process.env,
})
Expand Down
43 changes: 40 additions & 3 deletions packages/guardrails/managed/opencode.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"$schema": "https://opencode.ai/config.json",
"enabled_providers": [
"zai",
"zai-coding-plan",
"openai",
"openrouter"
],
Expand All @@ -18,8 +19,32 @@
"glm-4.5-air"
]
},
"zai-coding-plan": {
"whitelist": [
"glm-4.5",
"glm-4.5-air",
"glm-4.5-flash",
"glm-4.5v",
"glm-4.6",
"glm-4.6v",
"glm-4.7",
"glm-4.7-flash",
"glm-4.7-flashx",
"glm-5",
"glm-5-turbo",
"glm-5.1"
]
},
"openai": {
"whitelist": [
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.3-codex",
"gpt-5.2",
"gpt-5.2-codex",
"gpt-5.1-codex",
"gpt-5.1-codex-mini",
"gpt-5.1-codex-max",
"gpt-5",
"gpt-5-mini",
"gpt-5-nano",
Expand All @@ -28,10 +53,22 @@
},
"openrouter": {
"whitelist": [
"openai/gpt-5",
"openai/gpt-5-mini",
"anthropic/claude-haiku-4.5",
"anthropic/claude-opus-4.5",
"anthropic/claude-opus-4.6",
"anthropic/claude-sonnet-4.5",
"google/gemini-2.5-pro"
"anthropic/claude-sonnet-4.6",
"google/gemini-2.5-flash",
"google/gemini-2.5-pro",
"minimax/minimax-m2.1",
"minimax/minimax-m2.5",
"moonshotai/kimi-k2.5",
"openai/gpt-5.2",
"openai/gpt-5.2-codex",
"openai/gpt-5.3-codex",
"openai/gpt-5.4",
"openai/gpt-5.4-mini",
"qwen/qwen3-coder"
]
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/guardrails/profile/agents/provider-eval.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
description: Evaluate admitted OpenRouter-backed candidates without widening the default confidential-code lane.
mode: subagent
model: openrouter/openai/gpt-5-mini
model: openrouter/openai/gpt-5.4-mini
permission:
"*": deny
read: allow
Expand Down
43 changes: 40 additions & 3 deletions packages/guardrails/profile/opencode.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"default_agent": "implement",
"enabled_providers": [
"zai",
"zai-coding-plan",
"openai",
"openrouter"
],
Expand All @@ -19,8 +20,32 @@
"glm-4.5-air"
]
},
"zai-coding-plan": {
"whitelist": [
"glm-4.5",
"glm-4.5-air",
"glm-4.5-flash",
"glm-4.5v",
"glm-4.6",
"glm-4.6v",
"glm-4.7",
"glm-4.7-flash",
"glm-4.7-flashx",
"glm-5",
"glm-5-turbo",
"glm-5.1"
]
},
"openai": {
"whitelist": [
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.3-codex",
"gpt-5.2",
"gpt-5.2-codex",
"gpt-5.1-codex",
"gpt-5.1-codex-mini",
"gpt-5.1-codex-max",
"gpt-5",
"gpt-5-mini",
"gpt-5-nano",
Expand All @@ -29,10 +54,22 @@
},
"openrouter": {
"whitelist": [
"openai/gpt-5",
"openai/gpt-5-mini",
"anthropic/claude-haiku-4.5",
"anthropic/claude-opus-4.5",
"anthropic/claude-opus-4.6",
"anthropic/claude-sonnet-4.5",
"google/gemini-2.5-pro"
"anthropic/claude-sonnet-4.6",
"google/gemini-2.5-flash",
"google/gemini-2.5-pro",
"minimax/minimax-m2.1",
"minimax/minimax-m2.5",
"moonshotai/kimi-k2.5",
"openai/gpt-5.2",
"openai/gpt-5.2-codex",
"openai/gpt-5.3-codex",
"openai/gpt-5.4",
"openai/gpt-5.4-mini",
"qwen/qwen3-coder"
]
}
},
Expand Down
Loading