feat(quotas): per-workspace soft caps + AI recovery rule
Soft caps on the two resources a bad-actor signup could pump fastest:
- 3 active projects per workspace
- 3 active (running/provisioning) dev containers per workspace
Suspended dev containers don't count (they're free), so a power
user can have many projects with most containers idle. Limits are
overridable via env vars (VIBN_QUOTA_MAX_*) for a global lift.
Hits surface as HTTP 402 with structured payload {error, code,
current, limit}. AI's error-recovery middleware matches the
QUOTA_EXCEEDED code and synthesizes guidance: tell the user which
cap was hit, offer to suspend something or contact support, do NOT
retry blindly.
Wired:
- lib/quotas.ts — assertProjectQuota,
assertDevContainerQuota,
getQuotaStatus
- app/api/projects/create/route.ts — checks before create
- lib/dev-container.ts — checks before resume +
net-new ensure
- app/api/mcp/route.ts — devcontainer.ensure
translates QuotaExceededError
to 402
- lib/ai/error-recovery.ts — workspace-quota-exceeded rule
Closes BETA_LAUNCH_PLAN.md task 4.6.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -60,6 +60,7 @@ import {
|
||||
getSentryIssueDetail,
|
||||
resolveSentryIssue,
|
||||
} from '@/lib/integrations/sentry';
|
||||
import { QuotaExceededError } from '@/lib/quotas';
|
||||
import {
|
||||
composeUp,
|
||||
composePs,
|
||||
@@ -3461,6 +3462,14 @@ async function toolDevContainerEnsure(principal: Principal, params: Record<strin
|
||||
});
|
||||
return NextResponse.json({ result: r });
|
||||
} catch (err) {
|
||||
// Quota exceeded → 402 with structured payload so the AI's
|
||||
// tool-error recovery middleware can spot it (see error-recovery.ts).
|
||||
if (err instanceof QuotaExceededError) {
|
||||
return NextResponse.json(
|
||||
{ error: err.message, code: err.code, current: err.current, limit: err.limit },
|
||||
{ status: 402 },
|
||||
);
|
||||
}
|
||||
return NextResponse.json(
|
||||
{ error: err instanceof Error ? err.message : String(err) },
|
||||
{ status: 500 },
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createRepo, createWebhook, getRepo, listWebhooks, GITEA_ADMIN_USER_EXPO
|
||||
import { getOrCreateProvisionedWorkspace } from '@/lib/workspaces';
|
||||
import { ensureProjectCoolifyProject } from '@/lib/projects';
|
||||
import { ensureSentryProject } from '@/lib/integrations/sentry';
|
||||
import { assertProjectQuota, QuotaExceededError } from '@/lib/quotas';
|
||||
import { loadGithubIntegration } from '@/lib/integrations/github';
|
||||
import type { ProjectPhaseData, ProjectPhaseScores } from '@/lib/types/project-artifacts';
|
||||
|
||||
@@ -91,6 +92,20 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: 'Project slug already exists' }, { status: 400 });
|
||||
}
|
||||
|
||||
// Per-workspace project quota — soft cap at VIBN_QUOTA_MAX_PROJECTS_PER_WORKSPACE
|
||||
// (default 3) to bound runaway resource cost from bad-actor signups.
|
||||
try {
|
||||
await assertProjectQuota({ id: vibnWorkspace.id, slug: workspace });
|
||||
} catch (qe) {
|
||||
if (qe instanceof QuotaExceededError) {
|
||||
return NextResponse.json(
|
||||
{ error: qe.message, code: qe.code, current: qe.current, limit: qe.limit },
|
||||
{ status: 402 }, // 402 Payment Required — semantically "you've hit a tier limit".
|
||||
);
|
||||
}
|
||||
throw qe;
|
||||
}
|
||||
|
||||
const projectId = randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user