diff --git a/_assembler/tests/substrate_role.rs b/_assembler/tests/substrate_role.rs index 3c7c572..7392507 100644 --- a/_assembler/tests/substrate_role.rs +++ b/_assembler/tests/substrate_role.rs @@ -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"); } } diff --git a/_capabilities/tools/bash-allowlist/capability.toml b/_capabilities/tools/bash-allowlist/capability.toml new file mode 100644 index 0000000..fecb498 --- /dev/null +++ b/_capabilities/tools/bash-allowlist/capability.toml @@ -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" diff --git a/_capabilities/tools/cargo-only-bash/text.md b/_capabilities/tools/bash-allowlist/text.md similarity index 84% rename from _capabilities/tools/cargo-only-bash/text.md rename to _capabilities/tools/bash-allowlist/text.md index 04f6b3c..232f735 100644 --- a/_capabilities/tools/cargo-only-bash/text.md +++ b/_capabilities/tools/bash-allowlist/text.md @@ -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`, diff --git a/_capabilities/tools/cargo-only-bash/capability.toml b/_capabilities/tools/cargo-only-bash/capability.toml index 6e817b1..52b34ce 100644 --- a/_capabilities/tools/cargo-only-bash/capability.toml +++ b/_capabilities/tools/cargo-only-bash/capability.toml @@ -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" diff --git a/_capabilities/tools/deny-tools/capability.toml b/_capabilities/tools/deny-tools/capability.toml new file mode 100644 index 0000000..28ecf23 --- /dev/null +++ b/_capabilities/tools/deny-tools/capability.toml @@ -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" diff --git a/_capabilities/tools/read-only/text.md b/_capabilities/tools/deny-tools/text.md similarity index 96% rename from _capabilities/tools/read-only/text.md rename to _capabilities/tools/deny-tools/text.md index 82c8993..d496f1b 100644 --- a/_capabilities/tools/read-only/text.md +++ b/_capabilities/tools/deny-tools/text.md @@ -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. diff --git a/_capabilities/tools/read-only/capability.toml b/_capabilities/tools/read-only/capability.toml index b7a7004..92fa26c 100644 --- a/_capabilities/tools/read-only/capability.toml +++ b/_capabilities/tools/read-only/capability.toml @@ -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" diff --git a/_manifests/kei-architect.toml b/_manifests/kei-architect.toml index 50f3da6..d67d29f 100644 --- a/_manifests/kei-architect.toml +++ b/_manifests/kei-architect.toml @@ -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" diff --git a/_manifests/kei-critic.toml b/_manifests/kei-critic.toml index 449176f..942ae80 100644 --- a/_manifests/kei-critic.toml +++ b/_manifests/kei-critic.toml @@ -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" diff --git a/_manifests/kei-security-auditor.toml b/_manifests/kei-security-auditor.toml index c1357e6..b4117dc 100644 --- a/_manifests/kei-security-auditor.toml +++ b/_manifests/kei-security-auditor.toml @@ -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" diff --git a/_manifests/kei-validator.toml b/_manifests/kei-validator.toml index 7cacba5..848502f 100644 --- a/_manifests/kei-validator.toml +++ b/_manifests/kei-validator.toml @@ -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" diff --git a/_primitives/_rust/kei-agent-runtime/src/gates/mod.rs b/_primitives/_rust/kei-agent-runtime/src/gates/mod.rs index d9a795e..2787aa4 100644 --- a/_primitives/_rust/kei-agent-runtime/src/gates/mod.rs +++ b/_primitives/_rust/kei-agent-runtime/src/gates/mod.rs @@ -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; diff --git a/_primitives/_rust/kei-agent-runtime/src/gates/tools_cargo_only_bash.rs b/_primitives/_rust/kei-agent-runtime/src/gates/tools_bash_allowlist.rs similarity index 70% rename from _primitives/_rust/kei-agent-runtime/src/gates/tools_cargo_only_bash.rs rename to _primitives/_rust/kei-agent-runtime/src/gates/tools_bash_allowlist.rs index 7324a63..e0e1125 100644 --- a/_primitives/_rust/kei-agent-runtime/src/gates/tools_cargo_only_bash.rs +++ b/_primitives/_rust/kei-agent-runtime/src/gates/tools_bash_allowlist.rs @@ -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> = 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)), } } } diff --git a/_primitives/_rust/kei-agent-runtime/src/gates/tools_deny_tools.rs b/_primitives/_rust/kei-agent-runtime/src/gates/tools_deny_tools.rs new file mode 100644 index 0000000..760155a --- /dev/null +++ b/_primitives/_rust/kei-agent-runtime/src/gates/tools_deny_tools.rs @@ -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, + } + } +} diff --git a/_primitives/_rust/kei-agent-runtime/src/gates/tools_read_only.rs b/_primitives/_rust/kei-agent-runtime/src/gates/tools_read_only.rs deleted file mode 100644 index c8e1db7..0000000 --- a/_primitives/_rust/kei-agent-runtime/src/gates/tools_read_only.rs +++ /dev/null @@ -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, - } - } -} diff --git a/_primitives/_rust/kei-agent-runtime/src/registry.rs b/_primitives/_rust/kei-agent-runtime/src/registry.rs index 1b6e1b7..6050bf3 100644 --- a/_primitives/_rust/kei-agent-runtime/src/registry.rs +++ b/_primitives/_rust/kei-agent-runtime/src/registry.rs @@ -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>> = 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 `::` 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"), + ] +} diff --git a/_primitives/_rust/kei-agent-runtime/tests/capability_trait_smoke.rs b/_primitives/_rust/kei-agent-runtime/tests/capability_trait_smoke.rs index 419945e..0205462 100644 --- a/_primitives/_rust/kei-agent-runtime/tests/capability_trait_smoke.rs +++ b/_primitives/_rust/kei-agent-runtime/tests/capability_trait_smoke.rs @@ -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" + ); + } + } +} diff --git a/_primitives/_rust/kei-agent-runtime/tests/gate_smoke.rs b/_primitives/_rust/kei-agent-runtime/tests/gate_smoke.rs index 043d593..efa664a 100644 --- a/_primitives/_rust/kei-agent-runtime/tests/gate_smoke.rs +++ b/_primitives/_rust/kei-agent-runtime/tests/gate_smoke.rs @@ -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"}); diff --git a/_roles/edit-local.toml b/_roles/edit-local.toml index e7e53dc..768c9af 100644 --- a/_roles/edit-local.toml +++ b/_roles/edit-local.toml @@ -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] diff --git a/_roles/explorer.toml b/_roles/explorer.toml index 30d7c13..6e07179 100644 --- a/_roles/explorer.toml +++ b/_roles/explorer.toml @@ -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] diff --git a/_roles/read-only.toml b/_roles/read-only.toml index c2eaf4a..838b992 100644 --- a/_roles/read-only.toml +++ b/_roles/read-only.toml @@ -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", ] diff --git a/docs/AGENT-ROLES.md b/docs/AGENT-ROLES.md index 1e7977c..e257f1d 100644 --- a/docs/AGENT-ROLES.md +++ b/docs/AGENT-ROLES.md @@ -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 diff --git a/docs/AGENT-SUBSTRATE-SCHEMA.md b/docs/AGENT-SUBSTRATE-SCHEMA.md index 44e6873..55374c9 100644 --- a/docs/AGENT-SUBSTRATE-SCHEMA.md +++ b/docs/AGENT-SUBSTRATE-SCHEMA.md @@ -453,8 +453,8 @@ Execution flow: | `quality::tests-green` | quality | ✓/—/✓ | On return: `cargo test -p ` 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/`.