Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Feat/iteration mode#4

Open
Jackson57279 wants to merge 7 commits into
mainfrom
feat/iteration-mode
Open

Feat/iteration mode#4
Jackson57279 wants to merge 7 commits into
mainfrom
feat/iteration-mode

Conversation

@Jackson57279
Copy link
Copy Markdown
Collaborator

@Jackson57279 Jackson57279 commented Apr 9, 2026


Summary by cubic

Adds Iteration Mode that writes, runs, and refines code in an isolated @e2b/code-interpreter sandbox until it works or hits a limit. Includes a UI toggle, live iteration display, and preview screenshot capture stored via Convex.

  • New Features

    • Inngest processIteration loop using z-ai/glm-5.1 + @e2b/code-interpreter; per-iteration execution, reasoning, and results (JS/TS/Python).
    • Convex: iteration fields on messages; system.updateMessageIterationData, system.getMessageById; files.storeImage action; conversations.getPreviewCapture query.
    • API: POST /api/messages accepts iterationMode, language (javascript | typescript | python), and testCommand.
    • UI: iteration mode toggle/provider, badges, message rendering with progress view; preview capture panel with status and image.
    • Preview capture: tool + Inngest capturePreview using Browserless/Playwright; guide in docs/preview-capture-setup.md.
  • Migration

    • Env vars: E2B_API_KEY (required), POLARIS_CONVEX_INTERNAL_KEY (required for internal mutations/actions), BROWSERLESS_TOKEN (recommended).
    • Install deps and deploy: bun install (adds @e2b/code-interpreter) and deploy Convex schema changes.
    • Optional: set POLARIS_ITERATION_MODEL (defaults to z-ai/glm-5.1).
    • Compatibility updates: aligned with Inngest v4 function API; Next.js experimental flags set in next.config.ts; removed stale billing module from generated API types.

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

Summary by CodeRabbit

  • New Features

    • Iteration Mode: AI-driven iterative code generation/execution with per-iteration history, progress tracking, multi-language support (JS/TS/Python), test-command support, sandbox lifecycle, and final output summary.
    • Preview Capture: asynchronous screenshot capture for previews with queued processing, image storage, status updates, and duration reporting.
  • UI

    • New iteration badges, display panels, toggles, language selector, quick-toggle, and preview-capture UI components.
  • Documentation

    • Guide for configuring screenshot capture (Browserless, external API, local Playwright).
  • Chores

    • Dependency updates and integration plumbing for code interpreter/sandbox features.

- Add E2B (@e2b/code-interpreter) dependency for secure code execution
- Update convex schema with iteration mode fields and e2b_sandboxes table
- Add GLM-5.1 model configuration (zhipu/glm-5.1) for iteration role
- Create E2B client and sandbox manager utilities (src/lib/e2b/)
- Implement process-iteration Inngest function with iteration loop logic
- Update messages API to support iteration mode with language/test command options
- Add IterationModeProvider and toggle components for UI
- Create IterationDisplay component for showing iteration progress and results
- Add MessageIteration component for rendering iteration data in messages
- Register processIteration in Inngest route with 15min timeout and cancellation support

Users can now enable 'I don't care about time' mode to let GLM-5.1 iteratively
refine code in E2B sandbox until it works correctly.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 9, 2026

📝 Walkthrough
📝 Walkthrough
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 54.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title 'Feat/iteration mode' clearly describes the primary feature being added—iteration mode functionality for automated code refinement in a sandbox. It accurately reflects the main change in the changeset.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

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

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.

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.

15 issues found across 20 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="src/components/ai-elements/iteration-display.tsx">

<violation number="1" location="src/components/ai-elements/iteration-display.tsx:68">
P2: Use `??` instead of `||` to avoid treating `currentIteration: 0` as falsy. With `||`, an explicit `0` silently falls through to `iterations.length`, which misrepresents actual progress.</violation>
</file>

<file name="src/components/ai-elements/message.tsx">

<violation number="1" location="src/components/ai-elements/message.tsx:485">
P2: Using `require()` inside the component body does not achieve code splitting — the bundler resolves it statically, so `iteration-display` is bundled unconditionally. Use `next/dynamic` or `React.lazy` with `Suspense` to actually defer loading:

```tsx
import dynamic from "next/dynamic";

const IterationDisplay = dynamic(
  () => import("./iteration-display").then((m) => ({ default: m.IterationDisplay })),
  { ssr: false }
);
```</violation>

<violation number="2" location="src/components/ai-elements/message.tsx:493">
P1: `JSON.parse(i.executionResult)` will throw and crash the component if `executionResult` is not valid JSON. Wrap in a try/catch or use a safe parser.</violation>
</file>

<file name="src/components/ai-elements/preview-capture.tsx">

<violation number="1" location="src/components/ai-elements/preview-capture.tsx:135">
P2: If `capture.durationMs` is `0`, this renders the literal text `0` to the DOM instead of nothing. Use an explicit null/undefined check or convert to boolean.</violation>
</file>

<file name="src/features/conversations/inngest/tools/capture-preview.ts">

<violation number="1" location="src/features/conversations/inngest/tools/capture-preview.ts:36">
P2: Tool description promises "Returns the captured image URL when complete" but the handler only returns a status message — no URL is ever returned. This mismatch will mislead the LLM into expecting an image URL in the tool result, potentially causing hallucinated URLs or broken follow-up logic.</violation>
</file>

<file name="src/app/api/messages/route.ts">

<violation number="1" location="src/app/api/messages/route.ts:110">
P2: Magic number `10` for `maxIterations` is duplicated in two places in this file. Import and use the existing `E2B_CONFIG.maxIterations` from `@/lib/e2b` instead to keep the limit consistent across the codebase.</violation>
</file>

<file name="src/features/conversations/inngest/process-iteration.ts">

<violation number="1" location="src/features/conversations/inngest/process-iteration.ts:221">
P1: The `Sandbox` instance returned from `step.run` will lose its methods after Inngest serializes it for memoization. On any retry/replay, `sandbox` becomes a plain JSON object, so `sandbox.getInfo()` and `executeCode(sandbox!, ...)` will throw. Return only the `sandboxId` string from this step, then reconnect via `Sandbox.connect()` inside each subsequent step that needs the live sandbox.</violation>

<violation number="2" location="src/features/conversations/inngest/process-iteration.ts:299">
P1: The `iterations` array is mutated via `.push()` inside `step.run()` callbacks, but Inngest doesn't re-execute step bodies on replay — it returns memoized results. On any retry, `iterations` stays empty, breaking refinement logic (accessing previous iteration) and the finalize step. Either return iteration data from each step and reconstruct the array outside, or persist iteration state to the database and read it back.</violation>

<violation number="3" location="src/features/conversations/inngest/process-iteration.ts:339">
P1: `isComplete` is set to `true` in the failure branch (`shouldContinue?.success === false`), so the function returns `{ success: true }` even when all iterations exhausted without producing working code.</violation>
</file>

<file name="src/lib/e2b/code-executor.ts">

<violation number="1" location="src/lib/e2b/code-executor.ts:54">
P1: `maxIterations` is hardcoded to `10` instead of being passed as a parameter. The `onIterationStart` callback will always report 10 as the max, regardless of the actual `IterationContext.maxIterations` value. Accept `maxIterations` as a function parameter and forward it.</violation>
</file>

<file name="convex/system.ts">

<violation number="1" location="convex/system.ts:1067">
P1: This query scans the entire `e2b_sandboxes` table. `.withIndex("by_conversation", (q) => q)` applies no constraint — the `(q) => q` callback returns the builder unchanged — so every row is visited, then post-filtered by `sandboxId`. Add a `by_sandbox_id` index on `["sandboxId"]` to the schema and use it here instead.</violation>
</file>

<file name="src/features/conversations/inngest/capture-preview.ts">

<violation number="1" location="src/features/conversations/inngest/capture-preview.ts:90">
P1: `chromium.executablePath()` is synchronous in playwright-core — it returns a string, not a Promise. Calling `.catch()` on the string result throws `TypeError: ...catch is not a function`.

Wrap in a try/catch instead.</violation>

<violation number="2" location="src/features/conversations/inngest/capture-preview.ts:200">
P2: `startTime = Date.now()` is outside any step, so it's recalculated on every Inngest re-invocation. The computed `durationMs` will only measure the time since the latest re-invocation, not the total elapsed time from the function's original start.

Capture the start time inside a step so it's memoized, e.g.:
```ts
const startTime = await step.run("record-start", () => Date.now());
```</violation>

<violation number="3" location="src/features/conversations/inngest/capture-preview.ts:231">
P0: **Bug:** `Buffer` returned from `step.run` won't survive Inngest's JSON serialization. When the function re-invokes for the next step, `screenshotBuffer` will be a plain object `{type: "Buffer", data: [...]}`, not a `Buffer`. `Array.from()` on that plain object yields `[]`, so the upload will send empty image data.

Convert to a serializable format inside the capture step, e.g., return `Array.from(buffer)` or `buffer.toString('base64')`, and adjust the upload step accordingly.</violation>
</file>

<file name="src/lib/e2b/sandbox-manager.ts">

<violation number="1" location="src/lib/e2b/sandbox-manager.ts:48">
P2: Race condition: `getOrCreateSandbox` has a TOCTOU gap between `Sandbox.list()` and `Sandbox.create()`. Two concurrent calls for the same `conversationId` will both find no existing sandbox and create duplicates. Consider using an in-memory lock/mutex keyed by `conversationId` to serialize sandbox creation.</violation>
</file>

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

Comment thread src/features/conversations/inngest/capture-preview.ts
Comment thread src/components/ai-elements/message.tsx Outdated
Comment thread src/features/conversations/inngest/process-iteration.ts Outdated
Comment thread src/features/conversations/inngest/process-iteration.ts Outdated
Comment thread src/features/conversations/inngest/process-iteration.ts Outdated
Comment thread src/components/ai-elements/preview-capture.tsx Outdated
Comment thread src/features/conversations/inngest/tools/capture-preview.ts Outdated
Comment thread src/app/api/messages/route.ts Outdated
Comment thread src/features/conversations/inngest/capture-preview.ts Outdated
Comment thread src/lib/e2b/sandbox-manager.ts Outdated
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: 17

🧹 Nitpick comments (2)
src/app/api/messages/route.ts (1)

105-115: Remove the duplicated iteration limit.

10 is now hard-coded in the placeholder state and again in the emitted message/iteration event, while the worker already carries its own MAX_ITERATIONS default. These values will drift sooner or later, and the UI will stop matching the actual worker behavior. Pull the limit from one shared constant instead.

Also applies to: 120-132

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

In `@src/app/api/messages/route.ts` around lines 105 - 115, The placeholder
iteration state currently hard-codes the maxIterations value as 10 in the
iterationMode/iterationData object (and again where the message/iteration event
is emitted); replace those hard-coded literals by importing and using the single
shared MAX_ITERATIONS constant from the worker/shared module so the UI state
(iterationData.maxIterations) and the emitted message/iteration payload use the
same source of truth as the worker (also update the duplicate occurrence around
the other block noted near lines 120-132).
convex/system.ts (1)

1065-1069: Add an index on sandboxId to avoid post-filtering the by_conversation index.

The current query opens by_conversation without constraint and then filters on sandboxId—an unindexed field. As the e2b_sandboxes table grows, each status update will scan more data. Define .index("by_sandboxId", ["sandboxId"]) in convex/schema.ts, or use Convex's generated document id instead of storing a custom sandboxId string.

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

In `@convex/system.ts` around lines 1065 - 1069, The current query on
e2b_sandboxes uses .withIndex("by_conversation", ...) then filters by sandboxId
(causing an unindexed scan); add a proper index for sandboxId in your schema
(e.g., in convex/schema.ts define an index like .index("by_sandboxId",
["sandboxId"])) and then update the query in the code that calls
ctx.db.query("e2b_sandboxes") to use .withIndex("by_sandboxId", ...) (or
alternatively stop storing a custom sandboxId and use the Convex document id
instead) so the filter by sandboxId is executed via the index rather than
post-filtering.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/preview-capture-setup.md`:
- Around line 18-20: Add appropriate language tags to the fenced code blocks in
docs/preview-capture-setup.md (e.g., change ``` to ```bash or ```text) for the
shown BROWSERLESS_TOKEN block and the other listed blocks (lines around 31-33,
40-42, 44-47, 49-52) so Markdownlint stops flagging them; update each
triple-backtick fence enclosing environment variables or shell commands to use a
language tag like bash or sh, and use text for plain output snippets.
- Line 88: Change the heading text "Screenshots are blank or timeout" to clearer
user-facing copy such as "Screenshots are blank or time out" (or alternatively
"Screenshots are blank or timing out") in the docs; locate the heading string
"Screenshots are blank or timeout" in docs/preview-capture-setup.md and replace
it with the chosen corrected wording.

In `@src/components/ai-elements/iteration-mode.tsx`:
- Around line 121-124: The off-state label for the iteration-mode toggle is
unclear: replace the string shown when variant === "default" and enabled is
false (currently "I don't care about time") with a clearer description such as
"Iteration Mode Off" or "No time limit" so the span in the iteration-mode
component (the JSX branch using variant, enabled) directly communicates the
toggle state; update the conditional rendering where the span is returned to use
the new, explicit off-state label.

In `@src/components/ai-elements/message.tsx`:
- Around line 491-494: Guard the parsing of iteration executionResult in the
iterations mapping in message.tsx: when transforming iterationData.iterations
(the map that currently does JSON.parse(i.executionResult)), first check that
i.executionResult is present and a string (or non-empty) before calling
JSON.parse, and if it's absent or invalid, set executionResult to null (or the
original value) instead; optionally wrap JSON.parse in a try/catch to avoid
throwing on malformed JSON so the UI can render in-progress iterations safely.

In `@src/components/ai-elements/preview-capture.tsx`:
- Line 23: The component references api.conversations.getPreviewCapture which
isn't exported from the Convex API; add a Convex query named getPreviewCapture
in your convex/conversations.ts that accepts the messageId parameter (same shape
used by the component) and returns the preview capture data, then export it so
api.conversations.getPreviewCapture is available to the frontend; ensure the
exported symbol is named getPreviewCapture and matches the expected return shape
the component expects.

In `@src/features/conversations/inngest/capture-preview.ts`:
- Around line 258-268: The build fails because the preview upload calls
api.files.storeImage and api.files.getImageUrl which aren't exported; add
server-side Convex endpoints (an action storeImage and a query getImageUrl) and
export them from convex/files.ts so api.files has those members; implement
storeImage to accept { internalKey, data, contentType } and persist the image
(returning a storageId) and implement getImageUrl to accept { internalKey,
storageId } and return a signed or public URL, then update the convex/files.ts
exports and any types so capture-preview.ts can call api.files.storeImage and
api.files.getImageUrl successfully.

In `@src/features/conversations/inngest/process-iteration.ts`:
- Around line 333-343: The code sets isComplete=true for both real successes and
the exhausted-loop/failure path (when shouldContinue?.success === false),
causing callers to see success=true; change the failure/exhaustion branch so it
does not mark the overall result as successful—keep currentCode =
shouldContinue.finalCode and isComplete = true but ensure the returned success
flag (or a separate variable used for return) is set to false for the
shouldContinue?.success === false branch; make the same change for the analogous
block around the 386-388 logic so failures/exhaustions are distinguishable from
true successes.
- Around line 287-288: The loop currently only calls executeCode(sandbox!, code,
language) and ignores the persisted testCommand; update the iteration logic to,
after executing the main code, check for the presence of testCommand and execute
it in the same sandbox (e.g., call executeCode or the appropriate sandbox
command runner with sandbox!, testCommand, and language/command-mode), capture
its result, and use the testCommand result to determine success/failure of the
iteration (mark iteration failed if tests fail, include test output/errors in
logs/response). Ensure you reference and use the existing sandbox variable and
propagate errors from the testCommand execution instead of treating the
iteration as successful solely based on the main code execution.
- Around line 315-318: The current success boolean in process-iteration.ts is
too strict because it requires isSuccess(reasoning); change the success
calculation to only check executionResult.exitCode === 0 &&
!executionResult.error (remove the isSuccess(reasoning) clause), and preserve
isSuccess(reasoning) only for metadata/annotations if needed (do not gate
completion or iteration flow on it). Update any downstream uses that assume
success implied model token presence to instead use the separate
isSuccess(reasoning) value when computing final status or logs so the final
status calculation remains consistent with the new success definition.
- Around line 395-406: The error path currently updates the message content and
marks the message completed but never updates iterationData.status, leaving the
UI showing a running iteration; modify the error handler inside
step.run("handle-error", ...) to also persist the iteration as failed by calling
the appropriate Convex mutation to set iterationData.status = "failed" (e.g.,
call convex.mutation with a system mutation to update the iteration record)
before or alongside the existing api.system.updateMessageStatus call so the
iteration state in the DB reflects the failure.

In `@src/features/conversations/inngest/tools/capture-preview.ts`:
- Around line 62-100: After successfully creating the preview capture
(previewCaptureId via api.system.createPreviewCapture inside toolStep?.run),
ensure any exception thrown by inngest.send or the subsequent code updates the
Convex record to a failed state: call a Convex mutation (e.g.
api.system.markPreviewCaptureFailed or an updatePreviewCapture mutation) with
previewCaptureId and the error message when catching errors after
createPreviewCapture, swallow/log any errors from that failure-update call, and
then return the original error string; this guarantees the preview record is not
left queued indefinitely.

In `@src/lib/ai-models.ts`:
- Line 13: Update the model slug used as the fallback where the iteration
property is set to "zhipu/glm-5.1" in src/lib/ai-models.ts: replace that string
with OpenRouter's official ID "z-ai/glm-5.1" (the iteration property for the
fallback model) so the code uses the correct provider slug for GLM 5.1.

In `@src/lib/e2b/client.ts`:
- Around line 1-18: Add the server-only guard by importing the server-only
sentinel at the top of the module: insert import 'server-only'; as the first
statement in this file so the E2B_CONFIG export (and validateE2BConfig) cannot
be accidentally bundled into client code; leave E2B_CONFIG and validateE2BConfig
unchanged except for placing that import before them.

In `@src/lib/e2b/code-executor.ts`:
- Around line 47-55: The onIterationStart call in runIteration currently passes
a hardcoded 10; replace that with the real iteration limit used for the run
(e.g., the configured max iterations value on the Sandbox or a maxIterations
parameter) so callbacks?.onIterationStart?.(iterationNumber, REAL_LIMIT) is
invoked; update runIteration to read the actual limit (for example
sandbox.maxIterations or an existing local maxIterations variable) and pass that
identifier instead of 10 so progress reported to IterationCallbacks is correct.
- Around line 89-109: runTests currently allows runCommand to throw on
transport/timeout failures; catch any exception thrown by runCommand inside
runTests and return a normalized ExecutionResult instead of letting the
exception propagate. Specifically, wrap the await runCommand(sandbox,
testCommand, callbacks) call in a try/catch, compute executionTimeMs in both
paths, and on error return an ExecutionResult with stdout/stderr set (empty or
from the caught error if available), exitCode set to a sentinel (e.g. -1),
results as [], and error populated with name (e.g. "TestError" or from the
caught error), value (stringified message), and traceback (stack or error
message). Keep references to runTests, runCommand, ExecutionResult, Sandbox, and
ExecutionCallbacks so the change is localized to that function.

In `@src/lib/e2b/sandbox-manager.ts`:
- Around line 179-185: The setupProjectFiles function currently concatenates
user-supplied paths to "/home/user/" then writes them, allowing path traversal
like "../tmp/x"; fix by normalizing and validating each file.path before
writing: reject absolute paths, compute a safe resolved path using path.resolve
or path.posix.resolve with base "/home/user", then verify the resolved path
starts with the base directory (e.g., "/home/user" + path.sep) or equals the
base; if the check fails, throw an error or skip the file instead of calling
sandbox.files.write; update references in this function (setupProjectFiles and
the sandbox.files.write call) accordingly.

---

Nitpick comments:
In `@convex/system.ts`:
- Around line 1065-1069: The current query on e2b_sandboxes uses
.withIndex("by_conversation", ...) then filters by sandboxId (causing an
unindexed scan); add a proper index for sandboxId in your schema (e.g., in
convex/schema.ts define an index like .index("by_sandboxId", ["sandboxId"])) and
then update the query in the code that calls ctx.db.query("e2b_sandboxes") to
use .withIndex("by_sandboxId", ...) (or alternatively stop storing a custom
sandboxId and use the Convex document id instead) so the filter by sandboxId is
executed via the index rather than post-filtering.

In `@src/app/api/messages/route.ts`:
- Around line 105-115: The placeholder iteration state currently hard-codes the
maxIterations value as 10 in the iterationMode/iterationData object (and again
where the message/iteration event is emitted); replace those hard-coded literals
by importing and using the single shared MAX_ITERATIONS constant from the
worker/shared module so the UI state (iterationData.maxIterations) and the
emitted message/iteration payload use the same source of truth as the worker
(also update the duplicate occurrence around the other block noted near lines
120-132).
🪄 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: 4c899fbd-d485-400b-88a7-7285ea6844b2

📥 Commits

Reviewing files that changed from the base of the PR and between 8941c79 and fb18174.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • convex/schema.ts
  • convex/system.ts
  • docs/preview-capture-setup.md
  • package.json
  • src/app/api/inngest/route.ts
  • src/app/api/messages/route.ts
  • src/components/ai-elements/iteration-badge.tsx
  • src/components/ai-elements/iteration-display.tsx
  • src/components/ai-elements/iteration-mode.tsx
  • src/components/ai-elements/message.tsx
  • src/components/ai-elements/preview-capture.tsx
  • src/features/conversations/inngest/capture-preview.ts
  • src/features/conversations/inngest/process-iteration.ts
  • src/features/conversations/inngest/tools/capture-preview.ts
  • src/lib/ai-models.ts
  • src/lib/e2b/client.ts
  • src/lib/e2b/code-executor.ts
  • src/lib/e2b/index.ts
  • src/lib/e2b/sandbox-manager.ts

Comment thread docs/preview-capture-setup.md Outdated
Comment thread docs/preview-capture-setup.md Outdated
Comment thread src/components/ai-elements/iteration-mode.tsx
Comment thread src/components/ai-elements/message.tsx Outdated
Comment thread src/components/ai-elements/preview-capture.tsx
Comment thread src/lib/e2b/client.ts
Comment thread src/lib/e2b/code-executor.ts
Comment thread src/lib/e2b/code-executor.ts Outdated
Comment thread src/lib/e2b/sandbox-manager.ts
Comment thread src/lib/e2b/sandbox-manager.ts Outdated
Changed the iteration model reference from 'zhipu/glm-5.1' to 'z-ai/glm-5.1' in the AI models configuration to reflect the correct model source.
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.

1 issue found across 1 file (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="src/lib/ai-models.ts">

<violation number="1" location="src/lib/ai-models.ts:13">
P1: Invalid OpenRouter provider prefix `z-ai` — should be `zhipu`. This will cause iteration mode requests to fail because `z-ai/glm-5.1` is not a recognized model on OpenRouter. The PR description itself specifies `zhipu/glm-5.1`.</violation>
</file>

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

Comment thread src/lib/ai-models.ts
@vidoc-ai-security-engineer
Copy link
Copy Markdown

vidoc-ai-security-engineer Bot commented Apr 10, 2026

Vidoc Security Report

Detected 1 security issues.

Severity Name File Line
Medium Improper authentication for storage actions convex/files.ts 380
Details { "scanId": "019d9f00-2d6e-765d-bec5-eedf9a2435b7", "codebaseId": "019d74e8-a987-745e-b138-9ddbd90215b2", "installationId": "019d74e8-a4a8-772f-bdb7-b896739e2b23", "internalPullRequestId": "019d750e-42c7-739c-bb25-42839c113a42" }

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 16 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="src/components/ai-elements/message.tsx">

<violation number="1" location="src/components/ai-elements/message.tsx:496">
P1: Missing `else` branch: when `executionResult` is falsy or not a string, `parsedExecutionResult` remains `null` and will crash `IterationDisplay`. Add a fallback matching the shape the `catch` block uses.</violation>
</file>

<file name="src/features/conversations/inngest/process-iteration.ts">

<violation number="1" location="src/features/conversations/inngest/process-iteration.ts:455">
P2: `iterations: maxIterations` always returns the configured maximum, not the actual number of iterations performed. If the loop breaks early on success/failure, this misreports the count.</violation>
</file>

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

data={{
iterations: iterationData.iterations.map((i) => {
let parsedExecutionResult = null;
if (i.executionResult && typeof i.executionResult === "string") {
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

P1: Missing else branch: when executionResult is falsy or not a string, parsedExecutionResult remains null and will crash IterationDisplay. Add a fallback matching the shape the catch block uses.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/components/ai-elements/message.tsx, line 496:

<comment>Missing `else` branch: when `executionResult` is falsy or not a string, `parsedExecutionResult` remains `null` and will crash `IterationDisplay`. Add a fallback matching the shape the `catch` block uses.</comment>

<file context>
@@ -481,17 +487,30 @@ export const MessageIteration = ({
-          })),
+          iterations: iterationData.iterations.map((i) => {
+            let parsedExecutionResult = null;
+            if (i.executionResult && typeof i.executionResult === "string") {
+              try {
+                parsedExecutionResult = JSON.parse(i.executionResult);
</file context>
Fix with Cubic


return {
success: isSuccess,
iterations: maxIterations,
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

P2: iterations: maxIterations always returns the configured maximum, not the actual number of iterations performed. If the loop breaks early on success/failure, this misreports the count.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/features/conversations/inngest/process-iteration.ts, line 455:

<comment>`iterations: maxIterations` always returns the configured maximum, not the actual number of iterations performed. If the loop breaks early on success/failure, this misreports the count.</comment>

<file context>
@@ -384,15 +451,24 @@ export const processIteration = inngest.createFunction(
-        success: isComplete,
-        iterations: iterations.length,
+        success: isSuccess,
+        iterations: maxIterations,
         messageId,
       };
</file context>
Fix with Cubic

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

Caution

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

⚠️ Outside diff range comments (1)
src/app/api/messages/route.ts (1)

41-47: ⚠️ Potential issue | 🔴 Critical

Authorize the conversation/project against userId before using system queries.

This handler only checks that the caller is signed in, then trusts api.system.getConversationById to resolve the target conversation. Because the system endpoints are keyed by internalKey rather than end-user ownership, a logged-in user can post into or cancel work for another user's conversation by supplying its ID unless you add an explicit owner check here.

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

In `@src/app/api/messages/route.ts` around lines 41 - 47, The handler currently
trusts convex.query(api.system.getConversationById, { internalKey,
conversationId }) and must enforce ownership: after parsing requestSchema and
retrieving the signed-in user's id (e.g., userId/sessionUserId used earlier in
this file), fetch the conversation via api.system.getConversationById and verify
the conversation.ownerId (or equivalent owner field on the returned conversation
object) equals the signed-in userId; if the conversation is missing or owner !==
userId, return a 403/authorization error and stop processing (do not proceed to
mutations like cancel or post). Ensure you use the existing internalKey and
conversationId variables and fail fast when ownership check fails.
♻️ Duplicate comments (1)
src/lib/e2b/sandbox-manager.ts (1)

49-52: ⚠️ Potential issue | 🟠 Major

Scope sandbox reuse and cleanup by messageId, not just conversationId.

Two iteration-mode messages in the same conversation can still reconnect to or kill the same sandbox, which leaks files/process state across runs and lets one message tear down another message’s environment.

Also applies to: 242-246

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

In `@src/lib/e2b/sandbox-manager.ts` around lines 49 - 52, The current sandbox
lookup uses only conversationId so multiple iteration-mode messages can reuse or
kill the same sandbox; change the lookup to require both conversationId and
messageId by checking s.metadata?.conversationId === conversationId &&
s.metadata?.messageId === messageId (update the sandboxes.find(...) that assigns
existing and the analogous logic later around the second occurrence), and ensure
any creation, reuse and cleanup code (references to Sandbox.list(), the existing
variable, and sandbox kill/cleanup paths) store and check metadata.messageId
when creating or terminating sandboxes so each message gets its own isolated
sandbox.
🧹 Nitpick comments (2)
convex/files.ts (1)

370-376: Keep internal-key validation consistent with convex/system.ts.

This version collapses “missing env var” and “wrong key” into the same error, which makes rollout failures harder to diagnose and lets the validation logic drift across internal endpoints. Reuse the shared validator or preserve the distinct error cases here as well.

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

In `@convex/files.ts` around lines 370 - 376, The validateInternalKey function
currently conflates "missing env var" and "wrong key"; either import and reuse
the shared validator from convex/system.ts (e.g., the exported
validateInternalKey or validatePolarisInternalKey function there) or change this
function to match that file's behavior by first checking if
process.env.POLARIS_CONVEX_INTERNAL_KEY is set and throwing a distinct "Missing
internal key" (or similar) error, then separately comparing internalKey !==
expectedKey and throwing a distinct "Invalid internal key" error; update the
function name or import to reference the same shared symbol used in
convex/system.ts so behavior is consistent across endpoints.
convex/schema.ts (1)

89-112: Avoid storing the full iteration transcript inside messages.

This shape puts every code snapshot, execution log, and reasoning blob into one messages document, and each append rewrites the whole array. Iteration mode is exactly the kind of flow that grows fast, so this will become a size/perf bottleneck quickly. A separate message_iterations table keyed by messageId would scale much better, with messages.iterationData reduced to summary fields.

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

In `@convex/schema.ts` around lines 89 - 112, The current messages schema stores
full iteration transcripts in the iterationData field (iterationData on the
messages document), which causes large rewrites and performance problems;
refactor by removing large arrays from messages.iterationData and instead create
a new message_iterations table keyed by messageId (e.g., message_iterations with
fields iterationNumber, code, executionResult, reasoning, timestamp, sandboxId)
to append individual iterations; reduce messages.iterationData to lightweight
summary fields (e.g., iterationCount, lastIterationTimestamp, status, sandboxId,
finalOutput, maxIterations, currentIteration, testCommand, language) and update
code paths that append iterations to insert into message_iterations rather than
rewriting messages.iterationData, keeping references between messages and
message_iterations by messageId.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@convex/schema.ts`:
- Around line 89-112: The schema requires iterationData.status but the append
path in convex/system.ts constructs iterationData from existingData and may omit
status, causing validation failures; fix by making status optional in the
iterationData schema (wrap the current v.union(...) for status with
v.optional(...)) or alternatively ensure convex/system.ts initializes
iterationData.status (set to "running" or "completed"/"failed" as appropriate)
before performing the first append; update the symbol iterationData.status in
the schema or the code that builds iterationData to keep both sides consistent.
- Around line 242-252: The schema allows duplicate sandboxId values but
downstream code (in createE2BSandbox and updates that query by_sandbox_id and
call .first()) assumes sandboxId is unique; fix by making writes idempotent:
modify createE2BSandbox to first query the e2b_sandboxes index "by_sandbox_id"
for the sandboxId and return the existing record instead of inserting if found,
otherwise insert; alternatively (schema change) make sandboxId the primary id by
replacing sandboxId: v.string() with id: v.string() in the e2b_sandboxes table
so there can only be one record per sandboxId and update callers that expect the
Convex record id accordingly.

In `@src/features/conversations/inngest/capture-preview.ts`:
- Around line 163-170: The external fetch to screenshotUrl must use an
AbortController with a short configurable timeout so a stalled provider won't
pin the worker; create an AbortController, start a setTimeout to call
controller.abort() after the timeout (e.g., 10s or from config), pass
controller.signal into fetch(screenshotUrl.toString(), { signal }), clear the
timeout once response arrives before calling response.arrayBuffer(), and catch
the abort/timeout case to throw a normal capture failure (e.g., throw new
Error('Screenshot API timeout') or convert to the same error type used for other
screenshot failures) so callers see it as a regular capture error.
- Around line 20-31: The project.deploymentUrl value returned from getPreviewUrl
must be validated before use to prevent SSRF; update the getPreviewUrl function
to parse project.deploymentUrl with the URL constructor, ensure the protocol is
http or https, and reject (return null) if the hostname resolves to or is a
loopback (localhost, 127.0.0.0/8, ::1), link-local (169.254.0.0/16), or private
RFC1918 ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) or any non-FQDN that
could point to internal resources; perform hostname/IP checks by validating
hostname strings and, where necessary, resolving DNS to detect IPs before
returning project.deploymentUrl from getPreviewUrl.

In `@src/features/conversations/inngest/process-iteration.ts`:
- Around line 453-456: The return object currently always sets iterations:
maxIterations; change it to return the actual iteration counter used by the loop
(the variable that increments each loop, e.g., iteration, i, or iterationsRun)
so it reports the real number of iterations performed; if that counter name is
not already present in scope, introduce and increment a clear variable (e.g.,
iterationsRun) inside the loop and return iterations: iterationsRun (or fall
back to maxIterations only if the counter is undefined).
- Around line 290-309: The local const named "message" in this block shadows the
outer user prompt variable, causing generateRefinedCode(...) to receive an
object instead of the original prompt; rename the Convex-loaded document (e.g.,
to messageDoc or messageRecord) and update subsequent uses (previousIteration,
previousExecutionResult, and the call to generateRefinedCode) to pass the
original outer prompt variable (the user task string) rather than the shadowed
document so the refinement prompt contains the correct text.

In `@src/lib/e2b/sandbox-manager.ts`:
- Around line 216-236: readFile and listFiles accept absolute or traversal paths
which can escape the workspace; change both to re-root and sanitize inputs so
all resolved paths are under /home/user. For readFile, use
path.posix.join("/home/user", incomingPath) with path.posix.normalize and for
listFiles do the same for the path argument (default "/home/user"), then verify
the normalized result startsWith("/home/user") and throw an error if not. Apply
this logic inside the readFile and listFiles functions (referencing function
names readFile and listFiles and the sandbox.files.read/list calls) so no
absolute or ".." traversal can access outside the sandbox.

---

Outside diff comments:
In `@src/app/api/messages/route.ts`:
- Around line 41-47: The handler currently trusts
convex.query(api.system.getConversationById, { internalKey, conversationId })
and must enforce ownership: after parsing requestSchema and retrieving the
signed-in user's id (e.g., userId/sessionUserId used earlier in this file),
fetch the conversation via api.system.getConversationById and verify the
conversation.ownerId (or equivalent owner field on the returned conversation
object) equals the signed-in userId; if the conversation is missing or owner !==
userId, return a 403/authorization error and stop processing (do not proceed to
mutations like cancel or post). Ensure you use the existing internalKey and
conversationId variables and fail fast when ownership check fails.

---

Duplicate comments:
In `@src/lib/e2b/sandbox-manager.ts`:
- Around line 49-52: The current sandbox lookup uses only conversationId so
multiple iteration-mode messages can reuse or kill the same sandbox; change the
lookup to require both conversationId and messageId by checking
s.metadata?.conversationId === conversationId && s.metadata?.messageId ===
messageId (update the sandboxes.find(...) that assigns existing and the
analogous logic later around the second occurrence), and ensure any creation,
reuse and cleanup code (references to Sandbox.list(), the existing variable, and
sandbox kill/cleanup paths) store and check metadata.messageId when creating or
terminating sandboxes so each message gets its own isolated sandbox.

---

Nitpick comments:
In `@convex/files.ts`:
- Around line 370-376: The validateInternalKey function currently conflates
"missing env var" and "wrong key"; either import and reuse the shared validator
from convex/system.ts (e.g., the exported validateInternalKey or
validatePolarisInternalKey function there) or change this function to match that
file's behavior by first checking if process.env.POLARIS_CONVEX_INTERNAL_KEY is
set and throwing a distinct "Missing internal key" (or similar) error, then
separately comparing internalKey !== expectedKey and throwing a distinct
"Invalid internal key" error; update the function name or import to reference
the same shared symbol used in convex/system.ts so behavior is consistent across
endpoints.

In `@convex/schema.ts`:
- Around line 89-112: The current messages schema stores full iteration
transcripts in the iterationData field (iterationData on the messages document),
which causes large rewrites and performance problems; refactor by removing large
arrays from messages.iterationData and instead create a new message_iterations
table keyed by messageId (e.g., message_iterations with fields iterationNumber,
code, executionResult, reasoning, timestamp, sandboxId) to append individual
iterations; reduce messages.iterationData to lightweight summary fields (e.g.,
iterationCount, lastIterationTimestamp, status, sandboxId, finalOutput,
maxIterations, currentIteration, testCommand, language) and update code paths
that append iterations to insert into message_iterations rather than rewriting
messages.iterationData, keeping references between messages and
message_iterations by messageId.
🪄 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: 9fe4b0ee-eb07-47e2-b5ea-71731c9e3f01

📥 Commits

Reviewing files that changed from the base of the PR and between 6742285 and f0e8943.

📒 Files selected for processing (16)
  • convex/conversations.ts
  • convex/files.ts
  • convex/schema.ts
  • convex/system.ts
  • docs/preview-capture-setup.md
  • src/app/api/messages/route.ts
  • src/components/ai-elements/iteration-display.tsx
  • src/components/ai-elements/iteration-mode.tsx
  • src/components/ai-elements/message.tsx
  • src/components/ai-elements/preview-capture.tsx
  • src/features/conversations/inngest/capture-preview.ts
  • src/features/conversations/inngest/process-iteration.ts
  • src/features/conversations/inngest/tools/capture-preview.ts
  • src/lib/e2b/client.ts
  • src/lib/e2b/code-executor.ts
  • src/lib/e2b/sandbox-manager.ts
✅ Files skipped from review due to trivial changes (5)
  • src/lib/e2b/client.ts
  • docs/preview-capture-setup.md
  • src/features/conversations/inngest/tools/capture-preview.ts
  • src/components/ai-elements/preview-capture.tsx
  • src/components/ai-elements/iteration-display.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • convex/system.ts
  • src/components/ai-elements/iteration-mode.tsx

Comment thread convex/schema.ts
Comment on lines +89 to +112
iterationData: v.optional(
v.object({
iterations: v.array(
v.object({
iterationNumber: v.number(),
code: v.string(),
executionResult: v.optional(v.string()),
reasoning: v.optional(v.string()),
timestamp: v.number(),
})
),
sandboxId: v.optional(v.string()),
finalOutput: v.optional(v.string()),
status: v.union(
v.literal("running"),
v.literal("completed"),
v.literal("failed")
),
maxIterations: v.optional(v.number()),
currentIteration: v.optional(v.number()),
testCommand: v.optional(v.string()),
language: v.optional(v.union(v.literal("javascript"), v.literal("typescript"), v.literal("python"))),
})
),
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

Required iterationData.status does not match the append path.

Line 102 makes status mandatory, but convex/system.ts:992-1030 builds iterationData from existingData and only overwrites iterations. On a fresh message, that patch can omit status entirely, so the first append will be rejected by the schema. Please either initialize status before any append or make this field optional here.

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

In `@convex/schema.ts` around lines 89 - 112, The schema requires
iterationData.status but the append path in convex/system.ts constructs
iterationData from existingData and may omit status, causing validation
failures; fix by making status optional in the iterationData schema (wrap the
current v.union(...) for status with v.optional(...)) or alternatively ensure
convex/system.ts initializes iterationData.status (set to "running" or
"completed"/"failed" as appropriate) before performing the first append; update
the symbol iterationData.status in the schema or the code that builds
iterationData to keep both sides consistent.

Comment thread convex/schema.ts
Comment on lines +242 to +252
e2b_sandboxes: defineTable({
conversationId: v.id("conversations"),
messageId: v.optional(v.id("messages")),
sandboxId: v.string(),
status: v.union(v.literal("active"), v.literal("paused"), v.literal("destroyed")),
createdAt: v.number(),
lastUsedAt: v.number(),
})
.index("by_conversation", ["conversationId"])
.index("by_message", ["messageId"])
.index("by_sandbox_id", ["sandboxId"]),
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

sandboxId is treated as unique downstream, but this model does not make creation idempotent.

convex/system.ts:1056-1078 resolves by_sandbox_id with .first(), so later updates assume exactly one row per sandbox. But createE2BSandbox in convex/system.ts:1033-1055 always inserts, which means a retry can create duplicates and make status updates hit an arbitrary record. Please enforce one-record-per-sandboxId in the write path or remodel this so the lookup is unambiguous.

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

In `@convex/schema.ts` around lines 242 - 252, The schema allows duplicate
sandboxId values but downstream code (in createE2BSandbox and updates that query
by_sandbox_id and call .first()) assumes sandboxId is unique; fix by making
writes idempotent: modify createE2BSandbox to first query the e2b_sandboxes
index "by_sandbox_id" for the sandboxId and return the existing record instead
of inserting if found, otherwise insert; alternatively (schema change) make
sandboxId the primary id by replacing sandboxId: v.string() with id: v.string()
in the e2b_sandboxes table so there can only be one record per sandboxId and
update callers that expect the Convex record id accordingly.

Comment on lines +20 to +31
async function getPreviewUrl(projectId: Id<"projects">, internalKey: string): Promise<string | null> {
const project = await convex.query(api.system.getProjectById, {
internalKey,
projectId,
});

if (!project) return null;

// If project has a deployment URL, use that
if (project.deploymentUrl) {
return project.deploymentUrl;
}
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

Validate deploymentUrl before any server-side navigation.

This returns whatever URL is stored on the project and the worker later opens it with Browserless/Playwright or sends it to the screenshot API. Without scheme/host validation, a project pointing at loopback, link-local, or other internal addresses turns preview capture into an SSRF path.

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

In `@src/features/conversations/inngest/capture-preview.ts` around lines 20 - 31,
The project.deploymentUrl value returned from getPreviewUrl must be validated
before use to prevent SSRF; update the getPreviewUrl function to parse
project.deploymentUrl with the URL constructor, ensure the protocol is http or
https, and reject (return null) if the hostname resolves to or is a loopback
(localhost, 127.0.0.0/8, ::1), link-local (169.254.0.0/16), or private RFC1918
ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) or any non-FQDN that could
point to internal resources; perform hostname/IP checks by validating hostname
strings and, where necessary, resolving DNS to detect IPs before returning
project.deploymentUrl from getPreviewUrl.

Comment on lines +163 to +170
const response = await fetch(screenshotUrl.toString());

if (!response.ok) {
throw new Error(`Screenshot API failed: ${response.status} ${response.statusText}`);
}

const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
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

Add a timeout to the external screenshot API call.

A stalled provider connection here can pin the worker until the whole Inngest function times out. Please attach an abort signal / request timeout and surface that as a normal capture failure.

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

In `@src/features/conversations/inngest/capture-preview.ts` around lines 163 -
170, The external fetch to screenshotUrl must use an AbortController with a
short configurable timeout so a stalled provider won't pin the worker; create an
AbortController, start a setTimeout to call controller.abort() after the timeout
(e.g., 10s or from config), pass controller.signal into
fetch(screenshotUrl.toString(), { signal }), clear the timeout once response
arrives before calling response.arrayBuffer(), and catch the abort/timeout case
to throw a normal capture failure (e.g., throw new Error('Screenshot API
timeout') or convert to the same error type used for other screenshot failures)
so callers see it as a regular capture error.

Comment on lines +290 to +309
const message = await convex.query(api.system.getMessageById, {
internalKey,
messageId,
});
const iterationData = message?.iterationData;
const iterations = iterationData?.iterations || [];
const previousIteration = iterations[iterations.length - 1];

if (!previousIteration) {
throw new Error("No previous iteration found");
}

const previousExecutionResult: ExecutionResult = JSON.parse(previousIteration.executionResult || '{}');
const result = await generateRefinedCode(
message,
previousIteration.code,
previousExecutionResult,
iteration,
language
);
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

Don't shadow the user prompt before calling generateRefinedCode().

The Convex message document loaded here shadows the outer message string, so later iterations pass an object where the prompt expects the original task text. In practice that turns the refinement prompt into [object Object] and drops the user's requirements from every retry.

Suggested fix
-              const message = await convex.query(api.system.getMessageById, {
+              const messageDoc = await convex.query(api.system.getMessageById, {
                 internalKey,
                 messageId,
               });
-              const iterationData = message?.iterationData;
+              const iterationData = messageDoc?.iterationData;
               const iterations = iterationData?.iterations || [];
               const previousIteration = iterations[iterations.length - 1];
@@
               const result = await generateRefinedCode(
                 message,
                 previousIteration.code,
                 previousExecutionResult,
                 iteration,
                 language
               );
📝 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
const message = await convex.query(api.system.getMessageById, {
internalKey,
messageId,
});
const iterationData = message?.iterationData;
const iterations = iterationData?.iterations || [];
const previousIteration = iterations[iterations.length - 1];
if (!previousIteration) {
throw new Error("No previous iteration found");
}
const previousExecutionResult: ExecutionResult = JSON.parse(previousIteration.executionResult || '{}');
const result = await generateRefinedCode(
message,
previousIteration.code,
previousExecutionResult,
iteration,
language
);
const messageDoc = await convex.query(api.system.getMessageById, {
internalKey,
messageId,
});
const iterationData = messageDoc?.iterationData;
const iterations = iterationData?.iterations || [];
const previousIteration = iterations[iterations.length - 1];
if (!previousIteration) {
throw new Error("No previous iteration found");
}
const previousExecutionResult: ExecutionResult = JSON.parse(previousIteration.executionResult || '{}');
const result = await generateRefinedCode(
message,
previousIteration.code,
previousExecutionResult,
iteration,
language
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/conversations/inngest/process-iteration.ts` around lines 290 -
309, The local const named "message" in this block shadows the outer user prompt
variable, causing generateRefinedCode(...) to receive an object instead of the
original prompt; rename the Convex-loaded document (e.g., to messageDoc or
messageRecord) and update subsequent uses (previousIteration,
previousExecutionResult, and the call to generateRefinedCode) to pass the
original outer prompt variable (the user task string) rather than the shadowed
document so the refinement prompt contains the correct text.

Comment on lines +453 to +456
return {
success: isSuccess,
iterations: maxIterations,
messageId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Return the actual iteration count.

This always reports maxIterations, even when the loop exits on the first successful run. That makes monitoring and any downstream consumers think the job used the full budget every time.

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

In `@src/features/conversations/inngest/process-iteration.ts` around lines 453 -
456, The return object currently always sets iterations: maxIterations; change
it to return the actual iteration counter used by the loop (the variable that
increments each loop, e.g., iteration, i, or iterationsRun) so it reports the
real number of iterations performed; if that counter name is not already present
in scope, introduce and increment a clear variable (e.g., iterationsRun) inside
the loop and return iterations: iterationsRun (or fall back to maxIterations
only if the counter is undefined).

Comment thread src/lib/e2b/sandbox-manager.ts Outdated
Comment on lines +216 to +236
export async function readFile(
sandbox: Sandbox,
path: string
): Promise<string> {
const fullPath = path.startsWith("/") ? path : `/home/user/${path}`;
return await sandbox.files.read(fullPath);
}

/**
* List files in sandbox directory
*/
export async function listFiles(
sandbox: Sandbox,
path: string = "/home/user"
): Promise<Array<{ name: string; type: "file" | "dir"; path: string }>> {
const entries = await sandbox.files.list(path);
return entries.map((e) => ({
name: e.name,
type: e.type,
path: `${path}/${e.name}`,
}));
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

Re-root readFile() and listFiles() under /home/user.

setupProjectFiles() now rejects escapes, but these helpers still accept absolute paths or traversal-like inputs and can read/list arbitrary sandbox locations. That bypasses the workspace isolation you just added for writes.

Suggested hardening
 export async function readFile(
   sandbox: Sandbox,
-  path: string
+  filePath: string
 ): Promise<string> {
-  const fullPath = path.startsWith("/") ? path : `/home/user/${path}`;
-  return await sandbox.files.read(fullPath);
+  const baseDir = "/home/user";
+  if (path.isAbsolute(filePath)) {
+    throw new Error(`Absolute paths are not allowed: ${filePath}`);
+  }
+  const resolvedPath = path.posix.resolve(baseDir, filePath);
+  const relativeFromBase = path.posix.relative(baseDir, resolvedPath);
+  if (relativeFromBase.startsWith("..")) {
+    throw new Error(`Path traversal detected: ${filePath}`);
+  }
+  return await sandbox.files.read(resolvedPath);
 }
@@
 export async function listFiles(
   sandbox: Sandbox,
-  path: string = "/home/user"
+  dirPath: string = "."
 ): Promise<Array<{ name: string; type: "file" | "dir"; path: string }>> {
-  const entries = await sandbox.files.list(path);
+  const baseDir = "/home/user";
+  if (path.isAbsolute(dirPath)) {
+    throw new Error(`Absolute paths are not allowed: ${dirPath}`);
+  }
+  const resolvedPath = path.posix.resolve(baseDir, dirPath);
+  const relativeFromBase = path.posix.relative(baseDir, resolvedPath);
+  if (relativeFromBase.startsWith("..")) {
+    throw new Error(`Path traversal detected: ${dirPath}`);
+  }
+  const entries = await sandbox.files.list(resolvedPath);
   return entries.map((e) => ({
     name: e.name,
     type: e.type,
-    path: `${path}/${e.name}`,
+    path: `${resolvedPath}/${e.name}`,
   }));
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/e2b/sandbox-manager.ts` around lines 216 - 236, readFile and
listFiles accept absolute or traversal paths which can escape the workspace;
change both to re-root and sanitize inputs so all resolved paths are under
/home/user. For readFile, use path.posix.join("/home/user", incomingPath) with
path.posix.normalize and for listFiles do the same for the path argument
(default "/home/user"), then verify the normalized result
startsWith("/home/user") and throw an error if not. Apply this logic inside the
readFile and listFiles functions (referencing function names readFile and
listFiles and the sandbox.files.read/list calls) so no absolute or ".."
traversal can access outside the sandbox.

- Bump versions for several dependencies including @ai-sdk/google, @ai-sdk/openai-compatible, @clerk/nextjs, ai, inngest, lucide-react, posthog-js, and posthog-node to their latest releases for improved functionality and security.
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 3 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="business-plan.md">

<violation number="1" location="business-plan.md:1">
P0: This file contains highly sensitive business and investor strategy (competitive positioning, margin analysis, fundraising plans, proprietary model roadmap, risk disclosures) and appears to be committed to a public repository. Internal strategy documents like this should not live in the source tree — use a private docs platform or at minimum add the file to `.gitignore`. The document itself notes it is an *"Internal reference for positioning, marketing copy, and investor conversations."*</violation>

<violation number="2" location="business-plan.md:123">
P3: Header says "Three bullets" but four items follow. Update the header to "Four bullets" or move the roadmap item out of this list since it's explicitly a future teaser.</violation>
</file>

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

Comment thread business-plan.md
@@ -0,0 +1,206 @@
# LuminaWeb — Business & investor narrative
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

P0: This file contains highly sensitive business and investor strategy (competitive positioning, margin analysis, fundraising plans, proprietary model roadmap, risk disclosures) and appears to be committed to a public repository. Internal strategy documents like this should not live in the source tree — use a private docs platform or at minimum add the file to .gitignore. The document itself notes it is an "Internal reference for positioning, marketing copy, and investor conversations."

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At business-plan.md, line 1:

<comment>This file contains highly sensitive business and investor strategy (competitive positioning, margin analysis, fundraising plans, proprietary model roadmap, risk disclosures) and appears to be committed to a public repository. Internal strategy documents like this should not live in the source tree — use a private docs platform or at minimum add the file to `.gitignore`. The document itself notes it is an *"Internal reference for positioning, marketing copy, and investor conversations."*</comment>

<file context>
@@ -0,0 +1,206 @@
+# LuminaWeb — Business & investor narrative
+
+Internal reference for positioning, marketing copy, and investor conversations. Product facts are aligned with the current codebase (browser cloud IDE, AI agent, WebContainer preview, Convex real-time, Clerk billing).
</file context>
Fix with Cubic

Comment thread business-plan.md
**Homepage hero (example):**
*Ship code from your browser. LuminaWeb pairs a serious editor with an AI that edits your files, runs your app, and opens a real terminal—no install, no drift.*

**Three bullets for ads or landing sections:**
Copy link
Copy Markdown

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

Choose a reason for hiding this comment

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

P3: Header says "Three bullets" but four items follow. Update the header to "Four bullets" or move the roadmap item out of this list since it's explicitly a future teaser.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At business-plan.md, line 123:

<comment>Header says "Three bullets" but four items follow. Update the header to "Four bullets" or move the roadmap item out of this list since it's explicitly a future teaser.</comment>

<file context>
@@ -0,0 +1,206 @@
+**Homepage hero (example):**  
+*Ship code from your browser. LuminaWeb pairs a serious editor with an AI that edits your files, runs your app, and opens a real terminal—no install, no drift.*
+
+**Three bullets for ads or landing sections:**
+
+1. **Agentic editing** — The assistant doesn’t just answer; it applies multi-file changes in your tree.
</file context>
Suggested change
**Three bullets for ads or landing sections:**
**Four bullets for ads or landing sections:**
Fix with Cubic

Resolve schema/bun.lock conflicts (keep iteration fields + imageUrls).
Align Inngest v4 createFunction API, AI SDK maxOutputTokens, E2B list pagination.
Add preview capture and getMessageById Convex APIs; fix files storage Blob API.
Remove stale billing module from generated API types.

Made-with: Cursor
- Introduced experimental features: typedEnv set to false and reportSystemEnvInlining set to "warn".
- Updated continual-learning.json to reflect the latest run state with incremented turnsSinceLastRun and updated lastProcessedGenerationId.
Comment thread convex/files.ts
Comment on lines +402 to +411
/**
* Get public URL for stored image
*/
export const getImageUrl = query({
args: {
internalKey: v.string(),
storageId: v.id("_storage"),
},
handler: async (ctx, args) => {
validateInternalKey(args.internalKey);
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

Sensitive storage actions (storeImage, getImageUrl) rely on a shared secret passed as an argument, not proper authentication. If this secret is leaked, attackers can store arbitrary data and retrieve it via signed URLs.

Severity: Medium

Explanation

The storeImage action and getImageUrl query are intended for internal use, as indicated by the use of POLARIS_CONVEX_INTERNAL_KEY for validation. However, they are exposed as public Convex functions and do not perform proper user authentication using verifyAuth. Instead, they rely on passing the internalKey directly as an argument. This means if an attacker obtains the POLARIS_CONVEX_INTERNAL_KEY, they can call these functions to store arbitrary data in your Convex storage or retrieve data using generated URLs, potentially leading to storage abuse or unauthorized access to files.

To fix this, these functions should not accept the internalKey as an argument. Instead, they should be restricted to internal calls, similar to how verifyAuth is used in getFiles. This can be achieved by making them internalAction and internalQuery and removing the internalKey argument from their definitions and implementations.

Debug { "id": "019d9f02-849d-76df-80ca-ff41afe845b8", "codebaseId": "019d74e8-a987-745e-b138-9ddbd90215b2", "path": "convex/files.ts", "rangeStart": 368, "rangeEnd": 411, "line": 379, "signature": "019d9f02-8452-7602-9019-5f55346813f6" }
Possible fix - diff
--- a/convex/files.ts
+++ b/convex/files.ts
@@ -368,10 +368,8 @@

// Internal key validation for system operations
function validateInternalKey(internalKey: string): void {
-  const expectedKey = process.env.POLARIS_CONVEX_INTERNAL_KEY;
-  if (!expectedKey || internalKey !== expectedKey) {
-    throw new Error("Invalid internal key");
-  }
+  // This function is no longer needed as we are using internal actions/queries
+  // All internal actions/queries are already protected by Convex's internal auth mechanism.
}

export const storeImage = action({
@@ -379,16 +377,14 @@
   data: v.array(v.number()),
   contentType: v.string(),
 },
-  handler: async (ctx, args) => {
-    validateInternalKey(args.internalKey);
+  handler: async (ctx, { data, contentType }) => {
-    const storageId = await ctx.storage.store(blob);
+    const blob = new Blob([new Uint8Array(data)], { type: contentType });
+    const storageId = await ctx.storage.store(blob);

   return storageId;
 },
});

-export const getImageUrl = query({
+export const getImageUrl = internalQuery({
 args: {
   storageId: v.id("_storage"),
 },
-  handler: async (ctx, args) => {
-    validateInternalKey(args.internalKey);
-    const url = await ctx.storage.getUrl(args.storageId);
+  handler: async (ctx, { storageId }) => {
+    const url = await ctx.storage.getUrl(storageId);

   return url;
 },

Did we do a good job? 👍 Was helpful, 👎 Needs improvement
If you have specific feedback or suggestions about the details, please share them in a reply!

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