cm-api: quota — enforce max_active_runs on run enqueue
Adds a Quota.max_active_runs ceiling (queued + running topology runs at once, per workspace) to stop a single workspace flooding the shared queue. Free tier: 10, Pro: 25, Team: 100. Enforced at every /run enqueue site: run_org, run_company, run_team, and the webhook trigger. Webhooks return 429 rather than 402 so external callers can back off — the guard is what stops a leaked webhook token from being weaponized into a queue flood. A single team run also spawns a tier-tree of children, so the practical cap grows with the topology — this counts the outer runs, not every step.
This commit is contained in:
@@ -266,6 +266,7 @@ pub async fn run_company(
|
||||
Json(body): Json<RunCompanyRequest>,
|
||||
) -> Result<(StatusCode, Json<RunAccepted>), ApiError> {
|
||||
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();
|
||||
cm_db::repo::topology_runs::enqueue_run_tier(
|
||||
&state.pool,
|
||||
|
||||
@@ -209,6 +209,7 @@ pub async fn run_org(
|
||||
Json(body): Json<RunOrgRequest>,
|
||||
) -> Result<(StatusCode, Json<RunAccepted>), ApiError> {
|
||||
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();
|
||||
cm_db::repo::topology_runs::enqueue_run_tier(
|
||||
&state.pool,
|
||||
|
||||
@@ -371,6 +371,7 @@ pub async fn run_team(
|
||||
Json(body): Json<RunTeamRequest>,
|
||||
) -> Result<(StatusCode, Json<RunAccepted>), ApiError> {
|
||||
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();
|
||||
cm_db::repo::topology_runs::enqueue_run(
|
||||
&state.pool,
|
||||
|
||||
@@ -99,6 +99,12 @@ pub async fn trigger_hook(
|
||||
.filter(|t| !t.trim().is_empty())
|
||||
.or_else(|| (!default_task.trim().is_empty()).then_some(default_task))
|
||||
.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();
|
||||
if cm_db::repo::topology_runs::enqueue_run(&state.pool, run_id, ws, &task, &team.graph)
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user