Rip out Theia, bump submodules, retire platform/ scaffold, snapshot docs + design assets
Theia rip-out (parent):
- Remove theia submodule entry (the local fork, Gitea repo, Coolify app,
Cloud Run services, and Artifact Registry image are all gone)
- Drop README.md + INFRASTRUCTURE.md (obsolete "Project OS" snapshots
that also leaked API tokens) and setup.sh (Theia clone bootstrap)
- Delete UI-DESIGN-GUIDE.md, BACKEND_AGENTS_PLAN.md, VIBN_BUILD_PLAN.md,
VISUAL_EDITOR_PLAN.md, core-packages.md, ai-packages.md, tools-list.md
(all 100% Theia-specific or superseded)
- Surgical scrubs of remaining Theia mentions in
AGENT_EXECUTION_ARCHITECTURE.md and TURBOREPO_MIGRATION_PLAN.md
Submodule bumps:
- vibn-agent-runner: Theia rip-out + MCP refactor (api/wrapper/server
pattern across shell/file/git/memory/prd/search/agent/gitea/coolify)
- vibn-frontend: Theia rip-out + P5.1 attach E2E + Justine UI WIP
Retire platform/ scaffold:
- Remove platform/backend/ (control-plane, executors, mcp-adapter),
platform/client-ide/ (gcp-productos extension), platform/contracts/,
platform/infra/terraform/, platform/scripts/templates/turborepo/
(replaced by vibn-agent-runner + vibn-frontend + Coolify direct)
- Drop architecture.md, technical_spec.md, vision-ext.md,
"1.Generate Control Plane API scaffold.md" (same era)
Docs / planning snapshots (new):
- AI_CAPABILITIES.md, AI_CAPABILITIES_ROADMAP.md
- AGENT_TELEMETRY_STREAMING_PROJECT.md
- VIBN_PRD.md, product-idea-a.md
Design assets (new):
- branding/{coolify,gitea,ux-testing}/ static brand collateral
- justine/ HTML mockups for the new onboarding/build flows
- preview-assist-ui/ Vite scratch app
- master-ai.code-workspace
Infra helpers (new):
- setup-coolify-montreal.sh provisioner
- gitea-docker-compose.yml
- vibn-coolify-schema.sql for the Coolify Postgres extensions
- prd-agent-prompt.pdf, prompt, root.txt, remixed-9edec9e9.tsx scratch
- flatten.sh helper
.gitignore: ignore **/node_modules, **/.next, **/.turbo, **/coverage
Made-with: Cursor
This commit is contained in:
584
AI_CAPABILITIES.md
Normal file
584
AI_CAPABILITIES.md
Normal file
@@ -0,0 +1,584 @@
|
||||
# Vibn AI Capabilities
|
||||
|
||||
> The full set of actions an AI agent can take on behalf of a Vibn workspace,
|
||||
> along with the REST endpoints, MCP tools, and safety rails that back them.
|
||||
>
|
||||
> **Audience:** agent authors, Cursor rule writers, MCP tool designers, and
|
||||
> anyone building on the Vibn control plane.
|
||||
>
|
||||
> **Scope:** everything an agent sees through `https://vibnai.com/api/*` and
|
||||
> the `/api/mcp` bridge. No Firestore, no internal agent orchestration —
|
||||
> just the tenant-safe capability surface.
|
||||
|
||||
---
|
||||
|
||||
## 1. Mental model
|
||||
|
||||
Every capability in this document operates on a single **workspace**. A
|
||||
workspace is Vibn's tenant boundary and maps 1:1 to:
|
||||
|
||||
| Vibn concept | External identity | Example (`mark`) |
|
||||
|---|---|---|
|
||||
| Workspace | `vibn_workspaces.slug` | `mark` |
|
||||
| Gitea org | `gitea_org` | `vibn-mark` |
|
||||
| Gitea bot user | `gitea_bot_username` | `mark-bot` |
|
||||
| SSH deploy keypair | `coolify_private_key_uuid` + `gitea_bot_ssh_key_id` | registered on both sides |
|
||||
| Coolify project | `coolify_project_uuid` | `vibn-ws-mark` |
|
||||
| Coolify environment | `coolify_environment_name` | `production` |
|
||||
| Domain namespace | `*.{slug}.vibnai.com` | `*.mark.vibnai.com` |
|
||||
| AI token | `vibn_sk_…` | one per agent/device |
|
||||
|
||||
A single agent token can only act on the workspace it was minted for. Cross-
|
||||
workspace access is structurally impossible — enforced in
|
||||
[`lib/coolify.ts`](./vibn-frontend/lib/coolify.ts) by matching every Coolify
|
||||
resource's `environment_id` against the workspace's project environments
|
||||
(`ensureResourceInProject`).
|
||||
|
||||
### The three views
|
||||
|
||||
All capabilities roll up into three user-facing surfaces:
|
||||
|
||||
- **Code** — every Gitea repo under `vibn-{slug}/`.
|
||||
- **Live** — every Coolify app/database/service in `vibn-ws-{slug}`, each
|
||||
reachable under `*.{slug}.vibnai.com`.
|
||||
- **IDE** — Browser-based agent workspace sessions (outside the scope of this doc).
|
||||
|
||||
---
|
||||
|
||||
## 2. Authentication
|
||||
|
||||
Every agent-facing endpoint accepts **either**:
|
||||
|
||||
- `Authorization: Bearer vibn_sk_<base64url>` — a workspace-scoped API key
|
||||
minted in the settings panel. Stored as a sha256 hash server-side; the
|
||||
plaintext is shown exactly once on creation. Can be revoked at any time.
|
||||
- A NextAuth session cookie — used for the dashboard UI and for browser
|
||||
debugging. Not suitable for long-running agents.
|
||||
|
||||
Helper: [`requireWorkspacePrincipal()`](./vibn-frontend/lib/auth/workspace-auth.ts)
|
||||
resolves either to a `WorkspacePrincipal { workspace, user?, source }`.
|
||||
|
||||
**403 on a tenant mismatch means:** the token is valid, but the resource
|
||||
belongs to another workspace. The agent should stop and ask the user.
|
||||
|
||||
---
|
||||
|
||||
## 3. MCP surface
|
||||
|
||||
The MCP bridge lives at `POST https://vibnai.com/api/mcp`. It takes
|
||||
JSON-over-HTTP bodies shaped like:
|
||||
|
||||
```json
|
||||
{ "tool": "<tool-name>", "params": { /* tool-specific */ } }
|
||||
```
|
||||
|
||||
The Cursor / Claude Desktop config block is auto-generated in the settings
|
||||
panel and looks like:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"vibn-mark": {
|
||||
"url": "https://vibnai.com/api/mcp",
|
||||
"headers": { "Authorization": "Bearer vibn_sk_…" }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`GET /api/mcp` returns a self-description with the current tool list.
|
||||
Version: **2.1.0**.
|
||||
|
||||
### 3.1 Workspace & identity tools
|
||||
|
||||
| Tool | Purpose | Params |
|
||||
|---|---|---|
|
||||
| `workspace.describe` | Returns slug, Coolify project uuid, Gitea org, provision status. | — |
|
||||
| `gitea.credentials` | Returns the bot's username, PAT, clone URL template, and SSH remote template. Use this for every `git clone`/push — never other credentials. | — |
|
||||
|
||||
### 3.2 Project tools
|
||||
|
||||
| Tool | Purpose | Params |
|
||||
|---|---|---|
|
||||
| `projects.list` | Lists Vibn projects (PRDs, imports, etc.) in the workspace. | — |
|
||||
| `projects.get` | Single project details. | `{ projectId }` |
|
||||
|
||||
### 3.3 Application tools
|
||||
|
||||
| Tool | Purpose | Params |
|
||||
|---|---|---|
|
||||
| `apps.list` | All Coolify apps in the workspace. | — |
|
||||
| `apps.get` | Single app details (status, fqdn, domains, git info). | `{ uuid }` |
|
||||
| `apps.create` | Create a Coolify app from a Gitea repo in the workspace's org, pinned to the workspace's SSH deploy key. Auto-domain `{name}.{slug}.vibnai.com`. | `{ repo, branch?, name?, ports?, buildPack?, domain?, envs?, instantDeploy? }` |
|
||||
| `apps.update` | PATCH a whitelisted set of fields (name, description, git branch, ports, build commands, base directory, Dockerfile location…). | `{ uuid, patch }` |
|
||||
| `apps.delete` | Destroy the app. Volumes kept by default. | `{ uuid, confirm }` — `confirm` must equal the app's exact name |
|
||||
| `apps.deploy` | Trigger a new deployment. | `{ uuid, force? }` |
|
||||
| `apps.deployments` | List recent deployments + status. | `{ uuid }` |
|
||||
| `apps.domains.list` | Current domain set. | `{ uuid }` |
|
||||
| `apps.domains.set` | Replace the domain set. All entries must end with `.{slug}.vibnai.com`. | `{ uuid, domains: string[] }` |
|
||||
| `apps.envs.list` | List env vars. Values returned are redacted for `shown-once` secrets. | `{ uuid }` |
|
||||
| `apps.envs.upsert` | Create or update an env var. | `{ uuid, key, value, isBuildTime?, isMultiline?, isLiteral?, isShownOnce? }` |
|
||||
| `apps.envs.delete` | Delete an env var. | `{ uuid, key }` |
|
||||
|
||||
### 3.4 Database tools
|
||||
|
||||
| Tool | Purpose | Params |
|
||||
|---|---|---|
|
||||
| `databases.list` | All databases in the workspace, across all flavors. | — |
|
||||
| `databases.create` | Provision a database. Supported `type`: `postgresql`, `mysql`, `mariadb`, `mongodb`, `redis`, `keydb`, `dragonfly`, `clickhouse`. | `{ type, name?, isPublic?, publicPort?, image?, credentials?, limits? }` |
|
||||
| `databases.get` | Details + internal connection URL. | `{ uuid }` |
|
||||
| `databases.update` | PATCH name, public visibility, image, limits. | `{ uuid, patch }` |
|
||||
| `databases.delete` | Destroy the database. Volumes kept by default. | `{ uuid, confirm }` — `confirm` must equal the db's exact name |
|
||||
|
||||
### 3.5 Auth provider tools
|
||||
|
||||
Authentication is a first-class capability. An agent cannot spin up arbitrary
|
||||
Coolify services — only vetted auth providers from an allowlist.
|
||||
|
||||
| Tool | Purpose | Params |
|
||||
|---|---|---|
|
||||
| `auth.list` | Auth providers currently deployed in the workspace (classified by Coolify's `service_type`). | — |
|
||||
| `auth.create` | Provision one of the allowed providers. | `{ provider, name?, description?, instantDeploy? }` |
|
||||
| `auth.delete` | Destroy an auth provider. Volumes (user data) kept by default. | `{ uuid, confirm }` — `confirm` must equal the service's exact name |
|
||||
|
||||
**Allowed providers** (keys passed as `provider`):
|
||||
|
||||
- `pocketbase` — lightweight (SQLite) auth + data, single container.
|
||||
- `authentik` — feature-rich self-hosted IDP.
|
||||
- `keycloak` / `keycloak-with-postgres` — industry-standard OIDC/SAML.
|
||||
- `pocket-id` / `pocket-id-with-postgresql` — passkey-first OIDC.
|
||||
- `logto` — dev-first IDP.
|
||||
- `supertokens-with-postgresql` — session/auth backend.
|
||||
|
||||
Requesting anything outside this list returns 400 with a hint listing the
|
||||
allowed ones, so the agent can self-correct.
|
||||
|
||||
### 3.6 Domain tools (P5.1 — custom apex domains)
|
||||
|
||||
Custom apex domains are owned end-to-end by Vibn: the registrar is OpenSRS
|
||||
(Tucows), authoritative DNS is Google Cloud DNS in the Canadian project, and
|
||||
domains are pinned to the workspace that registered them. All four lifecycle
|
||||
steps — search, register, attach, inspect — are agent-callable.
|
||||
|
||||
| Tool | Purpose | Params |
|
||||
|---|---|---|
|
||||
| `domains.search` | Check availability + price for one or more candidate apex domains via OpenSRS. Stateless; does not reserve anything. | `{ names: string[], period?: number }` — `names` up to 25, `period` in years (auto-bumped for quirky TLDs like `.ai` which requires 2y minimum). |
|
||||
| `domains.register` | Register a domain through OpenSRS. Registers unlocked; locking happens automatically after `domains.attach` completes. Idempotent per `(workspace, domain)`. | `{ domain, period?, whoisPrivacy?, contact, nameservers?, ca?: { cprCategory, legalType } }` — `ca.*` required for `.ca`. |
|
||||
| `domains.list` | List all domains owned by the workspace with their status, registrar order id, expiry, and DNS provider/zone. | — |
|
||||
| `domains.get` | Full record + last 20 lifecycle events. | `{ domain }` |
|
||||
| `domains.attach` | Wire a registered domain to a Coolify app (or arbitrary IP/CNAME): create Cloud DNS zone, write A/CNAME rrsets, update registrar-side nameservers, append FQDNs to the Coolify app's domain list. Idempotent; safe to retry. | `{ domain, appUuid? \| ip? \| cname?, subdomains?: string[] (default ["@","www"]), updateRegistrarNs? }` |
|
||||
|
||||
**Residency note:** Cloud DNS is global anycast — configuration is not
|
||||
Canadian-pinned at the storage layer. The workspace-level `dns_provider`
|
||||
flag (default `cloud_dns`) will let us swap in CIRA D-Zone for strict
|
||||
Canadian residency without touching the MCP surface.
|
||||
|
||||
**Billing:** Every successful `domains.register` writes a `debit` row to
|
||||
`vibn_billing_ledger` with the OpenSRS order id as `ref_id`. The
|
||||
`vibn_domain_events` table keeps an append-only audit of every lifecycle
|
||||
call (`register.attempt`, `register.success`, `register.failed`,
|
||||
`attach.success`).
|
||||
|
||||
**Verified end-to-end (2026-04-22)** against PROD GCP + OpenSRS sandbox +
|
||||
PROD Coolify (Coolify `v4.0.0-beta.473`); see
|
||||
`vibn-frontend/scripts/smoke-attach-e2e.ts`. **All 5 sub-systems green.**
|
||||
|
||||
- ✓ OpenSRS register against Horizon (sandbox) returns order id, response 200.
|
||||
- ✓ Cloud DNS managed zone created in `master-ai-484822` with public anycast NS.
|
||||
- ✓ A records (`@`, `www`) written to the zone.
|
||||
- ✓ Registrar-side nameserver update accepts Cloud DNS NS values
|
||||
(trailing-dot normalization in `lib/opensrs.ts`); sandbox returns 480
|
||||
because its mock registry doesn't know real Google NS hosts, which is
|
||||
expected — live mode talks to real registries that accept any resolvable NS.
|
||||
- ✓ Unlock → update NS → relock fallback path verified (sandbox-recognized
|
||||
nameservers return 200; the unlock/relock sequence is exercised when the
|
||||
registry returns 405 lock-conflict).
|
||||
- ✓ Coolify domain-list PATCH adds the apex + `www` to the application
|
||||
`fqdn` column and the smoke test re-fetches it to confirm.
|
||||
|
||||
> **Operational gotcha — the destination server must be proxy-enabled.**
|
||||
> Coolify's `update_by_uuid` controller accepts `domains` as a comma-separated
|
||||
> list and only maps it onto the model's `fqdn` column when the destination
|
||||
> server's `Server::isProxyShouldRun()` returns `true`. That helper requires
|
||||
> **both** `proxy.type ∈ {TRAEFIK, CADDY}` *and* `is_build_server = false`.
|
||||
> If either is misconfigured the PATCH returns 200 but the field is silently
|
||||
> dropped (Laravel mass-assignment ignores `domains` because it isn't in
|
||||
> `$fillable`, and the controller never copies it into `fqdn`). We hit this
|
||||
> on `coolify-server-mtl` (`zg4cwgc44ogc08804000gggo`), which had
|
||||
> `proxy=null` and `is_build_server=true`. Fixed by:
|
||||
>
|
||||
> ```sql
|
||||
> UPDATE servers
|
||||
> SET proxy = jsonb_set(coalesce(proxy,'{}'::jsonb), '{type}', '"TRAEFIK"')
|
||||
> WHERE uuid = 'zg4cwgc44ogc08804000gggo';
|
||||
> UPDATE server_settings
|
||||
> SET is_build_server = false
|
||||
> WHERE server_id = (SELECT id FROM servers WHERE uuid = 'zg4cwgc44ogc08804000gggo');
|
||||
> ```
|
||||
>
|
||||
> followed by `docker restart coolify` to clear Laravel's in-memory config.
|
||||
> Sending `fqdn` directly is **not** an alternative — the controller's
|
||||
> `$allowedFields` whitelist rejects it with 422 "This field is not allowed."
|
||||
|
||||
### 3.7 Agent-side stdio MCP servers (`vibn-agent-runner`)
|
||||
|
||||
Separate from the control-plane MCP at `/api/mcp` (which is what external
|
||||
agents call *into* Vibn), the `vibn-agent-runner` exposes its own in-house
|
||||
tool surface *outward* over stdio MCP. This lets Cursor, Claude Desktop,
|
||||
Goose, or any MCP-speaking client drive the same Coolify / Gitea / workspace
|
||||
tooling the Coder/PM/Marketing sub-agents use internally — with the same
|
||||
protected-repo and protected-app guardrails enforced centrally.
|
||||
|
||||
Architecture: every tool now has three touch-points backed by one source of truth:
|
||||
|
||||
```
|
||||
vibn-agent-runner/src/tools/<domain>-api.ts ← pure, config-agnostic logic + security guards
|
||||
vibn-agent-runner/src/tools/<domain>.ts ← thin registerTool() wrappers for the in-process agent loop
|
||||
vibn-agent-runner/src/mcp/<domain>-server.ts ← stdio MCP server for external clients
|
||||
```
|
||||
|
||||
| Server | Tools | Required env |
|
||||
|---|---|---|
|
||||
| `vibn-coolify-mcp` | 7 — list_projects, list_applications, deploy, get_logs, list_all_apps, get_app_status, deploy_app | `COOLIFY_API_URL`, `COOLIFY_API_TOKEN` |
|
||||
| `vibn-gitea-mcp` | 6 — create/list/close issues, list_repos, list_all_issues, read_repo_file | `GITEA_API_URL`, `GITEA_API_TOKEN`, `GITEA_USERNAME` |
|
||||
| `vibn-workspace-mcp` | 8 — read/write/replace/list/find/search_code, execute_command, git_commit_and_push | `WORKSPACE_ROOT` (+ Gitea creds for git push) |
|
||||
| `vibn-platform-mcp` | 7 — save_memory, list_memory, list_skills, get_skill, finalize_prd, get_prd, web_search | `SESSION_KEY` (optional), Gitea creds (for skills) |
|
||||
| `vibn-agent-mcp` | 2 — spawn_agent, get_job_status (dispatches into the runner's HTTP API) | `AGENT_RUNNER_URL` (defaults to `http://localhost:3333`) |
|
||||
|
||||
Run locally with `npm run mcp:<name>` (or `:dev` via ts-node) in
|
||||
`vibn-agent-runner/`. Smoke-test any server with
|
||||
`node scripts/smoke-mcp.js <name>`. The in-process agent loop still sees
|
||||
the same 28 registered tools — no behavioral regression.
|
||||
|
||||
---
|
||||
|
||||
## 4. REST surface
|
||||
|
||||
Every MCP tool is also exposed as a plain HTTP endpoint under
|
||||
`/api/workspaces/{slug}/…`. Agents that prefer curl-style access can use
|
||||
these directly; the shape is identical to the MCP `params`. Auth is the
|
||||
same bearer header.
|
||||
|
||||
### 4.1 Workspace & key management
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/workspaces` | All workspaces the principal has access to. |
|
||||
| GET | `/api/workspaces/{slug}` | Workspace details. |
|
||||
| POST | `/api/workspaces/{slug}/provision` | Idempotent re-run of Gitea org + bot + SSH keypair + Coolify project setup. |
|
||||
| GET | `/api/workspaces/{slug}/keys` | List API keys (metadata only). |
|
||||
| POST | `/api/workspaces/{slug}/keys` | Mint a new API key. Full token returned once. |
|
||||
| DELETE | `/api/workspaces/{slug}/keys/{keyId}` | Revoke a key. |
|
||||
| GET | `/api/workspaces/{slug}/gitea-credentials` | Return bot username, PAT (decrypted), clone/SSH templates. |
|
||||
| GET | `/api/workspaces/{slug}/bootstrap.sh` | Shell script that writes `.cursor/rules`, `.cursor/mcp.json`, `.env.local` into the cwd. |
|
||||
|
||||
### 4.2 Applications
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/workspaces/{slug}/apps` | List apps. |
|
||||
| POST | `/api/workspaces/{slug}/apps` | Create an app from a workspace repo. |
|
||||
| GET | `/api/workspaces/{slug}/apps/{uuid}` | App details. |
|
||||
| PATCH | `/api/workspaces/{slug}/apps/{uuid}` | Update whitelisted fields. |
|
||||
| DELETE | `/api/workspaces/{slug}/apps/{uuid}?confirm=<exact-name>` | Destroy app. |
|
||||
| POST | `/api/workspaces/{slug}/apps/{uuid}/deploy` | Trigger deploy. |
|
||||
| GET | `/api/workspaces/{slug}/apps/{uuid}/deployments` | List deployments. |
|
||||
| GET | `/api/workspaces/{slug}/apps/{uuid}/domains` | List domains. |
|
||||
| PATCH | `/api/workspaces/{slug}/apps/{uuid}/domains` | Replace domain set. |
|
||||
| GET | `/api/workspaces/{slug}/apps/{uuid}/envs` | List env vars. |
|
||||
| PATCH | `/api/workspaces/{slug}/apps/{uuid}/envs` | Upsert env var(s). |
|
||||
| DELETE | `/api/workspaces/{slug}/apps/{uuid}/envs?key=FOO` | Delete env var. |
|
||||
| GET | `/api/workspaces/{slug}/deployments/{deploymentUuid}/logs` | Deployment logs. |
|
||||
|
||||
### 4.3 Databases
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/workspaces/{slug}/databases` | List databases. |
|
||||
| POST | `/api/workspaces/{slug}/databases` | Create a database (8 flavors). |
|
||||
| GET | `/api/workspaces/{slug}/databases/{uuid}` | Database details + internal connection URL. |
|
||||
| PATCH | `/api/workspaces/{slug}/databases/{uuid}` | Update fields. |
|
||||
| DELETE | `/api/workspaces/{slug}/databases/{uuid}?confirm=<exact-name>` | Destroy database. |
|
||||
|
||||
### 4.4 Auth providers
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| GET | `/api/workspaces/{slug}/auth` | List deployed auth providers + the allowlist. |
|
||||
| POST | `/api/workspaces/{slug}/auth` | Provision a provider from the allowlist. |
|
||||
| GET | `/api/workspaces/{slug}/auth/{uuid}` | Provider details. |
|
||||
| DELETE | `/api/workspaces/{slug}/auth/{uuid}?confirm=<exact-name>` | Destroy provider. |
|
||||
|
||||
### 4.5 Domains (P5.1)
|
||||
|
||||
| Method | Path | Description |
|
||||
|---|---|---|
|
||||
| POST | `/api/workspaces/{slug}/domains/search` | Availability + pricing for up to 25 candidate names. |
|
||||
| GET | `/api/workspaces/{slug}/domains` | List workspace-owned domains. |
|
||||
| POST | `/api/workspaces/{slug}/domains` | Register a domain (idempotent per `(workspace, domain)`). |
|
||||
| GET | `/api/workspaces/{slug}/domains/{domain}` | Full record + last 20 events. |
|
||||
| POST | `/api/workspaces/{slug}/domains/{domain}/attach` | Create Cloud DNS zone, write records, update registrar NS, wire Coolify domain list. |
|
||||
|
||||
---
|
||||
|
||||
## 5. Gitea surface
|
||||
|
||||
AI agents **never** talk to the root Gitea admin token. They use the
|
||||
workspace's dedicated bot user.
|
||||
|
||||
### 5.1 What the bot can do
|
||||
|
||||
- Fully own the `vibn-{slug}` org (added as the org's owner team).
|
||||
- Read/write every repo in that org via its PAT.
|
||||
- Push over SSH using the workspace's ed25519 deploy key (same keypair
|
||||
Coolify uses to pull code).
|
||||
- What it **cannot** do: touch any other org, the root admin surface, or
|
||||
Gitea's `/admin/*` endpoints.
|
||||
|
||||
### 5.2 How to get the bot credentials
|
||||
|
||||
```http
|
||||
GET /api/workspaces/{slug}/gitea-credentials
|
||||
Authorization: Bearer vibn_sk_…
|
||||
```
|
||||
|
||||
Returns:
|
||||
|
||||
```json
|
||||
{
|
||||
"bot": { "username": "mark-bot", "token": "…" },
|
||||
"gitea": {
|
||||
"apiBase": "https://git.vibnai.com/api/v1",
|
||||
"host": "git.vibnai.com",
|
||||
"cloneUrlTemplate": "https://mark-bot:{{token}}@git.vibnai.com/vibn-mark/{{repo}}.git",
|
||||
"sshRemoteTemplate": "git@git.vibnai.com:vibn-mark/{{repo}}.git",
|
||||
"webUrlTemplate": "https://git.vibnai.com/vibn-mark/{{repo}}"
|
||||
},
|
||||
"workspace": { "slug": "mark", "giteaOrg": "vibn-mark" }
|
||||
}
|
||||
```
|
||||
|
||||
The PAT is stored **encrypted at rest** using AES-256-GCM with the
|
||||
`VIBN_SECRETS_KEY` server secret; the decrypt step runs only on this endpoint.
|
||||
|
||||
### 5.3 Gitea operations via the standard Gitea API
|
||||
|
||||
Once the agent has `{bot.token, gitea.apiBase}`, it can call any standard
|
||||
Gitea v1 endpoint as the bot, scoped to the workspace org. Common ones:
|
||||
|
||||
- `POST /orgs/{org}/repos` — create a repo.
|
||||
- `PATCH /repos/{org}/{repo}` — update repo settings.
|
||||
- `GET /repos/{org}/{repo}/contents/{path}` — read files.
|
||||
- `PUT /repos/{org}/{repo}/contents/{path}` — write files (commits).
|
||||
- `POST /repos/{org}/{repo}/pulls` — open PRs.
|
||||
- `POST /repos/{org}/{repo}/branches` — create branches.
|
||||
|
||||
---
|
||||
|
||||
## 6. Domain policy
|
||||
|
||||
Every app gets an auto-generated domain under the workspace's namespace:
|
||||
|
||||
```
|
||||
{app-slug}.{workspace-slug}.vibnai.com
|
||||
```
|
||||
|
||||
For example, creating an app named `my-api` in workspace `mark` yields
|
||||
`my-api.mark.vibnai.com` automatically — no DNS config, no cert work,
|
||||
served by Coolify's wildcard Traefik.
|
||||
|
||||
### 6.1 What agents can do
|
||||
|
||||
- Accept the auto-generated domain (default path).
|
||||
- Replace the domain set via `PATCH /apps/{uuid}/domains`, provided every
|
||||
entry ends with `.{workspace-slug}.vibnai.com`.
|
||||
|
||||
### 6.2 What agents cannot do
|
||||
|
||||
- Point an app at a domain outside the workspace's namespace. The server
|
||||
rejects this with 403 regardless of DNS state:
|
||||
|
||||
```json
|
||||
{ "error": "Domain evil.com is not allowed; must end with .mark.vibnai.com",
|
||||
"hint": "Use my-api.mark.vibnai.com" }
|
||||
```
|
||||
|
||||
This is enforced by `isDomainUnderWorkspace()` in
|
||||
[`lib/naming.ts`](./vibn-frontend/lib/naming.ts).
|
||||
|
||||
### 6.3 Custom (external) domains
|
||||
|
||||
Not exposed to AI agents. A human can still add them through Coolify
|
||||
directly or through a future human-gated UI.
|
||||
|
||||
---
|
||||
|
||||
## 7. Safety model
|
||||
|
||||
### 7.1 Tenant enforcement
|
||||
|
||||
Every resource-returning helper in `lib/coolify.ts` runs through
|
||||
`ensureResourceInProject()`. It:
|
||||
|
||||
1. Trusts an explicit `project_uuid` on the resource if present, else
|
||||
2. Fetches the project's environment ids via `GET /projects/{uuid}` and
|
||||
verifies the resource's `environment_id` is in that set.
|
||||
|
||||
A token for `mark` that tries to read an app in `justine`'s project returns:
|
||||
|
||||
```json
|
||||
{ "error": "Application <uuid> does not belong to project <mark-project-uuid>" }
|
||||
```
|
||||
|
||||
with HTTP 403. Cross-workspace enumeration and access are not just
|
||||
discouraged — they fail at the helper level.
|
||||
|
||||
### 7.2 Destructive operations
|
||||
|
||||
Every delete endpoint requires `?confirm=<exact-resource-name>`:
|
||||
|
||||
```
|
||||
DELETE /apps/{uuid} → 409 "confirmation required"
|
||||
DELETE /apps/{uuid}?confirm=wrong → 409 "confirmation required"
|
||||
DELETE /apps/{uuid}?confirm=my-api → 200 deleted
|
||||
```
|
||||
|
||||
This means an agent hallucinating a delete call cannot cost you the
|
||||
resource — it must first know the exact name, which implies it just listed
|
||||
or just created it.
|
||||
|
||||
**Volumes are kept by default** on delete. To also remove volumes, pass
|
||||
`?volumes=delete` (apps/dbs) — this is opt-in, per-call, never the default.
|
||||
|
||||
### 7.3 Creation guardrails
|
||||
|
||||
- Apps can only be created from repos in the workspace's Gitea org.
|
||||
- Auth providers can only be created from the allowlist (see §3.5).
|
||||
- Database flavors are restricted to the 8 Coolify supports.
|
||||
- Env var keys must match `/^[A-Z_][A-Z0-9_]*$/` (no shell-escape tricks).
|
||||
|
||||
### 7.4 Secrets handling
|
||||
|
||||
- `VIBN_API_KEY` is only shown **once** on mint. Server keeps a sha256 hash.
|
||||
- Gitea bot PATs are **encrypted at rest** (AES-256-GCM with
|
||||
`VIBN_SECRETS_KEY`).
|
||||
- The SSH private key is held by Coolify, not by Vibn; the public key is
|
||||
pushed to the Gitea bot user's key list. Rotating is a re-provision.
|
||||
- Agent prompts and Cursor rules include a "treat VIBN_API_KEY like a
|
||||
password — never print or commit it" directive.
|
||||
|
||||
---
|
||||
|
||||
## 8. Worked examples
|
||||
|
||||
### 8.1 "Build me a Next.js app with a Postgres and Pocketbase auth"
|
||||
|
||||
From the agent's side, using MCP:
|
||||
|
||||
```json
|
||||
// 1. Ensure a repo exists in the workspace org (standard Gitea API,
|
||||
// using the bot PAT from gitea.credentials).
|
||||
POST https://git.vibnai.com/api/v1/orgs/vibn-mark/repos
|
||||
{ "name": "my-site", "private": true, "auto_init": true }
|
||||
|
||||
// 2. Create the Coolify app. Auto-domain my-site.mark.vibnai.com.
|
||||
{ "tool": "apps.create",
|
||||
"params": { "repo": "my-site", "ports": "3000", "instantDeploy": false } }
|
||||
|
||||
// 3. Provision a Postgres.
|
||||
{ "tool": "databases.create",
|
||||
"params": { "type": "postgresql", "name": "app-db" } }
|
||||
// → returns { internalUrl: "postgres://…@<uuid>:5432/postgres" }
|
||||
|
||||
// 4. Wire the db URL into the app as an env var.
|
||||
{ "tool": "apps.envs.upsert",
|
||||
"params": { "uuid": "<app-uuid>", "key": "DATABASE_URL",
|
||||
"value": "<internalUrl>" } }
|
||||
|
||||
// 5. Deploy Pocketbase as the auth layer.
|
||||
{ "tool": "auth.create",
|
||||
"params": { "provider": "pocketbase", "name": "auth" } }
|
||||
|
||||
// 6. First real deploy.
|
||||
{ "tool": "apps.deploy", "params": { "uuid": "<app-uuid>" } }
|
||||
|
||||
// 7. Poll.
|
||||
{ "tool": "apps.deployments", "params": { "uuid": "<app-uuid>" } }
|
||||
// → [{ uuid, status: "finished" | "in_progress" | "failed" | "queued" }]
|
||||
```
|
||||
|
||||
The agent hands the user back `https://my-site.mark.vibnai.com`.
|
||||
|
||||
### 8.2 "Add an `api` subdomain to my app"
|
||||
|
||||
```json
|
||||
{ "tool": "apps.domains.set",
|
||||
"params": {
|
||||
"uuid": "<app-uuid>",
|
||||
"domains": ["my-site.mark.vibnai.com", "api.mark.vibnai.com"]
|
||||
} }
|
||||
```
|
||||
|
||||
Valid — both end with `.mark.vibnai.com`. `evil.com` or `my-site.justine.vibnai.com`
|
||||
would return 403.
|
||||
|
||||
### 8.3 "Delete the whole thing"
|
||||
|
||||
Agent must learn the resource names first (or it'll hit the confirm gate):
|
||||
|
||||
```json
|
||||
// Learn the name.
|
||||
{ "tool": "apps.get", "params": { "uuid": "<app-uuid>" } }
|
||||
// → { name: "my-site", ... }
|
||||
|
||||
// Delete with matching confirm.
|
||||
{ "tool": "apps.delete",
|
||||
"params": { "uuid": "<app-uuid>", "confirm": "my-site" } }
|
||||
```
|
||||
|
||||
Wrong confirm returns `409 "Confirmation required"`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Error handling reference
|
||||
|
||||
| Status | Meaning | What the agent should do |
|
||||
|---|---|---|
|
||||
| 400 | Bad request body (invalid JSON, missing required field, invalid type). | Fix the body, retry. |
|
||||
| 401 | No / bad bearer token. | Ask the user to mint a fresh key. |
|
||||
| 403 | **Tenant mismatch** — resource belongs to another workspace, domain outside workspace namespace, or repo not in workspace org. | **Stop.** Do not retry with guessed values. Ask the user. |
|
||||
| 404 | Resource not found (app/db/service/repo uuid wrong). | Re-list to find the right uuid. |
|
||||
| 409 | Delete confirmation missing or wrong. | Fetch the resource name first, then retry with `confirm=<name>`. |
|
||||
| 422 | Coolify validation failure (e.g. malformed domain). | Check the `details` field. |
|
||||
| 502 | Upstream Coolify/Gitea error. | Retry with backoff. |
|
||||
| 503 | Workspace not fully provisioned yet. | Call `POST /provision`, then retry. |
|
||||
|
||||
---
|
||||
|
||||
## 10. Versioning
|
||||
|
||||
The MCP descriptor at `GET /api/mcp` reports a semver `version`. Tool names
|
||||
are append-only within a major version — agents can cache the tool list
|
||||
safely for the duration of a conversation but should re-fetch on 404.
|
||||
|
||||
Current version: **2.1.0**.
|
||||
|
||||
- **1.x** — session-cookie-only MCP, no tenant keys.
|
||||
- **2.0** — `vibn_sk_…` keys, workspace-scoped Gitea bot + Coolify project.
|
||||
- **2.1** — create/update/delete for apps, 8 database flavors, auth
|
||||
provider allowlist, domain policy enforcement, confirm-gated deletes.
|
||||
|
||||
---
|
||||
|
||||
## 11. Where to look in the code
|
||||
|
||||
- `lib/auth/workspace-auth.ts` — `requireWorkspacePrincipal`, the gate.
|
||||
- `lib/auth/secret-box.ts` — AES-256-GCM encryption of Gitea PATs.
|
||||
- `lib/workspaces.ts` — `ensureWorkspaceProvisioned` (the idempotent setup).
|
||||
- `lib/gitea.ts` — Gitea client (orgs, users, PATs, SSH keys).
|
||||
- `lib/coolify.ts` — Coolify client, tenant helpers, all resource CRUD.
|
||||
- `lib/naming.ts` — domain policy, slugify, SSH URL templates.
|
||||
- `lib/ssh-keys.ts` — ed25519 keypair generation + OpenSSH formatting.
|
||||
- `app/api/workspaces/[slug]/…` — REST surface.
|
||||
- `app/api/mcp/route.ts` — MCP dispatcher and tool implementations.
|
||||
- `components/workspace/WorkspaceKeysPanel.tsx` — settings UI.
|
||||
Reference in New Issue
Block a user