Skip to content
Open
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
20 changes: 20 additions & 0 deletions .claude/settings.local.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please remove this file

"permissions": {
"allow": [
"Task(*)",
"Read(//Users/jkneen/Documents/GitHub/flows/**)",
"Bash(git restore:*)",
"Bash(rm:*)",
"Bash(npm run dev:*)",
"Bash(cat:*)",
"Bash(PGPASSWORD=\"npg_2zcXUvGdrM1A\" /opt/homebrew/opt/libpq/bin/psql -h \"ep-bold-sky-agftkbbb-pooler.c-2.eu-central-1.aws.neon.tech\" -U \"neondb_owner\" -d \"neondb\" -c \"SELECT 1\")",
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

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

Database password is exposed in plaintext in the permissions configuration. This credential should be removed and stored in environment variables or a secure credential store instead.

Suggested change
"Bash(PGPASSWORD=\"npg_2zcXUvGdrM1A\" /opt/homebrew/opt/libpq/bin/psql -h \"ep-bold-sky-agftkbbb-pooler.c-2.eu-central-1.aws.neon.tech\" -U \"neondb_owner\" -d \"neondb\" -c \"SELECT 1\")",
"Bash(PGPASSWORD=\"$DB_PASSWORD\" /opt/homebrew/opt/libpq/bin/psql -h \"ep-bold-sky-agftkbbb-pooler.c-2.eu-central-1.aws.neon.tech\" -U \"neondb_owner\" -d \"neondb\" -c \"SELECT 1\")",

Copilot uses AI. Check for mistakes.
"Bash(npm run db:push:*)",
"Bash(curl:*)",
"Bash(node -e:*)",
"Bash(pnpm install:*)",
"Bash(pnpm db:push:*)"
],
"deny": [],
"ask": []
}
}
5 changes: 3 additions & 2 deletions lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// Rate limiting configuration
export const MAX_MESSAGES_PER_DAY = parseInt(process.env.MAX_MESSAGES_PER_DAY || '5', 10)

// Sandbox configuration (in minutes)
export const MAX_SANDBOX_DURATION = parseInt(process.env.MAX_SANDBOX_DURATION || '300', 10)
// Sandbox configuration (in minutes) - Vercel Sandbox API has max 2700000ms (45 minutes)
// We cap at 40 minutes to have a safety buffer
export const MAX_SANDBOX_DURATION = Math.min(parseInt(process.env.MAX_SANDBOX_DURATION || '40', 10), 40)
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

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

The Math.min() call is redundant and will always return 40. If MAX_SANDBOX_DURATION env var is set to a value less than 40, it will be ignored. This should be Math.min(parseInt(process.env.MAX_SANDBOX_DURATION || '40', 10), 40) which evaluates the parseInt first, then applies the cap.

Copilot uses AI. Check for mistakes.

// Vercel deployment configuration
export const VERCEL_DEPLOY_URL =
Expand Down
74 changes: 69 additions & 5 deletions lib/sandbox/creation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ export async function createSandbox(config: SandboxConfig, logger: TaskLogger):

// Use the specified timeout (maxDuration) for sandbox lifetime
// keepAlive only controls whether we shutdown after task completion
const timeoutMs = config.timeout ? parseInt(config.timeout.replace(/\D/g, '')) * 60 * 1000 : 60 * 60 * 1000 // Default 1 hour
// NOTE: Vercel Sandbox API has a maximum timeout of 2700000ms (45 minutes)
Copy link
Collaborator

Choose a reason for hiding this comment

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

const configTimeoutMinutes = config.timeout ? parseInt(config.timeout.replace(/\D/g, '')) : 40
const timeoutMs = Math.min(configTimeoutMinutes * 60 * 1000, 2700000) // Cap at 45 minutes
Comment on lines +65 to +67
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

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

The magic number 2700000 appears multiple times in the code. Consider extracting it as a named constant like VERCEL_MAX_TIMEOUT_MS = 2700000 to improve maintainability and ensure consistency.

Copilot uses AI. Check for mistakes.

// Create sandbox with proper source configuration
const sandboxConfig = {
Expand All @@ -81,14 +83,50 @@ export async function createSandbox(config: SandboxConfig, logger: TaskLogger):
resources: { vcpus: config.resources?.vcpus || 4 },
}

await logger.info(
`Sandbox config: timeout=${timeoutMs}ms (${timeoutMs / 1000 / 60} minutes), runtime=${sandboxConfig.runtime}, vcpus=${sandboxConfig.resources.vcpus}`,
)

// Validate sandbox config values
if (!process.env.SANDBOX_VERCEL_TEAM_ID || process.env.SANDBOX_VERCEL_TEAM_ID.length === 0) {
throw new Error('Invalid or missing SANDBOX_VERCEL_TEAM_ID environment variable')
}
if (!process.env.SANDBOX_VERCEL_PROJECT_ID || process.env.SANDBOX_VERCEL_PROJECT_ID.length === 0) {
throw new Error('Invalid or missing SANDBOX_VERCEL_PROJECT_ID environment variable')
}
if (!process.env.SANDBOX_VERCEL_TOKEN || process.env.SANDBOX_VERCEL_TOKEN.length === 0) {
throw new Error('Invalid or missing SANDBOX_VERCEL_TOKEN environment variable')
}
if (timeoutMs <= 0 || timeoutMs > 2700000) {
throw new Error(`Invalid timeout value: ${timeoutMs}ms. Must be between 1 and 2700000ms (45 minutes).`)
Comment on lines +100 to +101
Copy link

Copilot AI Oct 21, 2025

Choose a reason for hiding this comment

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

The validation condition timeoutMs <= 0 is unreachable because the default is 40 minutes and Math.min ensures it never exceeds 2700000ms, meaning timeoutMs will always be positive. Consider removing this check or validating configTimeoutMinutes before the calculation.

Copilot uses AI. Check for mistakes.
}

// Call progress callback before sandbox creation
if (config.onProgress) {
await config.onProgress(25, 'Validating configuration...')
}

let sandbox: Sandbox
try {
sandbox = await Sandbox.create(sandboxConfig)
await logger.info('Initiating Vercel Sandbox creation...')
await logger.info(`Using team: ${process.env.SANDBOX_VERCEL_TEAM_ID?.substring(0, 8)}...`)
await logger.info(`Using project: ${process.env.SANDBOX_VERCEL_PROJECT_ID?.substring(0, 8)}...`)

// Add a timeout for sandbox creation (60 seconds to be reasonable but not infinite)
const sandboxPromise = Sandbox.create(sandboxConfig)
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(
() =>
reject(
new Error(
'Sandbox creation timed out after 60 seconds. Check Vercel API credentials and network connectivity.',
),
),
60000,
),
)

sandbox = await Promise.race([sandboxPromise, timeoutPromise])
await logger.info('Sandbox created successfully')

// Register the sandbox immediately for potential killing
Expand Down Expand Up @@ -116,16 +154,42 @@ export async function createSandbox(config: SandboxConfig, logger: TaskLogger):

// Check if this is a timeout error
if (errorMessage?.includes('timeout') || errorCode === 'ETIMEDOUT' || errorName === 'TimeoutError') {
await logger.error(`Sandbox creation timed out after 5 minutes`)
await logger.error(`Sandbox creation timed out`)
await logger.error(`This usually happens when the repository is large or has many dependencies`)
throw new Error('Sandbox creation timed out. Try with a smaller repository or fewer dependencies.')
}

await logger.error('Sandbox creation failed')

// Log detailed error information
if (errorResponse) {
await logger.error('HTTP error occurred')
await logger.error('Error response received')
const status = errorResponse.status || 'unknown'
await logger.error(`HTTP error status: ${status}`)

// Try to extract error details from response
if (errorResponse.data) {
try {
const errorData =
typeof errorResponse.data === 'string' ? JSON.parse(errorResponse.data) : errorResponse.data
if (errorData.error?.message) {
await logger.error(`Vercel API error: ${errorData.error.message}`)
}
if (errorData.error?.code) {
await logger.error(`Error code: ${errorData.error.code}`)
}
} catch {
await logger.error(`Error details: ${JSON.stringify(errorResponse.data)}`)
}
}
}

// For 500 errors, suggest checking Vercel status
if (errorResponse?.status === 500) {
await logger.error(
'Vercel API returned a 500 error. Please check https://status.vercel.com for service status.',
)
}

throw error
}

Expand Down