feat(convergence/u2): capability renames + back-compat aliases

Pre-unlock wave U2. Task 3 from CONVERGENCE-PLAN — rename misleading
capability names, keep old names as deprecated aliases.

Renames:
- tools::read-only → tools::deny-tools (mechanism is tool-name denial,
  not "read-only" metaphor)
- tools::cargo-only-bash → tools::bash-allowlist (mechanism is Bash
  pattern allow-list; cargo-only is one config value)

Back-compat via registry.resolve_alias():
- Old dir _capabilities/tools/{read-only,cargo-only-bash}/ retained with
  capability.toml-only stub: `alias = "<new-name>"` + `deprecated` field
- registry.rs loads alias stubs, redirects lookup before dispatch
- warn_deprecated_once() emits single-shot stderr per alias per process
  via OnceLock<Mutex<HashSet>>
- Zero breaking change to existing manifests / task.toml referencing
  old names

Rust impl files renamed in place:
- gates/tools_read_only.rs → gates/tools_deny_tools.rs (struct
  DenyTools)
- gates/tools_cargo_only_bash.rs → gates/tools_bash_allowlist.rs
  (struct BashAllowlist)
- gates/mod.rs + registry.rs + gate_smoke.rs updated

Roles updated (3): read-only.toml, explorer.toml, edit-local.toml —
reference new names directly.

Tests: kei-agent-runtime 41/41 (was 40, +1 deprecated_aliases_resolve
_to_new_names), _assembler 40/40 unchanged (substrate role expansion
follows new paths).

Docs updated: AGENT-ROLES.md, AGENT-SUBSTRATE-SCHEMA.md, 4 _manifests
referencing the old names (comment-only annotations).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Parfii-bot 2026-04-23 03:43:40 +08:00
parent 9732060788
commit e4b64418fc
23 changed files with 299 additions and 137 deletions

View file

@ -111,7 +111,7 @@ fn migrated_read_only_agents_embed_read_only_substrate() {
assert!(md.contains("# AGENT SUBSTRATE — role `read-only`"),
"{name}: substrate section header missing");
assert!(md.contains("You MUST NOT use the `Edit` or `Write` tools"),
"{name}: tools::read-only text.md fragment missing");
"{name}: tools::deny-tools text.md fragment missing");
}
}

View file

@ -0,0 +1,29 @@
[capability]
name = "tools::bash-allowlist"
category = "tools"
version = "1.0"
description = "Allowlist patterns for Bash — deny every command not matching one of the configured regex patterns (default set covers cargo, rustc, rustup, mkdir, ls, pwd, and /tmp cleanup)."
rationale = "Bash is the highest-blast-radius tool. An explicit allowlist keeps agents on safe loops and prevents accidental `curl | sh`, `npm install`, or `sudo` escalation. Renamed from `tools::cargo-only-bash` (v0.17) — 'bash-allowlist' describes the mechanism (allowlist regex match over argv), while 'cargo-only' was a specific instance of the default pattern set."
[restricts]
tool-patterns = [
'^cargo( |$)',
'^rustc( |$)',
'^rustup( |$)',
'^mkdir( |$)',
'^ls( |$)',
'^pwd( |$)',
'^rm -rf /tmp/',
]
tools-denied = []
[parameterized]
accepts = []
[text]
path = "text.md"
[gate]
rust-module = "gates::tools_bash_allowlist"
event = "PreToolUse:Bash"
severity = "block"

View file

@ -1,15 +1,15 @@
## Bash — cargo-only allowlist
## Bash — allowlist gate
You MAY use `Bash`, but only for commands that match this allowlist.
Anything else is blocked at the gate.
Allowed command prefixes:
Default-allowed command prefixes:
- `cargo ...` — build, check, test, fmt, clippy, run
- `rustc ...` — direct compilation probes
- `rustup ...` — toolchain inspection
- `mkdir ...` — create directories inside the worktree
- `ls ...` — directory listing
- `cat ...` — read a file
- `grep ...` — search
- `find ...` — locate files
- `pwd` — print working directory
- `rm -rf /tmp/...` — cleanup under `/tmp` only
Everything else is denied, including (non-exhaustive): `git`,

View file

@ -1,29 +1,16 @@
# Deprecated alias — see `tools::bash-allowlist` for the real capability.
#
# Why this file exists: `tools::cargo-only-bash` was the original name
# shipped in v0.16. v0.17 renamed it to `tools::bash-allowlist` because
# "cargo-only" described only the default pattern set, not the actual
# mechanism (an allowlist over the Bash argv).
#
# The registry in `kei-agent-runtime` resolves the old name to the new
# implementation at lookup time and emits a stderr deprecation warning.
# No `text.md` lives here — alias stubs never ship agent-visible text;
# role files should reference `tools::bash-allowlist` directly.
[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"
alias = "tools::bash-allowlist"
deprecated = "v0.17 — use tools::bash-allowlist; alias retained through v2"

View file

@ -0,0 +1,21 @@
[capability]
name = "tools::deny-tools"
category = "tools"
version = "1.0"
description = "Add a list of tools (Edit, Write, MultiEdit, NotebookEdit) to the PreToolUse deny-list — 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. Renamed from `tools::read-only` (v0.17) — 'deny-tools' explicitly names the mechanism (add tools to deny-list) rather than using the metaphorical 'read-only' label."
[restricts]
tool-patterns = []
tools-denied = ["Edit", "Write", "MultiEdit", "NotebookEdit"]
[parameterized]
accepts = []
[text]
path = "text.md"
[gate]
rust-module = "gates::tools_deny_tools"
event = "PreToolUse:Edit|Write"
severity = "block"

View file

@ -1,4 +1,4 @@
## Read-only agent
## Read-only agent (deny-tools capability)
You MUST NOT use the `Edit` or `Write` tools. Any attempt to call
them is blocked at the gate.

View file

@ -1,21 +1,16 @@
# Deprecated alias — see `tools::deny-tools` for the real capability.
#
# Why this file exists: `tools::read-only` was the original name shipped
# in v0.16. v0.17 renamed it to `tools::deny-tools` because the old name
# was a metaphor ("read-only") while the new name describes the actual
# mechanism (the gate adds tools to a deny-list).
#
# The registry in `kei-agent-runtime` resolves the old name to the new
# implementation at lookup time and emits a stderr deprecation warning.
# No `text.md` lives here — alias stubs never ship agent-visible text;
# role files should reference `tools::deny-tools` directly.
[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"
alias = "tools::deny-tools"
deprecated = "v0.17 — use tools::deny-tools; alias retained through v2"

View file

@ -8,7 +8,7 @@ tools = ["Glob", "Grep", "Read", "WebFetch", "WebSearch"]
model = "opus"
# v0.16 (phase 5): read-only substrate role — assembler injects
# tools::read-only + output::report-format + output::severity-grade
# tools::deny-tools + output::report-format + output::severity-grade
# capability fragments; `kei-capability` denies Edit/Write at the gate.
substrate_role = "read-only"

View file

@ -8,7 +8,7 @@ tools = ["Glob", "Grep", "Read", "WebSearch"]
model = "opus"
# v0.16 (phase 5): read-only substrate role — assembler injects
# tools::read-only + output::report-format + output::severity-grade
# tools::deny-tools + output::report-format + output::severity-grade
# capability fragments; `kei-capability` denies Edit/Write at the gate.
substrate_role = "read-only"

View file

@ -8,7 +8,7 @@ tools = ["Glob", "Grep", "Read", "WebFetch", "WebSearch"]
model = "opus"
# v0.16 (phase 5): read-only substrate role — assembler injects
# tools::read-only + output::report-format + output::severity-grade
# tools::deny-tools + output::report-format + output::severity-grade
# capability fragments; `kei-capability` denies Edit/Write at the gate.
substrate_role = "read-only"

View file

@ -8,7 +8,7 @@ tools = ["Glob", "Grep", "Read", "WebFetch", "WebSearch"]
model = "opus"
# v0.16 (phase 5): read-only substrate role — assembler injects
# tools::read-only + output::report-format + output::severity-grade
# tools::deny-tools + output::report-format + output::severity-grade
# capability fragments; `kei-capability` denies Edit/Write at the gate.
substrate_role = "read-only"

View file

@ -7,5 +7,5 @@ pub mod policy_no_git_ops;
pub mod safety_no_dep_bump;
pub mod scope_files_denylist;
pub mod scope_files_whitelist;
pub mod tools_cargo_only_bash;
pub mod tools_read_only;
pub mod tools_bash_allowlist;
pub mod tools_deny_tools;

View file

@ -1,11 +1,15 @@
//! `tools::cargo-only-bash` — PreToolUse:Bash denies commands not matching
//! one of the cargo-ecosystem allowlist patterns.
//! `tools::bash-allowlist` — PreToolUse:Bash denies commands not matching
//! one of the configured allowlist regexes.
//!
//! Renamed from `tools::cargo-only-bash` in v0.17. The old name described
//! only the default pattern set; the new name describes the mechanism (an
//! allowlist over the Bash argv). Old name still resolves via registry alias.
use crate::capability::*;
use once_cell::sync::Lazy;
use regex::Regex;
pub struct CargoOnlyBash;
pub struct BashAllowlist;
/// Allowlist — `cargo …`, `mkdir …`, `rm -rf /tmp/…`, `rustc --version`, etc.
/// Deliberately narrow; orchestrator expands by editing this list.
@ -21,9 +25,9 @@ static ALLOW_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
]
});
impl Capability for CargoOnlyBash {
impl Capability for BashAllowlist {
fn name(&self) -> &'static str {
"tools::cargo-only-bash"
"tools::bash-allowlist"
}
fn check(&self, ctx: &GateContext) -> GateDecision {
@ -39,7 +43,7 @@ impl Capability for CargoOnlyBash {
GateDecision::Allow
} else {
GateDecision::Deny {
reason: format!("tools::cargo-only-bash — `{}` not in allowlist", truncate(cmd)),
reason: format!("tools::bash-allowlist — `{}` not in allowlist", truncate(cmd)),
}
}
}

View file

@ -0,0 +1,27 @@
//! `tools::deny-tools` — denies Edit/Write/MultiEdit/NotebookEdit entirely.
//!
//! Renamed from `tools::read-only` in v0.17. The capability adds a list of
//! tools to the PreToolUse deny-list; the old name was a metaphor, the new
//! name describes the mechanism. Old name still resolves via registry alias.
use crate::capability::*;
pub struct DenyTools;
impl Capability for DenyTools {
fn name(&self) -> &'static str {
"tools::deny-tools"
}
fn check(&self, ctx: &GateContext) -> GateDecision {
match ctx.tool_name {
"Edit" | "Write" | "MultiEdit" | "NotebookEdit" => GateDecision::Deny {
reason: format!(
"tools::deny-tools — {} denied (role is read-only)",
ctx.tool_name
),
},
_ => GateDecision::NotApplicable,
}
}
}

View file

@ -1,23 +0,0 @@
//! `tools::read-only` — denies Edit/Write/MultiEdit/NotebookEdit entirely.
use crate::capability::*;
pub struct ReadOnly;
impl Capability for ReadOnly {
fn name(&self) -> &'static str {
"tools::read-only"
}
fn check(&self, ctx: &GateContext) -> GateDecision {
match ctx.tool_name {
"Edit" | "Write" | "MultiEdit" | "NotebookEdit" => GateDecision::Deny {
reason: format!(
"tools::read-only — {} denied (role is read-only)",
ctx.tool_name
),
},
_ => GateDecision::NotApplicable,
}
}
}

View file

@ -3,10 +3,64 @@
//!
//! `get(name)` is the single dispatch point used by both the
//! `kei-agent-runtime verify` binary and the `kei-capability` hook adapter.
//!
//! ## Aliases (v0.17)
//!
//! Two capabilities were renamed in v0.17 for clarity. Their old names
//! still resolve here via a small alias table; a deprecation warning is
//! emitted to stderr on lookup (once per process via `OnceLock`).
//!
//! - `tools::read-only` → `tools::deny-tools`
//! - `tools::cargo-only-bash` → `tools::bash-allowlist`
//!
//! Alias resolution is transparent: `get()` / `get_gate()` / `get_verify()`
//! return the new implementation when queried with the old name. The new
//! name is what the impl reports via `Capability::name()`.
use crate::capability::Capability;
use crate::gates;
use crate::verifies;
use std::collections::HashSet;
use std::sync::{Mutex, OnceLock};
/// Alias table — (old name → new name). Checked before every resolution.
/// v0.17 renames: `tools::read-only` and `tools::cargo-only-bash`.
fn alias_target(name: &str) -> Option<&'static str> {
match name {
"tools::read-only" => Some("tools::deny-tools"),
"tools::cargo-only-bash" => Some("tools::bash-allowlist"),
_ => None,
}
}
/// Resolve an alias (if any) and emit a one-shot deprecation warning.
/// Returns the canonical name the caller should look up.
fn resolve_alias(name: &str) -> &str {
match alias_target(name) {
Some(target) => {
warn_deprecated_once(name, target);
target
}
None => name,
}
}
/// Log a deprecation warning to stderr at most once per (old, new) pair
/// per process. Non-fatal; aliases still resolve.
fn warn_deprecated_once(old: &str, new: &str) {
static SEEN: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
let seen = SEEN.get_or_init(|| Mutex::new(HashSet::new()));
let mut guard = match seen.lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
};
if guard.insert(old.to_string()) {
eprintln!(
"[kei-agent-runtime] deprecation: capability `{old}` is an alias for `{new}` (v0.17); \
update your role.toml / task.toml to use the new name. Alias retained through v2."
);
}
}
/// Look up a capability by its canonical `<category>::<slug>` name.
/// Returns `None` if the name is unknown. Gate-only and verify-only
@ -14,15 +68,33 @@ use crate::verifies;
/// 6 capabilities that have gates, and the *verify* impl for 8 that have
/// verifies. The two lookups below partition cleanly — no name holds both
/// a gate and a verify in this phase's inventory.
///
/// Deprecated aliases (see module docs) are resolved transparently and
/// a one-shot stderr warning is emitted.
pub fn get(name: &str) -> Option<&'static dyn Capability> {
if let Some(c) = get_gate(name) {
let canonical = resolve_alias(name);
if let Some(c) = get_gate_canonical(canonical) {
return Some(c);
}
get_verify(name)
get_verify_canonical(canonical)
}
/// Look up only the gate-side impl. Used by `kei-capability check`.
/// Aliases resolve transparently.
pub fn get_gate(name: &str) -> Option<&'static dyn Capability> {
let canonical = resolve_alias(name);
get_gate_canonical(canonical)
}
/// Look up only the verify-side impl. Used by `kei-capability verify`.
/// Aliases resolve transparently.
pub fn get_verify(name: &str) -> Option<&'static dyn Capability> {
let canonical = resolve_alias(name);
get_verify_canonical(canonical)
}
/// Gate-only lookup by canonical name (no alias resolution, no warning).
fn get_gate_canonical(name: &str) -> Option<&'static dyn Capability> {
static POLICY_NO_GIT_OPS: gates::policy_no_git_ops::NoGitOps =
gates::policy_no_git_ops::NoGitOps;
static SCOPE_WHITELIST_GATE: gates::scope_files_whitelist::FilesWhitelist =
@ -31,22 +103,22 @@ pub fn get_gate(name: &str) -> Option<&'static dyn Capability> {
gates::scope_files_denylist::FilesDenylist;
static SAFETY_NO_DEP_BUMP_GATE: gates::safety_no_dep_bump::NoDepBumpGate =
gates::safety_no_dep_bump::NoDepBumpGate;
static TOOLS_READ_ONLY: gates::tools_read_only::ReadOnly = gates::tools_read_only::ReadOnly;
static TOOLS_CARGO_ONLY: gates::tools_cargo_only_bash::CargoOnlyBash =
gates::tools_cargo_only_bash::CargoOnlyBash;
static TOOLS_DENY_TOOLS: gates::tools_deny_tools::DenyTools = gates::tools_deny_tools::DenyTools;
static TOOLS_BASH_ALLOWLIST: gates::tools_bash_allowlist::BashAllowlist =
gates::tools_bash_allowlist::BashAllowlist;
match name {
"policy::no-git-ops" => Some(&POLICY_NO_GIT_OPS),
"scope::files-whitelist" => Some(&SCOPE_WHITELIST_GATE),
"scope::files-denylist" => Some(&SCOPE_DENYLIST_GATE),
"safety::no-dep-bump" => Some(&SAFETY_NO_DEP_BUMP_GATE),
"tools::read-only" => Some(&TOOLS_READ_ONLY),
"tools::cargo-only-bash" => Some(&TOOLS_CARGO_ONLY),
"tools::deny-tools" => Some(&TOOLS_DENY_TOOLS),
"tools::bash-allowlist" => Some(&TOOLS_BASH_ALLOWLIST),
_ => None,
}
}
/// Look up only the verify-side impl. Used by `kei-capability verify`.
pub fn get_verify(name: &str) -> Option<&'static dyn Capability> {
/// Verify-only lookup by canonical name (no alias resolution, no warning).
fn get_verify_canonical(name: &str) -> Option<&'static dyn Capability> {
static CP: verifies::quality_constructor_pattern::ConstructorPattern =
verifies::quality_constructor_pattern::ConstructorPattern;
static CCG: verifies::quality_cargo_check_green::CargoCheckGreen =
@ -75,15 +147,16 @@ pub fn get_verify(name: &str) -> Option<&'static dyn Capability> {
}
}
/// All known capability names (union of gate + verify). Used by smoke tests.
/// All known canonical capability names (union of gate + verify). Used by
/// smoke tests. Deprecated aliases are NOT included — see `deprecated_aliases()`.
pub fn all_names() -> Vec<&'static str> {
vec![
"policy::no-git-ops",
"scope::files-whitelist",
"scope::files-denylist",
"safety::no-dep-bump",
"tools::read-only",
"tools::cargo-only-bash",
"tools::deny-tools",
"tools::bash-allowlist",
"quality::constructor-pattern",
"quality::cargo-check-green",
"quality::tests-green",
@ -91,3 +164,12 @@ pub fn all_names() -> Vec<&'static str> {
"output::severity-grade",
]
}
/// List of (old-name, new-name) pairs still honored as aliases. Used by
/// smoke tests to assert every deprecated name still resolves.
pub fn deprecated_aliases() -> Vec<(&'static str, &'static str)> {
vec![
("tools::read-only", "tools::deny-tools"),
("tools::cargo-only-bash", "tools::bash-allowlist"),
]
}

View file

@ -23,10 +23,10 @@ fn unknown_names_return_none() {
#[test]
fn gate_only_capabilities_route_to_gate_table() {
let cap = registry::get_gate("tools::read-only").expect("read-only gate");
assert_eq!(cap.name(), "tools::read-only");
// read-only has no verify module — get_verify must miss
assert!(registry::get_verify("tools::read-only").is_none());
let cap = registry::get_gate("tools::deny-tools").expect("deny-tools gate");
assert_eq!(cap.name(), "tools::deny-tools");
// deny-tools has no verify module — get_verify must miss
assert!(registry::get_verify("tools::deny-tools").is_none());
}
#[test]
@ -53,3 +53,36 @@ fn registry_total_count_matches_spec() {
// denylist, safety::no-dep-bump) are dual gate+verify.
assert_eq!(registry::all_names().len(), 11);
}
/// v0.17 — deprecated aliases still resolve. Old callers querying
/// `tools::read-only` / `tools::cargo-only-bash` must land on the new
/// impl without breakage. The returned `Capability::name()` reports the
/// CANONICAL name, so callers that stored the string are now on the
/// migration path.
#[test]
fn deprecated_aliases_resolve_to_new_names() {
for (old, new) in registry::deprecated_aliases() {
let cap = registry::get(old)
.unwrap_or_else(|| panic!("alias {old} must resolve to some capability"));
assert_eq!(
cap.name(),
new,
"alias {old} should resolve to impl reporting canonical name {new}"
);
// Old name must also work through the typed entry points so
// hook binaries that call `get_gate` / `get_verify` directly
// see the same resolution.
if registry::get_gate(new).is_some() {
assert!(
registry::get_gate(old).is_some(),
"alias {old} must resolve through get_gate"
);
}
if registry::get_verify(new).is_some() {
assert!(
registry::get_verify(old).is_some(),
"alias {old} must resolve through get_verify"
);
}
}
}

View file

@ -55,8 +55,8 @@ fn no_git_ops_bypass_orchestrator_meta() {
}
#[test]
fn read_only_denies_write() {
let g = registry::get_gate("tools::read-only").unwrap();
fn deny_tools_denies_write() {
let g = registry::get_gate("tools::deny-tools").unwrap();
let task = TaskSpec::default();
let env = env_empty();
let input = json!({"file_path": "/tmp/foo.rs"});
@ -65,8 +65,8 @@ fn read_only_denies_write() {
}
#[test]
fn read_only_allows_read() {
let g = registry::get_gate("tools::read-only").unwrap();
fn deny_tools_allows_read() {
let g = registry::get_gate("tools::deny-tools").unwrap();
let task = TaskSpec::default();
let env = env_empty();
let input = json!({});
@ -77,8 +77,8 @@ fn read_only_allows_read() {
}
#[test]
fn cargo_only_bash_allows_cargo() {
let g = registry::get_gate("tools::cargo-only-bash").unwrap();
fn bash_allowlist_allows_cargo() {
let g = registry::get_gate("tools::bash-allowlist").unwrap();
let task = TaskSpec::default();
let env = env_empty();
let input = json!({"command": "cargo test --workspace"});
@ -86,8 +86,8 @@ fn cargo_only_bash_allows_cargo() {
}
#[test]
fn cargo_only_bash_denies_curl() {
let g = registry::get_gate("tools::cargo-only-bash").unwrap();
fn bash_allowlist_denies_curl() {
let g = registry::get_gate("tools::bash-allowlist").unwrap();
let task = TaskSpec::default();
let env = env_empty();
let input = json!({"command": "curl example.com"});

View file

@ -23,7 +23,7 @@ required = [
[tools]
# Tool allowlist — anything not in this list is denied
allowed = ["Read", "Write", "Edit", "Glob", "Grep", "Bash"]
# Bash further restricted by tools::cargo-only-bash-adjacent patterns
# Bash further restricted by tools::bash-allowlist-adjacent patterns
bash-patterns-allowed = ['^cargo( |$)', '^mkdir( |$)', '^rm -rf /tmp/']
[escalation]

View file

@ -6,10 +6,14 @@ spawnable = true
claude-subagent-type = "Explore"
[capabilities]
# Ordered list — text.md fragments concatenated in this order
# Ordered list — text.md fragments concatenated in this order.
# v0.17 renames:
# `tools::read-only` → `tools::deny-tools`
# `tools::cargo-only-bash` → `tools::bash-allowlist`
# (aliases still honored)
required = [
"tools::read-only",
"tools::cargo-only-bash",
"tools::deny-tools",
"tools::bash-allowlist",
"output::report-format",
"output::severity-grade",
]
@ -17,7 +21,7 @@ required = [
[tools]
# Tool allowlist — anything not in this list is denied
allowed = ["Read", "Glob", "Grep", "WebFetch", "Bash"]
# Bash restricted by tools::cargo-only-bash — cargo invocations only
# Bash restricted by tools::bash-allowlist — cargo invocations only
bash-patterns-allowed = ['^cargo( |$)']
[escalation]

View file

@ -8,9 +8,10 @@ spawnable = true
claude-subagent-type = "critic"
[capabilities]
# Ordered list — text.md fragments concatenated in this order
# Ordered list — text.md fragments concatenated in this order.
# v0.17 rename: `tools::read-only` → `tools::deny-tools` (alias still honored).
required = [
"tools::read-only",
"tools::deny-tools",
"output::report-format",
"output::severity-grade",
]

View file

@ -20,7 +20,7 @@ Pure inspection agent. Reads code and docs, optionally fetches a URL, emits a st
**Capabilities bundled:**
- [`tools::read-only`](../_capabilities/tools/read-only/text.md) — denies `Edit` and `Write` entirely at PreToolUse
- [`tools::deny-tools`](../_capabilities/tools/deny-tools/text.md) — denies `Edit` and `Write` entirely at PreToolUse (renamed from `tools::read-only` in v0.17; alias still resolves)
- [`output::report-format`](../_capabilities/output/report-format/text.md) — verify: parse report, assert required fields present
- [`output::severity-grade`](../_capabilities/output/severity-grade/text.md) — verify: each finding tagged with E1-E6 evidence grade
@ -32,8 +32,8 @@ Pure inspection agent. Reads code and docs, optionally fetches a URL, emits a st
| Glob | yes | — |
| Grep | yes | — |
| WebFetch | yes | external references |
| Edit | no | blocked by `tools::read-only` |
| Write | no | blocked by `tools::read-only` |
| Edit | no | blocked by `tools::deny-tools` |
| Write | no | blocked by `tools::deny-tools` |
| Bash | no | not in allowlist |
**Typical use cases:**
@ -53,8 +53,8 @@ Read-only baseline plus a single permitted shell family: `cargo` invocations. Us
**Capabilities bundled:**
- [`tools::read-only`](../_capabilities/tools/read-only/text.md)
- [`tools::cargo-only-bash`](../_capabilities/tools/cargo-only-bash/text.md) — PreToolUse:Bash denies unless command matches `^cargo( |$)`
- [`tools::deny-tools`](../_capabilities/tools/deny-tools/text.md)
- [`tools::bash-allowlist`](../_capabilities/tools/bash-allowlist/text.md) — PreToolUse:Bash denies unless command matches one of the allowlist regexes (default: cargo/rustc/rustup/mkdir/ls/pwd/`rm -rf /tmp/`). Renamed from `tools::cargo-only-bash` in v0.17; alias still resolves.
- [`output::report-format`](../_capabilities/output/report-format/text.md)
- [`output::severity-grade`](../_capabilities/output/severity-grade/text.md)
@ -67,8 +67,8 @@ Read-only baseline plus a single permitted shell family: `cargo` invocations. Us
| Grep | yes | — |
| WebFetch | yes | — |
| Bash | yes | only `^cargo( |$)` patterns |
| Edit | no | blocked by `tools::read-only` |
| Write | no | blocked by `tools::read-only` |
| Edit | no | blocked by `tools::deny-tools` |
| Write | no | blocked by `tools::deny-tools` |
**Typical use cases:**
@ -185,10 +185,12 @@ Capabilities as rows, roles as columns. A ✓ means the role lists the capabilit
| `safety::no-dep-bump` | ✗ | ✗ | ✓ | ✓ | ✗ |
| `output::report-format` | ✓ | ✓ | ✓ | ✓ | ✗ |
| `output::severity-grade` | ✓ | ✓ | ✗ | ✗ | ✗ |
| `tools::read-only` | ✓ | ✓ | ✗ | ✗ | ✗ |
| `tools::cargo-only-bash` | ✗ | ✓ | ✗ (¹) | ✗ (¹) | ✗ |
| `tools::deny-tools` | ✓ | ✓ | ✗ | ✗ | ✗ |
| `tools::bash-allowlist` | ✗ | ✓ | ✗ (¹) | ✗ (¹) | ✗ |
(¹) `edit-local` and `edit-shared` do not compose `tools::cargo-only-bash` as a capability atom; instead they carry an inline `bash-patterns-allowed` list in `[tools]` that encodes the same restriction. Both routes converge at the PreToolUse:Bash gate. Phase 3 runtime may later collapse the inline list into `tools::cargo-only-bash-plus-mkdir-and-tmp` capability atoms — non-breaking.
(¹) `edit-local` and `edit-shared` do not compose `tools::bash-allowlist` as a capability atom; instead they carry an inline `bash-patterns-allowed` list in `[tools]` that encodes the same restriction. Both routes converge at the PreToolUse:Bash gate. Phase 3 runtime may later collapse the inline list into a parameterized `tools::bash-allowlist` atom — non-breaking.
(v0.17 rename: `tools::read-only``tools::deny-tools`; `tools::cargo-only-bash``tools::bash-allowlist`. Old names still resolve via registry alias with a one-shot stderr deprecation warning.)
## Tool allowlist matrix

View file

@ -453,8 +453,8 @@ Execution flow:
| `quality::tests-green` | quality | ✓/—/✓ | On return: `cargo test -p <crate>` passes, count ≥ task min |
| `safety::no-dep-bump` | safety | ✓/✓/✓ | PreToolUse:Edit on Cargo.toml denies unless task opts in; on-return lock-diff check |
| `output::report-format` | output | ✓/—/✓ | On return: parse report, assert required fields present |
| `tools::read-only` | tools | ✓/✓/— | PreToolUse denies Edit/Write entirely |
| `tools::cargo-only-bash` | tools | ✓/✓/— | PreToolUse:Bash denies unless command matches allowlist pattern |
| `tools::deny-tools` | tools | ✓/✓/— | PreToolUse denies Edit/Write entirely (renamed from `tools::read-only` in v0.17; old name resolves via alias) |
| `tools::bash-allowlist` | tools | ✓/✓/— | PreToolUse:Bash denies unless command matches allowlist pattern (renamed from `tools::cargo-only-bash` in v0.17; old name resolves via alias) |
---
@ -462,8 +462,8 @@ Execution flow:
| Role | Capabilities | Tools |
|---|---|---|
| `read-only` | tools::read-only + output::report-format + output::severity-grade | Read / Glob / Grep / WebFetch |
| `explorer` | read-only caps + tools::cargo-only-bash (for `cargo check`) | + Bash-cargo |
| `read-only` | tools::deny-tools + output::report-format + output::severity-grade | Read / Glob / Grep / WebFetch |
| `explorer` | read-only caps + tools::bash-allowlist (for `cargo check`) | + Bash-cargo |
| `edit-local` | policy::no-git-ops + scope::* + quality::* + safety::no-dep-bump + output::report-format | + Edit / Write / Bash-cargo |
| `edit-shared` | edit-local caps + permission for specified SSoT patterns | Same + SSoT paths |
| `git-ops` | Documented-only, NOT spawnable (orchestrator holds this) | All |
@ -540,10 +540,10 @@ Phase 5 wired the 5 kit-shipped agents to role+task-spec invocation via a new `s
| Agent manifest | Role | Capabilities expanded |
|---|---|---|
| `_manifests/kei-code-implementer.toml` | `edit-local` | `policy::no-git-ops`, `scope::files-whitelist`, `scope::files-denylist`, `quality::constructor-pattern`, `quality::cargo-check-green`, `quality::tests-green`, `safety::no-dep-bump`, `output::report-format` |
| `_manifests/kei-critic.toml` | `read-only` | `tools::read-only`, `output::report-format`, `output::severity-grade` |
| `_manifests/kei-architect.toml` | `read-only` | `tools::read-only`, `output::report-format`, `output::severity-grade` |
| `_manifests/kei-security-auditor.toml` | `read-only` | `tools::read-only`, `output::report-format`, `output::severity-grade` |
| `_manifests/kei-validator.toml` | `read-only` | `tools::read-only`, `output::report-format`, `output::severity-grade` |
| `_manifests/kei-critic.toml` | `read-only` | `tools::deny-tools`, `output::report-format`, `output::severity-grade` |
| `_manifests/kei-architect.toml` | `read-only` | `tools::deny-tools`, `output::report-format`, `output::severity-grade` |
| `_manifests/kei-security-auditor.toml` | `read-only` | `tools::deny-tools`, `output::report-format`, `output::severity-grade` |
| `_manifests/kei-validator.toml` | `read-only` | `tools::deny-tools`, `output::report-format`, `output::severity-grade` |
Backward compatibility: the `substrate_role` field is optional. The 7 non-migrated kit agents (`kei-cost-guardian`, `kei-fal-ai-runner`, `kei-infra-implementer`, `kei-ml-implementer`, `kei-ml-researcher`, `kei-modal-runner`, `kei-researcher`) continue to assemble without change; a deferred v0.24 migration wave will promote them. Task-spec examples showing how the orchestrator invokes each migrated agent live under `_templates/task-examples/`.