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
Adds end-to-end custom apex domain support: workspace-scoped
registration via OpenSRS (Tucows), authoritative DNS via Google
Cloud DNS, and one-call attach that wires registrar nameservers,
DNS records, and Coolify app routing in a single transactional
flow.
Schema (additive, idempotent — run /api/admin/migrate after deploy)
- vibn_workspaces.dns_provider TEXT DEFAULT 'cloud_dns'
Per-workspace DNS backend choice. Future: 'cira_dzone' for
strict CA-only residency on .ca.
- vibn_domains
One row per registered/intended apex. Tracks status
(pending|active|failed|expired), registrar order id, encrypted
registrar manage-user creds (AES-256-GCM, VIBN_SECRETS_KEY),
period, dates, dns_provider/zone_id/nameservers, and a
created_by audit field.
- vibn_domain_events
Append-only lifecycle audit (register.attempt/success/fail,
attach.success, ns.update, lock.toggle, etc).
- vibn_billing_ledger
Workspace-scoped money ledger (CAD by default) with
ref_type/ref_id back to the originating row.
OpenSRS XML client (lib/opensrs.ts)
- Mode-gated host/key (OPENSRS_MODE=test → horizon sandbox,
rejectUnauthorized:false; live → rr-n1-tor, strict TLS).
- MD5 double-hash signature.
- Pure Node https module (no undici dep).
- Verbs: lookupDomain, getDomainPrice, checkDomain, registerDomain,
updateDomainNameservers, setDomainLock, getResellerBalance.
- TLD policy: minPeriodFor() bumps .ai to 2y; CPR/legalType
plumbed through for .ca; registrations default to UNLOCKED so
immediate NS updates succeed without a lock toggle.
DNS provider abstraction (lib/dns/{provider,cloud-dns}.ts)
- DnsProvider interface (createZone/getZone/setRecords/deleteZone)
so the workspace residency knob can swap backends later.
- cloudDnsProvider implementation against Google Cloud DNS using
the existing vibn-workspace-provisioner SA (roles/dns.admin).
- Idempotent zone creation, additions+deletions diff for rrsets.
Shared GCP auth (lib/gcp-auth.ts)
- Single getGcpAccessToken() helper used by Cloud DNS today and
future GCP integrations. Prefers GOOGLE_SERVICE_ACCOUNT_KEY_B64,
falls back to ADC.
Workspace-scoped helpers (lib/domains.ts)
- listDomainsForWorkspace, getDomainForWorkspace, createDomainIntent,
markDomainRegistered, markDomainFailed, markDomainAttached,
recordDomainEvent, recordLedgerEntry.
Attach orchestrator (lib/domain-attach.ts)
Single function attachDomain() reused by REST + MCP. For one
apex it:
1. Resolves target → Coolify app uuid OR raw IP OR CNAME.
2. Ensures Cloud DNS managed zone exists.
3. Writes A / CNAME records (apex + requested subdomains).
4. Updates registrar nameservers, with auto unlock-retry-relock
fallback for TLDs that reject NS changes while locked.
5. PATCHes the Coolify application's domain list so Traefik
routes the new hostname.
6. Persists dns_provider/zone_id/nameservers and emits an
attach.success domain_event.
AttachError carries a stable .tag + http status so the caller
can map registrar/dns/coolify failures cleanly.
REST endpoints
- POST /api/workspaces/[slug]/domains/search
- GET /api/workspaces/[slug]/domains
- POST /api/workspaces/[slug]/domains
- GET /api/workspaces/[slug]/domains/[domain]
- POST /api/workspaces/[slug]/domains/[domain]/attach
All routes go through requireWorkspacePrincipal (session OR
Authorization: Bearer vibn_sk_...). Register is idempotent:
re-issuing for an existing intent re-attempts at OpenSRS without
duplicating the row or charging twice.
MCP bridge (app/api/mcp/route.ts → version 2.2.0)
Adds five tools backed by the same library code:
- domains.search (batch availability + pricing)
- domains.list (workspace-owned)
- domains.get (single + recent events)
- domains.register (idempotent OpenSRS register)
- domains.attach (full Cloud DNS + registrar + Coolify)
Sandbox smoke tests (scripts/smoke-opensrs-*.ts)
Standalone Node scripts validating each new opensrs.ts call against
horizon.opensrs.net: balance + lookup + check, TLD policy
(.ca/.ai/.io/.com), full register flow, NS update with systemdns
nameservers, and the lock/unlock toggle that backs the attach
fallback path.
Post-deploy checklist
1. POST https://vibnai.com/api/admin/migrate
-H "x-admin-secret: $ADMIN_MIGRATE_SECRET"
2. Set OPENSRS_* env vars on the vibn-frontend Coolify app
(RESELLER_USERNAME, API_KEY_LIVE, API_KEY_TEST, HOST_LIVE,
HOST_TEST, PORT, MODE). Without them, only domains.list/get
work; search/register/attach return 500.
3. GCP_PROJECT_ID is read from env or defaults to master-ai-484822.
4. Live attach end-to-end against a real apex is queued as a
follow-up — sandbox path is fully proven.
Not in this commit (deliberate)
- The 100+ unrelated in-flight files (mvp-setup wizard, justine
homepage rework, BuildLivePlanPanel, etc) — kept local to keep
blast radius minimal.
Made-with: Cursor
Workspace-owned deploy infra so AI agents can create and destroy
Coolify resources without ever touching the root admin token.
vibn_workspaces
+ coolify_server_uuid, coolify_destination_uuid
+ coolify_environment_name (default "production")
+ coolify_private_key_uuid, gitea_bot_ssh_key_id
ensureWorkspaceProvisioned
+ generates an ed25519 keypair per workspace
+ pushes pubkey to the Gitea bot user (read/write scoped by team)
+ registers privkey in Coolify as a reusable deploy key
New endpoints under /api/workspaces/[slug]/
apps/ POST (private-deploy-key from Gitea repo)
apps/[uuid] PATCH, DELETE?confirm=<name>
apps/[uuid]/domains GET, PATCH (policy: *.{ws}.vibnai.com only)
databases/ GET, POST (8 types incl. postgres, clickhouse, dragonfly)
databases/[uuid] GET, PATCH, DELETE?confirm=<name>
auth/ GET, POST (Pocketbase, Authentik, Keycloak, Pocket-ID, Logto, Supertokens)
auth/[uuid] DELETE?confirm=<name>
MCP (/api/mcp) gains 15 new tools that mirror the REST surface and
enforce the same workspace tenancy + delete-confirm guard.
Safety: destructive ops require ?confirm=<exact-resource-name>; volumes
are kept by default (pass delete_volumes=true to drop).
Made-with: Cursor
Ship Phases 1–3 of the multi-tenant AI access plan so an AI agent can
act on a Vibn workspace with one bearer token and zero admin reach.
Phase 1 — Gitea bot per workspace
- Add gitea_bot_username / gitea_bot_user_id / gitea_bot_token_encrypted
columns to vibn_workspaces (migrate route).
- New lib/auth/secret-box.ts (AES-256-GCM, VIBN_SECRETS_KEY) for PAT at rest.
- Extend lib/gitea.ts with createUser, createAccessTokenFor (Sudo PAT),
createOrgTeam, addOrgTeamMember, ensureOrgTeamMembership.
- ensureWorkspaceProvisioned now mints a vibn-bot-<slug> user, adds it to
a Writers team (write perms only) on the workspace's org, and stores
its PAT encrypted.
- GET /api/workspaces/[slug]/gitea-credentials returns a workspace-scoped
bot PAT + clone URL template; session or vibn_sk_ bearer auth.
Phase 2 — Tenant-safe Coolify proxy + real MCP
- lib/coolify.ts: projectUuidOf, listApplicationsInProject,
getApplicationInProject, TenantError, env CRUD, deployments list.
- Workspace-scoped REST endpoints (all filtered by coolify_project_uuid):
GET/POST /api/workspaces/[slug]/apps/[uuid](/deploy|/envs|/deployments),
GET /api/workspaces/[slug]/deployments/[deploymentUuid]/logs.
- Full rewrite of /api/mcp off legacy Firebase onto Postgres vibn_sk_
keys, exposing workspace.describe, gitea.credentials, projects.*,
apps.* (list/get/deploy/deployments, envs.list/upsert/delete).
Phase 3 — Settings UI AI bundle
- GET /api/workspaces/[slug]/bootstrap.sh: curl|sh installer that writes
.cursor/rules, .cursor/mcp.json and appends VIBN_* to .env.local.
Embeds the caller's vibn_sk_ token when invoked with bearer auth.
- WorkspaceKeysPanel: single AiAccessBundleCard with system-prompt block,
one-line bootstrap, Reveal-bot-PAT button, collapsible manual-setup
fallback. Minted-key modal also shows the bootstrap one-liner.
Ops prerequisites:
- Set VIBN_SECRETS_KEY (>=16 chars) on the frontend.
- Run /api/admin/migrate to add the three bot columns.
- GITEA_API_TOKEN must be a site-admin token (needed for admin/users
+ Sudo PAT mint); otherwise provision_status lands on 'partial'.
Made-with: Cursor
Adds logical multi-tenancy on top of Coolify + Gitea so every Vibn
account gets its own isolated tenant boundary, and exposes that
boundary to AI agents (Cursor, Claude Code, scripts) through
per-workspace bearer tokens.
Schema (additive, idempotent — run /api/admin/migrate once after deploy)
- vibn_workspaces: slug, name, owner, coolify_project_uuid,
coolify_team_id (reserved for when Coolify ships POST /teams),
gitea_org, provision_status
- vibn_workspace_members: room for multi-user workspaces later
- vibn_workspace_api_keys: sha256-hashed bearer tokens
- fs_projects.vibn_workspace_id: nullable FK linking projects
to their workspace
Provisioning
- On first sign-in, ensureWorkspaceForUser() inserts the row
(no network calls — keeps signin fast).
- On first project create, ensureWorkspaceProvisioned() lazily
creates a Coolify Project (vibn-ws-{slug}) and a Gitea org
(vibn-{slug}). Failures are recorded on the row, not thrown,
and POST /api/workspaces/{slug}/provision retries.
Auth surface
- lib/auth/workspace-auth.ts: requireWorkspacePrincipal() accepts
either a NextAuth session or "Authorization: Bearer vibn_sk_...".
The bearer key is hard-pinned to one workspace — it cannot
reach any other tenant.
- mintWorkspaceApiKey / listWorkspaceApiKeys / revokeWorkspaceApiKey
Routes
- GET /api/workspaces list
- GET /api/workspaces/[slug] details
- POST /api/workspaces/[slug]/provision retry provisioning
- GET /api/workspaces/[slug]/keys list keys
- POST /api/workspaces/[slug]/keys mint key (token shown once)
- DELETE /api/workspaces/[slug]/keys/[keyId] revoke
UI
- components/workspace/WorkspaceKeysPanel.tsx: identity card,
keys CRUD with one-time secret reveal, and a "Connect Cursor"
block with copy/download for:
.cursor/rules/vibn-workspace.mdc — rule telling the agent
about the API + workspace IDs + house rules
~/.cursor/mcp.json — MCP server registration with key
embedded (server URL is /api/mcp; HTTP MCP route lands next)
.env.local — VIBN_API_KEY + smoke-test curl
- Slotted into existing /[workspace]/settings between Workspace
and Notifications cards (no other layout changes).
projects/create
- Resolves the user's workspace (creating + provisioning lazily).
- Repos go under workspace.gitea_org (falls back to GITEA_ADMIN_USER
for backwards compat).
- Coolify services are created inside workspace.coolify_project_uuid
(renamed {slug}-{appName} to stay unique within the namespace) —
no more per-Vibn-project Coolify Project sprawl.
- Stamps vibn_workspace_id on fs_projects.
lib/gitea
- createOrg, getOrg, addOrgOwner, getUser
- createRepo now routes /orgs/{owner}/repos when owner != admin
Also includes prior-turn auth hardening that was already in
authOptions.ts (CredentialsProvider for dev-local, isLocalNextAuth
cookie config) bundled in to keep the auth layer in one consistent
state.
.env.example
- Documents GITEA_API_URL / GITEA_API_TOKEN / GITEA_ADMIN_USER /
GITEA_WEBHOOK_SECRET and COOLIFY_URL / COOLIFY_API_TOKEN /
COOLIFY_SERVER_UUID, with the canonical hostnames
(git.vibnai.com, coolify.vibnai.com).
Post-deploy
- Run once: curl -X POST https://vibnai.com/api/admin/migrate \\
-H "x-admin-secret: \$ADMIN_MIGRATE_SECRET"
- Existing users get a workspace row on next sign-in.
- Existing fs_projects keep working (legacy gitea owner + their
own per-project Coolify Projects); new projects use the
workspace-scoped path.
Not in this commit (follow-ups)
- Wiring requireWorkspacePrincipal into the rest of /api/projects/*
so API keys can drive existing routes
- HTTP MCP server at /api/mcp (the mcp.json snippet already
points at the right URL — no client re-setup when it lands)
- Backfill script to assign legacy fs_projects to a workspace
Made-with: Cursor