Skip to content

feat(security): rate-limit cost-bearing endpoints (sandbox, LLM, STT)#24

Open
Juanpaulo1128 wants to merge 1 commit into
recoupable:mainfrom
Juanpaulo1128:feat/rate-limit-cost-bearing-endpoints
Open

feat(security): rate-limit cost-bearing endpoints (sandbox, LLM, STT)#24
Juanpaulo1128 wants to merge 1 commit into
recoupable:mainfrom
Juanpaulo1128:feat/rate-limit-cost-bearing-endpoints

Conversation

@Juanpaulo1128
Copy link
Copy Markdown

@Juanpaulo1128 Juanpaulo1128 commented May 4, 2026

Why

open-agents currently has no rate limiting on any endpoint, including those that trigger paid third-party APIs. With this codebase becoming the customer-facing path, one shared sandbox preview URL or one prompt-injected demo can spike Vercel Sandbox / ElevenLabs / LLM gateway costs without a circuit breaker. Adding limits before enterprise sign-up is a ship-blocker.

This PR introduces a Redis-backed limiter (the repo already depends on ioredis via lib/redis.ts) and applies it to the cost-bearing endpoints. Limits are conservative defaults — tune up after observing real traffic.

What changed

New

  • apps/web/lib/rate-limit.ts — atomic fixed-window counter via INCR + EXPIRE. Exports enforceRateLimit(), withRateLimitHeaders(), and a centralised RATE_LIMITS budget table.

Updated (rate-limit applied; no behavioural change beyond 429 + headers)

  • app/api/sandbox/route.ts — POST + DELETE
  • app/api/sandbox/extend/route.ts — POST
  • app/api/sandbox/snapshot/route.ts — POST + PUT
  • app/api/transcribe/route.ts — POST
  • app/api/generate-title/route.ts — POST
  • app/api/sessions/[sessionId]/generate-commit-message/route.ts — POST

Default budgets

Bucket Limit Window Cost driver
sandbox:create 20 1h / user Vercel Sandbox provisioning
sandbox:snapshot 30 1h / user Pause/restore
sandbox:extend 60 1h / user Lifecycle extension
transcribe 60 1h / user ElevenLabs scribe_v1
generate-title 30 1h / user Anthropic claude-haiku-4.5
generate-commit-message 60 1h / user Anthropic claude-haiku-4.5

Per-user identity is derived from the Privy session id (correct under shared corporate IPs); falls back to x-forwarded-for when unauthenticated.

Design notes

  • Atomic — single MULTI(INCR, EXPIRE) round-trip per request. No read-then-write race.
  • Fails open — when Redis is down or REDIS_URL/KV_URL is unset, the limiter logs once and lets the request through. Rationale: a Redis blip should not take down the whole API. Pair with observability (the [rate-limit] redis_error log line) so cost spikes during outages are still visible.
  • Standard headersX-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset on every response (success and 429); Retry-After on 429.
  • Additive — auth flow, request validation, and response shapes are unchanged. Removing the limiter is a one-line revert per route.
  • Centralised budgets — every limit lives in RATE_LIMITS so future adjustments are one diff.

Test plan

  • pnpm install && pnpm build passes
  • With REDIS_URL set: 21 sequential POST /api/sandbox calls — last returns 429 with Retry-After
  • Without REDIS_URL (local dev): one [redis] rate-limit:sandbox:create is disabled… warning, requests pass through
  • Headers X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset present on success and 429 responses
  • Per-user isolation: user A hitting their cap doesn't 429 user B
  • If Redis becomes unreachable mid-traffic: requests succeed, [rate-limit] … redis_error — failing open appears in logs

Out of scope (intentional, separate PRs)

  • app/api/sandbox/reconnect/route.ts and app/api/sessions/[sessionId]/checks/fix/route.ts — same pattern applies; happy to add in a follow-up once the reviewer is comfortable with the approach here.
  • lib/managed-template-trial.ts:64isManagedTemplateTrialUser always returns false, so existing "trial" gates in /api/chat, /api/sessions, code-editor, etc. are dead. Tracked separately; complementary to rate limits, not a substitute.
  • Provider error text leaking through 500 responses (e.g. { error: \Failed to extend sandbox: ${message}` }`). Tightened opportunistically on the touched routes; a full audit is a separate PR.

Operational notes

  • Redis dependency: production environments must have REDIS_URL or KV_URL set for limits to take effect. Add a deploy preflight check if you want hard-fail behaviour.
  • Tuning: the limits in RATE_LIMITS are deliberately conservative. After 1–2 weeks of real traffic, raise the buckets that show legitimate 429s in logs; lower the ones that don't.
  • Migration to api/chat: when this code merges into the main api/chat codebases as part of the Phase 2.2 sandbox refactor, lib/rate-limit.ts should move with it. The route-level integration is one import + two lines per handler.

Summary by cubic

Adds a Redis-backed fixed-window rate limiter to cost-bearing endpoints (sandbox lifecycle, transcription, and LLM helpers) to prevent cost spikes. Requests over budget return 429 with standard rate limit headers; otherwise behavior is unchanged.

  • New Features

    • Added apps/web/app/lib/rate-limit.ts with enforceRateLimit, withRateLimitHeaders, and centralized RATE_LIMITS (built on existing ioredis client).
    • Applied limits to: POST/DELETE /api/sandbox, POST /api/sandbox/extend, POST/PUT /api/sandbox/snapshot, POST /api/transcribe, POST /api/sessions/[sessionId]/generate-commit-message, POST /api/generate-title.
    • Per-user buckets keyed by session id; falls back to x-forwarded-for for unauthenticated requests.
    • Sends X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset on success and 429; adds Retry-After on 429.
    • Fails open if Redis is misconfigured or down; logs once so outages don’t block the API.
  • Migration

    • Set REDIS_URL or KV_URL in production to enforce limits.
    • Adjust RATE_LIMITS after observing real traffic.

Written for commit d95149e. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Audio transcription capability now available
    • API rate limiting implemented across multiple endpoints for improved platform stability and fair resource usage
    • Sandbox session termination functionality for better lifecycle management
  • Bug Fixes

    • Enhanced error handling with standardized response formatting and improved request validation across API endpoints

Adds a Redis-backed sliding-window rate limiter (uses existing ioredis +
lib/redis.ts infra). Applied to every endpoint that triggers a paid
third-party API: Vercel sandbox provisioning, ElevenLabs transcription,
LLM-gated title/commit-message generation.

Per-user buckets via Privy session id; falls back to client IP when
unauthenticated. Fails open with logged errors when Redis is unreachable
so a Redis blip doesn't take down the API — pair with observability.

Limits are conservative defaults (e.g. 20 sandbox creates / hour / user);
tune up after observing real traffic.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 4, 2026

@Juanpaulo1128 is attempting to deploy a commit to the Recoupable Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 4, 2026

📝 Walkthrough

Walkthrough

This PR introduces Redis-backed rate limiting across multiple API routes. A new rate-limit.ts library provides per-endpoint, per-user rate enforcement with header wrapping. Six routes are updated to adopt rate limiting, with generate-title/route.ts rewritten to transcribe audio instead of generating titles, and sandbox/extend/route.ts refactored to delegate POST to sandbox creation and add a new DELETE operation.

Changes

Rate-Limiting Infrastructure & Route Adoption

Layer / File(s) Summary
Rate-Limit Foundation
apps/web/app/lib/rate-limit.ts
New Redis-backed fixed-window rate-limiting module. Exports enforceRateLimit(opts) to check per-bucket limits, getClientIp(req) for client IP extraction, and RATE_LIMITS preset constants for endpoints like sandboxCreate, transcribe, generateCommitMessage. Includes fail-open behavior when Redis is unavailable and build429Response for consistent rate-limit headers (retry-after, x-ratelimit-*).
Audio Transcription Route
apps/web/app/api/generate-title/route.ts
Completely rewritten from text title generation to audio transcription. POST now validates base64-encoded audio field, enforces RATE_LIMITS.transcribe per user, rejects payloads ≥10MB with 413, calls ElevenLabs transcribe, and returns { text } with rate-limit headers. Logs provider errors server-side; returns generic error to client.
Core Transcribe Route
apps/web/app/api/transcribe/route.ts
Adds rate limiting and consistent header wrapping. POST now enforces RATE_LIMITS.transcribe, wraps all responses (JSON parse errors, missing audio, size violations, success/failure) with withRateLimitHeaders. Errors return generic "Transcription failed" without provider details.
Sandbox Management Routes
apps/web/app/api/sandbox/route.ts, apps/web/app/api/sandbox/snapshot/route.ts
Both add enforceRateLimit for respective presets (sandboxCreate and sandboxSnapshot) and wrap all early-return and final responses with withRateLimitHeaders, including JSON parse failures, missing/invalid sessionId, authorization/state check failures, and success payloads. Ensures consistent rate-limit headers across all response paths.
Sandbox Extend Route Refactoring
apps/web/app/api/sandbox/extend/route.ts
Major refactor: POST now uses getServerSession, applies RATE_LIMITS.sandboxCreate, and delegates to handleCreateSandboxRequest with rate-limit header wrapping (removing old timeout-extension logic). New DELETE endpoint added: authenticates user, enforces rate limits, validates sessionId, loads owned session, stops sandbox via connectSandbox, clears state, and updates session with reset lifecycle/snapshot fields. All responses include rate-limit headers.
Commit Message Route
apps/web/app/api/sessions/[sessionId]/generate-commit-message/route.ts
Adds rate limiting: POST now accepts req parameter, enforces RATE_LIMITS.generateCommitMessage per user, wraps all error responses (missing session, no sandbox, invalid diff) and final success response with withRateLimitHeaders. Commit-message generation logic and fallback remain unchanged.

Sequence Diagram(s)

(Skipped: Changes are primarily infrastructure adoption—rate-limit library applied consistently across routes without introducing new multi-component control flow. Individual route changes are simple addition of rate-limiting checks and header wrapping, not complex sequential interactions between 3+ distinct components.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Rationale: The PR spans 6 API routes with non-trivial heterogeneous edits. The rate-limit library itself is moderately complex (Redis pipeline logic, client lifecycle, fail-open semantics). Most route changes follow a consistent pattern (enforce → wrap responses) but require careful verification across each route to ensure all code paths are wrapped correctly. The generate-title/route.ts rewrite and sandbox/extend/route.ts refactoring introduce functional changes beyond boilerplate rate-limiting, requiring separate reasoning. Testing implications and edge cases (JSON parsing, missing fields, Redis unavailability) add verification overhead.

Poem

🐰 A rabbit bounds through rate-limit streams,
Redis counters guard the API dreams,
Each route now wears its header crown,
No thundering herd shall slow us down!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding rate limiting to cost-bearing endpoints (sandbox, LLM, STT services). It is concise, specific, and clearly conveys the primary purpose of the changeset.
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.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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.

}

let body: unknown;
const limit = await enforceRateLimit(
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 rate limits necessary in this codebase?

What specific problem is being solved?

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

🧹 Nitpick comments (2)
apps/web/app/api/generate-title/route.ts (1)

1-93: 💤 Low value

Verify: This endpoint now duplicates transcribe/route.ts.

The AI summary notes this was "rewritten to perform audio transcription instead of generating title." The implementation is now functionally identical to /api/transcribe:

  • Same TranscribeRequestBody interface
  • Same ElevenLabs scribe_v1 model
  • Same providerOptions
  • Same validation and error handling

Is this intentional (e.g., different clients use different paths), or should this route retain its original title-generation behavior? If transcription was the intent, consider whether one endpoint could be deprecated to avoid maintaining duplicate code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/api/generate-title/route.ts` around lines 1 - 93, The route
apps/web/app/api/generate-title/route.ts now duplicates the transcription logic
(TranscribeRequestBody, transcribe call, ElevenLabs options) instead of
generating titles; either restore the original title-generation behavior in the
POST handler (replace the transcribe flow with the original generate-title
logic: accept input audio/text, call the title-generation function or model, and
return the generated title) or explicitly delegate to the existing transcribe
endpoint to avoid duplication (have POST call the transcribe API or import a
shared transcribe helper and then run the title-generation step), updating or
removing TranscribeRequestBody, the elevenlabs.transcription("scribe_v1") usage,
and any providerOptions accordingly; ensure the exported POST function
name/signature remains and update tests/routes that expect /api/generate-title
to either return titles or be redirected/deprecated.
apps/web/app/api/sandbox/extend/route.ts (1)

33-111: ⚡ Quick win

Code duplication: DELETE handler is identical to sandbox/route.ts DELETE.

Lines 33-111 are functionally identical to the DELETE handler in apps/web/app/api/sandbox/route.ts. Consider extracting the shared stop-sandbox logic to a helper function to avoid maintaining duplicate code.

Suggested approach

Extract to a shared helper in @/lib/sandbox/:

// In `@/lib/sandbox/stop-sandbox-handler.ts`
export async function handleStopSandboxRequest(
  req: Request,
  authResult: AuthResult,
  limitHeaders: RateLimitHeaders,
): Promise<Response> {
  // Shared validation, stop, and state-clearing logic
}

Then both route files can delegate:

export async function DELETE(req: Request) {
  // auth + rate limit
  return handleStopSandboxRequest(req, authResult, limit.headers);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/api/sandbox/extend/route.ts` around lines 33 - 111, The DELETE
handler in this file duplicates the DELETE in apps/web/app/api/sandbox/route.ts;
extract the shared stop-sandbox flow (validation of JSON+sessionId,
requireOwnedSession, canOperateOnSandbox check, connectSandbox/stop,
clearSandboxState and updateSession calls, and building the final Response with
withRateLimitHeaders) into a single helper (e.g., export async function
handleStopSandboxRequest(req: Request, authResult: AuthResult, limitHeaders:
RateLimitHeaders): Promise<Response>) placed under a shared module like
`@/lib/sandbox/stop-sandbox-handler.ts`, then have both route DELETE functions
perform authentication + enforceRateLimit and simply return
handleStopSandboxRequest(req, authResult, limit.headers); keep the same
referenced helpers (requireAuthenticatedUser, enforceRateLimit,
requireOwnedSession, canOperateOnSandbox, connectSandbox, clearSandboxState,
updateSession, withRateLimitHeaders) so behavior remains identical.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/web/app/api/sandbox/extend/route.ts`:
- Around line 20-31: The POST handler currently calls
handleCreateSandboxRequest, which creates new sandboxes (breaking the intended
"extend" semantics); change POST in this file to call the extension logic (e.g.,
a handleExtendSandboxRequest or an updated extendSandbox function) and pass the
sandbox identifier/params required to extend timeouts rather than
repoUrl/default creation params, preserving existing rate-limit and session
logic (symbols: POST, handleCreateSandboxRequest, enforceRateLimit,
withRateLimitHeaders). Also remove or consolidate duplicated DELETE logic
between this file and sandbox/route.ts by extracting the shared stop/teardown
logic into a single function (e.g., stopSandbox or handleStopSandbox) and
importing/using it from both routes or keeping it only in the canonical sandbox
route to avoid duplicate behavior.

In `@apps/web/app/api/sessions/`[sessionId]/generate-commit-message/route.ts:
- Around line 23-28: The code references RATE_LIMITS.generateCommitMessage which
doesn't exist; add a new entry named generateCommitMessage to the RATE_LIMITS
export in the rate-limit module (rate-limit.ts) so enforceRateLimit(req,
RATE_LIMITS.generateCommitMessage, session.user.id) compiles; define sensible
rate values consistent with other entries (same shape/keys as other RATE_LIMITS
items) and export it so the route.ts call to enforceRateLimit can use
RATE_LIMITS.generateCommitMessage without TypeScript errors.

In `@apps/web/app/lib/rate-limit.ts`:
- Around line 40-50: Update the enforceRateLimit function signature to accept
three parameters (req: Request, preset: { limit: number; windowSec: number },
userId: string | null) instead of a single RateLimitOptions object, and change
the RateLimitResult success variant to include a headers property (e.g.,
headers: Headers) so callers can read limit.headers; update any internal uses
inside enforceRateLimit (e.g., where you build rate-limit metadata and resetAt)
to populate and return the headers along with ok, limit, remaining and resetAt,
and ensure the failure variant still returns { ok: false, response: Response }
unchanged.
- Around line 179-186: Add a missing RATE_LIMITS entry for
generateCommitMessage: update the exported RATE_LIMITS constant to include a
generateCommitMessage key with limit 60 and windowSec 60 * 60 (3600) so that
references to RATE_LIMITS.generateCommitMessage in
generate-commit-message/route.ts resolve; ensure the new entry follows the same
shape as other entries and preserves the as const satisfies Record<string, {
limit: number; windowSec: number }> typing.
- Around line 7-10: Add and export the missing helper function
withRateLimitHeaders: implement a function named withRateLimitHeaders that
accepts a Response (or NextResponse/Response-like) and a headers object (or
values) and attaches standard rate-limit headers (e.g., X-RateLimit-Limit,
X-RateLimit-Remaining, X-RateLimit-Reset) to the returned response; locate where
rate-limit state is computed (references to cachedClient and
warnedClientFailure) and read the header/value sources used by route handlers,
set those headers on the response, then export the function (export function
withRateLimitHeaders...) from this module so the six route files can import it
without compile errors.

---

Nitpick comments:
In `@apps/web/app/api/generate-title/route.ts`:
- Around line 1-93: The route apps/web/app/api/generate-title/route.ts now
duplicates the transcription logic (TranscribeRequestBody, transcribe call,
ElevenLabs options) instead of generating titles; either restore the original
title-generation behavior in the POST handler (replace the transcribe flow with
the original generate-title logic: accept input audio/text, call the
title-generation function or model, and return the generated title) or
explicitly delegate to the existing transcribe endpoint to avoid duplication
(have POST call the transcribe API or import a shared transcribe helper and then
run the title-generation step), updating or removing TranscribeRequestBody, the
elevenlabs.transcription("scribe_v1") usage, and any providerOptions
accordingly; ensure the exported POST function name/signature remains and update
tests/routes that expect /api/generate-title to either return titles or be
redirected/deprecated.

In `@apps/web/app/api/sandbox/extend/route.ts`:
- Around line 33-111: The DELETE handler in this file duplicates the DELETE in
apps/web/app/api/sandbox/route.ts; extract the shared stop-sandbox flow
(validation of JSON+sessionId, requireOwnedSession, canOperateOnSandbox check,
connectSandbox/stop, clearSandboxState and updateSession calls, and building the
final Response with withRateLimitHeaders) into a single helper (e.g., export
async function handleStopSandboxRequest(req: Request, authResult: AuthResult,
limitHeaders: RateLimitHeaders): Promise<Response>) placed under a shared module
like `@/lib/sandbox/stop-sandbox-handler.ts`, then have both route DELETE
functions perform authentication + enforceRateLimit and simply return
handleStopSandboxRequest(req, authResult, limit.headers); keep the same
referenced helpers (requireAuthenticatedUser, enforceRateLimit,
requireOwnedSession, canOperateOnSandbox, connectSandbox, clearSandboxState,
updateSession, withRateLimitHeaders) so behavior remains identical.
🪄 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: 14171596-e974-4b4e-b0ae-65692bb6cb22

📥 Commits

Reviewing files that changed from the base of the PR and between 2640db5 and d95149e.

📒 Files selected for processing (7)
  • apps/web/app/api/generate-title/route.ts
  • apps/web/app/api/sandbox/extend/route.ts
  • apps/web/app/api/sandbox/route.ts
  • apps/web/app/api/sandbox/snapshot/route.ts
  • apps/web/app/api/sessions/[sessionId]/generate-commit-message/route.ts
  • apps/web/app/api/transcribe/route.ts
  • apps/web/app/lib/rate-limit.ts

Comment on lines +20 to 31
export async function POST(req: Request): Promise<Response> {
// Rate-limit by user identity when known; the underlying handler
// performs its own auth check, so this is purely additive.
const session = await getServerSession();
const userId = session?.user?.id ?? null;

const limit = await enforceRateLimit(req, RATE_LIMITS.sandboxCreate, userId);
if (!limit.ok) return limit.response;

interface ExtendRequest {
sessionId: string;
const response = await handleCreateSandboxRequest(req);
return withRateLimitHeaders(response, limit.headers);
}
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 | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the full route.ts file at the specified path
find . -type f -name "route.ts" | grep -i "sandbox" | head -20

Repository: recoupable/open-agents

Length of output: 324


🏁 Script executed:

# Check if the file exists and read it
cat -n apps/web/app/api/sandbox/extend/route.ts 2>/dev/null || echo "File not found at specified path"

Repository: recoupable/open-agents

Length of output: 3982


🏁 Script executed:

# Search for handleCreateSandboxRequest definition
rg "handleCreateSandboxRequest" --type ts --type tsx -A 5 | head -50

Repository: recoupable/open-agents

Length of output: 94


🏁 Script executed:

# Also check the sandbox/route.ts for comparison
cat -n apps/web/app/api/sandbox/route.ts 2>/dev/null | head -100

Repository: recoupable/open-agents

Length of output: 3698


🏁 Script executed:

# Find and examine handleCreateSandboxRequest
find . -type f -name "*create-sandbox*" | grep -v node_modules

Repository: recoupable/open-agents

Length of output: 211


🏁 Script executed:

# Search for handleCreateSandboxRequest definition and usage
rg "handleCreateSandboxRequest" -A 3 | head -40

Repository: recoupable/open-agents

Length of output: 1485


🏁 Script executed:

# Compare the DELETE handlers in both sandbox routes
echo "=== sandbox/extend/route.ts DELETE ===" && \
sed -n '33,111p' apps/web/app/api/sandbox/extend/route.ts && \
echo -e "\n=== sandbox/route.ts ===" && \
cat -n apps/web/app/api/sandbox/route.ts | head -80

Repository: recoupable/open-agents

Length of output: 5014


🏁 Script executed:

# Search for API client code that references /sandbox/extend
rg "sandbox/extend" --type ts --type js | head -20

Repository: recoupable/open-agents

Length of output: 48


🏁 Script executed:

# Check the full sandbox/route.ts to see if DELETE is identical
diff <(sed -n '33,111p' apps/web/app/api/sandbox/extend/route.ts) <(sed -n '33,111p' apps/web/app/api/sandbox/route.ts) || echo "Files differ or one doesn't exist"

Repository: recoupable/open-agents

Length of output: 206


🏁 Script executed:

# Check the handleCreateSandboxHandler to understand what it creates
head -50 apps/web/lib/sandbox/create-sandbox-handler.ts

Repository: recoupable/open-agents

Length of output: 1830


🏁 Script executed:

# Search for any documentation or comments about the extend endpoint
rg "extend" apps/web/app/api/sandbox/ --type ts -B 2 -A 2 | head -30

Repository: recoupable/open-agents

Length of output: 48


🏁 Script executed:

# Look for git history or comments about what extend endpoint should do
rg "timeout|extend" apps/web/lib/sandbox/ --type ts -B 1 -A 1 | head -50

Repository: recoupable/open-agents

Length of output: 2089


🏁 Script executed:

# Search for any timeout extension logic in the codebase
rg "timeout.*extend|extend.*timeout" --type ts -B 2 -A 2 | head -60

Repository: recoupable/open-agents

Length of output: 1845


🏁 Script executed:

# Look at the full sandbox/route.ts to confirm DELETE handlers are identical
wc -l apps/web/app/api/sandbox/route.ts && tail -100 apps/web/app/api/sandbox/route.ts

Repository: recoupable/open-agents

Length of output: 2931


🏁 Script executed:

# Search for client code that calls the extend endpoint
rg "api/sandbox/extend" --type ts --type js -B 3 -A 3

Repository: recoupable/open-agents

Length of output: 48


🏁 Script executed:

# Check if there's any other extend-related functionality
find apps/web -type f \( -name "*.ts" -o -name "*.tsx" \) -exec grep -l "extend" {} \; | head -20

Repository: recoupable/open-agents

Length of output: 779


🏁 Script executed:

# Search for any timeout extension logic in API routes
rg "extendTimeout" apps/web/app/api/ --type ts

Repository: recoupable/open-agents

Length of output: 48


🏁 Script executed:

# Check if there's an actual extend handler anywhere
find apps/web/app/api -name "*extend*" -type f

Repository: recoupable/open-agents

Length of output: 48


🏁 Script executed:

# Look for references to the extend route in the codebase
rg "sandbox.*extend|extend.*sandbox" apps/web --type ts -l

Repository: recoupable/open-agents

Length of output: 124


🏁 Script executed:

# Check git log for recent changes to the extend/route.ts file
git log --oneline apps/web/app/api/sandbox/extend/route.ts 2>/dev/null | head -10

Repository: recoupable/open-agents

Length of output: 138


🏁 Script executed:

# Search for any frontend references to extend endpoint
rg "sandbox/extend|/extend" apps/web/app --type ts --type tsx -B 2 -A 2 | head -40

Repository: recoupable/open-agents

Length of output: 94


🏁 Script executed:

# Check what handleCreateSandboxRequest actually validates/requires
cat -n apps/web/lib/sandbox/validate-create-sandbox-body.ts

Repository: recoupable/open-agents

Length of output: 1086


🏁 Script executed:

# Check if there's any documentation about the extend endpoint's purpose
find . -name "README*" -o -name "*.md" | xargs grep -l "extend" 2>/dev/null | head -5

Repository: recoupable/open-agents

Length of output: 226


🏁 Script executed:

# Verify the DELETE handler line-by-line comparison between both routes
python3 << 'EOF'
import difflib

# Read sandbox/extend/route.ts DELETE handler
with open('apps/web/app/api/sandbox/extend/route.ts') as f:
    extend_lines = f.readlines()[32:111]  # Lines 33-111 (0-indexed)

# Read sandbox/route.ts DELETE handler
with open('apps/web/app/api/sandbox/route.ts') as f:
    sandbox_lines = f.readlines()[32:111]  # Lines 33-111 (0-indexed)

# Compare
if extend_lines == sandbox_lines:
    print("DELETE handlers are IDENTICAL")
else:
    print("DELETE handlers differ:")
    for line in difflib.unified_diff(sandbox_lines, extend_lines, lineterm=''):
        print(line)
EOF

Repository: recoupable/open-agents

Length of output: 96


🏁 Script executed:

# Check if there are other files with "extend" in sandbox/extend directory
ls -la apps/web/app/api/sandbox/extend/

Repository: recoupable/open-agents

Length of output: 235


🏁 Script executed:

# Look at SANDBOX-LIFECYCLE.md to understand the intended flow
cat apps/web/SANDBOX-LIFECYCLE.md | head -100

Repository: recoupable/open-agents

Length of output: 4318


🏁 Script executed:

# Check if there's any POST handler that takes timeout/extend parameters
rg "extendTimeout|extend.*timeout" apps/web/app --type ts -B 5 -A 5 | head -50

Repository: recoupable/open-agents

Length of output: 48


🏁 Script executed:

# Final check: does handleCreateSandboxRequest require a repoUrl parameter (sandbox creation)?
rg "repoUrl" apps/web/lib/sandbox/ --type ts -B 2 -A 2 | head -30

Repository: recoupable/open-agents

Length of output: 1926


🏁 Script executed:

# Check if there are any test files or examples showing how extend endpoint should work
find . -name "*.test.ts" -o -name "*.spec.ts" | xargs grep -l "sandbox.*extend\|extend.*sandbox" 2>/dev/null | head -5

Repository: recoupable/open-agents

Length of output: 48


The POST /api/sandbox/extend endpoint creates new sandboxes instead of extending timeouts.

The handler delegates to handleCreateSandboxRequest, which requires a repoUrl parameter and creates new sandboxes with default timeouts. The endpoint path implies timeout extension, but the implementation is sandbox creation—a breaking semantic change. Existing clients expecting timeout extension will get unexpected behavior.

Additionally, the DELETE handler is duplicated identically in both sandbox/extend/route.ts and sandbox/route.ts, stopping sandboxes with identical logic. Consider consolidating or clarifying the endpoint's purpose.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/api/sandbox/extend/route.ts` around lines 20 - 31, The POST
handler currently calls handleCreateSandboxRequest, which creates new sandboxes
(breaking the intended "extend" semantics); change POST in this file to call the
extension logic (e.g., a handleExtendSandboxRequest or an updated extendSandbox
function) and pass the sandbox identifier/params required to extend timeouts
rather than repoUrl/default creation params, preserving existing rate-limit and
session logic (symbols: POST, handleCreateSandboxRequest, enforceRateLimit,
withRateLimitHeaders). Also remove or consolidate duplicated DELETE logic
between this file and sandbox/route.ts by extracting the shared stop/teardown
logic into a single function (e.g., stopSandbox or handleStopSandbox) and
importing/using it from both routes or keeping it only in the canonical sandbox
route to avoid duplicate behavior.

Comment on lines +23 to +28
const limit = await enforceRateLimit(
req,
RATE_LIMITS.generateCommitMessage,
session.user.id,
);
if (!limit.ok) return limit.response;
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 | 🔴 Critical | ⚡ Quick win

Blocked: RATE_LIMITS.generateCommitMessage doesn't exist.

This references RATE_LIMITS.generateCommitMessage which is not defined in rate-limit.ts. This will cause a TypeScript error. The entry needs to be added to RATE_LIMITS in the rate-limit library (flagged in that file's review).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/api/sessions/`[sessionId]/generate-commit-message/route.ts
around lines 23 - 28, The code references RATE_LIMITS.generateCommitMessage
which doesn't exist; add a new entry named generateCommitMessage to the
RATE_LIMITS export in the rate-limit module (rate-limit.ts) so
enforceRateLimit(req, RATE_LIMITS.generateCommitMessage, session.user.id)
compiles; define sensible rate values consistent with other entries (same
shape/keys as other RATE_LIMITS items) and export it so the route.ts call to
enforceRateLimit can use RATE_LIMITS.generateCommitMessage without TypeScript
errors.

Comment on lines +7 to +10

let cachedClient: Redis | null = null;
let warnedClientFailure = false;

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 | 🔴 Critical | ⚡ Quick win

Critical: withRateLimitHeaders is not exported but imported by all route handlers.

All 6 route files import withRateLimitHeaders from this module, but no such function is defined or exported. This will cause compilation failures.

Add the missing helper:

Proposed implementation
+/**
+ * Attach rate-limit headers to an existing Response.
+ */
+export function withRateLimitHeaders(
+  response: Response,
+  headers: RateLimitHeaders,
+): Response {
+  const newHeaders = new Headers(response.headers);
+  for (const [key, value] of Object.entries(headers)) {
+    newHeaders.set(key, value);
+  }
+  return new Response(response.body, {
+    status: response.status,
+    statusText: response.statusText,
+    headers: newHeaders,
+  });
+}
📝 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
let cachedClient: Redis | null = null;
let warnedClientFailure = false;
let cachedClient: Redis | null = null;
let warnedClientFailure = false;
/**
* Attach rate-limit headers to an existing Response.
*/
export function withRateLimitHeaders(
response: Response,
headers: RateLimitHeaders,
): Response {
const newHeaders = new Headers(response.headers);
for (const [key, value] of Object.entries(headers)) {
newHeaders.set(key, value);
}
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders,
});
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/lib/rate-limit.ts` around lines 7 - 10, Add and export the
missing helper function withRateLimitHeaders: implement a function named
withRateLimitHeaders that accepts a Response (or NextResponse/Response-like) and
a headers object (or values) and attaches standard rate-limit headers (e.g.,
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset) to the returned
response; locate where rate-limit state is computed (references to cachedClient
and warnedClientFailure) and read the header/value sources used by route
handlers, set those headers on the response, then export the function (export
function withRateLimitHeaders...) from this module so the six route files can
import it without compile errors.

Comment on lines +40 to +50
export type RateLimitResult =
| {
ok: true;
limit: number;
remaining: number;
resetAt: Date;
}
| {
ok: false;
response: Response;
};
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 | 🔴 Critical | ⚡ Quick win

Critical: enforceRateLimit signature and return type don't match how callers use it.

All route handlers call enforceRateLimit(req, RATE_LIMITS.xxx, userId) with 3 arguments and access limit.headers, but:

  1. The function signature accepts a single RateLimitOptions object
  2. RateLimitResult doesn't include a headers property

The function needs to be updated to accept (req: Request, preset: { limit: number; windowSec: number }, userId: string | null) and return headers in the success case.

Proposed signature fix
+export interface RateLimitHeaders {
+  "x-ratelimit-limit": string;
+  "x-ratelimit-remaining": string;
+  "x-ratelimit-reset": string;
+}
+
 export type RateLimitResult =
   | {
       ok: true;
       limit: number;
       remaining: number;
       resetAt: Date;
+      headers: RateLimitHeaders;
     }
   | {
       ok: false;
       response: Response;
     };

-export async function enforceRateLimit(
-  opts: RateLimitOptions,
-): Promise<RateLimitResult> {
+export async function enforceRateLimit(
+  req: Request,
+  preset: { limit: number; windowSec: number },
+  userId: string | null,
+): Promise<RateLimitResult> {
+  const identifier = userId ?? getClientIp(req);
+  const opts: RateLimitOptions = {
+    key: `${identifier}`, // caller should provide endpoint prefix
+    limit: preset.limit,
+    windowSec: preset.windowSec,
+  };

Also applies to: 122-157

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/lib/rate-limit.ts` around lines 40 - 50, Update the
enforceRateLimit function signature to accept three parameters (req: Request,
preset: { limit: number; windowSec: number }, userId: string | null) instead of
a single RateLimitOptions object, and change the RateLimitResult success variant
to include a headers property (e.g., headers: Headers) so callers can read
limit.headers; update any internal uses inside enforceRateLimit (e.g., where you
build rate-limit metadata and resetAt) to populate and return the headers along
with ok, limit, remaining and resetAt, and ensure the failure variant still
returns { ok: false, response: Response } unchanged.

Comment on lines +179 to +186
export const RATE_LIMITS = {
sandboxCreate: { limit: 10, windowSec: 60 * 60 },
sandboxExtend: { limit: 30, windowSec: 60 * 60 },
sandboxSnapshot: { limit: 60, windowSec: 60 * 60 },
transcribe: { limit: 30, windowSec: 60 * 60 },
llmCheap: { limit: 60, windowSec: 60 * 60 },
llmExpensive: { limit: 30, windowSec: 60 * 60 },
} as const satisfies Record<string, { limit: number; windowSec: number }>; No newline at end of file
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 | 🔴 Critical | ⚡ Quick win

Missing generateCommitMessage entry in RATE_LIMITS.

The route at generate-commit-message/route.ts references RATE_LIMITS.generateCommitMessage, but it's not defined here. Based on the PR summary (60 requests/hour for commit message generation), add it:

Proposed fix
 export const RATE_LIMITS = {
   sandboxCreate: { limit: 10, windowSec: 60 * 60 },
   sandboxExtend: { limit: 30, windowSec: 60 * 60 },
   sandboxSnapshot: { limit: 60, windowSec: 60 * 60 },
   transcribe: { limit: 30, windowSec: 60 * 60 },
   llmCheap: { limit: 60, windowSec: 60 * 60 },
   llmExpensive: { limit: 30, windowSec: 60 * 60 },
+  generateCommitMessage: { limit: 60, windowSec: 60 * 60 },
 } as const satisfies Record<string, { limit: number; windowSec: number }>;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/app/lib/rate-limit.ts` around lines 179 - 186, Add a missing
RATE_LIMITS entry for generateCommitMessage: update the exported RATE_LIMITS
constant to include a generateCommitMessage key with limit 60 and windowSec 60 *
60 (3600) so that references to RATE_LIMITS.generateCommitMessage in
generate-commit-message/route.ts resolve; ensure the new entry follows the same
shape as other entries and preserves the as const satisfies Record<string, {
limit: number; windowSec: number }> typing.

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.

2 issues found across 7 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/generate-title/route.ts">

<violation number="1" location="apps/web/app/api/generate-title/route.ts:30">
P2: Missing runtime validation allows malformed request bodies to reach `transcribe()` and be reported as 500s.</violation>
</file>

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

<violation number="1" location="apps/web/app/api/sandbox/extend/route.ts:29">
P1: `/api/sandbox/extend` no longer performs timeout extension; it now creates a sandbox, breaking the documented API contract.</violation>
</file>

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


interface ExtendRequest {
sessionId: string;
const response = await handleCreateSandboxRequest(req);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: /api/sandbox/extend no longer performs timeout extension; it now creates a sandbox, breaking the documented API contract.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/app/api/sandbox/extend/route.ts, line 29:

<comment>`/api/sandbox/extend` no longer performs timeout extension; it now creates a sandbox, breaking the documented API contract.</comment>

<file context>
@@ -1,92 +1,111 @@
 
-interface ExtendRequest {
-  sessionId: string;
+  const response = await handleCreateSandboxRequest(req);
+  return withRateLimitHeaders(response, limit.headers);
 }
</file context>

let body: TranscribeRequestBody;
try {
body = await req.json();
body = (await req.json()) as TranscribeRequestBody;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Missing runtime validation allows malformed request bodies to reach transcribe() and be reported as 500s.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/web/app/api/generate-title/route.ts, line 30:

<comment>Missing runtime validation allows malformed request bodies to reach `transcribe()` and be reported as 500s.</comment>

<file context>
@@ -1,75 +1,93 @@
+  let body: TranscribeRequestBody;
   try {
-    body = await req.json();
+    body = (await req.json()) as TranscribeRequestBody;
   } catch {
-    return Response.json({ error: "Invalid JSON body" }, { status: 400 });
</file context>

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