Rebrand: TeamClaw -> Clawmates (clawmates.work)

Full-depth rename per the approved plan; the 'claw' product vocabulary
(claws, /claws routes, clawId, Claw Chat) stays — it is now the brand.

- Display brand: Clawmates (manifest, titles, hero, login/rail logo
  'clawmates'); default host app.clawmates.work; registry
  ghcr.io/clawmates
- Crates tc-* -> cm-* (16 crates + all imports); binaries
  clawmates-server/broker/bundler; images clawmates/*; env prefix
  CLAWMATES_* (+ CM_TEST_DATABASE_URL / CM_LIVE_LLM); config
  clawmates.toml; helm chart deploy/helm/clawmates with clawmates-*
  resources; db names clawmates*; sockets /run/clawmates; cookie
  cm_session; kind cluster clawmates-test; seccomp node profile
  clawmates-agent-profile.json
- All 9 Playwright brand assertions updated in lockstep; historical
  spec document left untouched as the only remaining 'TeamClaw'
- Local env migrated: dev pg clawmates-dev-pg/clawmates_dev, shared
  test server clawmates-test-pg, kind cluster recreated with image +
  profile, compose images rebuilt under clawmates/*

Verified end to end: 161 Rust + 68 frontend tests, 29 Playwright
journeys, 4 live kind tests, helm/install/LOC/placeholder gates, and
the clean-room install rehearsal serving the clawmates login page from
a signed bundle of the rebuilt images.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-10 12:31:25 -05:00
co-authored by Claude Fable 5
parent 8046853feb
commit add4f79fed
209 changed files with 1429 additions and 1422 deletions
+230
View File
@@ -0,0 +1,230 @@
use cm_domain::{
AccessPolicy, Agent, AgentId, AgentScope, AgentStatus, HumanScope, UserId, WorkspaceId,
};
use sqlx::PgPool;
use uuid::Uuid;
use crate::DbError;
/// Inserts an agent together with its access policy in one transaction —
/// an agent without a policy must never be observable (§7.7).
pub async fn insert(pool: &PgPool, agent: &Agent, policy: &AccessPolicy) -> Result<(), DbError> {
let (humans_mode, human_ids): (&str, Vec<Uuid>) = match &policy.humans {
HumanScope::EntireTeam => ("entire_team", Vec::new()),
HumanScope::Specific(ids) => ("specific", ids.iter().map(UserId::as_uuid).collect()),
};
let (agents_mode, agent_ids): (&str, Vec<Uuid>) = match &policy.agents {
AgentScope::Any => ("any", Vec::new()),
AgentScope::Specific(ids) => ("specific", ids.iter().map(AgentId::as_uuid).collect()),
};
let mut tx = pool.begin().await.map_err(DbError::from)?;
sqlx::query!(
"INSERT INTO agents
(id, workspace_id, name, job_title, system_prompt, avatar, accent,
wallpaper, managed_by, status)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
agent.id.as_uuid(),
agent.workspace_id.as_uuid(),
agent.name,
agent.job_title,
agent.system_prompt,
agent.avatar,
agent.accent,
agent.wallpaper,
agent.managed_by.as_uuid(),
agent.status.as_str(),
)
.execute(&mut *tx)
.await?;
sqlx::query!(
"INSERT INTO access_policies
(agent_id, humans_mode, human_ids, agents_mode, agent_ids)
VALUES ($1, $2, $3, $4, $5)",
agent.id.as_uuid(),
humans_mode,
&human_ids,
agents_mode,
&agent_ids,
)
.execute(&mut *tx)
.await?;
tx.commit().await.map_err(DbError::from)?;
Ok(())
}
pub async fn get(pool: &PgPool, agent_id: AgentId) -> Result<Agent, DbError> {
let row = sqlx::query!(
"SELECT id, workspace_id, name, job_title, system_prompt, avatar,
accent, wallpaper, managed_by, status
FROM agents WHERE id = $1 AND deleted_at IS NULL",
agent_id.as_uuid(),
)
.fetch_one(pool)
.await?;
Ok(Agent {
id: AgentId::from(row.id),
workspace_id: WorkspaceId::from(row.workspace_id),
name: row.name,
job_title: row.job_title,
system_prompt: row.system_prompt,
avatar: row.avatar,
accent: row.accent,
wallpaper: row.wallpaper,
managed_by: UserId::from(row.managed_by),
status: row.status.parse().expect("status CHECK constraint"),
})
}
/// Patch-style profile update (§7.7 Edit profile): only provided fields
/// change; the system prompt is the Job Description textarea verbatim.
#[allow(clippy::too_many_arguments)]
pub async fn update_profile(
pool: &PgPool,
agent_id: AgentId,
name: Option<&str>,
job_title: Option<&str>,
system_prompt: Option<&str>,
avatar: Option<&str>,
accent: Option<&str>,
wallpaper: Option<&str>,
) -> Result<Agent, DbError> {
let result = sqlx::query!(
"UPDATE agents SET
name = COALESCE($2, name),
job_title = COALESCE($3, job_title),
system_prompt = COALESCE($4, system_prompt),
avatar = COALESCE($5, avatar),
accent = COALESCE($6, accent),
wallpaper = COALESCE($7, wallpaper)
WHERE id = $1 AND deleted_at IS NULL",
agent_id.as_uuid(),
name,
job_title,
system_prompt,
avatar,
accent,
wallpaper,
)
.execute(pool)
.await?;
if result.rows_affected() == 0 {
return Err(DbError::NotFound);
}
get(pool, agent_id).await
}
/// The left-rail roster (§4): live agents of a workspace, oldest first.
pub async fn roster(pool: &PgPool, workspace_id: WorkspaceId) -> Result<Vec<Agent>, DbError> {
let rows = sqlx::query!(
"SELECT id, workspace_id, name, job_title, system_prompt, avatar,
accent, wallpaper, managed_by, status
FROM agents
WHERE workspace_id = $1 AND deleted_at IS NULL
ORDER BY created_at, id",
workspace_id.as_uuid(),
)
.fetch_all(pool)
.await?;
Ok(rows
.into_iter()
.map(|row| Agent {
id: AgentId::from(row.id),
workspace_id: WorkspaceId::from(row.workspace_id),
name: row.name,
job_title: row.job_title,
system_prompt: row.system_prompt,
avatar: row.avatar,
accent: row.accent,
wallpaper: row.wallpaper,
managed_by: UserId::from(row.managed_by),
status: row.status.parse().expect("status CHECK constraint"),
})
.collect())
}
/// Replaces an agent's access policy (§7.7 access toggles).
pub async fn set_access_policy(
pool: &PgPool,
agent_id: AgentId,
policy: &AccessPolicy,
) -> Result<(), DbError> {
let (humans_mode, human_ids): (&str, Vec<Uuid>) = match &policy.humans {
HumanScope::EntireTeam => ("entire_team", Vec::new()),
HumanScope::Specific(ids) => ("specific", ids.iter().map(UserId::as_uuid).collect()),
};
let (agents_mode, agent_ids): (&str, Vec<Uuid>) = match &policy.agents {
AgentScope::Any => ("any", Vec::new()),
AgentScope::Specific(ids) => ("specific", ids.iter().map(AgentId::as_uuid).collect()),
};
let result = sqlx::query!(
"UPDATE access_policies SET humans_mode = $2, human_ids = $3,
agents_mode = $4, agent_ids = $5
WHERE agent_id = $1",
agent_id.as_uuid(),
humans_mode,
&human_ids,
agents_mode,
&agent_ids,
)
.execute(pool)
.await?;
if result.rows_affected() == 0 {
return Err(DbError::NotFound);
}
Ok(())
}
pub async fn access_policy(pool: &PgPool, agent_id: AgentId) -> Result<AccessPolicy, DbError> {
let row = sqlx::query!(
"SELECT humans_mode, human_ids, agents_mode, agent_ids
FROM access_policies WHERE agent_id = $1",
agent_id.as_uuid(),
)
.fetch_one(pool)
.await?;
let humans = if row.humans_mode == "entire_team" {
HumanScope::EntireTeam
} else {
HumanScope::Specific(row.human_ids.into_iter().map(UserId::from).collect())
};
let agents = if row.agents_mode == "any" {
AgentScope::Any
} else {
AgentScope::Specific(row.agent_ids.into_iter().map(AgentId::from).collect())
};
Ok(AccessPolicy { humans, agents })
}
pub async fn set_status(
pool: &PgPool,
agent_id: AgentId,
status: AgentStatus,
) -> Result<(), DbError> {
let result = sqlx::query!(
"UPDATE agents SET status = $2 WHERE id = $1 AND deleted_at IS NULL",
agent_id.as_uuid(),
status.as_str(),
)
.execute(pool)
.await?;
if result.rows_affected() == 0 {
return Err(DbError::NotFound);
}
Ok(())
}
/// Deleting a claw is destructive and gated (§7.7); rows are kept for audit.
pub async fn soft_delete(pool: &PgPool, agent_id: AgentId) -> Result<(), DbError> {
let result = sqlx::query!(
"UPDATE agents SET deleted_at = now(), status = 'offline'
WHERE id = $1 AND deleted_at IS NULL",
agent_id.as_uuid(),
)
.execute(pool)
.await?;
if result.rows_affected() == 0 {
return Err(DbError::NotFound);
}
Ok(())
}