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:
co-authored by
Claude Opus 5
parent
c573480955
commit
285d0c82f2
@@ -70,6 +70,7 @@ pub async fn compartments(
|
||||
Path(id): Path<AgentId>,
|
||||
) -> Result<Json<Vec<Compartment>>, ApiError> {
|
||||
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 personality = if agent.system_prompt.trim().is_empty() {
|
||||
vec![]
|
||||
@@ -96,34 +97,144 @@ pub async fn compartments(
|
||||
count: None,
|
||||
},
|
||||
Compartment {
|
||||
// The §15 "door": email/slack are gated MCP tools, browser gated,
|
||||
// shell blocked (claws are tool-free in the sandbox).
|
||||
// The §15 "door" tools are always available (every claw is
|
||||
// provisioned with the `clawmates_door` MCP bundle) and always
|
||||
// gated. Everything else comes from the claw's real risk_profile.
|
||||
key: "tools".into(),
|
||||
label: "Tools · Doors".into(),
|
||||
items: vec![
|
||||
"Email · gated".into(),
|
||||
"Slack · gated".into(),
|
||||
"Browser · gated".into(),
|
||||
"Shell · blocked".into(),
|
||||
],
|
||||
items: {
|
||||
let mut v = vec![
|
||||
"Email · gated".into(),
|
||||
"Slack · gated".into(),
|
||||
"Delegate · gated".into(),
|
||||
];
|
||||
v.extend(
|
||||
risk_profile_tools(&risk_profile)
|
||||
.iter()
|
||||
.map(|t| format!("{t} · allowed")),
|
||||
);
|
||||
v
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
Compartment {
|
||||
key: "capabilities".into(),
|
||||
label: "Capabilities".into(),
|
||||
items: vec!["File management".into(), "Scheduling".into()],
|
||||
items: risk_profile_capabilities(&risk_profile),
|
||||
count: None,
|
||||
},
|
||||
Compartment {
|
||||
key: "safety".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,
|
||||
},
|
||||
];
|
||||
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)
|
||||
/// 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`)
|
||||
|
||||
Reference in New Issue
Block a user