feat(sessions): replace body cloneUrl+branch with organizationId

Merged
SW
5mo ago
+140-228

Summary

Pairs with recoupable/docs#226. Replaces the legacy cloneUrl/branch fields on POST /api/sessions with a single optional organizationId. The api now derives the workspace repo URL itself in 100% of cases — callers never construct GitHub URLs.

New request shape

{
  title?: string;
  organizationId?: string;  // uuid; omit for personal session
  sandboxType?: "vercel";
}

Contract

BodyWorkspace repo (server-derived)
{}recoupable/<auth.accountId> (personal)
{ organizationId: "<uuid>" }recoupable/<organizationId> (org-access validated)

What changed

Removed:

  • cloneUrl from createSessionBodySchema — api owns URL construction now (via ensurePersonalRepo).
  • branch from createSessionBodySchema — was only stored on sessions.branch; the sandbox handler already falls back to the repo's default branch when null.
  • lib/sessions/resolveSessionCloneUrl.ts + tests — inlined into createSessionHandler (it was a 3-line wrapper around ensurePersonalRepo after cloneUrl support was dropped).

Added:

  • organizationId: z.string().uuid().optional() on createSessionBodySchema.
  • validateCreateSessionBody now forwards organizationId to validateAuthContext, which already had the input.organizationId path (validates org access + sets auth.orgId).

Inlined logic in createSessionHandler:

const workspaceAccountId = auth.orgId ?? auth.accountId;
const cloneUrl = await ensurePersonalRepo({ accountId: workspaceAccountId });
if (!cloneUrl) return 502;

Test coverage

  • validateCreateSessionBody.test.ts — adds: non-uuid organizationId rejected with 400; organizationId forwarded to validateAuthContext.
  • createSessionHandler.persistence.test.ts — adds: personal session calls ensurePersonalRepo({ accountId: auth.accountId }); org session calls ensurePersonalRepo({ accountId: auth.orgId }); 502 on ensure failure.
  • buildSessionInsertRow.test.ts — adds: branch column is always null now.
  • 3,322 / 3,322 tests pass; next build TS phase clean.

Docs drift cleanup

The OpenAPI spec also documented repoOwner, repoName, isNewBranch which the api never actually accepted (Zod silently dropped them). recoupable/docs#226 removes them too.

Caller-side rollout

  • ✅ open-agents personal flow (already on main since #44): sends POST /api/sessions {} — works identically with the new schema.
  • ⏳ open-agents org flow (next PR): will send POST /api/sessions { organizationId }. Blocked on this PR landing.
  • ⏳ chat #1748 (not yet merged): currently sends client-built cloneUrl. Will need to update to either omit (personal) or send organizationId (active org) before merging.

Test plan

  • Preview: POST /api/sessions {} with Privy Bearer (no org) → 200, returns canonical personal cloneUrl
  • Preview: POST /api/sessions {"organizationId":"<uuid you have access to>"} → 200, returns recoupable/<orgId>
  • Preview: POST /api/sessions {"organizationId":"<uuid you DON'T have access to>"} → 403
  • Preview: POST /api/sessions {"organizationId":"not-a-uuid"} → 400

🤖 Generated with Claude Code


Summary by cubic

Replaces cloneUrl and branch on POST /api/sessions with an optional organizationId, and drops sandboxType. The API now derives the workspace repo URL and always uses the default branch, so callers never build GitHub URLs.

  • New Features

    • Request shape: { title?, organizationId? }.
    • Server sets clone_url via ensurePersonalRepo to recoupable/<auth.orgId ?? auth.accountId>.
    • When organizationId is provided, org access is validated and auth.orgId is set.
    • sessions.branch is always null; sandbox is hard-coded to { type: "vercel" }.
  • Migration

    • Stop sending cloneUrl, branch, sandboxType, repoOwner, repoName, isNewBranch.
    • Personal session: send {}. Org session: send { organizationId }.
    • organizationId must be a UUID; 400 if invalid, 403 if no org access.