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
This commit is contained in:
2026-04-21 12:04:59 -07:00
parent b51fb6da21
commit 0797717bc1
14 changed files with 2274 additions and 118 deletions

View File

@@ -334,6 +334,54 @@ export async function ensureOrgTeamMembership(opts: {
return team;
}
// ──────────────────────────────────────────────────
// Admin: SSH keys on a user (for Coolify deploy-key flow)
// ──────────────────────────────────────────────────
export interface GiteaSshKey {
id: number;
key: string;
title: string;
fingerprint?: string;
read_only?: boolean;
}
/**
* Register an SSH public key under a target user via the admin API.
* The resulting key gives anyone holding the matching private key the
* same repo-read access as the user (bounded by that user's team
* memberships — for bots, usually read/write on one org only).
*/
export async function adminAddUserSshKey(opts: {
username: string;
title: string;
key: string; // OpenSSH-format public key, e.g. "ssh-ed25519 AAAAC3... comment"
readOnly?: boolean;
}): Promise<GiteaSshKey> {
return giteaFetch(`/admin/users/${opts.username}/keys`, {
method: 'POST',
body: JSON.stringify({
title: opts.title,
key: opts.key,
read_only: opts.readOnly ?? false,
}),
});
}
/**
* List SSH keys for a user (admin view).
*/
export async function adminListUserSshKeys(username: string): Promise<GiteaSshKey[]> {
return giteaFetch(`/users/${username}/keys`);
}
/**
* Delete an SSH key by id (owned by a user). Used when rotating keys.
*/
export async function adminDeleteUserSshKey(keyId: number): Promise<void> {
await giteaFetch(`/admin/users/keys/${keyId}`, { method: 'DELETE' });
}
/**
* Get an existing repo.
*/