KeiSeiKit-1.0/_primitives/_rust/kei-agent-runtime/tests/gate_smoke.rs
Parfii-bot e4b64418fc 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>
2026-04-23 03:43:40 +08:00

155 lines
4.9 KiB
Rust

//! Gate smoke tests — one happy + one deny + one bypass/boundary per gate.
use kei_agent_runtime::capability::{GateContext, GateDecision, TaskSpec};
use kei_agent_runtime::registry;
use serde_json::json;
use std::collections::HashMap;
fn ctx<'a>(
tool: &'a str,
input: &'a serde_json::Value,
task: &'a TaskSpec,
env: &'a HashMap<String, String>,
) -> GateContext<'a> {
GateContext { tool_name: tool, tool_input: input, task, env }
}
fn env_empty() -> HashMap<String, String> {
HashMap::new()
}
fn env_with(key: &str, val: &str) -> HashMap<String, String> {
let mut m = HashMap::new();
m.insert(key.into(), val.into());
m
}
#[test]
fn no_git_ops_denies_git_command() {
let g = registry::get_gate("policy::no-git-ops").unwrap();
let task = TaskSpec::default();
let env = env_empty();
let input = json!({"command": "git commit -m foo"});
match g.check(&ctx("Bash", &input, &task, &env)) {
GateDecision::Deny { .. } => {}
other => panic!("expected Deny, got {other:?}"),
}
}
#[test]
fn no_git_ops_allows_non_git_bash() {
let g = registry::get_gate("policy::no-git-ops").unwrap();
let task = TaskSpec::default();
let env = env_empty();
let input = json!({"command": "cargo build"});
assert_eq!(g.check(&ctx("Bash", &input, &task, &env)), GateDecision::Allow);
}
#[test]
fn no_git_ops_bypass_orchestrator_meta() {
let g = registry::get_gate("policy::no-git-ops").unwrap();
let task = TaskSpec::default();
let env = env_with("ORCHESTRATOR_META", "1");
let input = json!({"command": "git commit -m bypass"});
assert_eq!(g.check(&ctx("Bash", &input, &task, &env)), GateDecision::Allow);
}
#[test]
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"});
matches!(g.check(&ctx("Write", &input, &task, &env)), GateDecision::Deny { .. });
matches!(g.check(&ctx("Edit", &input, &task, &env)), GateDecision::Deny { .. });
}
#[test]
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!({});
assert_eq!(
g.check(&ctx("Read", &input, &task, &env)),
GateDecision::NotApplicable
);
}
#[test]
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"});
assert_eq!(g.check(&ctx("Bash", &input, &task, &env)), GateDecision::Allow);
}
#[test]
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"});
matches!(
g.check(&ctx("Bash", &input, &task, &env)),
GateDecision::Deny { .. }
);
}
#[test]
fn scope_whitelist_allows_matching_path() {
let g = registry::get_gate("scope::files-whitelist").unwrap();
let mut task = TaskSpec::default();
task.scope.files_whitelist = vec!["_primitives/_rust/kei-forge/**".into()];
let env = env_empty();
let input = json!({"file_path": "_primitives/_rust/kei-forge/src/lib.rs"});
assert_eq!(g.check(&ctx("Edit", &input, &task, &env)), GateDecision::Allow);
}
#[test]
fn scope_whitelist_denies_outside() {
let g = registry::get_gate("scope::files-whitelist").unwrap();
let mut task = TaskSpec::default();
task.scope.files_whitelist = vec!["_primitives/_rust/kei-forge/**".into()];
let env = env_empty();
let input = json!({"file_path": "hooks/foo.sh"});
matches!(
g.check(&ctx("Edit", &input, &task, &env)),
GateDecision::Deny { .. }
);
}
#[test]
fn scope_denylist_denies_match() {
let g = registry::get_gate("scope::files-denylist").unwrap();
let mut task = TaskSpec::default();
task.scope.files_denylist = vec!["_primitives/_rust/Cargo.toml".into()];
let env = env_empty();
let input = json!({"file_path": "_primitives/_rust/Cargo.toml"});
matches!(
g.check(&ctx("Edit", &input, &task, &env)),
GateDecision::Deny { .. }
);
}
#[test]
fn no_dep_bump_blocks_cargo_toml() {
let g = registry::get_gate("safety::no-dep-bump").unwrap();
let task = TaskSpec::default();
let env = env_empty();
let input = json!({"file_path": "foo/Cargo.toml"});
matches!(
g.check(&ctx("Edit", &input, &task, &env)),
GateDecision::Deny { .. }
);
}
#[test]
fn no_dep_bump_allow_bypass() {
let g = registry::get_gate("safety::no-dep-bump").unwrap();
let task = TaskSpec::default();
let env = env_with("ALLOW_DEP_BUMP", "1");
let input = json!({"file_path": "foo/Cargo.toml"});
assert_eq!(g.check(&ctx("Edit", &input, &task, &env)), GateDecision::Allow);
}