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
This commit is contained in:
2026-04-23 12:21:00 -07:00
parent 3192e0f7b9
commit fcd5d03894
5 changed files with 244 additions and 12 deletions

View File

@@ -155,6 +155,7 @@ export interface GiteaUser {
login: string;
full_name?: string;
email?: string;
active?: boolean;
}
/**
@@ -168,7 +169,7 @@ export async function createUser(opts: {
password: string;
fullName?: string;
}): Promise<GiteaUser> {
return giteaFetch(`/admin/users`, {
const created = await giteaFetch(`/admin/users`, {
method: 'POST',
body: JSON.stringify({
username: opts.username,
@@ -180,6 +181,26 @@ export async function createUser(opts: {
source_id: 0,
}),
});
// Gitea's admin-create endpoint returns users with `active=false` by
// default (a quirk of the admin API — UI-created users skip email
// verification but API-created ones don't). Inactive users fail
// permission checks and cannot clone private repos, so we flip the
// flag immediately via a PATCH. Idempotent: a second call is a noop.
try {
await giteaFetch(`/admin/users/${opts.username}`, {
method: 'PATCH',
body: JSON.stringify({
source_id: 0,
login_name: opts.username,
active: true,
}),
});
(created as GiteaUser).active = true;
} catch (err) {
console.warn('[gitea] failed to activate bot user', opts.username, err);
}
return created;
}
/**