Skip to content
This repository was archived by the owner on May 29, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/opencode.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: opencode

on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]

jobs:
opencode:
if: |

@cubic-dev-ai cubic-dev-ai Bot Jan 19, 2026

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: Guard the workflow against untrusted commenters. As written, any GitHub user can trigger this job via /oc and run a secret-backed action, which risks abuse of the OPENCODE_API_KEY and workflow resources. Add an author association check (owner/member/collaborator) or similar gating before running.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/opencode.yml, line 11:

<comment>Guard the workflow against untrusted commenters. As written, any GitHub user can trigger this job via `/oc` and run a secret-backed action, which risks abuse of the OPENCODE_API_KEY and workflow resources. Add an author association check (owner/member/collaborator) or similar gating before running.</comment>

<file context>
@@ -0,0 +1,33 @@
+
+jobs:
+  opencode:
+    if: |
+      contains(github.event.comment.body, ' /oc') ||
+      startsWith(github.event.comment.body, '/oc') ||
</file context>
Fix with Cubic

contains(github.event.comment.body, ' /oc') ||
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode')
runs-on: ubuntu-latest
Comment on lines +11 to +16

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.

⚠️ Potential issue | 🟠 Major

Restrict comment-triggered runs to trusted actors.

As written, any commenter can trigger a run that has access to secrets. Add author-association gating (and ideally PR-only checks) to prevent untrusted execution.

🔒 Suggested guardrail
-    if: |
-      contains(github.event.comment.body, ' /oc') ||
-      startsWith(github.event.comment.body, '/oc') ||
-      contains(github.event.comment.body, ' /opencode') ||
-      startsWith(github.event.comment.body, '/opencode')
+    if: |
+      (contains(github.event.comment.body, ' /oc') ||
+       startsWith(github.event.comment.body, '/oc') ||
+       contains(github.event.comment.body, ' /opencode') ||
+       startsWith(github.event.comment.body, '/opencode')) &&
+      (github.event.comment.author_association == 'OWNER' ||
+       github.event.comment.author_association == 'MEMBER' ||
+       github.event.comment.author_association == 'COLLABORATOR') &&
+      (
+        github.event_name == 'pull_request_review_comment' ||
+        (github.event_name == 'issue_comment' && github.event.issue.pull_request != null)
+      )
🤖 Prompt for AI Agents
In @.github/workflows/opencode.yml around lines 11 - 16, The current workflow
condition allowing any comment containing '/oc' or '/opencode' to run must be
tightened: update the if expression in .github/workflows/opencode.yml (the block
using contains(github.event.comment.body, ...) and startsWith(...)) to also
require the comment author to have a trusted author_association (e.g., OWNER,
COLLABORATOR, MEMBER, or CONTRIBUTOR) and to ensure the comment is on a pull
request (i.e., guard that github.event.issue.pull_request exists); implement
these two checks combined with the existing body-matching checks so only trusted
PR commenters can trigger runs with secrets.

permissions:
id-token: write
contents: read
pull-requests: read
issues: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false

- name: Run opencode
uses: anomalyco/opencode/github@latest

@cubic-dev-ai cubic-dev-ai Bot Jan 19, 2026

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: Pin the anomalyco/opencode action to a specific release tag or commit SHA instead of @latest to prevent unexpected changes and reduce supply-chain risk.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/opencode.yml, line 29:

<comment>Pin the `anomalyco/opencode` action to a specific release tag or commit SHA instead of `@latest` to prevent unexpected changes and reduce supply-chain risk.</comment>

<file context>
@@ -0,0 +1,33 @@
+          persist-credentials: false
+
+      - name: Run opencode
+        uses: anomalyco/opencode/github@latest
+        env:
+          OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
</file context>
Fix with Cubic

env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
with:
model: opencode/grok-code
15 changes: 4 additions & 11 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 12 additions & 6 deletions convex/importData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export const importProject = internalMutation({
v.literal("ANGULAR"),
v.literal("REACT"),
v.literal("VUE"),
v.literal("SVELTE")
v.literal("SVELTE"),
v.literal("EXPO")
),
createdAt: v.string(), // ISO date string
updatedAt: v.string(), // ISO date string
Expand Down Expand Up @@ -89,7 +90,8 @@ export const importFragment = internalMutation({
v.literal("ANGULAR"),
v.literal("REACT"),
v.literal("VUE"),
v.literal("SVELTE")
v.literal("SVELTE"),
v.literal("EXPO")
),
createdAt: v.string(),
updatedAt: v.string(),
Expand Down Expand Up @@ -130,7 +132,8 @@ export const importFragmentDraft = internalMutation({
v.literal("ANGULAR"),
v.literal("REACT"),
v.literal("VUE"),
v.literal("SVELTE")
v.literal("SVELTE"),
v.literal("EXPO")
),
createdAt: v.string(),
updatedAt: v.string(),
Expand Down Expand Up @@ -278,7 +281,8 @@ export const importProjectAction = action({
v.literal("ANGULAR"),
v.literal("REACT"),
v.literal("VUE"),
v.literal("SVELTE")
v.literal("SVELTE"),
v.literal("EXPO")
),
createdAt: v.string(),
updatedAt: v.string(),
Expand Down Expand Up @@ -320,7 +324,8 @@ export const importFragmentAction = action({
v.literal("ANGULAR"),
v.literal("REACT"),
v.literal("VUE"),
v.literal("SVELTE")
v.literal("SVELTE"),
v.literal("EXPO")
),
createdAt: v.string(),
updatedAt: v.string(),
Expand All @@ -343,7 +348,8 @@ export const importFragmentDraftAction = action({
v.literal("ANGULAR"),
v.literal("REACT"),
v.literal("VUE"),
v.literal("SVELTE")
v.literal("SVELTE"),
v.literal("EXPO")
),
createdAt: v.string(),
updatedAt: v.string(),
Expand Down
3 changes: 2 additions & 1 deletion convex/sandboxSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ export const create = mutation({
v.literal("ANGULAR"),
v.literal("REACT"),
v.literal("VUE"),
v.literal("SVELTE")
v.literal("SVELTE"),
v.literal("EXPO")
),
autoPauseTimeout: v.optional(v.number()), // Default 10 minutes
},
Expand Down
15 changes: 14 additions & 1 deletion convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@ export const frameworkEnum = v.union(
v.literal("ANGULAR"),
v.literal("REACT"),
v.literal("VUE"),
v.literal("SVELTE")
v.literal("SVELTE"),
v.literal("EXPO")
);

export const expoPreviewModeEnum = v.union(
v.literal("web"),
v.literal("expo-go"),
v.literal("android-emulator"),
v.literal("eas-build")
);

export const messageRoleEnum = v.union(
Expand Down Expand Up @@ -115,6 +123,11 @@ export default defineSchema({
files: v.any(),
metadata: v.optional(v.any()),
framework: frameworkEnum,
expoPreviewMode: v.optional(expoPreviewModeEnum),
expoQrCodeUrl: v.optional(v.string()),
expoVncUrl: v.optional(v.string()),
expoEasBuildUrl: v.optional(v.string()),
expoApkUrl: v.optional(v.string()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Schema fields added without updating corresponding mutation

Medium Severity

The fragments table schema was updated with new fields (expoPreviewMode, expoQrCodeUrl, expoVncUrl, expoEasBuildUrl, expoApkUrl), but the createFragmentForUser mutation in convex/messages.ts was not updated to accept these parameters. This means even if the code agent generated expo-specific data, there's no API path to persist it to the database. These fields will always remain empty for all fragments.

Fix in Cursor Fix in Web

createdAt: v.optional(v.number()),
updatedAt: v.optional(v.number()),
})
Expand Down
53 changes: 53 additions & 0 deletions convex/usage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,59 @@ const UNLIMITED_POINTS = Number.MAX_SAFE_INTEGER;
const DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
const GENERATION_COST = 1;

// Expo-specific limits by tier
export const EXPO_LIMITS = {
free: {
webPreview: true,
expoGo: true,
androidEmulator: false,
easBuild: false,
maxBuildsPerDay: 5,
maxEmulatorMinutes: 0
},
pro: {
webPreview: true,
expoGo: true,
androidEmulator: true,
easBuild: true,
maxBuildsPerDay: 50,
maxEmulatorMinutes: 120 // 2 hours per day
},
enterprise: {
webPreview: true,
expoGo: true,
androidEmulator: true,
easBuild: true,
maxBuildsPerDay: 500,
maxEmulatorMinutes: 600 // 10 hours per day
}
} as const;

export type ExpoPreviewMode = 'web' | 'expo-go' | 'android-emulator' | 'eas-build';
export type UserTier = 'free' | 'pro' | 'enterprise';

/**
* Check if user can use a specific Expo preview mode
*/
export function canUseExpoPreviewMode(
tier: UserTier,
mode: ExpoPreviewMode
): boolean {
const limits = EXPO_LIMITS[tier];
switch (mode) {
case 'web':
return limits.webPreview;
case 'expo-go':
return limits.expoGo;
case 'android-emulator':
return limits.androidEmulator;
case 'eas-build':
return limits.easBuild;
default:
return false;
}
}

/**
* Check and consume credits for a generation
* Returns true if credits were successfully consumed, false if insufficient credits
Expand Down
3 changes: 0 additions & 3 deletions env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ OPENROUTER_BASE_URL="https://openrouter.ai/api/v1"
# Cerebras API (Z.AI GLM 4.7 model - ultra-fast inference)
CEREBRAS_API_KEY="" # Get from https://cloud.cerebras.ai

# Vercel AI Gateway (fallback for Cerebras rate limits)
VERCEL_AI_GATEWAY_API_KEY="" # Get from https://vercel.com/dashboard/ai-gateway

# Brave Search API (web search for subagent research - optional)
BRAVE_SEARCH_API_KEY="" # Get from https://api-dashboard.search.brave.com/app/keys

Expand Down
Loading
Loading