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 max_agents: 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.
@@ -21,14 +25,17 @@ pub fn plan_quota(plan: &str) -> Quota {
"team" => Quota {
max_agents: 50,
max_live_containers: 50,
max_active_runs: 100,
},
"pro" => Quota {
max_agents: 20,
max_live_containers: 20,
max_active_runs: 25,
},
_ => Quota {
max_agents: 3,
max_live_containers: 3,
max_active_runs: 5,
},
}
}
@@ -61,6 +68,23 @@ pub async fn enforce_new_agent(
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.
pub async fn enforce_new_container(
state: &AppState,
@@ -86,6 +110,8 @@ pub struct QuotaUsage {
max_agents: i64,
containers_used: i64,
max_live_containers: i64,
active_runs: i64,
max_active_runs: i64,
}
/// `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 containers_used =
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 {
plan,
agents_used,
max_agents: quota.max_agents,
containers_used,
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>,
) -> 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,
+1
View File
@@ -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,
+1
View File
@@ -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,
+6
View File
@@ -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