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>
The *.preview.vibnai.com wildcard cert is issued via DNS-01 and stored
under the letsencrypt-dns resolver. Using letsencrypt (HTTP challenge)
would cause Traefik to attempt individual per-subdomain certs instead
of using the existing wildcard.
Made-with: Cursor
\` is not a valid escape sequence in YAML double-quoted strings,
causing Coolify to fail parsing the dev-container compose on provision.
Backticks are literal characters in YAML double-quoted strings and
need no escaping — only the JS template literal escape (\`) is required.
Made-with: Cursor
Five focused improvements rolled into one deploy:
1. Pre-allocated preview ports + Traefik labels.
Bake docker labels for ports 3000-3009 into every dev-container
compose at ensureDevContainer() time. Each port has its own
subdomain: preview-<slot>-<projectSlug>-<token>.preview.vibnai.com.
Token is derived from projectId so URLs are stable across restarts
but not enumerable across projects. Joins the coolify external
network so Traefik can reach the container.
This avoids the runtime compose-mutation approach (which would
have required a Coolify redeploy on every dev_server.start, ~30s
latency). The trade-off is a hard cap of 10 concurrent dev servers
per project — fine for the "frontend + API" scenario, the only one
we can practically envision.
Wildcard DNS + Traefik DNS-01 cert remain a manual one-time setup
(see vibn-dev/PREVIEWS.md).
2. dev_server.start: port-collision handling.
Detect listeners via `ss` + `lsof` before launching. Three outcomes:
- port out of slot range → PortOutOfRangeError → 400 with allowedRange
- port owned by a different process → PortBusyError → 409
- port owned by a tracked vibn dev server (same project) → kill
the stale row and reuse the slot (most-recent-write-wins; matches
AI mental model when it does an edit-restart loop)
Surfaced via dedicated MCP error codes so the AI can recover
intelligently instead of looping the same start call.
3. gitea_file_{read,write,delete}: hard-removed from AI tool list.
These tools competed with fs.* and tempted the AI into the slow
path. Pulled from VIBN_TOOL_DEFINITIONS but kept in the MCP
dispatcher for 30 days for any external clients still using them.
System prompt rewritten to make Path B the only documented way to
author code; gitea_repo_* + gitea_branches_* remain because they
handle one-time orchestration with no fs.* equivalent.
4. System prompt: HMR + preview-port discipline.
New section covering Vite HMR (clientPort:443 wss), Next dev
(-H 0.0.0.0), and Express (HOST=0.0.0.0). Explicit "ports must be
3000-3009" rule + "if PORT_BUSY don't blindly retry" guidance.
5. Cron docs (vibn-dev/CRON.md).
/etc/cron.d/vibn-path-b template + smoke commands for autosave
and idle-sweep. Wires both 5-minute jobs that already have admin
endpoints (POST /api/admin/path-b/{autosave,idle-sweep}).
MCP version bump 2.6.0 -> 2.7.0. Smoke test: 65 tool defs (down from
68 after gitea_file_* removal), all accepted by Gemini.
Made-with: Cursor
Two bugs caught by the live end-to-end test:
1. Tool dispatch mismatch.
Gemini tool name "dev_server_list" runs through executeMcpTool's
_-to-. converter (toolName.replace(/_/g, '.')) and arrives as
"dev.server.list". The dispatcher only had cases for "dev_server.list",
so all four dev_server.* tools 404'd as "Unknown tool".
The AI gracefully fell back to shell.exec + nohup, so Express still
ran — but the dev_servers table never got populated and the preview
URL machinery was bypassed. Add aliases for both underscore and
fully-dotted forms.
2. State machine never transitioned.
ensureDevContainer wrote state='provisioning'; nothing ever flipped
it to 'running'. As a result the idle-sweep (which filters by
state='running') never saw a candidate to suspend.
Use the first successful exec as the authoritative liveness signal:
touchActivity() now also flips provisioning|suspended → running and
clears suspended_at.
Surfaced by the live trace: AI tried dev_server_list, got 404, fell
back to manually grepping the process table.
Made-with: Cursor
Completes the rest of the Path B tool surface:
- dev_server.{start,stop,list,logs}: nohup processes inside the dev
container, track PID/port/preview-url in fs_dev_servers. Each gets
a randomized preview subdomain (preview.vibnai.com base; Traefik
wildcard wiring is staged in /vibn-dev/PREVIEWS.md but the Coolify
compose hot-update step is deferred — see file for the recommended
pre-allocated-port-range approach).
- ship: git init (if needed) -> add/commit/push to the project's
Gitea repo via the workspace bot PAT, then triggers a Coolify
production deploy if the project is linked to one. Returns push
output + deployment_uuid.
- /api/admin/path-b/autosave [POST { projectId | sweep:true }]:
force-pushes /workspace to vibn-autosave/main in Gitea. Throttled
to once per 5 min per project. Records every push in fs_dev_autosaves
for audit. Treat Gitea as canonical, container disk as ephemeral.
- /api/admin/path-b/idle-sweep [POST?minutes=30]: suspends every
running dev container whose last_active_at is older than `minutes`.
Wire to a 5-min cron. Idempotent.
- Compose template hardened: pull_policy: never (use locally-built
image, no registry round-trip) + per-project bridge network
(vibn-dev-net-<slug>) so dev containers can't reach internal Vibn
services.
- vibn-dev/setup-on-coolify.sh: one-shot script to build vibn-dev:latest
on the Coolify host. Run before first chat session uses Path B.
- vibn-tools.ts: dev_server_{start,stop,list,logs} + ship Gemini tool
defs added. Smoke test passes — 68 tool definitions accepted.
- MCP version 2.5.0 -> 2.6.0 so /api/mcp tells us when the new build
is live.
Plan doc updated to reflect what shipped vs what's still manual
(DNS wildcard, Traefik cert, build-on-host script run, gitea_file_*
hard-remove deferred to allow A/B).
Made-with: Cursor
Kicks off Path B (AI_PATH_B_EXECUTION_PLAN.md): each Vibn project gets
its own vibn-dev Coolify service that the AI drives directly via shell
and filesystem tools. Sub-second iteration vs the 5-min Gitea redeploy
loop.
What's in this commit (week 1, slice 1):
- vibn-dev Dockerfile: small Ubuntu base (~500 MB target). git, ripgrep,
python3, mise. Language toolchains lazy-install on first use.
- lib/dev-container.ts: ensureDevContainer / suspend / resume /
execInDevContainer. Backed by a new fs_project_dev_containers table.
- lib/feature-flags.ts + /api/admin/path-b/{disable,enable}: kill switch.
Bearer NEXTAUTH_SECRET flips path_b_disabled, propagates in ~10s.
- New MCP tools wired into /api/mcp: devcontainer.{ensure,status,suspend},
shell.exec, fs.{read,write,edit,list,delete,glob,grep}. All enforce
workspace isolation via fs_projects ownership check.
- vibn-tools.ts: 11 new Gemini tool defs (smoke test passes, 63 total).
- chat system prompt: shell-first guidance; gitea_file_* marked
deprecated for iterative work (still available, removed week 3).
Safety nets baked in:
- pathBGuard() returns 503 from every Path B tool when the kill switch
flips
- fs.* paths locked to /workspace
- ensureResourceInWorkspaceProjects via fs_project_dev_containers PK
- per-project resource limits (1 vCPU, 1 GiB RAM) on the compose spec
Still pending (queued):
- dev_server.* (preview URLs through Traefik)
- ship tool (push to Gitea + trigger prod deploy)
- auto-push autosave to vibn-autosave/main every 5 min
- idle-suspend cron after 30 min inactivity
- HMR-through-Traefik spike
- eval harness
Made-with: Cursor