Files
clawmates/crates/cm-brain/src/lib.rs
T
Omar SobhandClaude Opus 5 285d0c82f2 chore: delete dead scaffolding and stop fabricating claw capability cards
Tier 0 of the prompt-ablation pass -- subtraction only, none of this
reached a model.

- cm-brain: drop ClawBrain::export_markdown (zero callers).
- workflows: drop the `task_preamble` keys. No Rust code ever read them --
  WorkflowPhase.config is an opaque serde_json::Value -- so the comment
  calling the preamble "the belt, the skill the suspenders" described a belt
  that was never implemented. (`commit_policy` is unread for the same reason;
  left in place as documentation pending a decision.)
- mcp_door: derive the unknown-tool error from EXPOSED_TOOLS. The literal had
  drifted to naming one of the three tools the door exposes.
- Dashboard.tsx: drop TEAM_TEMPLATES/COMPANY_TEMPLATES, defined and never
  referenced, and disconnected from the real templates/teams/*.toml.

The substantive one: GET /api/claws/{id}/compartments returned hardcoded
strings for tools/capabilities/safety, identical for every claw. Every card
read "Network: none" and "Shell . blocked" regardless of the claw's real
risk_profile -- which is the actual capability boundary, so the card was
most wrong exactly where it mattered, on a coding_readwrite claw that does
have shell. Now derived from the claw's effective risk_profile (its team's
setting, else the same role-derived default the provisioner applies), with
the allowlists mirroring [risk_profiles.*] in the runtime config.

Note: cm-topology/src/heuristics.rs was slated for deletion here as unused.
It is not -- routes/topology.rs:43 serves it and p0_endpoints.rs:302 asserts
it. Left alone.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-30 10:44:33 -07:00

397 lines
15 KiB
Rust

//! `cm-brain` — ClawMates' facade over the canonical **`.brain`** (a `claw-brain`
//! / ClawhDF5 *brain-pack*): one HDF5 file holding an agent's full definition
//! (system prompt · personality · skills · tools · runtime · provenance) **and**
//! its memory, so claws can be built, deployed, loaded, upskilled, reloaded —
//! and pulled/pushed to ClawBrainHub + synced with ClawSync — as a single file.
//!
//! This is a thin, ClawMates-shaped wrapper over `claw_brain::BrainHandle` (the
//! brain-pack KV) + its keyword index for recall. Sections live under the
//! conventional brain-pack keys (`identity/system_prompt`, `skills/<name>`, …);
//! conversational memory lives under `memory/<ts>` and is keyword-searchable.
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
pub use claw_brain::RevisionInfo;
use claw_brain::{index_entry, keyword_search, BrainHandle};
pub mod hub;
const K_SYSTEM_PROMPT: &str = "identity/system_prompt";
const K_PERSONA: &str = "identity/persona";
const K_SOUL: &str = "identity/soul_md";
const K_AGENT_MD: &str = "identity/agent_md";
const K_RUNTIME: &str = "runtime/clawmates";
const K_PROVENANCE: &str = "provenance/clawmates";
const P_SKILL: &str = "skills/"; // skills/<name>
const P_TOOL: &str = "tools/"; // tools/<name>
const P_MEMORY: &str = "memory/"; // memory/<ts_nanos>
const K_SKILLS_MD: &str = "skills/skills_md"; // brain-pack narrative skills doc
#[derive(Debug, thiserror::Error)]
pub enum BrainError {
#[error("brain backend: {0}")]
Backend(String),
}
pub(crate) fn be<E: std::fmt::Display>(e: E) -> BrainError {
BrainError::Backend(e.to_string())
}
fn now_nanos() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
}
/// A claw's brain: a single `.brain` (ClawhDF5 brain-pack) file.
pub struct ClawBrain {
h: BrainHandle,
path: PathBuf,
}
impl ClawBrain {
/// Open an existing `.brain`, or create a fresh one. `agent_id` is currently
/// advisory (the brain is identified by its file path).
pub fn open_or_create(path: impl AsRef<Path>, _agent_id: &str) -> Result<Self, BrainError> {
let p = path.as_ref().to_path_buf();
let h = if p.exists() {
BrainHandle::open(&p).map_err(be)?
} else {
if let Some(dir) = p.parent() {
std::fs::create_dir_all(dir).map_err(be)?;
}
BrainHandle::create(&p).map_err(be)?
};
Ok(Self { h, path: p })
}
/// Path to the underlying `.brain` file (for registry push / ClawSync).
pub fn path(&self) -> &Path {
&self.path
}
// ── ClawSync: local revision history + rollback (`.onion` sidecar) ─────────
/// Commit the current brain state as a new revision in the `.brain.onion`
/// sidecar. The base `.brain` stays current; history lives in the sidecar.
/// First commit creates the sidecar; later commits store only changed pages.
pub fn commit(&self, annotation: Option<&str>) -> Result<u64, BrainError> {
self.h.flush().map_err(be)?;
claw_brain::commit_versioned(&self.h, annotation).map_err(be)
}
/// List the brain's revision history (empty if never committed).
pub fn revisions(&self) -> Result<Vec<claw_brain::RevisionInfo>, BrainError> {
claw_brain::list_brain_revisions(&self.path).map_err(be)
}
/// Materialize a prior revision back onto the `.brain` file (the sidecar
/// history is preserved, so a rollback is itself reversible).
pub fn rollback(&self, revision: u64) -> Result<(), BrainError> {
claw_brain::rollback_to_revision(&self.path, revision).map_err(be)
}
// ── raw key helpers ───────────────────────────────────────────────────────
fn put(&self, key: &str, content: &str) -> Result<(), BrainError> {
self.h.write(key, content.as_bytes().to_vec()).map_err(be)?;
self.h.flush().map_err(be)
}
fn get(&self, key: &str) -> Option<String> {
self.h
.read(key)
.ok()
.and_then(|b| String::from_utf8(b).ok())
}
fn del(&self, key: &str) -> Result<bool, BrainError> {
if self.h.read(key).is_err() {
return Ok(false);
}
self.h.remove(key).map_err(be)?;
self.h.flush().map_err(be)?;
Ok(true)
}
/// `(suffix, value)` for every active key under `prefix` (excluding nested).
fn list(&self, prefix: &str) -> Vec<(String, String)> {
let mut out = Vec::new();
for k in self.h.keys() {
if let Some(name) = k.strip_prefix(prefix) {
if name.is_empty() || name.contains('/') {
continue;
}
if let Some(v) = self.get(&k) {
out.push((name.to_string(), v));
}
}
}
out
}
// ── identity ──────────────────────────────────────────────────────────────
pub fn set_system_prompt(&mut self, s: &str) -> Result<(), BrainError> {
self.put(K_SYSTEM_PROMPT, s)
}
/// System prompt, falling back to the brain-pack `soul_md` (pulled brains
/// often carry the identity there with an empty `system_prompt`).
pub fn system_prompt(&self) -> Option<String> {
match self.get(K_SYSTEM_PROMPT) {
Some(s) if !s.trim().is_empty() => Some(s),
_ => self.get(K_SOUL).filter(|s| !s.trim().is_empty()),
}
}
pub fn set_personality(&mut self, s: &str) -> Result<(), BrainError> {
self.put(K_PERSONA, s)
}
pub fn personality(&self) -> Option<String> {
self.get(K_PERSONA).filter(|s| !s.trim().is_empty())
}
/// AGENTS.md — "how I operate" (workflow/rules), prepended to the prompt.
pub fn set_agent_md(&mut self, s: &str) -> Result<(), BrainError> {
self.put(K_AGENT_MD, s)
}
pub fn agent_md(&self) -> Option<String> {
self.get(K_AGENT_MD).filter(|s| !s.trim().is_empty())
}
/// Assemble the agent's full system prompt from the canonical identity files
/// (mirrors ZeroClaw's personality render): `system_prompt`‖`soul_md`, then
/// `agent_md` (AGENTS.md), then `persona`.
pub fn assembled_identity(&self) -> String {
let mut out = String::new();
if let Some(sp) = self.system_prompt() {
out.push_str(&sp);
}
if let Some(a) = self.agent_md() {
if !out.is_empty() {
out.push_str("\n\n");
}
out.push_str("## How I operate\n");
out.push_str(&a);
}
if let Some(p) = self.personality() {
if !out.is_empty() {
out.push_str("\n\n");
}
out.push_str("## Personality\n");
out.push_str(&p);
}
out
}
// ── skills ────────────────────────────────────────────────────────────────
pub fn set_skill(&mut self, name: &str, body: &str) -> Result<(), BrainError> {
self.put(&format!("{P_SKILL}{name}"), body)
}
/// Set the brain-pack narrative skills doc (`skills/skills_md`); `skills()`
/// parses its `## <name>` sections.
pub fn set_skills_md(&mut self, md: &str) -> Result<(), BrainError> {
self.put(K_SKILLS_MD, md)
}
/// The brain-pack narrative skills doc (`skills/skills_md`), if non-empty.
pub fn skills_md(&self) -> Option<String> {
self.get(K_SKILLS_MD).filter(|s| !s.trim().is_empty())
}
pub fn remove_skill(&mut self, name: &str) -> Result<bool, BrainError> {
self.del(&format!("{P_SKILL}{name}"))
}
/// All skills as `(name, body)` — both per-skill keys and, if present, the
/// brain-pack narrative `skills/skills_md` parsed into `## <name>` sections.
pub fn skills(&self) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = self
.list(P_SKILL)
.into_iter()
.filter(|(n, _)| format!("{P_SKILL}{n}") != K_SKILLS_MD)
.collect();
if let Some(md) = self.get(K_SKILLS_MD) {
for (n, b) in parse_skills_md(&md) {
if !out.iter().any(|(en, _)| *en == n) {
out.push((n, b));
}
}
}
out
}
// ── tools / doors (value = state, e.g. "gated" | "blocked") ────────────────
pub fn set_tool(&mut self, name: &str, state: &str) -> Result<(), BrainError> {
self.put(&format!("{P_TOOL}{name}"), state)
}
pub fn remove_tool(&mut self, name: &str) -> Result<bool, BrainError> {
self.del(&format!("{P_TOOL}{name}"))
}
pub fn tools(&self) -> Vec<(String, String)> {
self.list(P_TOOL)
}
// ── runtime + provenance (opaque JSON blobs) ───────────────────────────────
pub fn set_runtime(&mut self, json: &str) -> Result<(), BrainError> {
self.put(K_RUNTIME, json)
}
pub fn runtime(&self) -> Option<String> {
self.get(K_RUNTIME)
}
pub fn set_provenance(&mut self, json: &str) -> Result<(), BrainError> {
self.put(K_PROVENANCE, json)
}
pub fn provenance(&self) -> Option<String> {
self.get(K_PROVENANCE)
}
// ── conversational memory ──────────────────────────────────────────────────
/// Append a turn to the brain's memory (keyword-indexed, recallable later).
pub fn remember(
&mut self,
role: &str,
text: &str,
_session_id: &str,
) -> Result<(), BrainError> {
let key = format!("{P_MEMORY}{:020}", now_nanos());
let chunk = format!("{role}: {text}");
self.h.write(&key, chunk.into_bytes()).map_err(be)?;
index_entry(&self.h, &key, text).map_err(be)?;
self.h.flush().map_err(be)
}
/// Recall up to `k` past memory chunks most relevant to `query` (BM25 over
/// the keyword index; only `memory/` entries). Best-effort.
pub fn recall(&self, query: &str, k: usize) -> Vec<String> {
let hits = match keyword_search(&self.h, query, k.saturating_mul(2).max(k)) {
Ok(h) => h,
Err(_) => return Vec::new(),
};
hits.into_iter()
.filter(|r| r.key.starts_with(P_MEMORY))
.filter_map(|r| self.get(&r.key))
.take(k)
.collect()
}
/// Most recent memory chunks, newest first, as `(timestamp_secs, text)`.
pub fn recent_memory(&self, k: usize) -> Vec<(f64, String)> {
let mut keys: Vec<(u128, String)> = self
.h
.keys()
.into_iter()
.filter_map(|key| {
key.strip_prefix(P_MEMORY)
.and_then(|s| s.parse::<u128>().ok())
.map(|n| (n, key))
})
.collect();
keys.sort_by_key(|k| std::cmp::Reverse(k.0));
keys.into_iter()
.take(k)
.filter_map(|(n, key)| self.get(&key).map(|t| (n as f64 / 1e9, t)))
.collect()
}
pub fn memory_count(&self) -> usize {
self.h
.keys()
.iter()
.filter(|k| k.starts_with(P_MEMORY))
.count()
}
}
/// Split a `skills_md` doc into `(name, body)` by its `## <name>` headings.
fn parse_skills_md(md: &str) -> Vec<(String, String)> {
let mut out = Vec::new();
let mut name: Option<String> = None;
let mut body = String::new();
for line in md.lines() {
if let Some(h) = line.strip_prefix("## ") {
if let Some(n) = name.take() {
out.push((n, body.trim().to_string()));
body.clear();
}
name = Some(h.trim().to_string());
} else if name.is_some() {
body.push_str(line);
body.push('\n');
}
}
if let Some(n) = name.take() {
out.push((n, body.trim().to_string()));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
fn temp_path(tag: &str) -> PathBuf {
std::env::temp_dir().join(format!("cm-brain-{}-{}.brain", std::process::id(), tag))
}
#[test]
fn brain_roundtrip_definition_and_memory() {
let p = temp_path("rt");
let _ = std::fs::remove_file(&p);
{
let mut b = ClawBrain::open_or_create(&p, "agent-x").expect("create");
b.set_system_prompt("You are Atlas, a meticulous planner.")
.unwrap();
b.set_personality("calm, precise, terse").unwrap();
b.set_skill("python", "Write idiomatic, tested Python.")
.unwrap();
b.set_tool("browser", "gated").unwrap();
b.set_runtime(r#"{"model":"claude","risk":"toolfree"}"#)
.unwrap();
b.remember("user", "My favorite color is teal.", "s1")
.unwrap();
b.remember("user", "I live in Boston.", "s1").unwrap();
}
let b = ClawBrain::open_or_create(&p, "agent-x").expect("open");
assert_eq!(
b.system_prompt().as_deref(),
Some("You are Atlas, a meticulous planner.")
);
assert_eq!(b.personality().as_deref(), Some("calm, precise, terse"));
assert_eq!(
b.skills(),
vec![(
"python".to_string(),
"Write idiomatic, tested Python.".to_string()
)]
);
assert_eq!(
b.tools(),
vec![("browser".to_string(), "gated".to_string())]
);
assert!(b.runtime().unwrap().contains("toolfree"));
assert_eq!(b.memory_count(), 2);
let hits = b.recall("what is my favorite color", 3);
assert!(
hits.iter().any(|h| h.contains("teal")),
"recall should surface teal; got {hits:?}"
);
let _ = std::fs::remove_file(&p);
}
#[test]
fn upsert_and_remove_skill() {
let p = temp_path("skill");
let _ = std::fs::remove_file(&p);
let mut b = ClawBrain::open_or_create(&p, "a").unwrap();
b.set_skill("py", "v1").unwrap();
b.set_skill("py", "v2").unwrap();
assert_eq!(b.skills(), vec![("py".to_string(), "v2".to_string())]);
assert!(b.remove_skill("py").unwrap());
assert!(b.skills().is_empty());
let _ = std::fs::remove_file(&p);
}
#[test]
fn parses_skills_md() {
let md = "# Skills\n\n## web_search\n\nSearch the web.\n\n## read_file\n\nRead a file.\n";
let s = parse_skills_md(md);
assert_eq!(s.len(), 2);
assert_eq!(s[0].0, "web_search");
assert!(s[0].1.contains("Search the web"));
}
}