Commit Graph

87 Commits

Author SHA1 Message Date
b6eaa85733 fix(tenancy): stop leaking workspace-level Coolify services across projects
CRITICAL: every Vibn project was rendering every other project's
services in the same workspace (Twenty CRM, n8n, all databases,
all secrets). Tenancy was effectively broken — cross-project data
exposure inside a workspace.

Root cause:
  - Coolify's POST /projects validates `description` against a strict
    allowlist (letters, numbers, spaces, and `- _ . , ! ? ( ) ' " + = * / @ &`).
  - Our description "Vibn project: <name> (workspace: <slug>)" contains
    two colons. Every project-create on Coolify returned 422.
  - lib/projects.ts caught that 422 and fell back to
    `workspace.coolify_project_uuid` so deploys "weren't blocked."
  - That UUID is shared by every Vibn project in the workspace, so
    listServicesInProject(coolifyProjectUuid) returned the union of
    all projects' services, applications, and databases for any
    project in the workspace. The Product, Hosting, and Infrastructure
    tabs all rendered cross-tenant data as if it were the current
    project's.

Fixes (defense in depth — fix at every layer):

  1. lib/coolify.ts createProject(): sanitize the description against
     Coolify's allowlist at the boundary so no caller can ever ship
     a description that 422s. Replaces disallowed chars with `-`,
     collapses runs, caps at 255 chars.

  2. lib/projects.ts ensureProjectCoolifyProject():
     - Pre-sanitize the description we pass (belt + suspenders).
     - Detect when `stored === workspace.coolify_project_uuid` (the
       legacy bad state) and re-provision a dedicated project.
     - REMOVE the workspace-UUID fallback on create failure. A 422
       now leaves coolifyProjectUuid null and the UI shows an empty
       state, which is correct: better to surface "no resources" than
       to lie about which project owns what.
     - Export sanitizeCoolifyDescription helper for reuse.

  3. /api/projects/[projectId]/anatomy/route.ts: SELF-HEAL on every
     read. If the project's stored Coolify UUID matches the
     workspace's UUID, we treat it as missing, re-provision a
     dedicated Coolify project on the fly (idempotent — reuses the
     existing one if found by name), persist the new UUID, and
     continue serving with the corrected scope. If provisioning
     fails we fall back to undefined, NOT the workspace UUID, so
     no cross-tenant data ever surfaces again.

The self-heal means existing already-broken projects will fix
themselves on the next page load — no manual data migration needed.

Made-with: Cursor
2026-04-29 17:16:33 -07:00
90bed6ab31 feat(github): OAuth integration + repo picker for Import flow
User can now click "Connect GitHub" inside the Import-existing-code
flow, sign in via GitHub, and pick a repo from a searchable list of
their own + collaborator + org repos. Both public and private repos
work — the encrypted access token on the user's account is auto-
attached when the create endpoint runs the agent-runner mirror.

OAuth flow:
  - GET  /api/integrations/github/connect    — generates state, sets
         a 10-min httpOnly cookie, 302s to GitHub authorize.
  - GET  /api/integrations/github/callback   — verifies state,
         exchanges code for token, fetches /user, encrypts the
         token with secret-box (AES-256-GCM, VIBN_SECRETS_KEY) and
         persists it on fs_users.data.integrations.github.
         Bounces back to ?gh_connected=login or ?gh_error=msg.
  - GET  /api/integrations/github/repos      — server-side fetches
         the connected user's repos (per_page=100, sort=pushed,
         affiliation=owner+collaborator+org_member). Returns the
         GitHub login + a stripped repo summary; never the token.
  - POST /api/integrations/github/disconnect — drops the integration
         from fs_users (does NOT revoke on github.com).

Scopes requested: repo, read:user.

Token storage:
  - Encrypted at rest with secret-box (lib/auth/secret-box.ts) using
    VIBN_SECRETS_KEY. Tokens never leave the server.
  - One token per fs_users row, keyed by email.

ImportSetup UI:
  - On mount, fires /repos to detect connection state.
  - If connected: shows a connected-as-@login chip with disconnect
    link, a search-as-you-type repo picker (max 220px scroll, badges
    for Private / language), and a "paste a different URL instead"
    escape hatch.
  - If not connected: shows a Connect GitHub card with a public-URL
    fallback inline.
  - On return from OAuth (?gh_connected=… or ?gh_error=…), surfaces
    a toast and silently refreshes the repo list.
  - Selected repo carries default_branch + repo id into the create
    payload so we can store them on the project for later UI hints.

/api/projects/create:
  - When a githubRepoUrl is mirrored, falls back to the user's
    OAuth-linked token if no PAT is explicitly passed. Means the
    flow "just works" for private repos once GitHub is connected.

Required env (already set in production):
  - GITHUB_CLIENT_ID
  - GITHUB_CLIENT_SECRET

Made-with: Cursor
2026-04-29 16:44:13 -07:00
2260f3c280 fix(db-introspect): scan all non-template databases, not just $POSTGRES_DB
Coolify exposes a single `postgres_db` per database resource (usually
"postgres"), but the cluster typically holds more than one db inside.
Twenty CRM connects to `default`; our prior query connected to
`postgres` and so reported the database as empty even when Twenty had
hundreds of tables.

Fix:
  - pgListDatabases() enumerates every non-template, connectable db in
    the cluster (`SELECT datname FROM pg_database WHERE datistemplate
    = false AND datallowconn = true`).
  - pgListTables() now unions table listings across all of them.
    Schema is stamped as `<db>.<schema>` only when there's more than
    one db, so single-db clusters keep the bare `public` flatten in
    the UI.
  - pgPreviewTable() understands the dotted `db.schema` form and
    routes the preview `psql` invocation to the correct database.
    Identifier whitelist applied to all three components (db, schema,
    table) before splicing into SQL.

Hard caps unchanged (50 tables total, 8s SSH wall-clock).

Made-with: Cursor
2026-04-29 15:36:28 -07:00
7b359e399e feat(infra): collapse to 7 categories + live Postgres table inspection
UX rework after iteration with the user:

  - Drop SMS, Analytics, Search, Monitoring categories from the rail.
    They were detection-only with no first-class UX behind them; surface
    is cleaner without them and they can return when each gets real
    flows (auth-style "edit configurables", payment-style "connect").
  - Storage no longer tries to detect S3/R2/GCS env vars. Instead it
    surfaces the workspace's bundled Vibn-provisioned GCS bucket
    (S3-compatible HMAC), with status, region, access id, and a
    one-shot env snippet for app config.
  - Email category no longer mixes in SMS providers.
  - LLM renamed to "Models"; empty state mentions BYOK as upcoming.
  - Payments empty state has a "Connect Stripe (coming soon)" CTA;
    Stripe detail surfaces the webhook URL guidance.
  - Secrets detail now lists actual env-var key names per resource,
    grouped by detected provider (Stripe block, OpenAI block, etc.)
    with an "Other (project-defined)" catch-all. Each row has Edit +
    Rotate icon buttons (currently disabled with tooltips — wire-up
    to apps.envs.upsert / services.envs.upsert lands in iter 2).

Live database inspection (Postgres only for now):

  - New /api/projects/[id]/databases/[uuid]/tables — auth-scoped, lists
    user-tables across non-system schemas via SSH-exec into the
    database container's psql. Hard caps: 50 tables, 8s timeout, no
    mutating queries possible (only SELECT row_to_json with LIMIT).
  - New /api/projects/[id]/databases/[uuid]/preview — returns first 50
    rows of a single table. Identifiers locked to /[A-Za-z0-9_]+/ so
    splicing them into the SELECT is safe.
  - DatabaseTableTree (lazy-fetch, schema-grouped, public-flat,
    approximate row counts from pg_class.reltuples) and TableViewer
    (sticky-header data grid, zebra rows, per-cell ellipsis at 360px).
  - Fix in lib/coolify.ts: listDatabasesInProject was flattening every
    db endpoint array (postgresqls, redises, mongodbs…) without
    tagging the output rows with the engine. Every consumer was
    seeing type=undefined which then bucketed as "unknown" and
    blocked the table inspector. Now we tag at the flatten step so
    every CoolifyDatabase has a stable type.
  - Infrastructure tab: database tile is now expandable inline like
    Codebases on Product. Auto-expands the first DB; click any table
    to preview rows on the right.

Made-with: Cursor
2026-04-29 15:22:58 -07:00
4184baca77 feat(chat): expose Gemini's reasoning narration as a thinking pill
Today the chat shows ✓-icon tool trays with no narration between
calls — the user has no idea WHY the AI just called fs_edit or
ship. Meanwhile Gemini is producing 500-1000 chars of first-person
reasoning per round ("Updating the Express Server: A Quick
Production Deployment / Right, so we have a basic Express server
here, nothing fancy. I need to get a new version live...") and
billing us for those tokens — we just weren't asking for them.

Three layers:

1. lib/ai/gemini-chat.ts
   - generationConfig.thinkingConfig.includeThoughts = true (default
     true, opt-out via includeThoughts: false). We're already paying
     for thinking tokens regardless of this flag — it just controls
     whether the model returns the human-readable summary or only the
     compressed signature.
   - callGeminiChat now returns { text, thoughts, toolCalls,
     finishReason } and the parser splits parts by `part.thought`.
     CRITICAL bug avoided: previously `if (part.text) text += ...`
     would have lumped thoughts into the chat bubble verbatim.
   - streamGeminiChat yields `{ type: 'thinking' }` for thought parts.

2. app/api/chat/route.ts
   - New SSE event: `data: {"type":"thinking","text":"..."}`
   - Emitted on every round alongside text + tool_start.
   - Recovery-summary branch also emits thoughts so even when the
     model produces no user-facing prose, the user sees the model's
     reasoning instead of dead silence.

3. components/vibn-chat/chat-panel.tsx
   - Message gains optional `thoughts` field (in-memory only — we do
     NOT persist thoughts to fs_chat_messages; they're ephemeral and
     cheap to drop).
   - New ThinkingBubble component: dashed-border italic pill above
     the assistant bubble, collapsed by default to show one-line
     preview, click to expand for full chain. Strips Gemini's
     "**Section Heading**" prefixes from the preview.
   - SSE handler accumulates thinking chunks onto the in-flight
     assistant message.

UX impact: instead of staring at fs.read ✓ fs.edit ✓ ship ✓ icons,
the user sees "Examining the target server file..." → "Shipping the
twenty-crm project..." in real time. Costs zero additional tokens
(we already paid for the thoughts).

Cleanup: removed scripts/probe-gemini-raw.ts and
scripts/probe-recovery-summary.ts — diagnostic scripts that
identified this opportunity, no longer needed in-tree.

Made-with: Cursor
2026-04-28 15:24:49 -07:00
a897d07179 fix(ship): return commitSha + coolifyDeployUrl, prevent verification chain
After "ship" succeeded the AI was burning 7+ follow-up tool calls
(gitea_repos_list, gitea_credentials, shell.exec×4, apps_list) trying
to verify what actually got pushed and where it deployed. That ate
through MAX_TOOL_ROUNDS and the user got tool-icon spam with no
narrative summary.

Three fixes:

1. ship now returns commitSha (parsed from `git rev-parse HEAD`),
   giteaCommitUrl, giteaBranchUrl, coolifyDeployUrl, coolifyAppUuid,
   and a summaryHint string telling the AI exactly what to say next.
2. ship's tool description now explicitly tells Gemini "do NOT call
   gitea_*, shell_exec, or apps_* afterwards to verify — the result
   is authoritative."
3. MAX_TOOL_ROUNDS 12 → 18 as a safety net for genuinely long chains.

Net effect: ship goes from ~12 tool calls to verify a deploy down to
1 (just ship itself), and the next text turn has the SHA + URL
inline.

Made-with: Cursor
2026-04-28 14:46:18 -07:00
e0844b5f2e feat(path-b): preview-port slots, port-collision, gitea_file_* deprecation
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
2026-04-28 14:39:59 -07:00
fb31d111ef fix(path-b): dev_server tool dispatch + state-machine transition
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
2026-04-28 13:57:44 -07:00
41d4d3748f feat(path-b): dev_server.*, ship, autosave, idle-suspend (weeks 2-3)
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
2026-04-28 13:02:35 -07:00
4ba9407534 feat(path-b): persistent dev containers + shell.exec + fs.* tools
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
2026-04-28 12:53:16 -07:00
c8dec7c656 feat(mcp): add gitea_* tools so the AI can write code, not just deploy it
Closes the AI's self-reported gap: "I cannot directly commit or push code".

New MCP capabilities (8) — all scoped to the workspace's Gitea org via
requireGiteaOrg + ensureRepoOwnerInOrg:

- gitea.repos.list           — discover existing repos
- gitea.repo.get             — metadata (default branch, clone URL)
- gitea.repo.create          — mint a new private repo with auto-init
- gitea.file.read            — read a file (or list a directory)
- gitea.file.write           — create/update one file in one commit
- gitea.file.delete          — delete a file (auto-resolves sha)
- gitea.branches.list        — list branches with head sha
- gitea.branch.create        — branch off an existing branch

Wired through:
- lib/gitea.ts: giteaReadFile, giteaListContents, giteaListBranches,
  giteaCreateBranch, giteaListOrgRepos, giteaDeleteFile.
- lib/ai/vibn-tools.ts: 8 new Gemini tool declarations (53 total).
- app/api/chat/route.ts: system prompt now teaches the end-to-end
  scaffold-then-deploy recipe so the AI stops deferring to the user.

MCP capability descriptor bumped to version 2.5.0.

Made-with: Cursor
2026-04-28 11:52:16 -07:00
769fbdcba2 feat(mcp): per-resource Vibn-project ownership + backfill endpoint
Stage 3 of per-project Coolify isolation. Adds an authoritative ownership
table so apps_list { projectId } returns ONLY the resources actually owned
by that Vibn project, even when multiple Vibn projects share a single
Coolify project (the legacy workspace-level vibn-ws-{slug}).

- New table fs_project_resources (project_id, resource_uuid, type, workspace).
  Auto-created on first use.
- lib/projects.ts: linkResourceToProject / unlinkResource /
  getProjectResourceUuids / getProjectIdForResource helpers.
- apps_list { projectId }: when the project's coolifyProjectUuid equals the
  legacy workspace project, restrict results to explicitly-linked resources.
  When it has a dedicated Coolify project, return everything in that project.
- apps_create / databases_create: auto-link the newly-created resource to
  the requesting Vibn project.
- apps_delete / databases_delete / services_delete: unlink on success.
- projects_get → possibleDeployments: prefer explicit links; fuzzy-match
  fallback only fires when no link table entry exists yet.
- POST /api/projects/backfill-isolation: idempotent migration that mints a
  dedicated Coolify project for every Vibn project AND records existing
  coolifyServiceUuid/coolifyAppUuid/coolifyDatabaseUuid links. Resolves
  the "Twenty CRM project shows n8n" bug for legacy projects without
  needing to physically move services in Coolify.

Made-with: Cursor
2026-04-27 19:33:07 -07:00
766352ec00 feat(mcp): workspace-set-aware tenant safety + richer chat system prompt
Stage 2 of per-project Coolify isolation:
- Add getApplicationInWorkspace / getDatabaseInWorkspace / getServiceInWorkspace
  helpers that verify a resource belongs to ANY of the workspace's owned
  Coolify projects (legacy workspace project + per-Vibn-project projects).
- Replace all single-resource MCP lookups (apps.get/delete/deploy/exec/logs/
  domains/envs/volumes/repair, databases.*, services) to use the new
  workspace-set-aware variants. Single-resource tools now correctly find
  apps deployed under per-project Coolify namespaces.
- Fix missing queryOne import.

Chat system prompt overhaul:
- Add deployment recipes (third-party app, custom Docker image, database, domain)
- Add troubleshooting playbook (stuck deploys, 502s, tenant errors, repair)
- Restate hard rules: always pass projectId, always search templates first,
  destructive ops require name confirm, surface long-running op timing.

Made-with: Cursor
2026-04-27 19:21:20 -07:00
1a686c2a23 Per-project Coolify project isolation (Stage 1)
Each Vibn project now gets its OWN Coolify project named
vibn-{workspace-slug}-{project-slug}. All apps/databases/services
deployed for the project land inside that Coolify project, giving
us clean grouping, cascading delete, and per-project domain
namespaces.

Changes:
- New lib/projects.ts: ensureProjectCoolifyProject (idempotent
  create/lookup), getProjectCoolifyUuid, getOwnedCoolifyProjectUuids
- /api/projects/create: pre-insert row, mint per-project Coolify
  project, then complete the row with productData (preserves the
  coolifyProjectUuid that was just set)
- apps.list (MCP): without projectId, aggregates across ALL
  workspace-owned Coolify projects; with projectId, scopes to
  that project's Coolify project. Returns coolifyProjectUuid
  on each result so the AI knows where things live.
- apps.create (MCP): accepts projectId; auto-mints the Vibn
  project's Coolify project on first deploy if missing
- apps_list/apps_create tool defs: projectId param surfaced
- System prompt: Project as first-class — planning + live as
  facets of ONE thing, never as separate worlds. AI told to
  always pass projectId on apps_create.

Stage 2 (next): set-aware ensureResourceInProject across all
single-resource MCP tools (apps.get/delete/exec/etc.) and
cascading delete via projects.delete.

Made-with: Cursor
2026-04-27 19:02:43 -07:00
95ab91727e Fix Gemini schema validation: ARRAY needs items, replace free OBJECT with JSON strings
Gemini's function_declarations validator requires:
- ARRAY types must declare items schema
- Free-form OBJECT (without properties) is rejected

Renamed free-object params to *Json string fields (envsJson, patchJson,
headersJson) and added server-side JSON.parse before forwarding to MCP.
Any param ending in "Json" is automatically unpacked into its real key
(e.g. envsJson string is parsed into envs object).

Made-with: Cursor
2026-04-27 18:02:03 -07:00
c4ef30066f Expand chat panel to full MCP tool surface (35+ tools)
vibn-tools.ts previously exposed only 12 of the 35+ MCP tools.
Now includes the complete surface from AI_CAPABILITIES.md:
- workspace.describe, gitea.credentials
- apps: get, update, rewire_git, delete, deploy, deployments, exec,
  volumes.list/wipe, containers.up/ps, repair, domains.list/set,
  envs.list/upsert/delete
- databases: list, create, get, update, delete
- auth: list, create, delete
- domains: search, get, attach (+ existing register, list)
- storage: describe, provision, inject_env

Action dispatch simplified: toolName.replace(/_/g, '.') maps any
tool name to the MCP action with no explicit lookup table needed.
System prompt updated to reflect full capability set.

Made-with: Cursor
2026-04-27 17:55:57 -07:00
e08405ffbf Fix thought_signature: it's a sibling of functionCall, not nested inside it
The Gemini REST API returns thoughtSignature as a sibling part field:
  { "functionCall": {...}, "thoughtSignature": "..." }
not inside functionCall. We were reading part.functionCall.thought_signature
(always undefined) and writing fc.thought_signature inside the functionCall
object (also wrong). Now correctly reads part.thoughtSignature and writes
part.thoughtSignature when building history.

Made-with: Cursor
2026-04-27 17:28:49 -07:00
8872ab606b Fix tool calling: use non-streaming generateContent for tool rounds
Gemini 3.1 Pro thinking model requires thought_signature to be echoed
in functionResponse. SSE stream doesn't reliably include it in individual
chunks. Switch tool-calling rounds to non-streaming generateContent which
always returns the complete response with thought_signature present.

Made-with: Cursor
2026-04-27 17:18:34 -07:00
d246cbaf75 Fix Gemini 3.1 Pro thought_signature in tool calls
Thinking models attach a thought_signature to functionCall parts.
Must be echoed back in functionResponse or API returns 400.
Carry it through ToolCall -> ChatMessage -> toGeminiContents().

Made-with: Cursor
2026-04-27 16:37:09 -07:00
c41d018d79 Add github_search, github_file, http_fetch tools to chat AI
Gemini can now:
- Search GitHub for MIT-licensed OSS repos matching any description
- Read specific files from any public repo (READMEs, design systems,
  package.json, docker-compose.yml, component libraries, etc.)
- Fetch any public URL for docs, APIs, or reference material

No hardcoded pipelines — Gemini decides how to use these tools
based on what the user asks for.

Made-with: Cursor
2026-04-27 15:58:02 -07:00
1e138d69d6 Auto-mint default MCP token on workspace creation
- ensureWorkspaceForUser() now calls mintWorkspaceApiKey('default') on first workspace creation
- Legacy workspaces without a default key get one minted on first request
- GET /api/workspaces/[slug]/keys/default reveals (or mints) the default token for session users
- Chat panel fetches the token automatically on mount, caches it in localStorage
- No manual setup needed — tool calling works immediately on first sign-in

Made-with: Cursor
2026-04-27 15:43:27 -07:00
5e07bbf39d Add Vibn AI chat panel powered by Gemini 3.1 Pro
- Right-docked chat panel on all workspace pages ([workspace]/layout.tsx)
- Streaming SSE responses with Gemini 3.1 Pro preview via generativelanguage API
- Full tool-calling loop (up to 6 rounds): deploys apps, lists projects, registers
  domains, fetches logs — all via existing MCP dispatcher
- Persistent conversation history: fs_chat_threads + fs_chat_messages tables (Postgres)
- Thread management: create, list, rename (auto-title from first message), delete
- Panel collapses to a tab; open state persisted to localStorage
- Read-only mode hint when no MCP token is present
- Graceful content margin shift when panel is open

Made-with: Cursor
2026-04-27 15:40:32 -07:00
89eaff113c fix(mcp v2.4.8): use Coolify's :port URL convention, drop 170 lines of post-deploy hacks
The Coolify UI shows a "Required Port: 3000 — All domains must
include this port number" hint on service templates. That hint is
load-bearing: when the URL passed to `setServiceDomains` includes
:<upstream_port>, Coolify's template engine auto-generates everything
that 2.4.5-2.4.7 were doing by hand:

  - traefik.http.services.<svc>.loadbalancer.server.port label
  - SERVICE_FQDN_<APP>=<fqdn>      (no sslip.io leak)
  - SERVICE_URL_<APP>=https://<fqdn>
  - SERVICE_FQDN_<APP>_<PORT>=<fqdn>:<port>
  - SERVICE_URL_<APP>_<PORT>=https://<fqdn>:<port>

Verified end-to-end with twenty:
  setServiceDomains(uuid, [{ name:'twenty', url:'https://crm.mark.vibnai.com:3000' }])
followed by `compose up -d --force-recreate twenty` produced HTTP/2
200 from https://crm.mark.vibnai.com on first hit, with the
loadbalancer label present, .env clean, and zero env-rewriting
required.

Changes:
- apps.create template path now reads template.port from the catalog
  and calls setServiceDomains with https://<fqdn>:<port>
- listServiceTemplates now accepts port as either number or numeric
  string (Coolify ships both shapes in the catalog)
- applyCoolifyPostDeployFixes simplified from ~200 lines to ~50:
  drops env rewrite, label injection, and force-recreate steps;
  keeps proxy network attach + (background) proxy restart
- CoolifyPostDeployResult.steps shrinks to { proxyNetwork, proxyRestart }
- Removes the python:3-alpine SSH dependency entirely
- buildPythonRunner helper removed

Made-with: Cursor
2026-04-27 14:52:46 -07:00
167920dcc8 fix(mcp v2.4.7): defer coolify-proxy restart so it doesn't kill our own request
The post-deploy step that restarts coolify-proxy was running
synchronously inside the HTTP request handler. coolify-proxy is the
same gateway that's serving the request itself, so the restart
killed our outbound response mid-flight — the agent saw curl exit
16 (HTTP/2 framing error) instead of our nicely-formatted result.

Switch to a fire-and-forget shell:
  nohup sh -c '(sleep 3 && docker restart coolify-proxy) ...' &

The SSH command returns within ~50ms, we finish the HTTP response,
and Traefik re-discovers labels 3s later — same end state as before
but without breaking the calling request.

Made-with: Cursor
2026-04-27 14:41:09 -07:00
247b31bf2f fix(mcp v2.4.5): post-deploy fixes replace SSH compose-up fallback
apps.create for service templates now lets Coolify's queue do the
full deploy (compose generation, volumes, internal networking,
healthchecks) and applies three surgical post-deploy fixes that
Coolify's REST API does NOT expose:

  1. Rewrites SERVICE_FQDN_* / SERVICE_URL_* in the rendered .env so
     frontends that bake their backend URL into the SPA bundle
     (Twenty's SERVER_URL, n8n, etc.) point at the real custom domain
     instead of the auto-generated sslip.io URL. Without this fix
     Twenty's frontend loads on the real HTTPS domain but fires XHRs
     at insecure sslip.io, blocking everything as Mixed Content.
  2. Injects the missing
     traefik.http.services.<svc>.loadbalancer.server.port label.
     Coolify generates the routing rules but forgets the port, so
     Traefik logs "error: port is missing" and returns 503 forever.
  3. Connects coolify-proxy to the project network (Coolify writes a
     caddy_ingress_network=<uuid> hint label but never actually runs
     docker network connect), then force-recreates ONLY the
     public-facing container so the new env+label apply, and
     restarts the proxy so Traefik re-discovers.

Polling switches from service.status (which routinely lies as
"starting:unknown" while containers are actually healthy) to the
truthful per-application service.applications[*].status field.

Removes the SSH "docker compose up -d" fallback that v2.4.1-2.4.4
used. That fallback bypassed Coolify's full pipeline, causing
internal services like Postgres/Redis to land on the shared coolify
network where DNS aliases collided with coolify-db/coolify-redis,
producing the "password authentication failed" regression we saw
on Twenty deploys. With v2.4.5 internal services stay on their
isolated project network — only the public app crosses to the
proxy.

Response shape gains: reachable (boolean for HTTPS 2xx/3xx),
appStatus (truthful per-app status from Coolify), postDeploy
(step-by-step diagnostic for each of the three fixes). Existing
started/startDiag fields kept for back-compat.

apps.containers.up / apps.containers.ps remain unchanged for
manual user recovery.

Made-with: Cursor
2026-04-27 14:04:18 -07:00
d6b8ba4d67 fix(mcp v2.4.4): only attach traefik-enabled containers to coolify proxy net
v2.4.3 attached every stack container to the `coolify` network so
Traefik could reach the public container. But that network also hosts
coolify-db (alias `postgres`) and coolify-redis (alias `redis`).
Docker's embedded DNS resolves unqualified hostnames to the first
container with that name on the network, so once Twenty's
`postgres-<uuid>` joined the coolify network, Twenty's connection
string `postgres://postgres:5432/...` started resolving to coolify-db
and auth-failing in a tight restart loop.

Coolify's own pipeline only attaches the proxied container — filter
by the `traefik.enable=true` label so internal stack members (db,
redis, worker) stay isolated on the project network.

Made-with: Cursor
2026-04-27 12:36:44 -07:00
8b5c876f91 fix(mcp v2.4.3): attach stack containers to coolify proxy network
The Twenty (and any service-template) stack was reachable on its private
project network but invisible to coolify-proxy/Traefik because no
container was joined to the `coolify` network. Public URLs like
crm.mark.vibnai.com returned 503 "no available server" even though the
underlying app was healthy.

Coolify's UI deploy attaches the proxy network as a post-step after the
full stack is up. When a sidecar (e.g. Twenty's worker, which waits ~3
min on twenty's healthcheck) fails its depends_on gate, that post-step
can be skipped and the stack is left isolated.

composeUp now calls attachToCoolifyProxyNetwork() after compose
finishes (best-effort, idempotent), and ensureServiceUp does the same
on the Coolify-queue happy path. Single apps.create call should now
result in a publicly reachable app.

Made-with: Cursor
2026-04-27 12:08:27 -07:00
62cb77b5a7 feat(mcp v2.4.1): apps.containers.{up,ps} + auto-fallback for queued-start
Coolify's POST /services/{uuid}/start writes the rendered compose
files but its Laravel queue worker routinely fails to actually
invoke `docker compose up -d`. Until now agents had to SSH to
recover. For an MVP that promises "tell vibn what app you want,
get a URL", that's unacceptable.

- lib/coolify-compose.ts: composeUp/composeDown/composePs over SSH
  via a one-shot docker:cli container that bind-mounts the rendered
  compose dir (works around vibn-logs being in docker group but not
  having read access to /data/coolify/services).
- apps.create (template + composeRaw pathways) now uses
  ensureServiceUp which probes whether Coolify's queue actually
  spawned containers and falls back to direct docker compose up -d
  if not. Result includes startMethod for visibility.
- apps.containers.up / apps.containers.ps exposed as MCP tools for
  recovery scenarios and post-env-change recreations.
- Tenant safety: resolveAppOrService validates uuid against the
  caller's project before touching anything on the host.

Made-with: Cursor
2026-04-23 18:41:42 -07:00
e453e780cc feat(mcp v2.4): apps.create template pathway + apps.templates.{list,search}
Adds Coolify one-click template support — 320+ vetted apps deployable
in one MCP call (Twenty, n8n, Supabase, Ghost, etc).

- apps.create gains a 4th pathway: { template: "<slug>", ... }. Auto-
  rewrites the Coolify-assigned sslip URL to the workspace FQDN and
  applies user envs before starting.
- apps.templates.list / apps.templates.search expose the catalog so
  agents can discover slugs. Catalog is fetched from upstream GitHub
  and cached in-memory for 1h.
- lib/coolify.ts: + setServiceDomains, updateService, listService-
  Templates, searchServiceTemplates. Reuses existing createService.
- next.config.ts: externalize ssh2 + cpu-features from turbopack so
  `next build` can complete (native .node binaries can't be ESM-bundled).

Made-with: Cursor
2026-04-23 18:08:05 -07:00
7944db8ba4 fix(coolify): upsertServiceEnv falls back to PATCH on already-exists
Coolify auto-creates env entries (with empty values) for every ${VAR}
reference it finds in the compose YAML at service-creation time. So
POST /services/{uuid}/envs returns 'already exists' for any env we
try to set after creation. The fix is to fall back to PATCH on that
specific error, making the helper a true upsert.

Made-with: Cursor
2026-04-23 17:27:31 -07:00
5d4936346e fix: remove duplicate getService, fix project uuid check for services
Made-with: Cursor
2026-04-23 17:09:00 -07:00
040f0c6256 feat(mcp): proper Coolify Services support for composeRaw pathway
Coolify's /applications/dockercompose creates a Service (not Application)
with its own API surface. Wire it up correctly:

lib/coolify.ts
  - createDockerComposeApp returns { uuid, resourceType: 'service' }
  - Add startService, stopService, getService, listAllServices helpers
  - Add listServiceEnvs, upsertServiceEnv, bulkUpsertServiceEnvs for
    the /services/{uuid}/envs endpoint

app/api/mcp/route.ts
  - toolAppsList: includes Services (compose stacks) alongside Applications
  - toolAppsDeploy: falls back to /services/{uuid}/start for service UUIDs
  - toolAppsCreate composeRaw path: uses upsertServiceEnv + startService
    instead of Application deploy; notes that domain routing must be
    configured post-startup via SERVER_URL env

Made-with: Cursor
2026-04-23 17:02:21 -07:00
f27e572fdb fix: wait 2.5s before domain PATCH after dockercompose create (async creation)
Made-with: Cursor
2026-04-23 16:49:51 -07:00
8c8e39d102 fix: base64-encode docker_compose_raw for Coolify create endpoint
Made-with: Cursor
2026-04-23 16:43:33 -07:00
e09cad409e fix: remove autogenerate_domain from dockercompose create (not allowed)
Made-with: Cursor
2026-04-23 16:37:30 -07:00
1f37d4bc91 fix(coolify): remove disallowed fields from dockercompose create payload
Coolify's /applications/dockercompose endpoint rejects build_pack (it
hardcodes dockercompose), is_force_https_enabled, and
docker_compose_domains at creation time. Move those to a follow-up
PATCH call that runs immediately after creation.

Made-with: Cursor
2026-04-23 16:31:44 -07:00
6d71c63053 feat(mcp): apps.create image/composeRaw pathways + apps.volumes.list/wipe
Third-party apps (Twenty, Directus, Cal.com, Plane…) should never need
a Gitea repo. This adds two new apps.create pathways:

  image: "twentyhq/twenty:1.23.0"   → Coolify /applications/dockerimage
  composeRaw: "services:\n..."       → Coolify /applications/dockercompose

No repo is created, no git clone, no PAT embedding. Agents can fetch
the official docker-compose.yml and pass it inline, or just give an
image name. Pathway 1 (repo) is unchanged.

Also adds volume management tools so agents can self-recover from the
most common compose failure (stale DB volume blocking fresh migrations):

  apps.volumes.list { uuid }                        → list volumes + sizes
  apps.volumes.wipe { uuid, volume, confirm }       → stop containers,
                                                       rm volume, done

Both volume tools go through the same vibn-logs SSH channel. The wipe
tool requires confirm == volume name to prevent accidents and verifies
the volume belongs to the target app (uuid in name).

lib/coolify.ts: createDockerImageApp + createDockerComposeApp helpers,
  dockerimage added to CoolifyBuildPack union.
app/api/mcp/route.ts: resolveFqdn + applyEnvsAndDeploy extracted as
  shared helpers; toolAppsCreate now dispatches on image/composeRaw/repo.
  toolAppsVolumesList + toolAppsVolumesWipe added.
  sq() moved to module scope (shared by exec + volumes tools).
  Version bumped to 2.3.0.

Made-with: Cursor
2026-04-23 16:21:28 -07:00
8c83f8c490 feat(mcp): apps.exec — run one-shot commands in app containers
Companion to apps.logs. SSH to the Coolify host as vibn-logs, resolve
the target container by app uuid + service, and run the caller's
command through `docker exec ... sh -lc`. No TTY, no stdin — this is
the write-path sibling of apps.logs, purpose-built for migrations,
seeds, CLI invocations, and ad-hoc debugging.

- lib/coolify-containers.ts extracts container enumeration + service
  resolution into a shared helper used by both logs and exec.
- lib/coolify-exec.ts wraps docker exec with timeout (60s default,
  10-min cap), output byte cap (1 MB default, 5 MB cap), optional
  --user / --workdir, and structured audit logging of the command +
  target (never the output).
- app/api/mcp/route.ts wires `apps.exec` into the dispatcher and
  advertises it in the capabilities manifest.
- app/api/workspaces/[slug]/apps/[uuid]/exec/route.ts exposes the same
  tool over REST for session-cookie callers.

Tenant safety: every entrypoint runs getApplicationInProject before
touching SSH, so an agent can only exec in apps belonging to their
workspace.

Made-with: Cursor
2026-04-23 14:18:49 -07:00
e766315ecd fix(apps): compose-aware domains; loud apps.update ignore list
Two live-test bugs surfaced while deploying Twenty CRM:

1. apps.domains.set silently 422'd on compose apps
   Coolify hard-rejects top-level `domains` for dockercompose build
   packs — they must use `docker_compose_domains` (per-service JSON).
   setApplicationDomains now detects build_pack (fetched via GET if
   not passed) and dispatches correctly. Default service is `server`
   (matches Twenty, Plane, Cal.com); override with `service` param.

2. apps.update silently dropped unrecognised fields
   Caller got `{ok:true}` even when zero fields persisted. This
   created false-positive "bug reports" (e.g. the user-reported
   "fqdn returns ok but doesn't persist" — fqdn was never forwarded
   at all). apps.update now returns:
     - applied:  fields that were forwarded to Coolify
     - ignored:  unknown fields (agent typos, stale field names)
     - rerouted: fields that belong to a different tool
                 (fqdn/domains → apps.domains.set,
                  git_repository → apps.rewire_git)
   400 when nothing applied, 200 with diagnostics otherwise.

Made-with: Cursor
2026-04-23 13:25:16 -07:00
d86f2bea03 feat(mcp): apps.logs — compose-aware runtime logs
Adds apps.logs MCP tool + session REST endpoint for tailing runtime
container logs. Unblocks cold-start debugging for agent-deployed
compose apps (Twenty, Cal.com, Plane, etc.) where Coolify's own
/applications/{uuid}/logs endpoint returns empty.

Architecture:
  - dockerfile / nixpacks / static apps → Coolify's REST logs API
  - dockercompose apps                  → SSH into Coolify host,
                                          `docker logs` per service

New SSH path uses a dedicated `vibn-logs` user (docker group, no
sudo, no pty, no port-forwarding, single ed25519 key). Private key
lives in COOLIFY_SSH_PRIVATE_KEY_B64 on the vibn-frontend Coolify
app; authorized_key is installed by scripts/setup-vibn-logs-user.sh
on the Coolify host.

Tool shape:
  params:   { uuid, service?, lines? (default 200, max 5000) }
  returns:  { uuid, buildPack, source: 'coolify_api'|'ssh_docker'|'empty',
              services: { [name]: { container, lines, bytes, logs, status? } },
              warnings: string[], truncated: boolean }

Made-with: Cursor
2026-04-23 13:21:52 -07:00
fcd5d03894 fix(apps.create): clone via HTTPS+bot-PAT; activate bot users on creation
Coolify was failing all Gitea clones with "Permission denied (publickey)"
because the helper container's SSH hits git.vibnai.com:22 (Ubuntu host
sshd, which doesn't know Gitea keys), while Gitea's builtin SSH is on
host port 22222 (not publicly reachable).

Rather than fight the SSH topology, switch every Vibn-provisioned app
to clone over HTTPS with the workspace bot's PAT embedded in the URL.
The PAT is already stored encrypted per workspace and scoped to that
org, so this gives equivalent isolation with zero SSH dependency.

Changes:
- lib/naming.ts: add giteaHttpsUrl() + redactGiteaHttpsUrl(); mark
  giteaSshUrl() as deprecated-for-deploys with a comment.
- lib/coolify.ts: extend CreatePublicAppOpts with install/build/start
  commands, base_directory, dockerfile_location, docker_compose_location,
  manual_webhook_secret_gitea so it's at parity with the SSH variant.
- app/api/mcp/route.ts:
  - apps.create now uses createPublicApp(giteaHttpsUrl(...)) and pulls
    the bot PAT via getWorkspaceBotCredentials(). No more private-
    deploy-key path for new apps.
  - apps.update adds git_commit_sha + docker_compose_location to the
    whitelist.
  - New apps.rewire_git tool: re-points an app's git_repository at the
    canonical HTTPS+PAT URL. Unblocks older apps stuck on SSH URLs
    and provides a path for PAT rotation without rebuilding the app.
- lib/gitea.ts: createUser() now issues an immediate PATCH to set
  active: true. Gitea's admin-create endpoint creates users as inactive
  by default, and inactive users fail permission checks even though
  they're org members. GiteaUser gains optional `active` field.
- scripts/activate-workspace-bots.ts: idempotent backfill that flips
  active=true for any existing workspace bot that was created before
  this fix. Safe to re-run.
- AI_CAPABILITIES.md: document apps.rewire_git; clarify apps.create
  uses HTTPS+PAT (no SSH).

Already unblocked in prod for the mark workspace:
- vibn-bot-mark activated.
- twenty-crm's git_repository PATCHed to HTTPS+PAT form; git clone
  now succeeds (remaining unrelated error: docker-compose file path).

Made-with: Cursor
2026-04-23 12:21:00 -07:00
3192e0f7b9 fix(coolify): strip is_build_time from env writes; add reveal + GCS
Coolify v4's POST/PATCH /applications/{uuid}/envs only accepts key,
value, is_preview, is_literal, is_multiline, is_shown_once. Sending
is_build_time triggers a 422 "This field is not allowed." — it's now
a derived read-only flag (is_buildtime) computed from Dockerfile ARG
usage. Breaks agents trying to upsert env vars.

Three-layer fix so this can't regress:
  - lib/coolify.ts: COOLIFY_ENV_WRITE_FIELDS whitelist enforced at the
    network boundary, regardless of caller shape
  - app/api/workspaces/[slug]/apps/[uuid]/envs: stops forwarding the
    field; returns a deprecation warning when callers send it; GET
    reads both is_buildtime and is_build_time for version parity
  - app/api/mcp/route.ts: same treatment in the MCP dispatcher;
    AI_CAPABILITIES.md doc corrected

Also bundles (not related to the above):
  - Workspace API keys are now revealable from settings. New
    key_encrypted column stores AES-256-GCM(VIBN_SECRETS_KEY, token).
    POST /api/workspaces/[slug]/keys/[keyId]/reveal returns plaintext
    for session principals only; API-key principals cannot reveal
    siblings. Legacy keys stay valid for auth but can't reveal.
  - P5.3 Object storage: lib/gcp/storage.ts + lib/workspace-gcs.ts
    idempotently provision a per-workspace GCS bucket, service
    account, IAM binding and HMAC key. New POST /api/workspaces/
    [slug]/storage/buckets endpoint. Migration script + smoke test
    included. Proven end-to-end against prod master-ai-484822.

Made-with: Cursor
2026-04-23 11:46:50 -07:00
651ddf1e11 Rip out Theia, ship P5.1 attach E2E + Justine UI work-in-progress
Theia rip-out:
- Delete app/api/theia-auth/route.ts (Traefik ForwardAuth shim)
- Delete app/api/projects/[projectId]/workspace/route.ts and
  app/api/projects/prewarm/route.ts (Cloud Run Theia provisioning)
- Delete lib/cloud-run-workspace.ts and lib/coolify-workspace.ts
- Strip provisionTheiaWorkspace + theiaWorkspaceUrl/theiaAppUuid/
  theiaError from app/api/projects/create/route.ts response
- Remove Theia callbackUrl branch in app/auth/page.tsx
- Drop "Open in Theia" button + xterm/Theia PTY copy in build/page.tsx
- Drop theiaWorkspaceUrl from deployment/page.tsx Project type
- Strip Theia IDE line + theia-code-os from advisor + agent-chat
  context strings
- Scrub Theia mention from lib/auth/workspace-auth.ts comment

P5.1 (custom apex domains + DNS):
- lib/coolify.ts + lib/opensrs.ts: nameserver normalization, OpenSRS
  XML auth, Cloud DNS plumbing
- scripts/smoke-attach-e2e.ts: full prod GCP + sandbox OpenSRS +
  prod Coolify smoke covering register/zone/A/NS/PATCH/cleanup

In-progress (Justine onboarding/build, MVP setup, agent telemetry):
- New (justine)/stories, project (home) layouts, mvp-setup, run, tasks
  routes + supporting components
- Project shell + sidebar + nav refactor for the Stackless palette
- Agent session API hardening (sessions, events, stream, approve,
  retry, stop) + atlas-chat, advisor, design-surfaces refresh
- New scripts/sync-db-url-from-coolify.mjs +
  scripts/prisma-db-push.mjs + docker-compose.local-db.yml for
  local Prisma workflows
- lib/dev-bypass.ts, lib/chat-context-refs.ts, lib/prd-sections.ts
- Misc: stories CSS, debug/prisma route, modal-theme, BuildLivePlanPanel

Made-with: Cursor
2026-04-22 18:05:01 -07:00
d6c87a052e feat(domains): P5.1 — OpenSRS registration + Cloud DNS + Coolify attach
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
2026-04-21 16:30:39 -07:00
de1cd96ec2 fix(auth): classify services by service_type, not name heuristics
Coolify exposes the template slug on `service_type`; the list endpoint
returns only summaries, so the auth list handler now fetches each
service individually to classify it reliably. Users can name auth
services anything (e.g. "my-login") and they still show up as auth
providers.

Made-with: Cursor
2026-04-21 12:37:21 -07:00
62c52747f5 fix(coolify): list project databases across per-flavor arrays
GET /projects/{uuid}/{envName} returns databases split into
postgresqls/mysqls/mariadbs/mongodbs/redis/keydbs/dragonflies/clickhouses
sibling arrays instead of a unified `databases` list. Combine all of
them in listDatabasesInProject. Also normalize setApplicationDomains
to prepend https:// on bare hostnames (Coolify validates as URL).

Made-with: Cursor
2026-04-21 12:30:36 -07:00
b1670c7035 fix(coolify): tenant-match by environment_id via project envs
The v4 /applications, /databases, /services list endpoints don't
return project_uuid; authoritative link is environment_id. Replace
the explicit-only tenant check (which was rejecting every resource)
with a check that:
  - trusts explicit project_uuid if present
  - else looks up project envs via GET /projects/{uuid} and matches
    environment_id
Also switch the project list helpers to use GET /projects/{uuid}/{env}
so listing returns only the resources scoped to the workspace's
project + environments.

Made-with: Cursor
2026-04-21 12:23:09 -07:00
eacec74701 fix(coolify): use /deploy?uuid=... endpoint (Coolify v4)
Made-with: Cursor
2026-04-21 12:07:12 -07:00
a591c55fc4 fix(coolify): use correct /deployments/applications/{uuid} endpoint
Made-with: Cursor
2026-04-21 12:05:51 -07:00
0797717bc1 Phase 4: AI-driven app/database/auth lifecycle
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
2026-04-21 12:04:59 -07:00