KeiSeiKit-1.0/_assembler/src/substrate.rs
Parfii-bot a4e667de10 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

222 lines
7.8 KiB
Rust

//! Substrate-role expansion — reads `_roles/<name>.toml` and pulls each
//! capability's `text.md` for injection into the generated agent prompt.
//!
//! Constructor Pattern: one cube = one concern. This module does ONLY
//! role → capability-fragments, nothing else. `assembler.rs` calls into
//! it when a manifest declares `substrate_role`.
use serde::Deserialize;
use std::collections::HashSet;
use std::path::Path;
#[derive(Deserialize)]
struct RoleFile {
#[serde(default)]
capabilities: RoleCapabilities,
}
#[derive(Default, Deserialize)]
struct RoleCapabilities {
/// Optional parent role — its `required` list is loaded recursively
/// and combined with this role's `required` (parent first, dedup, then
/// `relaxes` removed). Cycles are detected and rejected.
#[serde(default)]
extends: Option<String>,
#[serde(default)]
required: Vec<String>,
/// Capability names to drop from the parent's `required` list. Only
/// meaningful when `extends` is set.
#[serde(default)]
relaxes: Vec<String>,
}
/// Load `_roles/<role>.toml` and return the ordered capability names.
/// If the role declares `extends`, the parent's required list is loaded
/// recursively and merged (parent first, dedup, `relaxes` applied).
pub fn load_role_capabilities(root: &Path, role: &str) -> Result<Vec<String>, String> {
let mut visited: HashSet<String> = HashSet::new();
load_role_capabilities_inner(root, role, &mut visited)
}
fn load_role_capabilities_inner(
root: &Path,
role: &str,
visited: &mut HashSet<String>,
) -> Result<Vec<String>, String> {
if !visited.insert(role.to_string()) {
return Err(format!(
"role '{role}' has a cyclic `extends` chain: {visited:?}"
));
}
let path = root.join("_roles").join(format!("{role}.toml"));
let text = std::fs::read_to_string(&path)
.map_err(|e| format!("read role {}: {e}", path.display()))?;
let parsed: RoleFile = toml::from_str(&text)
.map_err(|e| format!("parse role {}: {e}", path.display()))?;
let caps = &parsed.capabilities;
let combined = match &caps.extends {
Some(parent) => merge_extends(root, parent, &caps.required, &caps.relaxes, visited)?,
None => caps.required.clone(),
};
if combined.is_empty() {
return Err(format!(
"role '{role}' at {} resolves to an empty capability list",
path.display()
));
}
Ok(combined)
}
/// Resolve `extends` inheritance: load parent's full list, append this
/// role's `required` (skipping duplicates), then remove anything in
/// `relaxes`. Order: parent fragments come first, child overrides come
/// after, child relaxations subtract from the union.
fn merge_extends(
root: &Path,
parent_role: &str,
own_required: &[String],
relaxes: &[String],
visited: &mut HashSet<String>,
) -> Result<Vec<String>, String> {
let parent_caps = load_role_capabilities_inner(root, parent_role, visited)?;
let mut seen: HashSet<&str> = HashSet::new();
let mut out: Vec<String> = Vec::with_capacity(parent_caps.len() + own_required.len());
for c in parent_caps.iter().chain(own_required.iter()) {
if seen.insert(c.as_str()) {
out.push(c.clone());
}
}
let drop: HashSet<&str> = relaxes.iter().map(String::as_str).collect();
out.retain(|c| !drop.contains(c.as_str()));
Ok(out)
}
/// Load a capability's `text.md` fragment.
///
/// `cap_name` is `<category>::<slug>` (e.g. `policy::no-git-ops`).
pub fn load_capability_text(root: &Path, cap_name: &str) -> Result<String, String> {
let (category, slug) = split_cap_name(cap_name)?;
let path = root
.join("_capabilities")
.join(category)
.join(slug)
.join("text.md");
std::fs::read_to_string(&path)
.map_err(|e| format!("read capability {cap_name} at {}: {e}", path.display()))
}
fn split_cap_name(cap: &str) -> Result<(&str, &str), String> {
match cap.split_once("::") {
Some((cat, slug)) if !cat.is_empty() && !slug.is_empty() => Ok((cat, slug)),
_ => Err(format!(
"malformed capability name '{cap}' — expected <cat>::<slug>"
)),
}
}
/// Build the full substrate block: `# AGENT SUBSTRATE` header + each
/// fragment joined with the canonical `\n\n---\n\n` separator used by
/// `kei-agent-runtime::compose`.
pub fn build_substrate_section(root: &Path, role: &str) -> Result<String, String> {
let caps = load_role_capabilities(root, role)?;
let mut fragments: Vec<String> = Vec::with_capacity(caps.len());
for cap in &caps {
let text = load_capability_text(root, cap)?;
fragments.push(text.trim().to_string());
}
let mut out = String::new();
out.push_str("# AGENT SUBSTRATE — role `");
out.push_str(role);
out.push_str("`\n\n");
out.push_str("> Enforced by `kei-capability` gates + verifies. The rules below are not advisory.\n\n");
out.push_str(&fragments.join("\n\n---\n\n"));
out.push_str("\n\n");
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::path::PathBuf;
#[test]
fn split_cap_name_ok() {
assert_eq!(split_cap_name("policy::no-git-ops").unwrap(), ("policy", "no-git-ops"));
}
#[test]
fn split_cap_name_rejects_missing_sep() {
assert!(split_cap_name("policy-no-git-ops").is_err());
}
#[test]
fn split_cap_name_rejects_empty_side() {
assert!(split_cap_name("::slug").is_err());
assert!(split_cap_name("cat::").is_err());
}
fn tmp_kit(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("substrate-test-{name}"));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(dir.join("_roles")).unwrap();
dir
}
fn write_role(root: &Path, name: &str, body: &str) {
fs::write(root.join("_roles").join(format!("{name}.toml")), body).unwrap();
}
#[test]
fn extends_inherits_parent_required() {
let root = tmp_kit("inherit");
write_role(&root, "parent", "[capabilities]\nrequired = [\"a\", \"b\"]\n");
write_role(&root, "child", "[capabilities]\nextends = \"parent\"\nrequired = [\"c\"]\n");
let caps = load_role_capabilities(&root, "child").unwrap();
assert_eq!(caps, vec!["a", "b", "c"]);
}
#[test]
fn extends_with_relaxes_drops_parent_items() {
let root = tmp_kit("relax");
write_role(&root, "parent", "[capabilities]\nrequired = [\"a\", \"b\", \"c\"]\n");
write_role(
&root,
"child",
"[capabilities]\nextends = \"parent\"\nrequired = [\"d\"]\nrelaxes = [\"b\"]\n",
);
let caps = load_role_capabilities(&root, "child").unwrap();
assert_eq!(caps, vec!["a", "c", "d"]);
}
#[test]
fn extends_dedups_when_child_repeats_parent() {
let root = tmp_kit("dedup");
write_role(&root, "parent", "[capabilities]\nrequired = [\"a\", \"b\"]\n");
write_role(
&root,
"child",
"[capabilities]\nextends = \"parent\"\nrequired = [\"b\", \"c\"]\n",
);
let caps = load_role_capabilities(&root, "child").unwrap();
assert_eq!(caps, vec!["a", "b", "c"]);
}
#[test]
fn extends_cycle_rejected() {
let root = tmp_kit("cycle");
write_role(&root, "a", "[capabilities]\nextends = \"b\"\nrequired = [\"x\"]\n");
write_role(&root, "b", "[capabilities]\nextends = \"a\"\nrequired = [\"y\"]\n");
let err = load_role_capabilities(&root, "a").unwrap_err();
assert!(err.contains("cyclic"), "err: {err}");
}
#[test]
fn empty_required_no_extends_rejects() {
let root = tmp_kit("empty");
write_role(&root, "lonely", "[capabilities]\nrequired = []\n");
assert!(load_role_capabilities(&root, "lonely").is_err());
}
}