WIP cleanup: split branch stash into 8 focused commits #2

Merged
osobh merged 8 commits from chore/wip-followups into main 2026-07-06 01:52:14 +00:00
5 changed files with 39 additions and 0 deletions
Showing only changes of commit d2b1f0569e - Show all commits
+30
View File
@@ -13,6 +13,10 @@ use crate::{ApiError, AppState, Authed};
pub struct Quota { pub struct Quota {
pub max_agents: i64, pub max_agents: i64,
pub max_live_containers: i64, pub max_live_containers: i64,
/// Ceiling on `queued` + `running` topology runs at once. Prevents one
/// workspace flooding the shared queue (a single team run also spawns a
/// tier-tree of children, so the practical cap grows with the topology).
pub max_active_runs: i64,
} }
/// Per-plan limits. Unknown plans fall back to the free tier. /// Per-plan limits. Unknown plans fall back to the free tier.
@@ -21,14 +25,17 @@ pub fn plan_quota(plan: &str) -> Quota {
"team" => Quota { "team" => Quota {
max_agents: 50, max_agents: 50,
max_live_containers: 50, max_live_containers: 50,
max_active_runs: 100,
}, },
"pro" => Quota { "pro" => Quota {
max_agents: 20, max_agents: 20,
max_live_containers: 20, max_live_containers: 20,
max_active_runs: 25,
}, },
_ => Quota { _ => Quota {
max_agents: 3, max_agents: 3,
max_live_containers: 3, max_live_containers: 3,
max_active_runs: 5,
}, },
} }
} }
@@ -61,6 +68,23 @@ pub async fn enforce_new_agent(
Ok(()) Ok(())
} }
/// Reject enqueueing another topology run if the workspace is at its plan cap.
pub async fn enforce_new_run(
state: &AppState,
workspace_id: WorkspaceId,
) -> Result<(), ApiError> {
let plan = plan_of(state, workspace_id).await?;
let quota = plan_quota(&plan);
let used = cm_db::repo::topology_runs::count_active(&state.pool, workspace_id).await?;
if used >= quota.max_active_runs {
return Err(ApiError::Quota(format!(
"active-run limit reached ({} on the {plan} plan) — wait for a run to finish or upgrade",
quota.max_active_runs
)));
}
Ok(())
}
/// Reject spinning up another container if the workspace is at its plan cap. /// Reject spinning up another container if the workspace is at its plan cap.
pub async fn enforce_new_container( pub async fn enforce_new_container(
state: &AppState, state: &AppState,
@@ -86,6 +110,8 @@ pub struct QuotaUsage {
max_agents: i64, max_agents: i64,
containers_used: i64, containers_used: i64,
max_live_containers: i64, max_live_containers: i64,
active_runs: i64,
max_active_runs: i64,
} }
/// `GET /api/quota` — the caller's workspace usage + limits (for the UI). /// `GET /api/quota` — the caller's workspace usage + limits (for the UI).
@@ -98,11 +124,15 @@ pub async fn get_quota(
let agents_used = cm_db::repo::agents::count_active(&state.pool, user.workspace_id).await?; let agents_used = cm_db::repo::agents::count_active(&state.pool, user.workspace_id).await?;
let containers_used = let containers_used =
cm_db::repo::agent_containers::count_for_workspace(&state.pool, user.workspace_id).await?; cm_db::repo::agent_containers::count_for_workspace(&state.pool, user.workspace_id).await?;
let active_runs =
cm_db::repo::topology_runs::count_active(&state.pool, user.workspace_id).await?;
Ok(Json(QuotaUsage { Ok(Json(QuotaUsage {
plan, plan,
agents_used, agents_used,
max_agents: quota.max_agents, max_agents: quota.max_agents,
containers_used, containers_used,
max_live_containers: quota.max_live_containers, max_live_containers: quota.max_live_containers,
active_runs,
max_active_runs: quota.max_active_runs,
})) }))
} }
+1
View File
@@ -266,6 +266,7 @@ pub async fn run_company(
Json(body): Json<RunCompanyRequest>, Json(body): Json<RunCompanyRequest>,
) -> Result<(StatusCode, Json<RunAccepted>), ApiError> { ) -> Result<(StatusCode, Json<RunAccepted>), ApiError> {
let company = cm_db::repo::companies::get(&state.pool, id, user.workspace_id).await?; let company = cm_db::repo::companies::get(&state.pool, id, user.workspace_id).await?;
crate::quota::enforce_new_run(&state, user.workspace_id).await?;
let run_id = Uuid::now_v7(); let run_id = Uuid::now_v7();
cm_db::repo::topology_runs::enqueue_run_tier( cm_db::repo::topology_runs::enqueue_run_tier(
&state.pool, &state.pool,
+1
View File
@@ -209,6 +209,7 @@ pub async fn run_org(
Json(body): Json<RunOrgRequest>, Json(body): Json<RunOrgRequest>,
) -> Result<(StatusCode, Json<RunAccepted>), ApiError> { ) -> Result<(StatusCode, Json<RunAccepted>), ApiError> {
let org = cm_db::repo::orgs::get(&state.pool, id, user.workspace_id).await?; let org = cm_db::repo::orgs::get(&state.pool, id, user.workspace_id).await?;
crate::quota::enforce_new_run(&state, user.workspace_id).await?;
let run_id = Uuid::now_v7(); let run_id = Uuid::now_v7();
cm_db::repo::topology_runs::enqueue_run_tier( cm_db::repo::topology_runs::enqueue_run_tier(
&state.pool, &state.pool,
+1
View File
@@ -371,6 +371,7 @@ pub async fn run_team(
Json(body): Json<RunTeamRequest>, Json(body): Json<RunTeamRequest>,
) -> Result<(StatusCode, Json<RunAccepted>), ApiError> { ) -> Result<(StatusCode, Json<RunAccepted>), ApiError> {
let team = cm_db::repo::teams::get_team(&state.pool, id, user.workspace_id).await?; let team = cm_db::repo::teams::get_team(&state.pool, id, user.workspace_id).await?;
crate::quota::enforce_new_run(&state, user.workspace_id).await?;
let run_id = Uuid::now_v7(); let run_id = Uuid::now_v7();
cm_db::repo::topology_runs::enqueue_run( cm_db::repo::topology_runs::enqueue_run(
&state.pool, &state.pool,
+6
View File
@@ -99,6 +99,12 @@ pub async fn trigger_hook(
.filter(|t| !t.trim().is_empty()) .filter(|t| !t.trim().is_empty())
.or_else(|| (!default_task.trim().is_empty()).then_some(default_task)) .or_else(|| (!default_task.trim().is_empty()).then_some(default_task))
.unwrap_or_else(|| "webhook trigger".to_string()); .unwrap_or_else(|| "webhook trigger".to_string());
// Webhooks are unauthenticated public endpoints — the enforce_new_run
// guard is what stops a leaked token from being weaponized into a queue
// flood. 429 (not 402) so external callers can back off.
if crate::quota::enforce_new_run(&state, ws).await.is_err() {
return StatusCode::TOO_MANY_REQUESTS;
}
let run_id = Uuid::now_v7(); let run_id = Uuid::now_v7();
if cm_db::repo::topology_runs::enqueue_run(&state.pool, run_id, ws, &task, &team.graph) if cm_db::repo::topology_runs::enqueue_run(&state.pool, run_id, ws, &task, &team.graph)
.await .await