Teams (deploy ladder rung 1): schema + provisioning + API
A team = a baseline topology staffed with real claws. Migration 0010 (teams + team_members node→claw bindings) + cm-db repo/teams.rs. runtime_provision.rs turns a claw into a live runtime agent claw_<id> via the synced gateway config API (#7468): create agent + bind model_provider (mapped from chosen model) + risk_profile=toolfree + clawmates_door bundle — atomic, immediately drivable. routes/teams.rs: POST /api/teams (create claws + provision + build(kind,roles) + bind node.attrs["agent"]=claw_<id> + persist), GET /api/teams[/{id}], POST /api/teams/{id}/run (enqueue a durable run of the team graph — reuses the topology worker + SSE). v1 persona = topology role via the prompt builder; the claw's system_prompt stays its chat identity. Spike confirmed: runtime agent provisioning works; IDENTITY.md persona works for API models (Gemini/Groq), masked by CLI models (Claude/Kimi Code). 16 cm-api tests + provision unit tests pass, clippy clean. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3402a3b56d
commit
8123a27bcf
@@ -0,0 +1,150 @@
|
||||
//! Persistence for deployed teams — a baseline topology staffed with real
|
||||
//! workspace claws. `teams` holds the topology graph; `team_members` is the
|
||||
//! durable node→claw binding.
|
||||
|
||||
use cm_domain::WorkspaceId;
|
||||
use serde_json::Value;
|
||||
use sqlx::PgPool;
|
||||
use time::OffsetDateTime;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::DbError;
|
||||
|
||||
/// A team row summary (list view).
|
||||
pub struct TeamSummary {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub status: String,
|
||||
pub created_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
/// A full team (graph + metadata).
|
||||
pub struct Team {
|
||||
pub id: Uuid,
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub graph: Value,
|
||||
pub status: String,
|
||||
pub created_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
/// A node→claw binding within a team.
|
||||
pub struct TeamMember {
|
||||
pub node_id: String,
|
||||
pub claw_id: Uuid,
|
||||
pub role: String,
|
||||
}
|
||||
|
||||
/// Insert a team (the topology graph). Members are added separately.
|
||||
pub async fn insert_team(
|
||||
pool: &PgPool,
|
||||
id: Uuid,
|
||||
workspace_id: WorkspaceId,
|
||||
name: &str,
|
||||
kind: &str,
|
||||
graph: &Value,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO teams (id, workspace_id, name, kind, graph)
|
||||
VALUES ($1, $2, $3, $4, $5)",
|
||||
id,
|
||||
workspace_id.as_uuid(),
|
||||
name,
|
||||
kind,
|
||||
graph,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bind a claw to a topology node within a team.
|
||||
pub async fn add_member(
|
||||
pool: &PgPool,
|
||||
team_id: Uuid,
|
||||
node_id: &str,
|
||||
claw_id: Uuid,
|
||||
role: &str,
|
||||
) -> Result<(), DbError> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO team_members (team_id, node_id, claw_id, role)
|
||||
VALUES ($1, $2, $3, $4)",
|
||||
team_id,
|
||||
node_id,
|
||||
claw_id,
|
||||
role,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// The most recent teams for a workspace, newest first.
|
||||
pub async fn list_for_workspace(
|
||||
pool: &PgPool,
|
||||
workspace_id: WorkspaceId,
|
||||
limit: i64,
|
||||
) -> Result<Vec<TeamSummary>, DbError> {
|
||||
let rows = sqlx::query!(
|
||||
"SELECT id, name, kind, status, created_at FROM teams
|
||||
WHERE workspace_id = $1 ORDER BY created_at DESC LIMIT $2",
|
||||
workspace_id.as_uuid(),
|
||||
limit,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| TeamSummary {
|
||||
id: r.id,
|
||||
name: r.name,
|
||||
kind: r.kind,
|
||||
status: r.status,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// A single team, workspace-scoped.
|
||||
pub async fn get_team(
|
||||
pool: &PgPool,
|
||||
id: Uuid,
|
||||
workspace_id: WorkspaceId,
|
||||
) -> Result<Team, DbError> {
|
||||
let row = sqlx::query!(
|
||||
"SELECT id, name, kind, graph, status, created_at FROM teams
|
||||
WHERE id = $1 AND workspace_id = $2",
|
||||
id,
|
||||
workspace_id.as_uuid(),
|
||||
)
|
||||
.fetch_optional(pool)
|
||||
.await?
|
||||
.ok_or(DbError::NotFound)?;
|
||||
Ok(Team {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
kind: row.kind,
|
||||
graph: row.graph,
|
||||
status: row.status,
|
||||
created_at: row.created_at,
|
||||
})
|
||||
}
|
||||
|
||||
/// The node→claw bindings for a team.
|
||||
pub async fn members_for_team(pool: &PgPool, team_id: Uuid) -> Result<Vec<TeamMember>, DbError> {
|
||||
let rows = sqlx::query!(
|
||||
"SELECT node_id, claw_id, role FROM team_members WHERE team_id = $1 ORDER BY node_id",
|
||||
team_id,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|r| TeamMember {
|
||||
node_id: r.node_id,
|
||||
claw_id: r.claw_id,
|
||||
role: r.role,
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
Reference in New Issue
Block a user