api: gate create_loop + create_topic on empty workspace roster (W2)
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 28s
ci / rust (push) Failing after 3m31s
ci / e2e (push) Has been skipped
ci / publish (push) Has been skipped

Frontend already disables the create button when the roster is empty,
but nothing stopped a direct POST from materializing an orphan loop /
topic with nothing to staff it. Both handlers now check the workspace
agent count up front and return 409 Conflict when it's zero.

- crates/cm-api/src/routes/loops.rs: gate at top of create_loop
- crates/cm-api/src/routes/research.rs: gate at top of create_topic

Uses the existing cm_db::repo::agents::count_active(pool, ws) helper
(count of workspace agents where deleted_at IS NULL). 409 is the right
mapping: state-of-the-workspace-prevents-this, not client-input-bad.
This commit is contained in:
Omar Sobh
2026-07-09 14:00:54 -07:00
parent 059873b59f
commit 763b95a253
2 changed files with 13 additions and 0 deletions
+6
View File
@@ -349,6 +349,12 @@ pub async fn create_topic(
if !VALID_TOPOLOGY_KINDS.contains(&topology_kind) {
return Err(ApiError::BadRequest);
}
// Empty-roster gate: a workspace with zero agents cannot host research.
// The frontend already disables the create button; this closes the
// direct-POST hole so we never materialize an unstaffed topic.
if cm_db::repo::agents::count_active(&state.pool, user.workspace_id).await? == 0 {
return Err(ApiError::Conflict);
}
// Ownership check before any writes: every agent must be in the caller's
// workspace. Refuses to leak "agent exists" if it isn't visible.