teams: per-team runtime posture (risk_profile + mcp_bundles) + FK from loops/topics
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 26s
ci / rust (push) Successful in 3m24s
ci / e2e (push) Skipped
ci / publish (push) Successful in 2m19s

Foundation slice for letting a coding loop bring its own team instead
of reusing the paired research topic's team. Turns out the teams
table already exists (0010_teams.sql) with full CRUD — this scales
back to the minimal missing bits:

Schema (0045_teams.sql)
- ALTER TABLE teams ADD risk_profile TEXT (NULL = template default)
- ALTER TABLE teams ADD mcp_bundles JSONB DEFAULT '[]'
- ALTER TABLE loops ADD team_id UUID REFERENCES teams ON DELETE SET NULL
- ALTER TABLE research_topics ADD team_id UUID REFERENCES teams
- Two partial indexes (team_id NOT NULL) for the future cascade queries

cm-db (dynamic sqlx::query so the existing get_team's compile-time
cache doesn't need regenerating):
- TeamRuntimeConfig struct
- get_team_runtime_config / set_team_runtime_config
- team_for_loop / team_for_research_topic (resolvers)
- set_team_for_loop / set_team_for_research_topic (binders)

cm-api
- GET /api/teams/{id} now surfaces risk_profile + mcp_bundles
- PATCH /api/teams/{id}/runtime-config sets them

Not touched (comes in follow-up slices):
- Wizard picker exposing 'reuse research team' vs 'fresh coding team'
- Runtime container spawn keyed on team_id
- Migration of existing paired coding loops onto their own team
This commit is contained in:
Omar Sobh
2026-07-16 18:16:28 -07:00
parent b2a38da3f9
commit 6066e93889
4 changed files with 229 additions and 0 deletions
+46
View File
@@ -321,6 +321,45 @@ pub struct TeamDetail {
pub created_at: String,
pub graph: Value,
pub members: Vec<TeamMemberOut>,
/// Per-team runtime posture (0045). `None` risk_profile ⇒
/// container inherits the template default at spawn time.
#[serde(skip_serializing_if = "Option::is_none")]
pub risk_profile: Option<String>,
#[serde(default)]
pub mcp_bundles: Vec<String>,
}
/// `PATCH /api/teams/{id}/runtime-config` — set the per-team
/// risk_profile + mcp_bundles. Wizards call this after creating a team
/// to bind it as either "read-heavy research" or "write-capable coding"
/// without weakening the sibling team's posture.
#[derive(Deserialize)]
pub struct RuntimeConfigRequest {
#[serde(default)]
pub risk_profile: Option<String>,
#[serde(default)]
pub mcp_bundles: Vec<String>,
}
pub async fn set_runtime_config(
State(state): State<AppState>,
Authed(user): Authed,
Path(id): Path<Uuid>,
Json(body): Json<RuntimeConfigRequest>,
) -> Result<StatusCode, ApiError> {
// Workspace-scope check via the existing get_team.
let _ = cm_db::repo::teams::get_team(&state.pool, id, user.workspace_id).await?;
cm_db::repo::teams::set_team_runtime_config(
&state.pool,
id,
user.workspace_id,
&cm_db::repo::teams::TeamRuntimeConfig {
risk_profile: body.risk_profile,
mcp_bundles: body.mcp_bundles,
},
)
.await?;
Ok(StatusCode::NO_CONTENT)
}
/// `GET /api/teams/{id}` — a team's graph + node→claw bindings.
@@ -331,6 +370,11 @@ pub async fn get_team(
) -> Result<Json<TeamDetail>, ApiError> {
let team = cm_db::repo::teams::get_team(&state.pool, id, user.workspace_id).await?;
let members = cm_db::repo::teams::members_for_team(&state.pool, id).await?;
let runtime = cm_db::repo::teams::get_team_runtime_config(&state.pool, id, user.workspace_id)
.await
.ok()
.flatten()
.unwrap_or_default();
Ok(Json(TeamDetail {
id: team.id.to_string(),
name: team.name,
@@ -346,6 +390,8 @@ pub async fn get_team(
role: m.role,
})
.collect(),
risk_profile: runtime.risk_profile,
mcp_bundles: runtime.mcp_bundles,
}))
}