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
+250
View File
@@ -0,0 +1,250 @@
use axum::extract::{Query, State};
use axum::http::StatusCode;
use axum::Json;
use cm_db::repo::audit::Actor;
use cm_domain::AgentId;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use uuid::Uuid;
use crate::routes::claws::workspace_agent;
use crate::{ApiError, AppState, Authed};
#[derive(Serialize)]
pub struct DirectoryApp {
pub id: &'static str,
pub name: &'static str,
pub description: &'static str,
pub category: &'static str,
}
fn catalog() -> Vec<DirectoryApp> {
vec![
DirectoryApp {
id: "gmail",
name: "Gmail",
description: "Read and draft email on your behalf.",
category: "Email",
},
DirectoryApp {
id: "google-calendar",
name: "Google Calendar",
description: "Check availability and schedule events.",
category: "Calendar",
},
DirectoryApp {
id: "google-drive",
name: "Google Drive",
description: "Search and read team documents.",
category: "Storage",
},
DirectoryApp {
id: "notion",
name: "Notion",
description: "Read and update pages and databases.",
category: "Docs",
},
DirectoryApp {
id: "linear",
name: "Linear",
description: "Track and file engineering issues.",
category: "Project management",
},
DirectoryApp {
id: "github",
name: "GitHub",
description: "Review repos, issues, and pull requests.",
category: "Engineering",
},
DirectoryApp {
id: "figma",
name: "Figma",
description: "Inspect design files and comments.",
category: "Design",
},
DirectoryApp {
id: "zoom",
name: "Zoom",
description: "Schedule and summarize meetings.",
category: "Meetings",
},
DirectoryApp {
id: "stripe",
name: "Stripe",
description: "Look up customers, invoices, and payments.",
category: "Finance",
},
DirectoryApp {
id: "hubspot",
name: "HubSpot",
description: "Manage contacts and deals.",
category: "CRM",
},
DirectoryApp {
id: "google-sheets",
name: "Google Sheets",
description: "Read and update spreadsheets.",
category: "Docs",
},
DirectoryApp {
id: "slack",
name: "Slack",
description: "Respond on @mention in your channels.",
category: "Chat",
},
DirectoryApp {
id: "telegram",
name: "Telegram",
description: "Send and receive messages.",
category: "Chat",
},
DirectoryApp {
id: "webhook",
name: "HTTP / Webhook",
description: "Call any HTTP endpoint.",
category: "Developer",
},
DirectoryApp {
id: "postgres",
name: "Postgres",
description: "Query your databases.",
category: "Data",
},
DirectoryApp {
id: "aws",
name: "AWS",
description: "Inspect cloud resources.",
category: "Infrastructure",
},
DirectoryApp {
id: "sendgrid",
name: "SendGrid",
description: "Deliver transactional email.",
category: "Email",
},
]
}
#[derive(Deserialize)]
pub struct DirectoryQuery {
#[serde(rename = "clawId")]
claw_id: Option<AgentId>,
}
/// GET /api/apps[?clawId=] — the connect directory (§8.2), with live
/// connection status when scoped to a claw.
pub async fn directory(
State(state): State<AppState>,
Authed(user): Authed,
Query(query): Query<DirectoryQuery>,
) -> Result<Json<Vec<Value>>, ApiError> {
let connections = match query.claw_id {
Some(claw_id) => {
let agent = workspace_agent(&state, &user, claw_id).await?;
cm_db::repo::connections::list_for_agent(&state.pool, user.workspace_id, agent.id)
.await?
}
None => Vec::new(),
};
let merged = catalog()
.into_iter()
.map(|app| {
let connection = connections.iter().find(|c| c.provider == app.id);
json!({
"id": app.id,
"name": app.name,
"description": app.description,
"category": app.category,
"connected": connection.is_some(),
"connection_id": connection.map(|c| c.id),
})
})
.collect();
Ok(Json(merged))
}
#[derive(Deserialize)]
pub struct ConnectRequest {
#[serde(rename = "clawId")]
claw_id: AgentId,
provider: String,
#[serde(rename = "authType")]
auth_type: String,
/// The API key / token (keys) or "user:password" (basic). Sent once,
/// stored encrypted by the broker, never readable again.
secret: String,
}
/// POST /api/apps/connect — custom app credentials (§10 Add Custom App,
/// keys/basic). OAuth and MCP-OAuth flows follow.
pub async fn connect(
State(state): State<AppState>,
Authed(user): Authed,
Json(body): Json<ConnectRequest>,
) -> Result<(StatusCode, Json<Value>), ApiError> {
if !matches!(body.auth_type.as_str(), "keys" | "basic") {
return Err(ApiError::Conflict);
}
let agent = workspace_agent(&state, &user, body.claw_id).await?;
let socket = state.broker_socket.as_ref().ok_or(ApiError::Internal)?;
let mut broker = cm_secrets::BrokerClient::connect(socket)
.await
.map_err(|_| ApiError::Internal)?;
let secret_ref = broker
.store_secret(
user.workspace_id,
&format!("{}_{}", body.provider, body.auth_type),
&body.secret,
)
.await
.map_err(|_| ApiError::Internal)?;
let connection = cm_db::repo::connections::insert(
&state.pool,
user.workspace_id,
Some(agent.id),
&body.provider,
&body.auth_type,
secret_ref,
)
.await?;
cm_db::repo::audit::append(
&state.pool,
user.workspace_id,
Actor::User(user.user_id),
"app.connected",
"app_connection",
&connection.id.to_string(),
json!({"provider": body.provider, "auth_type": body.auth_type}),
)
.await?;
Ok((
StatusCode::CREATED,
Json(json!({"id": connection.id, "provider": connection.provider, "status": "connected"})),
))
}
#[derive(Deserialize)]
pub struct DisconnectRequest {
#[serde(rename = "connectionId")]
connection_id: Uuid,
}
/// POST /api/apps/disconnect
pub async fn disconnect(
State(state): State<AppState>,
Authed(user): Authed,
Json(body): Json<DisconnectRequest>,
) -> Result<StatusCode, ApiError> {
cm_db::repo::connections::disconnect(&state.pool, body.connection_id).await?;
cm_db::repo::audit::append(
&state.pool,
user.workspace_id,
Actor::User(user.user_id),
"app.disconnected",
"app_connection",
&body.connection_id.to_string(),
json!({}),
)
.await?;
Ok(StatusCode::NO_CONTENT)
}