Commit Graph

288 Commits

Author SHA1 Message Date
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
afd76b394f fix(chat): preserve thread history across page reloads
Two bugs, one symptom (every reload silently spawned a blank thread,
the previous conversation was orphaned in the sidebar):

1. Race in the auto-thread effect.
   On mount: threads = [], activeThread = null, /api/chat/threads
   fetch in flight. The auto-create effect re-ran the moment the
   workspace + auth resolved, saw threads.length === 0, and called
   newThread() before the fetch ever returned. When the historical
   threads finally landed, activeThread was already pinned to the new
   empty one.

   Gate on a `threadsLoaded` flag that flips true after the first
   loadThreads() resolves. Auto-create can no longer fire before
   history is known.

2. activeThread wasn't persisted.
   Even with the race fixed, refreshing the page would reset the
   sidebar to the top thread (most recently updated). After a deploy
   that's usually the brand-new empty thread we just spawned, not the
   conversation the user was actually in.

   Persist activeThread to localStorage keyed by workspace. Reload
   restores the same thread; switching workspaces resets cleanly.

Made-with: Cursor
2026-04-28 14:26:34 -07:00
115cf7eb28 fix(chat): always emit narrative summary, even when tool-round cap is hit
Surfaced by the live Path B test: AI fired 7 tool calls (fs.read,
fs.edit, kill, dev_server.start, curl, dev_server.logs, ...) in a single
turn, the loop exited at MAX_TOOL_ROUNDS, and the user saw only a tray
of ✓ icons — no text reply.

Two changes:

1. Bump MAX_TOOL_ROUNDS 6 → 12. Path B iteration chains routinely run
   long; 6 was tuned for Path A's much-shorter Coolify-orchestration
   sequences.

2. When the loop exits because of the cap (the last assistant turn was
   a tool call, not a finish), force one more no-tools Gemini call
   with an explicit "summarize the result, do NOT call tools" prompt.
   That gives the user a sentence or two of context instead of a wall
   of green checkmarks. Wrapped in try/catch so the stream still
   terminates cleanly if Gemini errors.

Made-with: Cursor
2026-04-28 14:17:40 -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
5d2a8c5734 fix(path-b): escape cron "*/5" in idle-sweep JSDoc — was closing the comment block and breaking the build
Made-with: Cursor
2026-04-28 13:34:21 -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
2de3c5ce57 fix(backfill): move endpoint to /api/admin/ to avoid [projectId] catch-all
Made-with: Cursor
2026-04-27 19:45:06 -07:00
a83cc45f6a feat(backfill): support ops-secret bootstrap auth for backfill-isolation
Made-with: Cursor
2026-04-27 19:39:04 -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
b9c8457eb3 Stop auto-scaffolding 4 sub-apps + turborepo on project creation
Every new Vibn project was being seeded with:
  - a turborepo scaffold pushed to its Gitea repo
    (apps/product, apps/website, apps/admin, apps/storybook)
  - 4 corresponding Coolify services that nobody ever deployed

Both predate templates / GitHub imports / on-demand AI deploys and
created noise in every workspace's Coolify view (and confused the AI
about what was actually running).

Now project creation provisions just:
  - a Gitea repo (empty unless GitHub mirror is requested)
  - a dedicated Coolify project ready to receive deploys

Apps land in the project via apps_create on demand — what the user
actually wants, not a guess. The lib/scaffold/turborepo/ files remain
in source for future opt-in re-introduction.

Made-with: Cursor
2026-04-27 19:06:47 -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
ddc5c37a8e projects.get: auto-enrich with possibleDeployments[] from fuzzy app/service name match
Project records and Coolify deployments live in separate worlds —
nothing writes the Coolify UUID back into fs_projects.data on deploy.
Now projects.get also scans apps + services in the workspace and
returns any whose name fuzzy-matches the project (lowercased token
overlap), plus any explicitly-linked one. Self-healing forever; the
AI can immediately tell the user what's running for a project even
when the link was never stored.

Made-with: Cursor
2026-04-27 18:38:01 -07:00
4c804d670b Fix apps.list: filter compose services by environment_id, not non-existent project field
Coolify's /api/v1/services response does not include a `project` field.
Services belong to environments and environments belong to projects.
The old filter checked s.project.uuid (always undefined) and silently
dropped every service from the result, so compose-stack apps like
Twenty CRM never showed up in apps.list.

Now we resolve the project's environment IDs via getProject() and
filter services where environment_id is in that set. Also surface the
public service's fqdn in the response (extracted from s.applications)
so the AI can immediately tell the user where the app lives.

Made-with: Cursor
2026-04-27 18:09: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
bea23cf307 Add missing /api/activity route for workspace activity page
The workspace Activity page (/[workspace]/activity) was calling
/api/activity which did not exist, so the feed was always empty.
New route aggregates agent_sessions (builds/deploys) and fs_projects
(creation/status changes) across all user projects, returning
ActivityItem[] sorted by date descending.

Made-with: Cursor
2026-04-27 17:42:51 -07:00
0cb5d8bc50 Fix AI confusion: clean projects_get output, clarify project vs app in system prompt
projects_get was dumping raw JSONB including turborepo scaffold fields
(product/website/admin/storybook sub-app configs), which Gemini mistook
for live deployed services. Now returns a clean summary with only the
fields relevant to the AI. Also updated the system prompt to explicitly
distinguish Vibn project records (planning artifacts) from Coolify apps
(actual running services), instructing the model to call apps_list when
the user asks what's live.

Made-with: Cursor
2026-04-27 17:37:18 -07:00
7138f86427 Auto-create chat tables on first request (IF NOT EXISTS)
fs_chat_threads and fs_chat_messages were referenced in code but
never added to the migration script. Added ensureChatTables() called
at startup of both /api/chat and /api/chat/threads routes — safe,
idempotent, and runs once per process lifetime. Also backfilled the
SQL migration file for documentation.

Made-with: Cursor
2026-04-27 17:34:30 -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
210fba4e08 Fix chat panel token fetch: use /api/workspaces not URL slug
URL param 'mark-account' != workspace slug 'mark'. Fetch default token
from /api/workspaces?include_default_token=true which resolves the real
slug server-side.

Made-with: Cursor
2026-04-27 16:26:55 -07:00
56e7c2fb5c Fix auto-token fetch: use keys?include_default_token=true instead of /keys/default
Avoids the Next.js dynamic route conflict with [keyId].
Chat panel now correctly bootstraps MCP token on mount with no user action.

Made-with: Cursor
2026-04-27 16:04:38 -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
d1f8c3d34b fix(mcp v2.4.6): poll robustness + apps.repair recovery tool
Two fixes for transient Coolify queue lag observed when smoke-testing
v2.4.5:

1. ensureServiceReachable no longer false-fails on early `exited` status.
   Coolify's queue worker can take 60-120s to dequeue a `start` request;
   during that window service.applications[*].status returns the stale
   `exited` (= "never started") state. Previously the polling loop
   treated that as terminal failure after 90s, returning started:false
   on stacks that were about to come up healthy.

   The new logic requires "evidence of activity" (status starting:* or
   running:* seen at least once) before treating subsequent `exited`
   reports as terminal. Until activity is observed, the loop just keeps
   polling up to the 8-min health timeout.

2. apps.repair (new tool). Re-runs the three post-deploy patches
   (env rewrite, traefik port label, coolify-proxy network attach +
   force-recreate + proxy restart) against an existing service without
   recreating it. Useful when:
     - apps.create returned started:false but containers eventually
       came up (now the polling fix should make this rare)
     - a deploy succeeded mechanically but is serving Traefik 503 or
       Mixed Content
     - a user rotates a custom domain on an existing app

   Params: { uuid, fqdn, publicAppName, port? }
   Returns: { reachable, postDeploy: { steps }, probe }

Version bumped to 2.4.6.

Made-with: Cursor
2026-04-27 14:30:27 -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
723cc5fdd1 fix(mcp): strip ANSI/control chars from compose startDiag for JSON safety
Made-with: Cursor
2026-04-23 20:25:26 -07:00
efb2082400 fix(mcp v2.4.2): apps.create reports started=true on partial sidecar failure
Coolify's `compose up -d` returns non-zero whenever any sidecar container
hits a `depends_on: condition: service_healthy` timeout. For slow-booting
apps like Twenty (where the worker waits ~3 min for twenty's healthcheck),
this caused apps.create to return started=false even when the primary
stack was running fine.

Now ensureServiceUp probes the host with `docker ps` after a non-zero
compose exit and returns started=true whenever any container is running,
surfacing the compose stderr in startDiag so agents can decide whether
to retry apps.containers.up later.

Made-with: Cursor
2026-04-23 20:12:03 -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