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]>
This commit is contained in:
Omar Sobh
2026-07-30 10:44:33 -07:00
co-authored by Claude Opus 5
parent c573480955
commit 285d0c82f2
7 changed files with 136 additions and 62 deletions
+11 -1
View File
@@ -468,7 +468,17 @@ pub async fn mcp(
return tool_result( return tool_result(
req.id, req.id,
true, true,
format!("unknown tool {mcp_name:?} (this door exposes: email_send)"), // Derived from EXPOSED_TOOLS rather than hand-written: the
// literal list here had already drifted to name only one of
// the three tools the door actually exposes.
format!(
"unknown tool {mcp_name:?} (this door exposes: {})",
EXPOSED_TOOLS
.iter()
.map(|(m, _)| *m)
.collect::<Vec<_>>()
.join(", ")
),
); );
}; };
+119 -8
View File
@@ -70,6 +70,7 @@ pub async fn compartments(
Path(id): Path<AgentId>, Path(id): Path<AgentId>,
) -> Result<Json<Vec<Compartment>>, ApiError> { ) -> Result<Json<Vec<Compartment>>, ApiError> {
let agent = workspace_agent(&state, &user, id).await?; let agent = workspace_agent(&state, &user, id).await?;
let risk_profile = effective_risk_profile(&state.pool, &agent).await?;
let skills = cm_db::repo::skills::installed(&state.pool, agent.id).await?; let skills = cm_db::repo::skills::installed(&state.pool, agent.id).await?;
let personality = if agent.system_prompt.trim().is_empty() { let personality = if agent.system_prompt.trim().is_empty() {
vec![] vec![]
@@ -96,34 +97,144 @@ pub async fn compartments(
count: None, count: None,
}, },
Compartment { Compartment {
// The §15 "door": email/slack are gated MCP tools, browser gated, // The §15 "door" tools are always available (every claw is
// shell blocked (claws are tool-free in the sandbox). // provisioned with the `clawmates_door` MCP bundle) and always
// gated. Everything else comes from the claw's real risk_profile.
key: "tools".into(), key: "tools".into(),
label: "Tools · Doors".into(), label: "Tools · Doors".into(),
items: vec![ items: {
let mut v = vec![
"Email · gated".into(), "Email · gated".into(),
"Slack · gated".into(), "Slack · gated".into(),
"Browser · gated".into(), "Delegate · gated".into(),
"Shell · blocked".into(), ];
], v.extend(
risk_profile_tools(&risk_profile)
.iter()
.map(|t| format!("{t} · allowed")),
);
v
},
count: None, count: None,
}, },
Compartment { Compartment {
key: "capabilities".into(), key: "capabilities".into(),
label: "Capabilities".into(), label: "Capabilities".into(),
items: vec!["File management".into(), "Scheduling".into()], items: risk_profile_capabilities(&risk_profile),
count: None, count: None,
}, },
Compartment { Compartment {
key: "safety".into(), key: "safety".into(),
label: "Safety · §15".into(), label: "Safety · §15".into(),
items: vec!["Sandbox: isolated".into(), "Network: none".into()], items: vec![
format!("Risk profile: {risk_profile}"),
format!(
"Shell: {}",
if risk_profile_tools(&risk_profile).contains(&"shell") {
"granted"
} else {
"blocked"
}
),
format!(
"Web: {}",
if risk_profile_tools(&risk_profile).contains(&"web_fetch") {
"read-only"
} else {
"none"
}
),
],
count: None, count: None,
}, },
]; ];
Ok(Json(out)) Ok(Json(out))
} }
/// The strict `allowed_tools` allowlist each risk profile grants, mirroring
/// `[risk_profiles.*]` in `deploy/clawmates-runtime/agent.config.example.toml`.
///
/// Kept in sync by hand because the profiles live in the runtime's config file,
/// not in our schema. An unknown profile reports no grants rather than guessing
/// generously — under-reporting a capability is the safe direction here.
fn risk_profile_tools(profile: &str) -> &'static [&'static str] {
match profile {
"coding_readwrite" => &[
"file_read",
"file_edit",
"content_search",
"glob_search",
"git_operations",
"shell",
],
"research_readonly" => &["file_read", "content_search", "glob_search"],
"research_web_readonly" => &[
"file_read",
"content_search",
"glob_search",
"web_search",
"web_fetch",
],
// `toolfree` and anything unrecognised: door only.
_ => &[],
}
}
/// Plain-language capability summary derived from the same allowlist, so the
/// anatomy card can't drift from what the claw can actually do.
fn risk_profile_capabilities(profile: &str) -> Vec<String> {
let tools = risk_profile_tools(profile);
let mut out = Vec::new();
if tools.contains(&"file_edit") {
out.push("Read + write workspace files".into());
} else if tools.contains(&"file_read") {
out.push("Read workspace files".into());
}
if tools.contains(&"content_search") || tools.contains(&"glob_search") {
out.push("Search the workspace".into());
}
if tools.contains(&"git_operations") {
out.push("Git operations".into());
}
if tools.contains(&"shell") {
out.push("Shell in sandbox".into());
}
if tools.contains(&"web_search") || tools.contains(&"web_fetch") {
out.push("Public web read".into());
}
out.push("Messaging + scheduling via the door".into());
out
}
/// The claw's effective risk profile: its team's explicit setting when it has
/// one, else the same role-derived default the provisioner would apply.
///
/// Mirrors what `runtime_provision` actually writes to the runtime, so the
/// anatomy cards report the real capability boundary instead of a fixed string.
async fn effective_risk_profile(
pool: &sqlx::PgPool,
agent: &cm_domain::Agent,
) -> Result<String, ApiError> {
use sqlx::Row;
let row = sqlx::query(
"SELECT t.risk_profile FROM team_members tm
JOIN teams t ON t.id = tm.team_id
WHERE tm.claw_id = $1 AND t.workspace_id = $2
LIMIT 1",
)
.bind(agent.id.as_uuid())
.bind(agent.workspace_id.as_uuid())
.fetch_optional(pool)
.await?;
let from_team = row.and_then(|r| r.try_get::<Option<String>, _>("risk_profile").ok().flatten());
Ok(from_team.unwrap_or_else(|| {
crate::runtime_provision::RuntimeProvisioner::default_risk_profile_for_role(
&agent.job_title,
)
.to_string()
}))
}
/// `GET /api/claws/{id}/brain` — the claw's `.brain` (cm-brain / ClawhDF5) /// `GET /api/claws/{id}/brain` — the claw's `.brain` (cm-brain / ClawhDF5)
/// rendered for the anatomy cards: its six sections + recent memory + stats. /// rendered for the anatomy cards: its six sections + recent memory + stats.
/// Best-effort: if the brain can't be opened, returns an empty (`exists:false`) /// Best-effort: if the brain can't be opened, returns an empty (`exists:false`)
-22
View File
@@ -294,28 +294,6 @@ impl ClawBrain {
.count() .count()
} }
/// Render identity + skills as Markdown (for ZeroClaw workspace hydration).
pub fn export_markdown(&self) -> String {
let mut s = String::new();
if let Some(sp) = self.system_prompt() {
s.push_str("# System Prompt\n\n");
s.push_str(&sp);
s.push_str("\n\n");
}
if let Some(p) = self.personality() {
s.push_str("# Personality\n\n");
s.push_str(&p);
s.push_str("\n\n");
}
let skills = self.skills();
if !skills.is_empty() {
s.push_str("# Skills\n\n");
for (name, body) in skills {
s.push_str(&format!("## {name}\n\n{body}\n\n"));
}
}
s
}
} }
/// Split a `skills_md` doc into `(name, body)` by its `## <name>` headings. /// Split a `skills_md` doc into `(name, body)` by its `## <name>` headings.
@@ -94,27 +94,6 @@ const NODE_GRADS: [string, string][] = [
["linear-gradient(135deg,#c98af0,#9a5ad8)", "#1a0a2a"], ["linear-gradient(135deg,#c98af0,#9a5ad8)", "#1a0a2a"],
]; ];
// A few starter templates surfaced in the Templates tab (deploy + visualize).
interface Template {
id: string;
name: string;
topo: string;
blurb: string;
roles: string[];
}
const TEAM_TEMPLATES: Template[] = [
{ id: "t-research", name: "Research Pod", topo: "blackboard", blurb: "A lead curates a shared blackboard while researchers and a critic read/write findings in parallel.", roles: ["lead", "researcher", "researcher", "critic", "writer"] },
{ id: "t-growth", name: "Growth Squad", topo: "hub_spoke", blurb: "A coordinator routes work to specialists and aggregates their output back.", roles: ["lead", "researcher", "writer", "analyst", "critic"] },
{ id: "t-pipeline", name: "Content Pipeline", topo: "pipeline", blurb: "Linear stages: intake → draft → edit → publish, each agent feeding the next.", roles: ["intake", "drafter", "editor", "publisher"] },
{ id: "t-debate", name: "Debate Room", topo: "debate", blurb: "A proposer and a critic argue; a judge resolves. Good for high-stakes decisions.", roles: ["proposer", "critic", "judge"] },
{ id: "t-swarm", name: "Swarm Recon", topo: "swarm", blurb: "Many autonomous peers attack a problem in parallel; consensus emerges.", roles: ["scout", "scout", "scout", "scout", "synthesizer"] },
];
const COMPANY_TEMPLATES: Template[] = [
{ id: "c-pipeline", name: "Pipeline Co", topo: "pipeline", blurb: "Teams arranged as a value chain — intake feeds growth feeds research feeds ops.", roles: ["Intake", "Growth", "Research", "Ops"] },
{ id: "c-federated", name: "Federated Co", topo: "federated", blurb: "Semi-autonomous teams with a light coordination layer between them.", roles: ["Team A", "Team B", "Team C"] },
{ id: "c-holacratic", name: "Holacratic Co", topo: "holacratic", blurb: "Self-organizing circles with distributed authority and no fixed hierarchy.", roles: ["Circle 1", "Circle 2", "Circle 3"] },
];
const railIcon: Record<Tier, React.ReactNode> = { const railIcon: Record<Tier, React.ReactNode> = {
world: (<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="3.5" r="1.9" fill="currentColor" /><circle cx="3.8" cy="11" r="1.9" fill="currentColor" /><circle cx="16.2" cy="11" r="1.9" fill="currentColor" /><circle cx="10" cy="16.5" r="1.9" fill="currentColor" /><path d="M10 3.5 L3.8 11 M10 3.5 L16.2 11 M3.8 11 L10 16.5 M16.2 11 L10 16.5" stroke="currentColor" strokeWidth="1.1" opacity=".5" /></svg>), world: (<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="3.5" r="1.9" fill="currentColor" /><circle cx="3.8" cy="11" r="1.9" fill="currentColor" /><circle cx="16.2" cy="11" r="1.9" fill="currentColor" /><circle cx="10" cy="16.5" r="1.9" fill="currentColor" /><path d="M10 3.5 L3.8 11 M10 3.5 L16.2 11 M3.8 11 L10 16.5 M16.2 11 L10 16.5" stroke="currentColor" strokeWidth="1.1" opacity=".5" /></svg>),
// Flag on a pole — missions (unified research + loops). // Flag on a pole — missions (unified research + loops).
+2 -3
View File
@@ -8,9 +8,8 @@ kind = "coding"
order_idx = 0 order_idx = 0
[phases.config] [phases.config]
loop = "single_pass" loop = "single_pass"
# Preamble asks the planner to run `cargo tree`, `cargo outdated`, # The planner runs `cargo tree`, `cargo outdated`, `npm outdated`,
# `npm outdated`, etc. and produce INT-XX items per stale dep. # etc. and produces INT-XX items per stale dep.
task_preamble = "dependency_audit_v1"
commit_policy = "on_green_tests" commit_policy = "on_green_tests"
# Bench before + after the pass so we can measure impact. # Bench before + after the pass so we can measure impact.
benchmark = { mode = "before_after" } benchmark = { mode = "before_after" }
+2 -4
View File
@@ -19,10 +19,8 @@ order_idx = 1
# equals the artifact's declared set. # equals the artifact's declared set.
loop = "until_no_more_int_items" loop = "until_no_more_int_items"
# Preamble injected at the head of each iteration's task text so # Preamble injected at the head of each iteration's task text so
# the agents know where the repo lives + how to commit. Slice 3.5c's # the agents know where the repo lives + how to commit. Covered by
# `workspace-repo-commit-protocol` skill also covers this — the # Slice 3.5c's `workspace-repo-commit-protocol` skill.
# preamble is the belt, the skill the suspenders.
task_preamble = "workspace_repo_v1"
# Only commit when tests pass. Enforced by the team's TEST_PASS # Only commit when tests pass. Enforced by the team's TEST_PASS
# marker before the committer runs. If a coding role wants to bypass # marker before the committer runs. If a coding role wants to bypass
# (rare — pure docs commit), it emits COMMIT_POLICY_OVERRIDE: <reason>. # (rare — pure docs commit), it emits COMMIT_POLICY_OVERRIDE: <reason>.
@@ -26,7 +26,6 @@ kind = "coding"
order_idx = 2 order_idx = 2
[phases.config] [phases.config]
loop = "until_all_findings_closed" loop = "until_all_findings_closed"
task_preamble = "workspace_repo_v1"
# Security requires reviewer approval on top of green tests. # Security requires reviewer approval on top of green tests.
commit_policy = "on_reviewer_approval" commit_policy = "on_reviewer_approval"
mcp_bundles = ["clawmates_door", "clawmates_skills", "gitea_forge", "security_scan"] mcp_bundles = ["clawmates_door", "clawmates_skills", "gitea_forge", "security_scan"]