Skip to content

Comments

feat: add metadata_only param to GET /api/sandboxes#215

Open
sweetmantech wants to merge 2 commits intotestfrom
sweetmantech/myc-4201-bash-debug-failing-setup-sandbox-task
Open

feat: add metadata_only param to GET /api/sandboxes#215
sweetmantech wants to merge 2 commits intotestfrom
sweetmantech/myc-4201-bash-debug-failing-setup-sandbox-task

Conversation

@sweetmantech
Copy link
Contributor

@sweetmantech sweetmantech commented Feb 10, 2026

Summary

  • Adds metadata_only=true query parameter to GET /api/sandboxes that skips expensive Sandbox.get() API calls to Vercel
  • When metadata_only=true, returns only snapshot_id, github_repo, and filetree (empty sandboxes array)
  • Prevents 100+ parallel Vercel API calls that cause 410 errors in the setup-sandbox task

Root Cause

The Tasks repo's getAccountSandboxes() only needs snapshot_id and github_repo, but the GET endpoint was making Sandbox.get() for every sandbox record (100+). This caused the newly created sandbox to become unavailable (HTTP 410) by the time setupSandboxTask tried to use it.

Test plan

  • Added failing test (TDD RED): getSandboxStatus should NOT be called when metadataOnly is true
  • Implemented fix (TDD GREEN): all 103 sandbox tests pass
  • Deploy and verify setup-sandbox task no longer fails with 410

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added metadata_only parameter for sandbox retrieval to optimize requests by returning only metadata without live status checks.
  • Bug Fixes

    • Improved sandbox creation resilience with automatic fallback when snapshot-based creation encounters errors.
  • Documentation

    • Updated organizations API documentation with clearer authentication requirements and refined parameter specifications.

When an expired Vercel snapshot is stored in the database, Sandbox.create()
fails. This adds a try-catch fallback that creates a fresh sandbox and clears
the expired snapshot record. Includes test for the fallback behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

This PR refactors the GET /organizations endpoint's validation flow to use request-based authentication context, replaces URL parameter handling with optional account_id filtering, introduces snapshot fallback logic for sandbox creation, and optimizes sandbox retrieval with a metadata_only parameter for performance.

Changes

Cohort / File(s) Summary
Organizations API Validation Refactoring
app/api/organizations/route.ts, lib/organizations/buildGetOrganizationsParams.ts, lib/organizations/validateGetOrganizationsRequest.ts, lib/organizations/getOrganizationsHandler.ts, lib/organizations/validateOrganizationsQuery.ts
Restructures GET /organizations validation: replaces URL-parameter-based validateOrganizationsQuery with request-based validateGetOrganizationsRequest; introduces buildGetOrganizationsParams to construct params based on authentication context (personal key, org key, or Recoup admin); changes account_id from required to optional and scope-restricted to org keys; updates route documentation.
Sandbox Metadata Optimization
lib/sandbox/validateGetSandboxesRequest.ts, lib/sandbox/getSandboxesHandler.ts, lib/supabase/account_sandboxes/selectAccountSandboxes.ts
Adds optional metadata_only query parameter to sandbox retrieval; implements early-return path in handler to bypass live status fetches when metadata is requested, returning only snapshot info and filetree; preserves response structure with empty sandboxes array.
Sandbox Creation Error Handling
lib/sandbox/createSandboxPostHandler.ts
Adds defensive snapshot fallback: attempts snapshot-based sandbox creation within try-catch; on failure, logs error, deletes the stale snapshot, and falls back to creating a fresh sandbox.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant validateGetOrganizationsRequest
    participant validateAuthContext
    participant buildGetOrganizationsParams
    participant Database
    
    Client->>validateGetOrganizationsRequest: GET /api/organizations?account_id=<uuid>
    validateGetOrganizationsRequest->>validateGetOrganizationsRequest: Parse & validate account_id query param
    validateGetOrganizationsRequest->>validateAuthContext: Validate API key / Bearer token
    
    alt Auth Success
        validateAuthContext-->>validateGetOrganizationsRequest: Auth context (orgId, accountId, role)
        validateGetOrganizationsRequest->>buildGetOrganizationsParams: Build params with targetAccountId, orgId
        
        alt Has targetAccountId (account_id provided)
            buildGetOrganizationsParams->>buildGetOrganizationsParams: Check access via canAccessAccount
            alt Access Granted
                buildGetOrganizationsParams-->>validateGetOrganizationsRequest: { accountId: targetAccountId, error: null }
            else Access Denied
                buildGetOrganizationsParams-->>validateGetOrganizationsRequest: { params: null, error: "Forbidden" }
            end
        else No targetAccountId
            alt orgId === RECOUP_ORG_ID (admin)
                buildGetOrganizationsParams-->>validateGetOrganizationsRequest: { params: {}, error: null } (all records)
            else orgId present (org key)
                buildGetOrganizationsParams-->>validateGetOrganizationsRequest: { organizationId: orgId, error: null }
            else Personal key
                buildGetOrganizationsParams-->>validateGetOrganizationsRequest: { accountId: accountId, error: null }
            end
        end
        
        validateGetOrganizationsRequest->>Database: Query organizations with params
        Database-->>Client: Organizations list
    else Auth Failure
        validateAuthContext-->>Client: 401 Unauthorized
    end
Loading
sequenceDiagram
    participant Client
    participant validateGetSandboxesRequest
    participant getSandboxesHandler
    participant Database
    
    Client->>validateGetSandboxesRequest: GET /api/sandboxes?metadata_only=true
    validateGetSandboxesRequest->>validateGetSandboxesRequest: Parse metadata_only parameter
    validateGetSandboxesRequest-->>getSandboxesHandler: Return params with metadataOnly: boolean
    
    getSandboxesHandler->>getSandboxesHandler: Check metadataOnly flag
    
    alt metadataOnly === true
        getSandboxesHandler->>Database: Query snapshot info & filetree only
        Database-->>getSandboxesHandler: Metadata (snapshot_id, github_repo, filetree)
        getSandboxesHandler-->>Client: { sandboxes: [], snapshot_id, github_repo, filetree }
    else metadataOnly === false (full data)
        getSandboxesHandler->>Database: Query sandboxes + live status
        Database-->>getSandboxesHandler: Full sandbox records
        getSandboxesHandler-->>Client: { sandboxes: [...], filetree, ... }
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

✨ From query strings to auth-aware flows,
Sandboxes now show just what matters most,
Snapshots fall back with grace and care,
Each request finds its rightful path,
The API breathes a sigh of clean design. 🏗️

🚥 Pre-merge checks | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Pull request violates DRY principle through duplicated validator patterns and has incomplete metadataOnly feature implementation. Extract common validator logic into shared utilities, complete metadataOnly implementation in selectAccountSandboxes, and apply consistent non-null assertions across validators.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch sweetmantech/myc-4201-bash-debug-failing-setup-sandbox-task

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.

@vercel
Copy link
Contributor

vercel bot commented Feb 10, 2026

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

Project Deployment Actions Updated (UTC)
recoup-api Ready Ready Preview Feb 10, 2026 9:10pm

Request Review

…216)

Replace unauthenticated accountId query param with auth-derived account
using validateAuthContext + buildGetOrganizationsParams pattern.

- Add buildGetOrganizationsParams with access control (canAccessAccount)
- Add validateGetOrganizationsRequest (auth + Zod query validation)
- Update getOrganizationsHandler to use new validation pipeline
- Add 7 unit tests for buildGetOrganizationsParams
- Delete replaced validateOrganizationsQuery.ts
- Rename param from accountId to account_id (snake_case)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
@sweetmantech sweetmantech changed the title fix: fall back to fresh sandbox when snapshot creation fails feat: add metadata_only param to GET /api/sandboxes Feb 10, 2026
Copy link

@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: 2

🤖 Fix all issues with AI agents
In `@lib/organizations/buildGetOrganizationsParams.ts`:
- Around line 49-53: The Recoup-admin branch in buildGetOrganizationsParams
returns empty params which getAccountOrganizations treats as "no filters" and
triggers the early guard that returns an empty array; change the contract so
Recoup admin signals unrestricted access explicitly (for example return a
payload like { params: null, error: null, unrestricted: true } from
buildGetOrganizationsParams when orgId === RECOUP_ORG_ID) and then update
getAccountOrganizations to check that unrestricted flag (instead of treating
empty params as the early-no-filter case) and fetch all organizations; ensure
you update the signatures/consumers of buildGetOrganizationsParams and the early
guard in getAccountOrganizations to use the new unrestricted indicator (or
equivalent explicit flag) so Recoup admin actually receives all orgs.

In `@lib/organizations/validateGetOrganizationsRequest.ts`:
- Around line 65-75: The destructured params variable can still be typed as
GetAccountOrganizationsParams | null after the error branch; explicitly narrow
it before returning by asserting non-null or using a type guard: in
validateGetOrganizationsRequest, after the existing if (error) return, convert
params to GetAccountOrganizationsParams (e.g., use a non-null assertion or cast
to that type) and return the narrowed value; apply the same fix pattern to the
other validators mentioned (validateGetSandboxesRequest,
validateGetPulsesRequest, validateGetChatsRequest) for consistency.
🧹 Nitpick comments (4)
lib/supabase/account_sandboxes/selectAccountSandboxes.ts (1)

8-8: metadataOnly doesn't belong in the Supabase query params interface.

This field is a handler-level concern (skip expensive API calls), not a database query parameter. Adding it here couples presentation logic to the data-access layer, violating the Single Responsibility Principle. Consider defining a separate validated-request type (e.g., GetSandboxesValidatedRequest) in validateGetSandboxesRequest.ts that extends SelectAccountSandboxesParams with metadataOnly, keeping the Supabase interface focused purely on DB query filtering.

As per coding guidelines, "For Supabase operations, ensure: Follow naming convention: select*, insert*, update*, delete*, get* (for complex queries)" — this interface should remain scoped to query concerns.

lib/sandbox/createSandboxPostHandler.ts (1)

41-42: Consider logging the actual error for debuggability.

The bare catch discards the error object. When diagnosing snapshot failures in production, the original error message/stack would be valuable.

♻️ Suggested improvement
-      } catch {
-        console.error("Snapshot creation failed, falling back to fresh sandbox", { snapshotId });
+      } catch (error) {
+        console.error("Snapshot creation failed, falling back to fresh sandbox", { snapshotId, error });
lib/sandbox/validateGetSandboxesRequest.ts (1)

77-77: Return type couples handler concerns into Supabase params — see related comment on selectAccountSandboxes.ts.

This line adds metadataOnly into the SelectAccountSandboxesParams return type, which as noted in the other file, bleeds handler logic into the data-access interface. If you introduce a dedicated validated-request type, this would be the natural place to define and return it.

app/api/organizations/route.ts (1)

43-45: Pre-existing guideline violation: accountId in request body.

The POST handler's JSDoc documents accountId as a required body parameter (Line 45). Per your coding guidelines, account ID should always be derived from authentication context, never passed in request bodies. This isn't introduced by this PR, but given the direction of this change (moving toward validateAuthContext()-based flows), it's worth a follow-up to align the POST handler as well.

As per coding guidelines, "Never use account_id in request bodies or tool schemas - always derive account ID from authentication".

#!/bin/bash
# Check if createOrganizationHandler uses accountId from request body
rg -n -C5 'accountId|account_id' --type=ts -g '**/organizations/createOrganization*'

Comment on lines +49 to +53
// No account_id filter - determine what to return based on key type
if (orgId === RECOUP_ORG_ID) {
// Recoup admin: return empty params to indicate ALL records
return { params: {}, error: null };
}
Copy link

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
# Locate and examine getAccountOrganizations function
fd "getAccountOrganizations" --type f

Repository: recoupable/api

Length of output: 122


🏁 Script executed:

#!/bin/bash
# Search for the function implementation
rg "export.*function getAccountOrganizations" -A 30

Repository: recoupable/api

Length of output: 2780


Recoup admin path returns no organizations instead of all.

The early guard in getAccountOrganizations (if (!accountId && !organizationId) return [];) means passing empty params {} yields an empty array, not all organizations. The intent stated in the comment conflicts with the implementation. Either buildGetOrganizationsParams should pass different params for Recoup admin, or getAccountOrganizations needs separate logic to handle unrestricted access.

🤖 Prompt for AI Agents
In `@lib/organizations/buildGetOrganizationsParams.ts` around lines 49 - 53, The
Recoup-admin branch in buildGetOrganizationsParams returns empty params which
getAccountOrganizations treats as "no filters" and triggers the early guard that
returns an empty array; change the contract so Recoup admin signals unrestricted
access explicitly (for example return a payload like { params: null, error:
null, unrestricted: true } from buildGetOrganizationsParams when orgId ===
RECOUP_ORG_ID) and then update getAccountOrganizations to check that
unrestricted flag (instead of treating empty params as the early-no-filter case)
and fetch all organizations; ensure you update the signatures/consumers of
buildGetOrganizationsParams and the early guard in getAccountOrganizations to
use the new unrestricted indicator (or equivalent explicit flag) so Recoup admin
actually receives all orgs.

Comment on lines +65 to +75
if (error) {
return NextResponse.json(
{
status: "error",
error,
},
{ status: 403, headers: getCorsHeaders() },
);
}

return params;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, locate the validateGetOrganizationsRequest.ts file
fd -t f "validateGetOrganizationsRequest.ts"

Repository: recoupable/api

Length of output: 110


🏁 Script executed:

# Find buildGetOrganizationsParams to understand the return type
rg -n "buildGetOrganizationsParams" --type=ts

Repository: recoupable/api

Length of output: 1526


🏁 Script executed:

# Check the similar validateGetSandboxesRequest pattern mentioned in scratchpad
fd -t f "validateGetSandboxesRequest.ts"

Repository: recoupable/api

Length of output: 100


🏁 Script executed:

# Read the validateGetOrganizationsRequest.ts file
cat -n lib/organizations/validateGetOrganizationsRequest.ts

Repository: recoupable/api

Length of output: 3025


🏁 Script executed:

# Read the buildGetOrganizationsParams file to understand the return type
cat -n lib/organizations/buildGetOrganizationsParams.ts

Repository: recoupable/api

Length of output: 2769


🏁 Script executed:

# Read the validateGetSandboxesRequest for comparison
cat -n lib/sandbox/validateGetSandboxesRequest.ts

Repository: recoupable/api

Length of output: 3213


🏁 Script executed:

# Also check the buildGetSandboxesParams to see the pattern
fd -t f "buildGetSandboxesParams.ts" | xargs cat -n

Repository: recoupable/api

Length of output: 3269


🏁 Script executed:

# Check if there are any TypeScript errors in the build or tests
find . -name "*.json" -path "*tsconfig*" | head -5

Repository: recoupable/api

Length of output: 73


🏁 Script executed:

# Verify the same pattern appears in both files and how common this pattern is
rg -n 'const { params, error } = await build' --type=ts

Repository: recoupable/api

Length of output: 185


🏁 Script executed:

# Fixed regex - escape the braces
rg -n 'const.*params.*error.*await build' --type=ts

Repository: recoupable/api

Length of output: 693


🏁 Script executed:

# Simpler search - look for destructuring of params and error
rg -n 'params.*error.*build' --type=ts | head -20

Repository: recoupable/api

Length of output: 693


🏁 Script executed:

# Check one of the other validators to see if they have the same issue
cat -n lib/pulse/validateGetPulsesRequest.ts | sed -n '60,75p'

Repository: recoupable/api

Length of output: 460


🏁 Script executed:

# Check the buildGetPulsesParams return type
cat -n lib/pulse/buildGetPulsesParams.ts | head -25

Repository: recoupable/api

Length of output: 1205


🏁 Script executed:

# Check how the return statement looks in validateGetPulsesRequest to compare
cat -n lib/pulse/validateGetPulsesRequest.ts | sed -n '72,85p'

Repository: recoupable/api

Length of output: 324


🏁 Script executed:

# Let's also check if there's a TypeScript configuration that might affect this
cat tsconfig.json | grep -A5 -B5 "strict"

Repository: recoupable/api

Length of output: 308


Type-safety improvement recommended: explicitly narrow params after error check.

While the code compiles and tests pass (due to strict: false in tsconfig), the destructured params retains type GetAccountOrganizationsParams | null even after the if (error) check. TypeScript doesn't narrow destructured union members together. The code is functionally safe—the error branch always returns early—but a non-null assertion or explicit type guard improves clarity and future-proofs against stricter type checking:

♻️ Recommended approach
  if (error) {
    return NextResponse.json(
      { status: "error", error },
      { status: 403, headers: getCorsHeaders() },
    );
  }

- return params;
+ return params!;

Note: This pattern appears consistently across multiple validators (validateGetSandboxesRequest, validateGetPulsesRequest, validateGetChatsRequest, and MCP tools). Consider applying the same fix across all occurrences for consistency.

🤖 Prompt for AI Agents
In `@lib/organizations/validateGetOrganizationsRequest.ts` around lines 65 - 75,
The destructured params variable can still be typed as
GetAccountOrganizationsParams | null after the error branch; explicitly narrow
it before returning by asserting non-null or using a type guard: in
validateGetOrganizationsRequest, after the existing if (error) return, convert
params to GetAccountOrganizationsParams (e.g., use a non-null assertion or cast
to that type) and return the narrowed value; apply the same fix pattern to the
other validators mentioned (validateGetSandboxesRequest,
validateGetPulsesRequest, validateGetChatsRequest) for consistency.

@sweetmantech sweetmantech force-pushed the sweetmantech/myc-4201-bash-debug-failing-setup-sandbox-task branch from a8eb87b to 8136ce3 Compare February 10, 2026 21:08
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.

1 participant