feat(sessions): replace body cloneUrl+branch with organizationId
Merged
SW
5mo ago+140+140+012345678901234567890123456789-228-228-012345678901234567890123456789
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
| Body | Workspace repo (server-derived) |
|---|---|
{} | recoupable/<auth.accountId> (personal) |
{ organizationId: "<uuid>" } | recoupable/<organizationId> (org-access validated) |
What changed
Removed:
cloneUrlfromcreateSessionBodySchema— api owns URL construction now (viaensurePersonalRepo).branchfromcreateSessionBodySchema— was only stored onsessions.branch; the sandbox handler already falls back to the repo's default branch when null.lib/sessions/resolveSessionCloneUrl.ts+ tests — inlined intocreateSessionHandler(it was a 3-line wrapper aroundensurePersonalRepoaftercloneUrlsupport was dropped).
Added:
organizationId: z.string().uuid().optional()oncreateSessionBodySchema.validateCreateSessionBodynow forwardsorganizationIdtovalidateAuthContext, which already had theinput.organizationIdpath (validates org access + setsauth.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-uuidorganizationIdrejected with 400;organizationIdforwarded tovalidateAuthContext.createSessionHandler.persistence.test.ts— adds: personal session callsensurePersonalRepo({ accountId: auth.accountId }); org session callsensurePersonalRepo({ accountId: auth.orgId }); 502 on ensure failure.buildSessionInsertRow.test.ts— adds: branch column is always null now.- 3,322 / 3,322 tests pass;
next buildTS 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 sendorganizationId(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, returnsrecoupable/<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_urlviaensurePersonalRepotorecoupable/<auth.orgId ?? auth.accountId>. - When
organizationIdis provided, org access is validated andauth.orgIdis set. sessions.branchis always null; sandbox is hard-coded to{ type: "vercel" }.
- Request shape:
-
Migration
- Stop sending
cloneUrl,branch,sandboxType,repoOwner,repoName,isNewBranch. - Personal session: send
{}. Org session: send{ organizationId }. organizationIdmust be a UUID; 400 if invalid, 403 if no org access.
- Stop sending