feat(agent-substrate/phase-1): capability library — 11 declarative bundles

22 files per locked §Initial capability atom inventory:

policy/no-git-ops/               (gate: PreToolUse:Bash, bypass ORCHESTRATOR_META)
scope/files-whitelist/           (gate + verify worktree)
scope/files-denylist/            (gate + verify worktree)
quality/constructor-pattern/     (verify worktree)
quality/cargo-check-green/       (verify both — worktree short-circuit + simulated-merge)
quality/tests-green/             (verify both)
safety/no-dep-bump/              (gate + verify both)
output/report-format/            (verify worktree)
output/severity-grade/           (verify worktree)
tools/read-only/                 (gate: deny Edit/Write)
tools/cargo-only-bash/           (gate: Bash allowlist)

All capability.toml share [capability]/[restricts]/[parameterized]/[text]/
[gate]/[verify] section layout. rust-module paths pre-wired to match
phase-3 file layout. All text.md under 200 words, imperative,
self-contained (composer concatenates with --- separator).

Cross-refs to rule files preserved:
- policy::no-git-ops → RULE 0.13 (orchestrator-branch-first.md)
- quality::constructor-pattern → RULE ZERO (code-style.md)
- output::severity-grade → debugging.md §Security Review
- safety::no-dep-bump → supply-chain rationale

Agent attempted wc -w for word counts — sandbox correctly denied Bash
per RULE 0.13, observable reinforcement of the very policy this
capability encodes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Parfii-bot 2026-04-23 02:35:52 +08:00
parent b8b9c12913
commit c0c3483f02
22 changed files with 549 additions and 0 deletions

View file

@ -0,0 +1,21 @@
[capability]
name = "output::report-format"
category = "output"
version = "1.0"
description = "Require the agent's return report to contain a declared set of fields."
rationale = "Structured return lets orchestrator parse a file-list for `git add`, test counts for verification cross-check, and LOC deltas for audit. Free-text returns force re-prompting."
[restricts]
tool-patterns = []
tools-denied = []
[parameterized]
accepts = ["report-fields-required"]
[text]
path = "text.md"
[verify]
rust-module = "verifies::output_report_format"
run-mode = "worktree"
when = "on-return"

View file

@ -0,0 +1,31 @@
## Report format
Your final return message MUST contain every field listed in your
task's `output.report-fields-required`. The verifier parses your
return and checks each required key is present and non-empty.
Use one section per field. Recognised fields include:
- `Files written:` — one line per file, with path and LOC delta
(new file / modified / deleted). Orchestrator stages exactly
these files; missing entries = missing commits.
- `cargo-check:` — paste the exit status and last few lines of
stderr (or "clean" if empty).
- `cargo-test:` — paste the real `test result:` line with pass
count. Do not paraphrase.
- `loc-delta:` — per-file net lines added minus removed.
- `blockers:` — open issues you hit; empty list if none.
- `next:` — what a follow-up agent should take on, if anything.
Example skeleton:
Files written:
- _primitives/_rust/kei-forge/src/lib.rs (new, 120 LOC)
- _primitives/_rust/kei-forge/tests/render.rs (new, 45 LOC)
cargo-check: clean
cargo-test: test result: ok. 44 passed; 0 failed; 0 ignored
loc-delta: +165 / -0
Keep each field on its own section. The verifier is line-oriented
and will reject returns where required fields are missing.

View file

@ -0,0 +1,21 @@
[capability]
name = "output::severity-grade"
category = "output"
version = "1.0"
description = "Require critic-role agents to tag every finding with a [HIGH|MEDIUM|LOW] severity grade."
rationale = "~/.claude/rules/debugging.md Security Review phase 2 output format. Lets the orchestrator triage fixes by priority; ungraded findings become unactionable backlog."
[restricts]
tool-patterns = []
tools-denied = []
[parameterized]
accepts = []
[text]
path = "text.md"
[verify]
rust-module = "verifies::output_severity_grade"
run-mode = "worktree"
when = "on-return"

View file

@ -0,0 +1,34 @@
## Severity grade on findings
Every finding in your return MUST carry a severity grade:
`[HIGH]`, `[MEDIUM]`, or `[LOW]`. Write the grade as the first
token of the finding's header.
Grading rubric:
- **[HIGH]** — auth, crypto, memory safety, data loss, IP leak,
network protocol flaw, unsound FFI, secret in source, or any
issue that could compromise a production deploy.
- **[MEDIUM]** — input validation, error handling, resource
exhaustion, config drift, missing test coverage on a critical
path, performance regression with measurable impact.
- **[LOW]** — docs inaccuracy, formatting, non-idiomatic code,
comment drift, minor style, opportunistic refactor.
Example:
**[HIGH]** Unbounded allocation in request parser
- File: crates/api/src/parse.rs:47
- Class: resource exhaustion
- Scenario: attacker sends 2GB body, process OOMs
- Fix: cap read at 16 MiB via `take(...)`
**[LOW]** Typo in module docstring
- File: crates/api/src/lib.rs:3
The verifier parses your return, locates every `## ` section
containing the word "Finding" (case-insensitive) or matching the
format above, and rejects the return if any finding lacks a
`[HIGH|MEDIUM|LOW]` token.
Empty finding lists are fine — state "No findings" and no grade
is required.

View file

@ -0,0 +1,26 @@
[capability]
name = "policy::no-git-ops"
category = "policy"
version = "1.0"
description = "Forbid git, gh repo, and gh api /repos shell operations from the agent."
rationale = "RULE 0.13 (orchestrator-branch-first.md): orchestrator owns branch + commit + push; agents sandbox-deny Bash inside .claude/worktrees/<agent>/. See ~/.claude/rules/orchestrator-branch-first.md."
[restricts]
tool-patterns = [
'^git( |$)',
'^gh repo',
'^gh api /?repos',
]
tools-denied = []
[parameterized]
accepts = []
[text]
path = "text.md"
[gate]
rust-module = "gates::policy_no_git_ops"
event = "PreToolUse:Bash"
severity = "block"
bypass-env = "ORCHESTRATOR_META"

View file

@ -0,0 +1,20 @@
## No git operations
You MUST NOT invoke `git`, `gh repo`, `gh api /repos`, or any shell
command that modifies git state. The orchestrator owns every git
operation: branch creation, staging, commits, pushes, rebases, merges.
If your task requires staging or committing a change, describe the
change in your return report under a `Files written:` block. Include
one line per file with its path and approximate LOC delta. The
orchestrator will stage exactly those files and author the commit.
Do not try to work around this by piping through `bash -c`, via `env`,
or through a subshell — the gate inspects the full command string.
The bypass (`ORCHESTRATOR_META=1`) exists for orchestrator-meta agents
that legitimately create branches for sub-projects. It is not
available to you. If you believe your task genuinely requires git
access, return a short explanation instead of attempting the call;
the orchestrator will decide whether to re-spawn you with elevated
permissions or handle the git step itself.

View file

@ -0,0 +1,21 @@
[capability]
name = "quality::cargo-check-green"
category = "quality"
version = "1.0"
description = "Require `cargo check --workspace` to pass on return, in worktree AND in simulated merge onto main."
rationale = "Worktree-green is not enough — E1 jsonschema regression showed agent-local green can break main integration. Simulated-merge pass catches that class of failure before merge."
[restricts]
tool-patterns = []
tools-denied = []
[parameterized]
accepts = ["cargo-check-crates"]
[text]
path = "text.md"
[verify]
rust-module = "verifies::quality_cargo_check_green"
run-mode = "both"
when = "on-return"

View file

@ -0,0 +1,27 @@
## Cargo check must be green
On return, `cargo check --workspace` MUST pass cleanly. This is
enforced in two passes:
1. **Worktree pass** — runs from inside your worktree. This is what
you saw while iterating. It must be green before you hand off.
2. **Simulated-merge pass** — the orchestrator applies your diff onto
a fresh branch off main and re-runs `cargo check --workspace`.
Your change must still compile once integrated.
Both passes must succeed. Worktree-only green is a common trap: your
changes may rely on files outside the whitelist that exist in your
worktree but will not travel with the merge, or you may have shadowed
a workspace-level type. The simulated-merge pass catches that.
Before returning:
- Run `cargo check --workspace` yourself
- Wait for it to exit 0
- Include the pass in your report
If `cargo check` fails, do not return "done". Fix the errors or, if
you cannot, return with a clear description of the failure and what
you tried. Do not claim green without evidence.
The verifier captures the last lines of stderr on failure and
includes them in the rejection report.

View file

@ -0,0 +1,21 @@
[capability]
name = "quality::constructor-pattern"
category = "quality"
version = "1.0"
description = "Enforce Constructor Pattern: file <= 200 LOC, function <= 30 LOC."
rationale = "~/.claude/rules/code-style.md (RULE ZERO). Keeps one file = one class = one responsibility; forces decomposition before complexity compounds."
[restricts]
tool-patterns = []
tools-denied = []
[parameterized]
accepts = []
[text]
path = "text.md"
[verify]
rust-module = "verifies::quality_constructor_pattern"
run-mode = "worktree"
when = "on-return"

View file

@ -0,0 +1,25 @@
## Constructor Pattern — size limits
You MUST keep every file you write or edit under 200 lines of code,
and every function under 30 lines of code. These are hard limits,
not guidelines.
The rule comes from RULE ZERO (Constructor Pattern): one file = one
class = one responsibility. Files that breach 200 LOC should be
decomposed into sibling modules. Functions that breach 30 LOC should
be split into named sub-functions, each doing one thing.
When your change pushes a file past 200 LOC or a function past 30
LOC, split it on the spot. Do not commit with `TODO: refactor later`.
Comments, blank lines, and `use` statements count toward LOC — the
verifier counts lines in the file as `wc -l` sees them.
Exceptions:
- Auto-generated code (e.g. `include!(...)` expansions) is skipped.
- Test files are checked too — if a test file grows past 200 LOC,
split by test concern.
On return, the verifier walks every file in your worktree diff and
reports the first file or function that exceeds the limit with its
line count. No partial credit.

View file

@ -0,0 +1,21 @@
[capability]
name = "quality::tests-green"
category = "quality"
version = "1.0"
description = "Require `cargo test -p <crate>` to pass, with a minimum test count per task, in worktree AND simulated merge."
rationale = "Self-reported green without a run is the most common substrate v1 failure. Test-count minimum prevents silently deleting tests to make the bar. Dual-pass catches integration regressions."
[restricts]
tool-patterns = []
tools-denied = []
[parameterized]
accepts = ["cargo-test-crates", "test-count-min"]
[text]
path = "text.md"
[verify]
rust-module = "verifies::quality_tests_green"
run-mode = "both"
when = "on-return"

View file

@ -0,0 +1,26 @@
## Tests must be green
On return, `cargo test -p <crate>` MUST pass for each crate listed in
your task's `verification.cargo-test-crates`. Passing is two checks:
1. Exit code 0
2. Test count greater than or equal to `verification.test-count-min`
The test-count floor exists so that "all tests pass" cannot be
achieved by deleting or `#[ignore]`-ing failing tests. If the floor
says 44, the run must show `test result: ok. 44 passed` or more.
Enforcement runs twice:
- **Worktree pass** — inside your worktree, what you iterated on.
- **Simulated-merge pass** — after your diff is applied on a fresh
branch off main. Tests must still pass once integrated.
Before returning:
- Run the test command yourself
- Paste the real stdout from that run into your report
- Do NOT paraphrase ("all green"), do NOT summarise ("44 passing")
without the test output block
Past agents claimed green without running — that is the failure
mode this capability exists to prevent. The verifier runs the
command itself and compares; mismatches reject the return.

View file

@ -0,0 +1,26 @@
[capability]
name = "safety::no-dep-bump"
category = "safety"
version = "1.0"
description = "Block dependency additions/upgrades: deny Edit to Cargo.toml dep sections; verify Cargo.lock is unchanged on return."
rationale = "Supply-chain risk. A silent dep bump expands the attack surface and may trigger breaking-change cascades. Requires explicit task opt-in; orchestrator reviews Cargo.lock diff separately."
[restricts]
tool-patterns = []
tools-denied = []
[parameterized]
accepts = []
[text]
path = "text.md"
[gate]
rust-module = "gates::safety_no_dep_bump"
event = "PreToolUse:Edit|Write"
severity = "block"
[verify]
rust-module = "verifies::safety_no_dep_bump"
run-mode = "both"
when = "on-return"

View file

@ -0,0 +1,27 @@
## No dependency bumps
You MUST NOT add, remove, or upgrade dependencies. Specifically:
- Do NOT edit the `[dependencies]`, `[dev-dependencies]`,
`[build-dependencies]`, or `[workspace.dependencies]` sections of
any `Cargo.toml`
- Do NOT write or regenerate `Cargo.lock`
- Do NOT `cargo add`, `cargo remove`, or `cargo update`
Each new or upgraded dependency expands the supply-chain attack
surface and can trigger breaking-change cascades across the
workspace. Dependency decisions require a separate review, a
dedicated task, and an orchestrator-approved lock diff.
Editing other sections of `Cargo.toml` (e.g. `[package]`,
`[features]`, `[[bin]]`, `[lib]`, `[package.metadata.*]`) is allowed
if the file is in your whitelist and not in your denylist. The gate
inspects the specific region of the diff.
If your task genuinely requires a new dependency, STOP. Describe the
crate, version, and reason in your return. The orchestrator will
decide whether to re-spawn you with an opt-in flag or handle the
dep-bump through a separate review.
On return, the verifier diffs `Cargo.lock` against main; any change
rejects the return.

View file

@ -0,0 +1,26 @@
[capability]
name = "scope::files-denylist"
category = "scope"
version = "1.0"
description = "Block Edit/Write to paths matching a per-task denylist, even if otherwise whitelisted."
rationale = "Protects SSoT files (Cargo.toml / Cargo.lock / rules / settings.json / CI configs) that are easy to touch accidentally and hard to recover once committed. Denylist overrides whitelist."
[restricts]
tool-patterns = []
tools-denied = []
[parameterized]
accepts = ["files-denylist"]
[text]
path = "text.md"
[gate]
rust-module = "gates::scope_files_denylist"
event = "PreToolUse:Edit|Write"
severity = "block"
[verify]
rust-module = "verifies::scope_files_denylist"
run-mode = "worktree"
when = "on-return"

View file

@ -0,0 +1,24 @@
## Scope — files denylist
You MUST NOT Edit or Write any file whose path matches a glob in your
task's `scope.files-denylist` list. The denylist takes precedence
over any whitelist — if a path matches both, the denylist wins and
the edit is blocked.
Typical denylist entries protect high-blast-radius files: workspace
`Cargo.toml`, `Cargo.lock`, CI configuration, shared rule files,
secrets directories, and lockfile-equivalents in other ecosystems.
Changing these demands a separate review and a different role.
Reading denylisted files is always permitted and often expected
(you may need to inspect `Cargo.toml` to understand a crate's
dependencies, for example). The restriction applies only to mutating
tools.
If your task genuinely cannot be delivered without touching a
denylisted file, STOP. Do not try to work around the restriction.
Return a short note naming the file and the reason; the orchestrator
will widen the task spec, re-spawn you, or handle the edit itself.
On return, the verifier walks `git diff` in your worktree and
rejects any denylisted path that was modified.

View file

@ -0,0 +1,26 @@
[capability]
name = "scope::files-whitelist"
category = "scope"
version = "1.0"
description = "Restrict Edit/Write to paths matching a per-task whitelist of glob patterns."
rationale = "Scope violations surfaced only after merge in substrate v1 audit waves. Whitelist makes scope explicit at spawn time; gate blocks at PreToolUse, verify walks git diff on return to catch any bypass."
[restricts]
tool-patterns = []
tools-denied = []
[parameterized]
accepts = ["files-whitelist"]
[text]
path = "text.md"
[gate]
rust-module = "gates::scope_files_whitelist"
event = "PreToolUse:Edit|Write"
severity = "block"
[verify]
rust-module = "verifies::scope_files_whitelist"
run-mode = "worktree"
when = "on-return"

View file

@ -0,0 +1,24 @@
## Scope — files whitelist
You MUST only Edit or Write files whose path matches one of the glob
patterns in your task's `scope.files-whitelist` list. Any other path
is outside your scope.
The whitelist is the full set of files you are authorised to touch.
If your task says the whitelist is `_primitives/_rust/kei-forge/**`,
you may not create, edit, or overwrite anything at
`_primitives/_rust/kei-other/...`, at `scripts/...`, or at the
workspace root.
Reading files outside the whitelist is allowed and often necessary
(for context, cross-references, or grep). The restriction applies
only to mutating tools (Edit, Write).
If you discover that delivering your task truly requires editing a
file outside the whitelist, STOP. Do not attempt the edit. Return a
short note describing the file and the reason. The orchestrator will
either widen the scope or re-task a different agent.
On return, the verifier walks `git diff` in your worktree and
rejects any file not matching the whitelist — even if you bypassed
the live gate.

View file

@ -0,0 +1,29 @@
[capability]
name = "tools::cargo-only-bash"
category = "tools"
version = "1.0"
description = "Restrict Bash to cargo and a handful of safe read/navigate/cleanup helpers."
rationale = "Bash is the highest-blast-radius tool. A narrow allowlist keeps agents on the cargo + inspect loop and prevents accidental `curl | sh`, `npm install`, or `sudo` escalation."
[restricts]
tool-patterns = [
'^cargo( |$)',
'^mkdir( |$)',
'^ls( |$)',
'^cat( |$)',
'^grep( |$)',
'^find( |$)',
'^rm -rf /tmp/',
]
tools-denied = []
[parameterized]
accepts = []
[text]
path = "text.md"
[gate]
rust-module = "gates::tools_cargo_only_bash"
event = "PreToolUse:Bash"
severity = "block"

View file

@ -0,0 +1,28 @@
## Bash — cargo-only allowlist
You MAY use `Bash`, but only for commands that match this allowlist.
Anything else is blocked at the gate.
Allowed command prefixes:
- `cargo ...` — build, check, test, fmt, clippy, run
- `mkdir ...` — create directories inside the worktree
- `ls ...` — directory listing
- `cat ...` — read a file
- `grep ...` — search
- `find ...` — locate files
- `rm -rf /tmp/...` — cleanup under `/tmp` only
Everything else is denied, including (non-exhaustive): `git`,
`gh`, `curl`, `wget`, `npm`, `pip`, `python`, `node`, `bash -c`,
`sudo`, `sh`, `env VAR=...`, `docker`, `kubectl`, `ssh`, `scp`,
process-tree manipulation, and compound commands that chain an
allowed prefix with a denied one via `;`, `&&`, `||`, or pipes.
The gate inspects the full command string. Do not try to hide a
denied call behind a heredoc, variable expansion, or `xargs`. If
you need a tool that is not on the allowlist, STOP and describe
the need in your return — the orchestrator will either widen the
role or handle the step directly.
Prefer dedicated tools over Bash whenever possible: `Read`/`Write`
for files, `Glob`/`Grep` for search.

View file

@ -0,0 +1,21 @@
[capability]
name = "tools::read-only"
category = "tools"
version = "1.0"
description = "Deny Edit and Write tools entirely — agent may read but not mutate the filesystem."
rationale = "Read-only agents (research, critic, explorer) must never alter source. A denial at the tool level is simpler and more robust than per-path scope checks."
[restricts]
tool-patterns = []
tools-denied = ["Edit", "Write"]
[parameterized]
accepts = []
[text]
path = "text.md"
[gate]
rust-module = "gates::tools_read_only"
event = "PreToolUse:Edit|Write"
severity = "block"

View file

@ -0,0 +1,24 @@
## Read-only agent
You MUST NOT use the `Edit` or `Write` tools. Any attempt to call
them is blocked at the gate.
You are a read-only role. Your job is to inspect, explain, analyse,
or review — never to mutate the filesystem. Use `Read`, `Glob`,
`Grep`, and (where permitted) `Bash` for read-only commands and
`WebFetch` to work through what is already on disk and on the web.
If your task appears to require an edit, STOP. Do not try to work
around the tool denial (e.g. by shelling out `sed`/`awk` through
`Bash`, by creating a file via `cat > file <<EOF`, or by piping a
heredoc into `tee`). The orchestrator considers such attempts a
policy violation and will reject your return.
Return your findings as a structured report (see the
`output::report-format` and, if applicable, `output::severity-grade`
capabilities that accompany this role). Include every file path
and line number you think the follow-up editor should touch — the
orchestrator will route the actual edits to an `edit-local` or
`edit-shared` agent.
Reading any file in the repository is permitted and encouraged.