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
@@ -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(", ")
|
||||||
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
"Email · gated".into(),
|
let mut v = vec![
|
||||||
"Slack · gated".into(),
|
"Email · gated".into(),
|
||||||
"Browser · gated".into(),
|
"Slack · gated".into(),
|
||||||
"Shell · blocked".into(),
|
"Delegate · gated".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`)
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user