feat: org selector for faster sandbox startup#7
Conversation
… 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR adds organization slug support throughout the session and sandbox creation flow. It introduces an organization selector UI, threads Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
KISS - Why is this API route required? Why not simply call fetchAccountOrgs from the client side?
| const { ready, getAccessToken } = usePrivy(); | ||
|
|
||
| const { data, error, isLoading } = useSWR<OrgsResponse>( | ||
| ready ? "/api/orgs" : null, |
There was a problem hiding this comment.
Why is an internal API being used instead of directly querying the recoup api?
There was a problem hiding this comment.
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 | 🟠 MajorPreserve the persisted or resolved org scope when the request omits
orgSlug.Line 121 overwrites
resolved.source.orgSlugwithundefined, and the handler does not fall back tosessionRecord.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 | 🟠 MajorAdd runtime validation for
orgSlugbefore persisting.
bodyis only type-cast from JSON, soorgSlugcan be a non-string or malformed slug and still reachsessions.orgSlugand downstream path construction (.openclaw/workspace/orgs/${orgSlug}). Add runtime validation for this field by moving the request shape to a Zod schema and derivingCreateSessionRequestwithz.infer, consistent with howapps/web/app/api/github/webhook/route.tsandapps/web/app/api/generate-title/route.tshandle 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 whereorgSlugmight receive unexpected values.The
onOpenNewSessionprop inInboxSidebaris typed as() => voidand currently called without arguments. However,createBlankSessionaccepts an optionalorgSlugparameter. 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_slugadded as free-form nullable text — consider an index and format constraint.
sessions.org_slugis 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 withuser_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 togglingdisabled.
isSelected && disabledonly shows the spinner because the parent passesdisabled={isCreatingBlank}right after click. If a future caller forgets to toggledisabled, the spinner never appears even though selection is in flight. Safer to track a local "pending" state set inhandleOrgClickand cleared via effect/prop rather than coupling to the externaldisabled.🤖 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/orgswill 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: DuplicateOrgtype drifts from the server-side Zod schema.
organization_imageis declaredstring | nullhere but the server schema infetch-account-orgs.tstypes 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-exportingRecoupableOrgfromfetch-account-orgs.ts) rather than redeclaring it. As per coding guidelines: "Use Zod schemas for validation and derive types withz.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
📒 Files selected for processing (23)
apps/web/app/api/orgs/route.tsapps/web/app/api/sessions/route.tsapps/web/app/sessions/sessions-index-shell.tsxapps/web/app/sessions/sessions-route-shell.tsxapps/web/app/sessions/sessions-shell-context.tsxapps/web/components/org-selector.tsxapps/web/hooks/use-orgs.tsapps/web/hooks/use-sandbox-create.tsapps/web/hooks/use-sessions.tsapps/web/lib/db/migrations/0031_ambiguous_martin_li.sqlapps/web/lib/db/migrations/meta/0031_snapshot.jsonapps/web/lib/db/migrations/meta/_journal.jsonapps/web/lib/db/schema.tsapps/web/lib/recoupable/fetch-account-orgs.tsapps/web/lib/sandbox/create-sandbox-handler.tsapps/web/lib/sandbox/create-sandbox.tsapps/web/lib/sandbox/resolve-account-repo-source.tsapps/web/lib/sandbox/validate-create-sandbox-body.tspackages/sandbox/types.tspackages/sandbox/vercel/config.tspackages/sandbox/vercel/connect.tspackages/sandbox/vercel/init-submodules.tspackages/sandbox/vercel/sandbox.ts
| if (error || orgs.length === 0) { | ||
| return null; | ||
| } |
There was a problem hiding this comment.
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.
| 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), | ||
| }); |
There was a problem hiding this comment.
🧩 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 2Repository: 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 2Repository: 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.tsRepository: 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 2Repository: 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 3Repository: 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.tsxRepository: 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 fRepository: 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 8Repository: 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 5Repository: 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 -100Repository: 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 2Repository: 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 -20Repository: recoupable/open-agents
Length of output: 1117
🏁 Script executed:
# Read the full use-orgs hook
cat -n apps/web/hooks/use-orgs.tsRepository: 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 1Repository: 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(), |
There was a problem hiding this comment.
🧩 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.
| 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.
sweetmantech
left a comment
There was a problem hiding this comment.
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?
…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>
There was a problem hiding this comment.
Why does the db need changed in this pr?
There was a problem hiding this comment.
Why does the db need changed in this pr?
There was a problem hiding this comment.
Why does the db need changed in this pr?
There was a problem hiding this comment.
Why does the db need changed in this pr?
There was a problem hiding this comment.
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.
| ); | ||
| } | ||
|
|
||
| if (error || orgs.length === 0) { |
There was a problem hiding this comment.
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>
…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>
There was a problem hiding this comment.
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), |
There was a problem hiding this comment.
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>
| repoUrl: z.string().min(1), | |
| repoUrl: z.string().min(1), | |
| orgSlug: z.string().min(1).optional(), |
| }, | ||
| options: { | ||
| githubToken: cloneToken ?? githubToken ?? undefined, | ||
| githubToken: cloneToken, |
There was a problem hiding this comment.
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>
| cloneToken = resolved.cloneToken; | ||
| } | ||
| } | ||
| const source = { |
There was a problem hiding this comment.
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>
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 { | |||
There was a problem hiding this comment.
SRP
- actual: toKebabCase defined in apps/web/lib/recoupable/build-org-repo-url.ts
- required: new lib file for toKebabCase
| onStartNewSession: (cloneUrl: string) => void; | ||
| isCreatingNewSession: boolean; |
There was a problem hiding this comment.
Why are changes needed to this file?
There was a problem hiding this comment.
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>
* 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>
Summary
orgSlugcolumn to the sessions table (migration 0031) to persist the selected orgChanges
GET /api/orgsendpoint,OrgSelectorcomponent,useOrgshook,fetchAccountOrgsliborgSlugthrough validation → handler → SDKinitSubmodulesselectively inits only.openclaw/workspace/orgs/<orgSlug>when providedorg_slugcolumn to sessions schemaTest plan
org_slugis persisted in the sessions table🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Improvements
Database
Summary by cubic
Added an Org Selector to create sandboxes by picking an org first. We now clone the selected org’s
recoupableGitHub repo withGITHUB_TOKEN, cutting startup from minutes to seconds; the Inbox “+” button routes to the selector.New Features
OrgSelectoron the empty Sessions page;useOrgspulls orgs from the Recoupable API,buildOrgRepoUrlbuilds the per‑org repo URL, and sessions start viacreateBlankSession(cloneUrl)./api/sandboxnow requiresrepoUrland clones that repo.Refactors
GITHUB_TOKEN. Deleted submodule init code/tests and its sandbox call site./api/orgspassthrough;useOrgscalls the Recoupable API directly. Inbox “New Session” now navigates to/sessions. Rolled back theorg_slugmigration and persist viasessions.cloneUrl.Written for commit 423472e. Summary will update on new commits.