Two new crates implementing the substrate runtime per locked §Runtime
execution contract + §Capability trait contract (Rust) + §Verify
execution worktree→simulated-merge.
kei-agent-runtime — library + CLI binary:
- src/capability.rs — Capability trait (name/check/verify) + GateContext
+ GateDecision + VerifyContext + VerifyResult + RunMode + TaskSpec
- src/registry.rs — &str → &'static dyn Capability dispatch for 14 impls
- src/gates/ — 6 PreToolUse modules (policy::no-git-ops,
scope::files-{whitelist,denylist}, safety::no-dep-bump,
tools::read-only, tools::cargo-only-bash)
- src/verifies/ — 8 on-return modules (quality::constructor-pattern,
quality::cargo-check-green, quality::tests-green, safety::no-dep-bump,
scope::files-{whitelist,denylist}, output::{report-format,severity-grade})
- src/compose.rs — task.toml + role + capabilities → prompt.md
- src/spawn.rs — ledger fork + prompt write (actual Agent invocation
remains orchestrator's tool call)
- src/verify.rs — runs all capability verifies per role; collects
VerifyReport {passed, failed}
- src/simulated_merge.rs — git worktree add test-merge/<id> + apply diff
+ run verify; cleanup on Drop
- src/main.rs — clap CLI: compose | spawn | verify | run
kei-capability — thin CLI adapter crate:
- Depends on kei-agent-runtime path dep
- Subcommand `check <cap-name>` (PreToolUse gate; stdin JSON, exit 0|2)
- Subcommand `verify <cap-name>` (on-return; env-driven, exit 0 or fail)
- Pattern: shell hook = 3-line `exec kei-capability check "$CAP_NAME"`
Workspace Cargo.toml: both crates registered as members (under agent
substrate v1 marker).
cargo check --workspace: PASS
cargo test -p kei-agent-runtime: 37/37 green
- 6 capability_trait_smoke (registry lookups, unknown name → None)
- 3 compose_smoke (fixture role + caps → composed prompt)
- 12 gate_smoke (each gate: happy + deny + bypass)
- 4 simulated_merge_smoke (git worktree lifecycle)
- 12 verify_smoke (each verify: pass + fail + edge cases)
cargo test -p kei-capability: 0/0 (CLI binary, tested via lib)
(Agent completion report cut off by rate-limit at 60 tool-uses; code
itself is green — verified by orchestrator post-commit.)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
69 lines
2.1 KiB
Rust
69 lines
2.1 KiB
Rust
//! Compose smoke test — load fake role + 2 capabilities from a tempdir
|
|
//! fixture, assert composed prompt contains both text fragments and the
|
|
//! task body.
|
|
|
|
use kei_agent_runtime::capability::TaskSpec;
|
|
use kei_agent_runtime::compose::compose_prompt;
|
|
use tempfile::TempDir;
|
|
|
|
#[test]
|
|
fn compose_concatenates_fragments_and_body() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let root = tmp.path();
|
|
|
|
std::fs::create_dir_all(root.join("_capabilities/policy/no-git-ops")).unwrap();
|
|
std::fs::write(
|
|
root.join("_capabilities/policy/no-git-ops/text.md"),
|
|
"## No git\n\nYou must not git.\n",
|
|
)
|
|
.unwrap();
|
|
std::fs::create_dir_all(root.join("_capabilities/output/report-format")).unwrap();
|
|
std::fs::write(
|
|
root.join("_capabilities/output/report-format/text.md"),
|
|
"## Report\n\nEmit a report.\n",
|
|
)
|
|
.unwrap();
|
|
std::fs::create_dir_all(root.join("_roles")).unwrap();
|
|
std::fs::write(
|
|
root.join("_roles/fake.toml"),
|
|
r#"
|
|
[role]
|
|
name = "fake"
|
|
|
|
[capabilities]
|
|
required = ["policy::no-git-ops", "output::report-format"]
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let mut task = TaskSpec::default();
|
|
task.task.role = "fake".into();
|
|
task.task.agent_id = "abc123".into();
|
|
task.body.text = "Do the thing.".into();
|
|
|
|
let prompt = compose_prompt(&task, root).expect("compose");
|
|
assert!(prompt.contains("You must not git"));
|
|
assert!(prompt.contains("Emit a report"));
|
|
assert!(prompt.contains("Do the thing."));
|
|
assert!(prompt.contains("---")); // separator
|
|
}
|
|
|
|
#[test]
|
|
fn compose_missing_role_errors() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let mut task = TaskSpec::default();
|
|
task.task.role = "nonexistent".into();
|
|
task.task.agent_id = "x".into();
|
|
let err = compose_prompt(&task, tmp.path()).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(msg.contains("role") || msg.contains("nonexistent"));
|
|
}
|
|
|
|
#[test]
|
|
fn compose_empty_role_errors() {
|
|
let tmp = TempDir::new().unwrap();
|
|
let task = TaskSpec::default();
|
|
let err = compose_prompt(&task, tmp.path()).unwrap_err();
|
|
let msg = format!("{err:#}");
|
|
assert!(msg.contains("role"));
|
|
}
|