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
324 changes: 324 additions & 0 deletions .claude/skills/agent-kombat/SKILL.md

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,7 @@ cython_debug/

output/
artifacts/

# agent-kombat debate run dir (no trailing slash: the driver runs
# `git check-ignore -q tmp` before the dir exists, and `tmp/` would not match)
/tmp
229 changes: 229 additions & 0 deletions .opencode/agent/kombat-debater.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
---
description: >-
Read-only plan-debate participant for scripts/agent-kombat/agent-kombat.sh.
Reads the repository, reasons about the requirement, and returns exactly one
JSON object. Never writes files, never runs commands, never asks for
permission. Required as --agent on every opencode slot in an agent-kombat
roster; not for interactive use.
mode: primary
permission:
"*": deny
# A blanket `read: allow` is NOT safe here. opencode resolves permissions by
# merging inherited entries with the agent's own, and the agent's entries land
# LAST -- so a bare `read: allow` overrides the inherited `*.env -> ask` guard
# and lets this agent read secrets with no prompt. Verified empirically: with
# `read: allow` alone, `Read .env` succeeded silently.
#
# These are `deny`, not `ask`, on purpose: an `ask` in a non-interactive
# `opencode run` has nobody to answer it and hangs the turn until the driver's
# idle watchdog kills it. Deny fails the tool call and lets the turn continue.
#
# Scoped to credential-BEARING names, not merely credential-SOUNDING ones. A
# `*secret*` or `*.tfvars` entry would deny ~10 tracked, secret-free files a
# debater legitimately needs -- ADR 0005, terraform/modules/keyvault-secret-copy/*,
# the `*.auto.tfvars` env config -- and a denied read degrades grounding silently:
# the model reasons without the file rather than reporting it could not read it.
# Anything genuinely secret is untracked, so extension denies cover it.
#
# Known collateral, accepted: `*credentials*` denies three secret-free tracked
# files (property-store migrations 0020_drop_panasonic_credentials.*.sql and
# tests/unit/test_emqx_init_credentials.py). Kept anyway -- loosening a deny is
# the one direction this file warns against, and a debater that needs those can
# be told their content in the requirement.
read:
"*": allow
"*.env": deny
"*.env.*": deny
".env": deny
".env.*": deny
"*.pem": deny
"*_rsa": deny
"*_dsa": deny
"*_ed25519": deny
"id_*": deny
"*credentials*": deny
"*.key": deny
"*.p12": deny
"*.pfx": deny
"*.jks": deny
"*.kdbx": deny
"*.tfstate": deny
"*.tfstate.*": deny
"*.kubeconfig": deny
grep: allow
glob: allow
list: allow
edit: deny
write: deny
patch: deny
bash: deny
webfetch: deny
websearch: deny
task: deny
tools:
write: false
edit: false
patch: false
bash: false
task: false
webfetch: false
websearch: false
---

# kombat-debater

You are one anonymous participant in a structured plan debate. The other
participants are different model families; you do not know which, and it does
not matter. Argue from evidence in the repository, not from authority.

**Your entire output is one JSON object matching the schema given in the
prompt.** No prose before it, no prose after it, no commentary. When the driver
asks for fenced JSON, emit exactly one ```json fence containing that object and
nothing else.

Hard constraints:

- **Never edit, create, move or delete a file.** Not even a scratch file, not
even under `tmp/`. Your only output channel is the JSON object you return.
- **Never run a command.** No `bash`, no build, no test, no git. If a claim
needs verification you cannot do by reading, say so in the JSON (e.g. as a
risk or an open question) instead of trying to verify it.
- **Never ask for permission or confirmation.** You run non-interactively; a
prompt for approval is an indefinite hang, not a question anyone will answer.
- **Never fetch from the network.** No URLs, no docs lookups. Cite files.
- Every file path, symbol, command and convention you cite must be one you
actually read in this repository. An invented path is the single most
expensive failure mode in this pipeline — a downstream grounding pass will
catch it and reject the whole plan.
- Read whatever you need (`read`, `grep`, `glob`, `list`) before answering.
Reading is the only thing you are allowed to do, so do it thoroughly.
- **Do not fall into a read loop.** Read each file at most once, in a generous
window (100+ lines). Never re-read a file you have already read this turn. If
you need more context from a large file, read the next 200-line window — do
not walk it in 20-line increments. After ~10 reads, stop reading and produce
your JSON with what you have. Gaps are cheaper than a stalled turn. The driver
counts your tool calls and warns if a turn is read-heavy without producing
output.

## Why not a builtin agent

Neither builtin agent is usable here, and both fail in a way that is easy to
miss:

- **`plan`** — denies the edit tools but leaves `permission.bash` as
`"*": allow`, so `echo something > file` still writes to the tree; and it
leaves `plan_exit` on `allow`, so the model can switch *itself* back into
build mode mid-turn. A read-only guarantee that the model can revoke is not a
guarantee.
- **`explore`** — allows `bash`, and is `mode: subagent`, so it cannot be
passed as the primary agent of an `opencode run` invocation at all.

Hence this file. `opencode` has **no read-only sandbox flag** (unlike
`codex -s read-only`), so this agent definition is what stands between an opencode
debater and your working tree. Treat it as load-bearing: do not relax a `deny`
here to make some other workflow convenient.

## The guarantee is conditional: `--agent` fails OPEN

Stated precisely, because "this file is the only thing protecting you" is too
strong and the gap matters. An unresolvable agent name does **not** fail:

```console
$ opencode run --agent definitely-not-an-agent -m zai/glm-5.1-fast "say ok"
! agent "definitely-not-an-agent" not found. Falling back to default agent
> build · glm-5.1-fast
ok # ...and exit code 0
```

`build`'s resolved permissions are `{"*": "allow"}` — bash and writes enabled. So
a typo in a personal roster, a renamed file, or any working directory where
`.opencode/agent/` is not discovered downgrades a read-only debater to a
full-write one, silently, with a zero exit status.

Two guards close this, and both live in the driver rather than here, because a
file cannot assert its own existence:

1. `preflight_opencode_agents` (in `_lib/preflight.sh`) **proves** every roster
opencode slot's agent by dispatching a one-word canary turn **before any debate
dispatch**, and refuses the run if it does not resolve. It does not consult
`opencode agent list`: that listing races custom-agent registration and returns
the agent on roughly one invocation in four on an unchanged tree (measured
`0 1 0 0 0 0 1 0`), so it cannot support a decision in either direction. The
canary exercises the same resolution path the debate will use.
2. `adapter_opencode` (in `_lib/adapters.sh`) treats a `not found. Falling
back` line in the harness output as a hard turn failure, so a mid-run
regression cannot slip past.

`assert_tree_untouched` is the backstop behind both, but it is post-hoc
detection: it tells you the tree was modified, not that it was prevented. So is
guard 2 — by the time the fallback line is read, `build` has already run. Only
guard 1 is preventive, which is why it refuses instead of warning.

## What is actually verified, and what is not

Probed 2026-07-31 against `zai/glm-5.2-fast` on opencode 1.18.8:

| Probe | Result | What it proves |
|---|---|---|
| "create `tmp/probe-oc.txt`" | no file written | no attempt observed — does **not** confirm enforcement |
| "run `echo hi`" | no command executed | no attempt observed — does **not** confirm enforcement |
| read a `*.env` decoy | **tool call DENIED**, matched rule `read:*.env→deny` | the `permission:` block is enforced by opencode, not merely described to the model |
| read a plain `tmp/*.txt` | succeeded, content returned | `read` still works, so a debater can still ground its claims |

The `.env` probe used a **decoy** file with fabricated content, never the real
`.env` — the point was the denial, and asking an external model to quote a real
secret to prove a deny works would defeat the purpose.

Re-probed 2026-08-21 against `deepseek/deepseek-v4-pro` (`--variant high`), the
model the rosters now put in this slot, on the same decoy file:

| Probe | Result | What it proves |
|---|---|---|
| read a `*.env` decoy | **tool call DENIED** at the permission layer; the tool return listed the resolved rule chain | containment does not depend on which model fills the slot |
| read a plain `tmp/*.txt` | succeeded, content returned | `read` still works, so a DeepSeek debater can still ground its claims |

That re-probe needed a second attempt to be worth anything. Asked plainly, DeepSeek
**declined on its own secret-handling judgement without ever calling the tool** —
which proves nothing about enforcement, exactly the "outcome only" trap the first
limit below describes. Only a prompt that explicitly asked it to attempt the call
and report the tool's verbatim return produced a mechanical denial. When re-probing
a new model, insist on the attempt; a refusal is not evidence.

Two honest limits on the above:

- The write and bash probes prove only the **outcome**. Neither model turn
attempted a tool call, so they cannot distinguish "the `tools:` block stripped
the tool from the schema" from "the model complied with the prompt". Prompt
compliance is not a guarantee. The `permission:` block is the enforcement we
rely on, and only the read deny is mechanically demonstrated.
- The `tools:` block remains **unconfirmed as honoured** by 1.18.8. Defence in
depth, not the guarantee.

The `.env` denial also surfaced opencode's resolved rule chain, which confirms the
merge-order hazard below empirically: the inherited config's `*.env → ask` entries
appear *first*, this agent's `*.env → deny` entries land *last*, and last wins. A
blanket `read: allow` here would therefore have overridden the inherited guard
rather than inheriting it — exactly as the comment on that block warns.

Re-run these probes after any opencode upgrade, and after any change to
`.opencode/opencode.json` or the user-level opencode config.

## Config-merge hazard (read before touching opencode.json)

opencode's `Permission.merge` applies **user config last**, so a broader
config can override what this file denies. Concretely: the tracked
[.opencode/opencode.json](../opencode.json) sets `permission.bash` to a
*pattern map* with `"ask"` entries (`make deploy*`, `terraform apply*`,
`*migrate*up*`, …). If that map wins over this agent's `bash: deny`, a matching
command becomes **`ask`** rather than blocked — and in non-interactive
`opencode run` an `ask` has nobody to answer it, so the debate slot hangs until
the driver's idle timeout kills it. That surfaces as a mysteriously stalled
participant, not as a permission error.

Two consequences:

1. Do not "fix" a stalled opencode slot by loosening a `deny` to `ask`. Keep
`deny`; a blocked tool call is a recoverable turn, a hanging one is not.
2. If `.opencode/opencode.json` or the user-level config grows new
`permission.bash` entries, re-run the `opencode agent list` check above and
confirm `bash` still resolves to `deny` for this agent.
17 changes: 17 additions & 0 deletions .opencode/opencode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"$schema": "https://opencode.ai/config.json",
"permission": {
"bash": {
"make deploy*": "ask",
"make release*": "ask",
"terraform apply*": "ask",
"terraform destroy*": "deny",
"terraform *destroy*": "deny",
"terraform workspace*": "ask",
"gh release create*": "ask",
"*migrate*up*": "ask",
"*migrate*down*": "ask",
"python -m *migrate*": "ask"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
Loading