Skip to content

feat: org selector for faster sandbox startup#7

Merged
sweetmantech merged 5 commits into
mainfrom
feat/org-selector-faster-sandbox
Apr 22, 2026
Merged

feat: org selector for faster sandbox startup#7
sweetmantech merged 5 commits into
mainfrom
feat/org-selector-faster-sandbox

Conversation

@recoup-coding-agent
Copy link
Copy Markdown
Contributor

@recoup-coding-agent recoup-coding-agent commented Apr 22, 2026

Summary

  • Replaces the "New Session" button with an Org Selector UI so users pick a specific organization before creating a sandbox
  • Only the selected org's submodule is initialized instead of all org submodules, reducing sandbox startup from 2+ minutes to seconds
  • Adds orgSlug column to the sessions table (migration 0031) to persist the selected org

Changes

  • New: GET /api/orgs endpoint, OrgSelector component, useOrgs hook, fetchAccountOrgs lib
  • Modified: Sandbox creation flow passes orgSlug through validation → handler → SDK
  • Modified: initSubmodules selectively inits only .openclaw/workspace/orgs/<orgSlug> when provided
  • DB: Added org_slug column to sessions schema

Test plan

  • Verify org selector loads and displays orgs from Recoupable API
  • Click an org → sandbox creates with only that org's submodule initialized
  • Confirm sandbox startup time is significantly faster than cloning all submodules
  • Verify org_slug is persisted in the sessions table

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added organization selector component for selecting organizations when creating new sessions.
    • Sessions now support organization assignment for targeted resource initialization.
  • Improvements

    • Optimized sandbox startup time by enabling organization-scoped submodule initialization.
  • Database

    • Added organization slug field to sessions table.

Summary by cubic

Added an Org Selector to create sandboxes by picking an org first. We now clone the selected org’s recoupable GitHub repo with GITHUB_TOKEN, cutting startup from minutes to seconds; the Inbox “+” button routes to the selector.

  • New Features

    • OrgSelector on the empty Sessions page; useOrgs pulls orgs from the Recoupable API, buildOrgRepoUrl builds the per‑org repo URL, and sessions start via createBlankSession(cloneUrl).
    • /api/sandbox now requires repoUrl and clones that repo.
  • Refactors

    • Removed account‑repo fallback and GitHub user‑token flow; server always uses GITHUB_TOKEN. Deleted submodule init code/tests and its sandbox call site.
    • Dropped the /api/orgs passthrough; useOrgs calls the Recoupable API directly. Inbox “New Session” now navigates to /sessions. Rolled back the org_slug migration and persist via sessions.cloneUrl.

Written for commit 423472e. Summary will update on new commits.

… startup

Instead of cloning all org submodules on sandbox creation (2+ min),
users now select a specific org from a selector UI. Only that org's
submodule is initialized, dramatically reducing sandbox startup time.

- Add GET /api/orgs endpoint to fetch orgs from Recoupable API
- Add OrgSelector component with org buttons, icons, and loading states
- Add orgSlug field to sessions schema (migration 0031)
- Pass orgSlug through sandbox creation flow for selective submodule init
- Update initSubmodules to only init the selected org's submodule

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 22, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
open-agents Ready Ready Preview Apr 22, 2026 4:22pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

📝 Walkthrough

Walkthrough

This PR adds organization slug support throughout the session and sandbox creation flow. It introduces an organization selector UI, threads orgSlug through session creation APIs and database persistence, and enables org-scoped submodule initialization during sandbox provisioning on Vercel.

Changes

Cohort / File(s) Summary
Session API and Context
apps/web/app/api/sessions/route.ts, apps/web/app/sessions/sessions-shell-context.tsx, apps/web/hooks/use-sessions.ts
Extended session creation request interface to accept optional orgSlug field; updated context type to pass orgSlug to createBlankSession callback.
Session UI Components
apps/web/app/sessions/sessions-index-shell.tsx, apps/web/app/sessions/sessions-route-shell.tsx
Replaced auto-provisioning logic with org-driven flow; updated createBlankSession to accept orgSlug and route through organization selection when needed.
Organization Selection
apps/web/components/org-selector.tsx, apps/web/hooks/use-orgs.ts
New organization selector component with loading/error states and org grid UI; new hook fetching organizations via Recoupable API with SWR caching.
Recoupable Integration
apps/web/lib/recoupable/fetch-account-orgs.ts
New module handling authenticated requests to Recoupable API to fetch user's organizations with Zod validation and error handling.
Database Schema and Migrations
apps/web/lib/db/schema.ts, apps/web/lib/db/migrations/0031_ambiguous_martin_li.sql, apps/web/lib/db/migrations/meta/0031_snapshot.json, apps/web/lib/db/migrations/meta/_journal.json
Added nullable org_slug text column to sessions table via migration; updated schema definition and migration snapshots.
Sandbox Creation Flow
apps/web/lib/sandbox/create-sandbox.ts, apps/web/lib/sandbox/create-sandbox-handler.ts, apps/web/lib/sandbox/validate-create-sandbox-body.ts, apps/web/lib/sandbox/resolve-account-repo-source.ts
Extended sandbox creation to accept and pass through optional orgSlug in request validation, handler processing, and resolved source data.
Sandbox Configuration and Initialization
packages/sandbox/types.ts, packages/sandbox/vercel/config.ts, packages/sandbox/vercel/connect.ts, packages/sandbox/vercel/init-submodules.ts, packages/sandbox/vercel/sandbox.ts
Updated sandbox source and config types to include orgSlug; modified initSubmodules to target single org's submodule path when orgSlug is provided.
Sandbox Create Hook
apps/web/hooks/use-sandbox-create.ts
Extended session fields type to include orgSlug and threaded it into createSandbox calls with dependency tracking.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as SessionsIndexShell
    participant OrgSel as OrgSelector
    participant API as Sessions API
    participant DB as Database
    participant SandboxAPI as Sandbox Creation
    participant Vercel as Vercel Init

    User->>UI: Visits sessions page
    UI->>UI: No sessions exist
    UI->>OrgSel: Render organization selector
    OrgSel->>OrgSel: Load organizations
    User->>OrgSel: Click organization
    OrgSel->>UI: handleSelectOrg(orgSlug)
    UI->>API: createBlankSession(orgSlug)
    API->>DB: Insert session with orgSlug
    DB-->>API: Session created
    API-->>UI: Return session ID
    UI->>SandboxAPI: createSandbox(orgSlug)
    SandboxAPI->>Vercel: Initialize with orgSlug
    Vercel->>Vercel: initSubmodules targeting .openclaw/workspace/orgs/{orgSlug}
    Vercel-->>SandboxAPI: Submodules initialized
    SandboxAPI-->>UI: Sandbox ready
    UI->>User: Redirect to session
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 Organizations now sprouting up so bright,
Each session knows its org with pure delight,
Submodules grow where they belong with care,
Scoped initialization—precision in the air! 🌱✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main objective: introducing an org selector to enable faster sandbox startup by initializing only selected org submodules.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/org-selector-faster-sandbox

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread apps/web/app/api/orgs/route.ts Outdated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

KISS - Why is this API route required? Why not simply call fetchAccountOrgs from the client side?

Comment thread apps/web/hooks/use-orgs.ts Outdated
const { ready, getAccessToken } = usePrivy();

const { data, error, isLoading } = useSWR<OrgsResponse>(
ready ? "/api/orgs" : null,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is an internal API being used instead of directly querying the recoup api?

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/web/lib/sandbox/create-sandbox-handler.ts (1)

76-122: ⚠️ Potential issue | 🟠 Major

Preserve the persisted or resolved org scope when the request omits orgSlug.

Line 121 overwrites resolved.source.orgSlug with undefined, and the handler does not fall back to sessionRecord.orgSlug. That can regress selected-org sessions back to full submodule initialization.

Proposed fix
   if (sessionId) {
     const sessionContext = await requireOwnedSession({
       userId: session.user.id,
       sessionId,
@@
 
     sessionRecord = sessionContext.sessionRecord;
   }
 
+  const effectiveOrgSlug = orgSlug ?? sessionRecord?.orgSlug ?? undefined;
   const sandboxName = sessionId ? getSessionSandboxName(sessionId) : undefined;
@@
   if (repoUrl) {
     source = {
       repo: repoUrl,
       branch: isNewBranch ? undefined : branch,
       newBranch: isNewBranch ? branch : undefined,
-      orgSlug,
+      ...(effectiveOrgSlug ? { orgSlug: effectiveOrgSlug } : {}),
     };
   } else {
     const resolved = await resolveAccountRepoSource(req);
     if (resolved) {
-      source = { ...resolved.source, orgSlug };
+      source = {
+        ...resolved.source,
+        ...(effectiveOrgSlug ? { orgSlug: effectiveOrgSlug } : {}),
+      };
       cloneToken = resolved.cloneToken;
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/sandbox/create-sandbox-handler.ts` around lines 76 - 122, The
handler currently overwrites resolved.source.orgSlug with the incoming orgSlug
(which may be undefined), losing the persisted org scope; fix by assigning
source.orgSlug to orgSlug ?? resolved.source.orgSlug ?? sessionRecord?.orgSlug
so the resolved orgSlug is preserved when the request omits orgSlug (update the
block that sets source and cloneToken after calling resolveAccountRepoSource in
create-sandbox-handler.ts, referencing the variables resolved, source, orgSlug,
and sessionRecord).
apps/web/app/api/sessions/route.ts (1)

23-34: ⚠️ Potential issue | 🟠 Major

Add runtime validation for orgSlug before persisting.

body is only type-cast from JSON, so orgSlug can be a non-string or malformed slug and still reach sessions.orgSlug and downstream path construction (.openclaw/workspace/orgs/${orgSlug}). Add runtime validation for this field by moving the request shape to a Zod schema and deriving CreateSessionRequest with z.infer, consistent with how apps/web/app/api/github/webhook/route.ts and apps/web/app/api/generate-title/route.ts handle request validation.

Suggested shape
-interface CreateSessionRequest {
+const createSessionRequestSchema = z.strictObject({
+  title: z.string().optional(),
+  repoOwner: z.string().optional(),
+  repoName: z.string().optional(),
+  branch: z.string().optional(),
+  cloneUrl: z.string().optional(),
+  isNewBranch: z.boolean().optional(),
+  sandboxType: z.literal("vercel").optional(),
+  autoCommitPush: z.boolean().optional(),
+  autoCreatePr: z.boolean().optional(),
+  orgSlug: z.string().optional(),
+});
+
+type CreateSessionRequest = z.infer<typeof createSessionRequestSchema>;
-  title?: string;
-  repoOwner?: string;
-  repoName?: string;
-  branch?: string;
-  cloneUrl?: string;
-  isNewBranch?: boolean;
-  sandboxType?: "vercel";
-  autoCommitPush?: boolean;
-  autoCreatePr?: boolean;
-  orgSlug?: string;
-}
-    body = (await req.json()) as CreateSessionRequest;
+    body = createSessionRequestSchema.parse(await req.json());

You can apply further schema validation (e.g., regex for slug format) if needed.

Also applies to: 175-177, 224-264

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/api/sessions/route.ts` around lines 23 - 34, The request body
type CreateSessionRequest is only a TS cast and allows invalid orgSlug values to
be persisted into sessions.orgSlug and used in path construction; replace the
manual interface with a Zod schema (e.g., const createSessionSchema =
z.object({...})) that validates all fields and in particular enforces orgSlug as
a string (and optionally a slug regex), derive the TypeScript type with
z.infer<typeof createSessionSchema>, and use createSessionSchema.parse(await
req.json()) inside the route handler (the function handling the POST that writes
sessions and builds .openclaw/workspace/orgs/${orgSlug}) so invalid requests
throw and are rejected before persisting; apply the same pattern to the other
request parsing sites referenced (the other handlers around where sessions are
created/updated).
🧹 Nitpick comments (5)
apps/web/app/sessions/sessions-route-shell.tsx (1)

89-100: Add a type guard to handle edge cases where orgSlug might receive unexpected values.

The onOpenNewSession prop in InboxSidebar is typed as () => void and currently called without arguments. However, createBlankSession accepts an optional orgSlug parameter. Adding a defensive check ensures robustness if the callback signature or usage ever changes.

Defensive fix
  const createBlankSession = useCallback(
    async (orgSlug?: string) => {
+      const selectedOrgSlug =
+        typeof orgSlug === "string" ? orgSlug : undefined;
       if (isCreatingBlank) return;
       setIsCreatingBlank(true);
       try {
         const { session: created, chat } = await createSession({
@@
           sandboxType: preferences?.defaultSandboxType ?? DEFAULT_SANDBOX_TYPE,
           autoCommitPush: preferences?.autoCommitPush ?? false,
           autoCreatePr: preferences?.autoCreatePr ?? false,
-          orgSlug,
+          orgSlug: selectedOrgSlug,
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/app/sessions/sessions-route-shell.tsx` around lines 89 - 100, The
createBlankSession useCallback should defensively validate the optional orgSlug
parameter before using it: inside createBlankSession (the function passed to
useCallback) add a type guard that allows only undefined or a string (e.g., if
(orgSlug !== undefined && typeof orgSlug !== 'string') return or normalize it)
so unexpected values from callers like InboxSidebar.onOpenNewSession don’t
propagate to createSession; ensure you reference createBlankSession and the
createSession call when adding this guard.
apps/web/lib/db/migrations/meta/0031_snapshot.json (1)

627-632: org_slug added as free-form nullable text — consider an index and format constraint.

sessions.org_slug is now written on every session and will be read back into the sandbox path. Two operational suggestions:

  • If you plan to query or filter sessions by org (e.g., "list sessions for org X"), add a btree index on org_slug (optionally composite with user_id).
  • Add a CHECK constraint matching the slug format enforced on the write path (e.g., org_slug ~ '^[a-z0-9][a-z0-9-]*$') to prevent persisting values that would later break submodule init.

Not blocking for this PR, but worth addressing before the column sees production traffic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/db/migrations/meta/0031_snapshot.json` around lines 627 - 632,
sessions.org_slug was added as nullable free-form text; add a btree index
(single or composite with user_id) and a CHECK constraint enforcing the slug
format (e.g., regex like '^[a-z0-9][a-z0-9-]*$') to avoid invalid values and
speed queries. Update the migration that defines sessions.org_slug to create an
index on org_slug (or on (org_slug, user_id) if you need per-user filtering) and
add a CHECK constraint on org_slug using the same slug validation used by the
write path so persisted values cannot break sandbox/submodule init.
apps/web/components/org-selector.tsx (1)

53-53: Per-button spinner depends on parent toggling disabled.

isSelected && disabled only shows the spinner because the parent passes disabled={isCreatingBlank} right after click. If a future caller forgets to toggle disabled, the spinner never appears even though selection is in flight. Safer to track a local "pending" state set in handleOrgClick and cleared via effect/prop rather than coupling to the external disabled.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/org-selector.tsx` at line 53, The spinner visibility
currently depends on the external disabled prop (expression isSelected &&
disabled), which may be missed by callers; add a local pending boolean state in
the OrgSelector component, set pending = true inside handleOrgClick when
starting selection, and clear pending in a useEffect that watches the external
prop that signals completion (e.g., isCreatingBlank or the selection prop
change) or on successful selection; switch the spinner condition from isSelected
&& disabled to isSelected && pending so the button shows its own per-click
loading independent of the parent.
apps/web/hooks/use-orgs.ts (2)

22-31: Unvalidated JSON cast.

res.json() as Promise<OrgsResponse> is an unchecked assertion; any shape drift from /api/orgs will surface as a runtime crash inside consumers rather than a typed error here. Since a Zod schema already exists server-side, consider parsing with it (or a trimmed client-side schema) before returning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/hooks/use-orgs.ts` around lines 22 - 31, The fetcher in use-orgs.ts
currently returns res.json() as Promise<OrgsResponse>, which is an unchecked
cast; instead parse and validate the JSON before returning. Update the async
fetcher (the anonymous async (url: string) => { ... } used by the hook) to await
res.json(), run it through a Zod schema (either the existing server-side
OrgsResponse schema ported/imported or a lightweight client-side schema) and
throw a descriptive error if validation fails; return the parsed/validated value
(typed as OrgsResponse). Keep references to getAccessToken and the existing
error branch for res.ok, only replacing the unchecked res.json() cast with
schema.parse/parseAsync validation.

6-11: Duplicate Org type drifts from the server-side Zod schema.

organization_image is declared string | null here but the server schema in fetch-account-orgs.ts types it as .nullable().optional() (i.e., string | null | undefined). Since this hook consumes the same payload the API handler returns, the two definitions can silently diverge.

Prefer deriving this type via z.infer<typeof orgSchema> from a shared module (or re-exporting RecoupableOrg from fetch-account-orgs.ts) rather than redeclaring it. As per coding guidelines: "Use Zod schemas for validation and derive types with z.infer".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/hooks/use-orgs.ts` around lines 6 - 11, The local Org type in
use-orgs.ts diverges from the server Zod schema; replace the manual declaration
by importing and using the shared Zod-derived type instead (either import
orgSchema and use z.infer<typeof orgSchema> or import/export the RecoupableOrg
type from fetch-account-orgs.ts) so organization_image correctly becomes string
| null | undefined and the client type stays in sync with the server; update any
references to Org to the imported/inferred type and remove the duplicate type
declaration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/components/org-selector.tsx`:
- Around line 33-35: The component currently returns null when (error ||
orgs.length === 0) which hides failures and gives no CTA; change the early
return in org-selector.tsx (the block checking error and orgs.length) to render
two distinct states: if error is truthy render an inline error message with a
"Retry" button that triggers the existing data refetch (call the query's
refetch/mutate or a provided reloadOrgList handler), and if orgs.length === 0
render a clear empty-state message explaining there are no organizations and a
CTA (e.g., "Create an organization" or a link to account settings). Ensure you
update any JSX that referenced the old return null behavior so the selector
still displays when orgs exist and that the Retry action wires to the same
data-fetch logic used to load orgs.
- Around line 18-22: The handler handleOrgClick is passing org.organization_name
into onSelectOrg which becomes orgSlug for initSubmodules and thus used in
filesystem paths; replace that with a proper slug field (org.organization_slug)
from the API response, or validate/transform it earlier: update the API response
parsing (fetch-account-orgs.ts) to include and return organization_slug (or
enforce a slug format via Zod) and then change handleOrgClick to call
onSelectOrg(org.organization_slug) and setSelectedOrgSlug(org.organization_slug)
so initSubmodules receives a safe, kebab-case lowercase slug.

In `@apps/web/lib/recoupable/fetch-account-orgs.ts`:
- Around line 4-14: Add a slug-safe identifier: update the Recoupable response
schema (orgSchema and orgsResponseSchema) to include organization_slug:
z.string().regex(/^[a-z0-9-]+$/) and then change OrgSelector (org-selector.tsx)
to use org.organization_slug as orgSlug (so init-submodules.ts writes
.openclaw/workspace/orgs/<orgSlug> safely); if the backend cannot be changed,
enforce validation on organization_name in orgSchema with
z.string().regex(/^[a-z0-9-]+$/) and surface a clear error when it fails so the
fast-path init doesn't silently break.

In `@apps/web/lib/sandbox/validate-create-sandbox-body.ts`:
- Line 9: The orgSlug schema currently allows unsafe values; update the orgSlug
field in the create-sandbox Zod schema to validate and reject path
traversal/empty values (e.g. use
z.string().trim().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/i).optional())
so any present orgSlug is non-empty, trimmed, length-bounded and matches a safe
slug pattern before it is interpolated into the workspace path used by the
init-submodules logic.

In `@packages/sandbox/vercel/init-submodules.ts`:
- Around line 50-53: Validate orgSlug against a strict slug regex before
interpolating it into the git pathspec: in the init-submodules flow where
orgSlug is pushed into args (the code that builds
`.openclaw/workspace/orgs/${orgSlug}`), enforce something like
/^[a-z0-9][a-z0-9-]*$/ and throw an error on violation to prevent
traversal/invalid values; then use the validated slug when constructing the
pathspec. After running the git submodule update command (the same routine that
uses args), verify at least one submodule was actually initialized by checking
that the resolved directory `.openclaw/workspace/orgs/<validatedSlug>` exists
and contains expected files (or by checking git output/exit details) and throw
an error if nothing was initialized so a typo doesn't silently produce an empty
sandbox.

---

Outside diff comments:
In `@apps/web/app/api/sessions/route.ts`:
- Around line 23-34: The request body type CreateSessionRequest is only a TS
cast and allows invalid orgSlug values to be persisted into sessions.orgSlug and
used in path construction; replace the manual interface with a Zod schema (e.g.,
const createSessionSchema = z.object({...})) that validates all fields and in
particular enforces orgSlug as a string (and optionally a slug regex), derive
the TypeScript type with z.infer<typeof createSessionSchema>, and use
createSessionSchema.parse(await req.json()) inside the route handler (the
function handling the POST that writes sessions and builds
.openclaw/workspace/orgs/${orgSlug}) so invalid requests throw and are rejected
before persisting; apply the same pattern to the other request parsing sites
referenced (the other handlers around where sessions are created/updated).

In `@apps/web/lib/sandbox/create-sandbox-handler.ts`:
- Around line 76-122: The handler currently overwrites resolved.source.orgSlug
with the incoming orgSlug (which may be undefined), losing the persisted org
scope; fix by assigning source.orgSlug to orgSlug ?? resolved.source.orgSlug ??
sessionRecord?.orgSlug so the resolved orgSlug is preserved when the request
omits orgSlug (update the block that sets source and cloneToken after calling
resolveAccountRepoSource in create-sandbox-handler.ts, referencing the variables
resolved, source, orgSlug, and sessionRecord).

---

Nitpick comments:
In `@apps/web/app/sessions/sessions-route-shell.tsx`:
- Around line 89-100: The createBlankSession useCallback should defensively
validate the optional orgSlug parameter before using it: inside
createBlankSession (the function passed to useCallback) add a type guard that
allows only undefined or a string (e.g., if (orgSlug !== undefined && typeof
orgSlug !== 'string') return or normalize it) so unexpected values from callers
like InboxSidebar.onOpenNewSession don’t propagate to createSession; ensure you
reference createBlankSession and the createSession call when adding this guard.

In `@apps/web/components/org-selector.tsx`:
- Line 53: The spinner visibility currently depends on the external disabled
prop (expression isSelected && disabled), which may be missed by callers; add a
local pending boolean state in the OrgSelector component, set pending = true
inside handleOrgClick when starting selection, and clear pending in a useEffect
that watches the external prop that signals completion (e.g., isCreatingBlank or
the selection prop change) or on successful selection; switch the spinner
condition from isSelected && disabled to isSelected && pending so the button
shows its own per-click loading independent of the parent.

In `@apps/web/hooks/use-orgs.ts`:
- Around line 22-31: The fetcher in use-orgs.ts currently returns res.json() as
Promise<OrgsResponse>, which is an unchecked cast; instead parse and validate
the JSON before returning. Update the async fetcher (the anonymous async (url:
string) => { ... } used by the hook) to await res.json(), run it through a Zod
schema (either the existing server-side OrgsResponse schema ported/imported or a
lightweight client-side schema) and throw a descriptive error if validation
fails; return the parsed/validated value (typed as OrgsResponse). Keep
references to getAccessToken and the existing error branch for res.ok, only
replacing the unchecked res.json() cast with schema.parse/parseAsync validation.
- Around line 6-11: The local Org type in use-orgs.ts diverges from the server
Zod schema; replace the manual declaration by importing and using the shared
Zod-derived type instead (either import orgSchema and use z.infer<typeof
orgSchema> or import/export the RecoupableOrg type from fetch-account-orgs.ts)
so organization_image correctly becomes string | null | undefined and the client
type stays in sync with the server; update any references to Org to the
imported/inferred type and remove the duplicate type declaration.

In `@apps/web/lib/db/migrations/meta/0031_snapshot.json`:
- Around line 627-632: sessions.org_slug was added as nullable free-form text;
add a btree index (single or composite with user_id) and a CHECK constraint
enforcing the slug format (e.g., regex like '^[a-z0-9][a-z0-9-]*$') to avoid
invalid values and speed queries. Update the migration that defines
sessions.org_slug to create an index on org_slug (or on (org_slug, user_id) if
you need per-user filtering) and add a CHECK constraint on org_slug using the
same slug validation used by the write path so persisted values cannot break
sandbox/submodule init.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7a2a3948-46fb-4e85-8e6e-734c4619622e

📥 Commits

Reviewing files that changed from the base of the PR and between 8091630 and b16277f.

📒 Files selected for processing (23)
  • apps/web/app/api/orgs/route.ts
  • apps/web/app/api/sessions/route.ts
  • apps/web/app/sessions/sessions-index-shell.tsx
  • apps/web/app/sessions/sessions-route-shell.tsx
  • apps/web/app/sessions/sessions-shell-context.tsx
  • apps/web/components/org-selector.tsx
  • apps/web/hooks/use-orgs.ts
  • apps/web/hooks/use-sandbox-create.ts
  • apps/web/hooks/use-sessions.ts
  • apps/web/lib/db/migrations/0031_ambiguous_martin_li.sql
  • apps/web/lib/db/migrations/meta/0031_snapshot.json
  • apps/web/lib/db/migrations/meta/_journal.json
  • apps/web/lib/db/schema.ts
  • apps/web/lib/recoupable/fetch-account-orgs.ts
  • apps/web/lib/sandbox/create-sandbox-handler.ts
  • apps/web/lib/sandbox/create-sandbox.ts
  • apps/web/lib/sandbox/resolve-account-repo-source.ts
  • apps/web/lib/sandbox/validate-create-sandbox-body.ts
  • packages/sandbox/types.ts
  • packages/sandbox/vercel/config.ts
  • packages/sandbox/vercel/connect.ts
  • packages/sandbox/vercel/init-submodules.ts
  • packages/sandbox/vercel/sandbox.ts

Comment thread apps/web/components/org-selector.tsx
Comment on lines +33 to +35
if (error || orgs.length === 0) {
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent return null on error/empty hides failure from users.

If /api/orgs errors or returns zero orgs, the selector renders nothing and the parent shell keeps showing "Select an Organization" with no CTA — the user has no way to proceed or understand why. Consider rendering an inline error with a retry, and a distinct empty-state message when the account genuinely has no orgs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/org-selector.tsx` around lines 33 - 35, The component
currently returns null when (error || orgs.length === 0) which hides failures
and gives no CTA; change the early return in org-selector.tsx (the block
checking error and orgs.length) to render two distinct states: if error is
truthy render an inline error message with a "Retry" button that triggers the
existing data refetch (call the query's refetch/mutate or a provided
reloadOrgList handler), and if orgs.length === 0 render a clear empty-state
message explaining there are no organizations and a CTA (e.g., "Create an
organization" or a link to account settings). Ensure you update any JSX that
referenced the old return null behavior so the selector still displays when orgs
exist and that the Retry action wires to the same data-fetch logic used to load
orgs.

Comment on lines +4 to +14
const orgSchema = z.object({
id: z.string(),
organization_id: z.string(),
organization_name: z.string(),
organization_image: z.string().nullable().optional(),
});

const orgsResponseSchema = z.object({
status: z.literal("success"),
organizations: z.array(orgSchema),
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Find OrgSelector and related files
fd -type f -name "*OrgSelector*" -o -name "*org-selector*"

Repository: recoupable/open-agents

Length of output: 236


🏁 Script executed:

# Search for usage of organization_name as slug
rg "organization_name.*slug|slug.*organization_name" -A 2 -B 2

Repository: recoupable/open-agents

Length of output: 48


🏁 Script executed:

# Find where the .openclaw/workspace path is used
rg "\.openclaw.*workspace|workspace.*orgs" -A 2 -B 2

Repository: recoupable/open-agents

Length of output: 1238


🏁 Script executed:

# Check the fetch-account-orgs file for context
cat -n apps/web/lib/recoupable/fetch-account-orgs.ts

Repository: recoupable/open-agents

Length of output: 2141


🏁 Script executed:

# Find calls to init-submodules and how orgSlug is passed
rg "init.*submodules|initSubmodules" -A 5 -B 2

Repository: recoupable/open-agents

Length of output: 5587


🏁 Script executed:

# Search for OrgSelector component
fd -i "orgsel" -o -i "org-sel"

Repository: recoupable/open-agents

Length of output: 182


🏁 Script executed:

# Find where orgSlug is being passed or set from org data
rg "orgSlug|org-slug" -A 3 -B 3

Repository: recoupable/open-agents

Length of output: 14222


🏁 Script executed:

# Read the org-selector component to see what's passed as orgSlug
cat -n apps/web/components/org-selector.tsx

Repository: recoupable/open-agents

Length of output: 2923


🏁 Script executed:

# Find the use-orgs hook to see Org type and where orgSlug comes from
fd "use-orgs" -type f

Repository: recoupable/open-agents

Length of output: 236


🏁 Script executed:

# Find use-orgs hook file
find apps/web -name "*use-orgs*" -o -name "*useOrgs*"

Repository: recoupable/open-agents

Length of output: 92


🏁 Script executed:

# Also search for Org type definition
rg "type Org = |interface Org" -A 8

Repository: recoupable/open-agents

Length of output: 2233


🏁 Script executed:

# Check Recoupable API documentation or real responses
rg "Recoupable|recoupable" -i apps/web/lib/recoupable -A 5

Repository: recoupable/open-agents

Length of output: 7133


🏁 Script executed:

# Check for test files that might show example organization_name values
rg "organization_name" -A 2 -B 2 --type ts --type tsx | head -100

Repository: recoupable/open-agents

Length of output: 93


🏁 Script executed:

# Look for any slug normalization or sanitization functions
rg "slug|normalize|sanitize" -i apps/web/lib -A 2 -B 2

Repository: recoupable/open-agents

Length of output: 42372


🏁 Script executed:

# Check if there are any mocks or test data for Recoupable API
find apps/web -name "*.test.*" -o -name "*.mock.*" -o -name "__mocks__" | head -20

Repository: recoupable/open-agents

Length of output: 1117


🏁 Script executed:

# Read the full use-orgs hook
cat -n apps/web/hooks/use-orgs.ts

Repository: recoupable/open-agents

Length of output: 1267


🏁 Script executed:

# Search for tests related to fetch-account-orgs or org fetching
find apps/web -name "*fetch-account-orgs*" -o -name "*use-orgs*"

Repository: recoupable/open-agents

Length of output: 138


🏁 Script executed:

# Look for any example organization names or test data for Recoupable
rg "organization_name.*:" apps/web -A 2 -B 1

Repository: recoupable/open-agents

Length of output: 580


Schema and OrgSelector both lack validation that organization_name is slug-safe for filesystem paths.

OrgSelector (org-selector.tsx line 20-21) passes org.organization_name directly as orgSlug, which flows to init-submodules.ts where it becomes .openclaw/workspace/orgs/<orgSlug>. The Recoupable API response and schema provide no organization_slug field and no validation that organization_name conforms to a path-safe slug format ([a-z0-9-]+). Organization names with spaces, punctuation, or mixed case will fail the fast-path init silently.

Either add organization_slug to the API response and schema, then use that field, or document that organization_name is pre-normalized to slug format and enforce validation on the schema with z.string().regex(/^[a-z0-9-]+$/). Confirm which guarantee the Recoupable API actually provides.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/recoupable/fetch-account-orgs.ts` around lines 4 - 14, Add a
slug-safe identifier: update the Recoupable response schema (orgSchema and
orgsResponseSchema) to include organization_slug:
z.string().regex(/^[a-z0-9-]+$/) and then change OrgSelector (org-selector.tsx)
to use org.organization_slug as orgSlug (so init-submodules.ts writes
.openclaw/workspace/orgs/<orgSlug> safely); if the backend cannot be changed,
enforce validation on organization_name in orgSchema with
z.string().regex(/^[a-z0-9-]+$/) and surface a clear error when it fails so the
fast-path init doesn't silently break.

isNewBranch: z.boolean().optional(),
sessionId: z.string().optional(),
sandboxType: z.literal("vercel").optional(),
orgSlug: z.string().optional(),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how orgSlug is consumed downstream (path interpolation / shell usage).
rg -nP -C4 '\borgSlug\b'
fd -t f 'init-submodules' | xargs -I{} sh -c 'echo "=== {} ==="; cat "{}"'

Repository: recoupable/open-agents

Length of output: 22540


Constrain orgSlug to prevent path traversal attacks.

orgSlug is interpolated directly into a filesystem path at .openclaw/workspace/orgs/<orgSlug> in init-submodules.ts (line 52). As currently declared (z.string().optional()), path traversal payloads like ../../etc or empty/whitespace values are accepted and passed to git submodule. Add validation to enforce a safe slug pattern and non-empty length at the schema boundary.

Suggested validation
-  orgSlug: z.string().optional(),
+  orgSlug: z
+    .string()
+    .min(1)
+    .max(64)
+    .regex(/^[a-z0-9][a-z0-9-_]*$/i, "Invalid orgSlug")
+    .optional(),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
orgSlug: z.string().optional(),
orgSlug: z
.string()
.min(1)
.max(64)
.regex(/^[a-z0-9][a-z0-9-_]*$/i, "Invalid orgSlug")
.optional(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/lib/sandbox/validate-create-sandbox-body.ts` at line 9, The orgSlug
schema currently allows unsafe values; update the orgSlug field in the
create-sandbox Zod schema to validate and reject path traversal/empty values
(e.g. use
z.string().trim().min(1).max(64).regex(/^[a-z0-9][a-z0-9-]*[a-z0-9]$/i).optional())
so any present orgSlug is non-empty, trimmed, length-bounded and matches a safe
slug pattern before it is interpolated into the workspace path used by the
init-submodules logic.

Comment thread packages/sandbox/vercel/init-submodules.ts Outdated
Copy link
Copy Markdown
Contributor

@sweetmantech sweetmantech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

overall, this pr seems to be passing orgSlug around a lot.
How can we simplify the code changes to prevent so many files from being touched?

Comment thread apps/web/lib/recoupable/fetch-account-orgs.ts
…seOrgs

Why: the internal route added no value — it only forwarded the Privy
access token to the Recoup API. Addresses review feedback on PR #7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the db need changed in this pr?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the db need changed in this pr?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the db need changed in this pr?

Comment thread apps/web/lib/db/schema.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does the db need changed in this pr?

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 23 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/web/app/api/sessions/route.ts">

<violation number="1" location="apps/web/app/api/sessions/route.ts:264">
P2: Validate `orgSlug` at runtime before persisting it; the current nullish check allows non-string JSON values through.</violation>
</file>

<file name="apps/web/components/org-selector.tsx">

<violation number="1" location="apps/web/components/org-selector.tsx:21">
P1: The selector passes `organization_name` as `orgSlug`; use the org identifier field instead to avoid invalid submodule targeting.</violation>

<violation number="2" location="apps/web/components/org-selector.tsx:33">
P2: Don’t return `null` for org fetch failure/empty data; show a fallback message so the empty state isn’t silently blank.</violation>
</file>

You're on the cubic free plan with 20 free PR reviews remaining this month. Upgrade for unlimited reviews.

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread apps/web/components/org-selector.tsx Outdated
Comment thread apps/web/app/api/sessions/route.ts Outdated
);
}

if (error || orgs.length === 0) {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Don’t return null for org fetch failure/empty data; show a fallback message so the empty state isn’t silently blank.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/components/org-selector.tsx, line 33:

<comment>Don’t return `null` for org fetch failure/empty data; show a fallback message so the empty state isn’t silently blank.</comment>

<file context>
@@ -0,0 +1,76 @@
+    );
+  }
+
+  if (error || orgs.length === 0) {
+    return null;
+  }
</file context>
Fix with Cubic

…submodule

Selecting an org now clones https://github.com/recoupable/org-{kebab(name)}-{id}
directly into the sandbox. The account-repo-with-submodules approach is gone.

- New buildOrgRepoUrl helper constructs the per-org repo URL from the selected
  Org's name + organization_id
- OrgSelector emits the constructed cloneUrl; createBlankSession persists it to
  the existing sessions.cloneUrl column (no new column needed)
- Sandbox create handler always clones the supplied repoUrl with the service
  GITHUB_TOKEN — all repos are owned by recoupable, so no per-user auth
- Removed the account-repo fallback path: resolveAccountRepoSource,
  extractBearerToken, fetchAccountGithubRepo (+ test) all deleted
- Rolled back the org_slug column and migration 0031 (never merged to main)
- Stripped orgSlug-scoping from initSubmodules and the sandbox package types

Addresses review feedback on PR #7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 24 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/sandbox/vercel/sandbox.ts">

<violation number="1" location="packages/sandbox/vercel/sandbox.ts:599">
P2: This change drops org-scoped submodule initialization, so startup reverts to initializing all submodules instead of just the selected org.</violation>
</file>

<file name="apps/web/lib/sandbox/validate-create-sandbox-body.ts">

<violation number="1" location="apps/web/lib/sandbox/validate-create-sandbox-body.ts:4">
P1: Add `orgSlug` back to the request schema; otherwise it is stripped during validation and cannot be passed through for org-scoped sandbox initialization/persistence.</violation>
</file>

<file name="apps/web/lib/sandbox/create-sandbox-handler.ts">

<violation number="1" location="apps/web/lib/sandbox/create-sandbox-handler.ts:95">
P2: `source` no longer carries `orgSlug`, so selected organization context is dropped before sandbox provisioning.</violation>

<violation number="2" location="apps/web/lib/sandbox/create-sandbox-handler.ts:110">
P1: Passing only a shared `GITHUB_TOKEN` to `connectSandbox` removes per-user repository authorization and can expose private repos accessible to the service token.</violation>
</file>

You're on the cubic free plan with 20 free PR reviews remaining this month. Upgrade for unlimited reviews.

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


const createSandboxBodySchema = z.object({
repoUrl: z.string().optional(),
repoUrl: z.string().min(1),
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Add orgSlug back to the request schema; otherwise it is stripped during validation and cannot be passed through for org-scoped sandbox initialization/persistence.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/lib/sandbox/validate-create-sandbox-body.ts, line 4:

<comment>Add `orgSlug` back to the request schema; otherwise it is stripped during validation and cannot be passed through for org-scoped sandbox initialization/persistence.</comment>

<file context>
@@ -1,12 +1,11 @@
 
 const createSandboxBodySchema = z.object({
-  repoUrl: z.string().optional(),
+  repoUrl: z.string().min(1),
   branch: z.string().optional(),
   isNewBranch: z.boolean().optional(),
</file context>
Suggested change
repoUrl: z.string().min(1),
repoUrl: z.string().min(1),
orgSlug: z.string().min(1).optional(),
Fix with Cubic

},
options: {
githubToken: cloneToken ?? githubToken ?? undefined,
githubToken: cloneToken,
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Passing only a shared GITHUB_TOKEN to connectSandbox removes per-user repository authorization and can expose private repos accessible to the service token.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/lib/sandbox/create-sandbox-handler.ts, line 110:

<comment>Passing only a shared `GITHUB_TOKEN` to `connectSandbox` removes per-user repository authorization and can expose private repos accessible to the service token.</comment>

<file context>
@@ -132,7 +107,7 @@ export async function handleCreateSandboxRequest(
       },
       options: {
-        githubToken: cloneToken ?? githubToken ?? undefined,
+        githubToken: cloneToken,
         gitUser,
         timeout: DEFAULT_SANDBOX_TIMEOUT_MS,
</file context>
Fix with Cubic

Comment thread packages/sandbox/vercel/sandbox.ts Outdated
cloneToken = resolved.cloneToken;
}
}
const source = {
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot Apr 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: source no longer carries orgSlug, so selected organization context is dropped before sandbox provisioning.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/lib/sandbox/create-sandbox-handler.ts, line 95:

<comment>`source` no longer carries `orgSlug`, so selected organization context is dropped before sandbox provisioning.</comment>

<file context>
@@ -101,27 +90,13 @@ export async function handleCreateSandboxRequest(
-      cloneToken = resolved.cloneToken;
-    }
-  }
+  const source = {
+    repo: repoUrl,
+    branch: isNewBranch ? undefined : branch,
</file context>
Fix with Cubic

Comment thread apps/web/app/sessions/sessions-shell-context.tsx Outdated
The + button in the inbox sidebar now opens a Popover containing the
OrgSelector. Picking an org triggers session creation directly — no
navigation hop, no separate openOrgPicker prop.

Addresses review feedback on PR #7.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@@ -0,0 +1,18 @@
function toKebabCase(input: string): string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SRP

  • actual: toKebabCase defined in apps/web/lib/recoupable/build-org-repo-url.ts
  • required: new lib file for toKebabCase

Comment thread apps/web/components/inbox-sidebar.tsx Outdated
Comment on lines +64 to +65
onStartNewSession: (cloneUrl: string) => void;
isCreatingNewSession: boolean;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are changes needed to this file?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

YAGNI - Is this file still required? With Orgs now being installed natively, instead of as a submodule of an account, I don't think we need to do any submodule init now.

- Extract toKebabCase into its own lib/string/to-kebab-case.ts (SRP)
- Delete init-submodules.ts + test and remove its sandbox call site —
  org repos are cloned directly now, no submodule init needed (YAGNI)
- Sidebar + button reverted to a one-line navigate-to-/sessions; no new
  props on InboxSidebar, no popover wiring inside it

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@sweetmantech sweetmantech merged commit 66abefb into main Apr 22, 2026
3 checks passed
@sweetmantech sweetmantech deleted the feat/org-selector-faster-sandbox branch April 22, 2026 16:28
sweetmantech added a commit that referenced this pull request Apr 23, 2026
* fix: use service GITHUB_TOKEN for all git push + GitHub API calls

Fixes the "Permission denied. Check your GitHub access." error when
clicking "Commit and Push" for org-backed sessions. Primary bug:
generate-pr/route.ts guarded push-auth setup behind
`sessionRecord.repoOwner && sessionRecord.repoName`, which are null
for sessions created via the OrgSelector (only cloneUrl is populated).
The guard short-circuited, remote URL stayed unauthenticated, and
GitHub returned 403.

- New helper `ensureAuthenticatedOrigin({ sandbox, cloneUrl })` parses
  owner/repo from the session cloneUrl and runs `git remote set-url
  origin https://x-access-token:${GITHUB_TOKEN}@github.com/owner/repo.git`
  in the sandbox
- `/api/generate-pr` replaces the buggy guard with the helper; user-
  token fetch is removed (fork-fallback remains dead code, cleaned up
  in follow-up YAGNI PR)
- Swaps 7 GitHub-API-op sites to the service token: `/api/pr`,
  `/api/check-pr`, session merge/merge-readiness/close-pr/checks/fix,
  chat runtime, and archive-session
- `auto-commit-direct.ts` and `auto-pr-direct.ts` also swap to the
  service token (no effect for org sessions today since they are gated
  by repoOwner+repoName elsewhere, but brings the token source into
  alignment with the clone + push path)

All repos are recoupable-owned per PR #7 policy; the service
`GITHUB_TOKEN` always has access, so the per-user OAuth token is no
longer needed on these paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor: extract getServiceGitHubToken helper (DRY)

Addresses PR #10 review: process.env.GITHUB_TOKEN?.trim() was duplicated
across 13 call sites with three different fallback conventions (null,
undefined, nothing). New lib/github/service-token.ts centralizes the
env-var name and trim logic in one place; each call site becomes a
one-liner.

Call sites needing undefined convert with ?? undefined at the use site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants