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
9 changes: 9 additions & 0 deletions .env.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,12 @@ NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL_HERE
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY_HERE
SUPABASE_SERVICE_ROLE_KEY=YOUR_SUPABASE_SERVICE_ROLE_KEY_HERE
DATABASE_URL=postgresql://postgres:[YOUR-POSTGRES-PASSWORD]@[YOUR-SUPABASE-DB-HOST]:[PORT]/postgres

# Vercel Sandbox (MicroVM Infrastructure)
# Required for OIDC-based microVM authentication
# VERCEL_TOKEN: Create a Personal Access Token in Vercel Dashboard > Settings > Tokens
VERCEL_TOKEN=your_vercel_token
# VERCEL_TEAM_ID: Found in Vercel Dashboard > Team Settings > General (starts with 'team_')
VERCEL_TEAM_ID=your_vercel_team_id
# VERCEL_PROJECT_ID: Found in Vercel Dashboard > [Project] > Settings > General (starts with 'prj_')
VERCEL_PROJECT_ID=your_vercel_project_id
17 changes: 17 additions & 0 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { CopilotDisplay } from '@/components/copilot-display'
import RetrieveSection from '@/components/retrieve-section'
import { VideoSearchSection } from '@/components/video-search-section'
import { MapQueryHandler } from '@/components/map/map-query-handler'
import { SandboxSection } from '@/components/sandbox-section'

// Define the type for related queries
type RelatedQueries = {
Expand Down Expand Up @@ -897,6 +898,22 @@ export const getUIStateFromAIState = (aiState: AIState): UIState => {
),
isCollapsed: isCollapsed.value
}
case 'sandbox': {
const logs = createStreamableValue(toolOutput.logs)
logs.done(toolOutput.logs)
return {
id,
component: (
<SandboxSection
logs={logs.value}
previewUrl={toolOutput.previewUrl}
exitCode={toolOutput.exitCode}
error={toolOutput.error}
/>
),
isCollapsed: isCollapsed.value
}
}
default:
console.warn(
`Unhandled tool result in getUIStateFromAIState: ${name}`
Expand Down
1,047 changes: 511 additions & 536 deletions bun.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion components/map/map-3d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ export const Map3D = forwardRef(
<div style={props.style}>
<gmp-map-3d
ref={map3dRef}
center={center}
center={center && typeof center === 'object' && 'lat' in center ? { lat: center.lat, lng: center.lng, altitude: 0 } : center}
range={range}
heading={heading}
tilt={tilt}
Expand Down
52 changes: 52 additions & 0 deletions components/sandbox-logs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use client'

import { useEffect, useRef } from 'react'
import { cn } from '@/lib/utils'

interface LogEntry {
type: 'stdout' | 'stderr'
content: string
}

interface SandboxLogsProps {
logs: LogEntry[]
isExecuting?: boolean
}

export function SandboxLogs({ logs, isExecuting }: SandboxLogsProps) {
const scrollRef = useRef<HTMLDivElement>(null)

useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
}
}, [logs])

return (
<div
ref={scrollRef}
className="bg-zinc-950 text-zinc-50 font-mono text-sm p-4 rounded-md overflow-auto max-h-[300px] border border-zinc-800"
>
{logs.map((log, index) => (
<div
key={index}
className={cn(
"whitespace-pre-wrap mb-1",
log.type === 'stderr' ? "text-red-400" : "text-zinc-300"
)}
>
{log.content}
</div>
))}
{isExecuting && (
<div className="flex items-center gap-2 text-zinc-500 mt-2">
<span className="w-1.5 h-1.5 bg-zinc-500 rounded-full animate-pulse" />
<span>Executing...</span>
</div>
)}
{logs.length === 0 && !isExecuting && (
<div className="text-zinc-500 italic">No output</div>
)}
</div>
)
}
51 changes: 51 additions & 0 deletions components/sandbox-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client'

import { useState } from 'react'
import { ExternalLink, Loader2, RefreshCw } from 'lucide-react'
import { Button } from '@/components/ui/button'

interface SandboxPreviewProps {
url: string
}

export function SandboxPreview({ url }: SandboxPreviewProps) {
const [isLoading, setIsLoading] = useState(true)
const [key, setKey] = useState(0)

const reload = () => {
setIsLoading(true)
setKey(prev => prev + 1)
}

return (
<div className="flex flex-col border rounded-md overflow-hidden bg-background">
<div className="flex items-center justify-between px-4 py-2 border-b bg-muted/50">
<div className="text-xs font-mono truncate mr-4">{url}</div>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={reload}>
<RefreshCw className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7" asChild>
<a href={url} target="_blank" rel="noopener noreferrer">
<ExternalLink className="h-3.5 w-3.5" />
</a>
Comment on lines +25 to +31

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

Add accessible labels to preview controls and iframe.

Line 25 and Line 28 render icon-only actions without accessible names, and Line 41 renders an unlabeled iframe. This makes the preview controls/frame hard to use with assistive tech.

Suggested fix
-          <Button variant="ghost" size="icon" className="h-7 w-7" onClick={reload}>
+          <Button
+            variant="ghost"
+            size="icon"
+            className="h-7 w-7"
+            onClick={reload}
+            aria-label="Reload sandbox preview"
+          >
             <RefreshCw className="h-3.5 w-3.5" />
           </Button>
           <Button variant="ghost" size="icon" className="h-7 w-7" asChild>
-            <a href={url} target="_blank" rel="noopener noreferrer">
+            <a
+              href={url}
+              target="_blank"
+              rel="noopener noreferrer"
+              aria-label="Open sandbox preview in a new tab"
+            >
               <ExternalLink className="h-3.5 w-3.5" />
             </a>
           </Button>
@@
         <iframe
           key={key}
           src={url}
+          title="Sandbox live preview"
           className="w-full h-full border-none"
           sandbox="allow-scripts allow-same-origin allow-forms"
           onLoad={() => setIsLoading(false)}
         />

Also applies to: 41-47

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

In `@components/sandbox-preview.tsx` around lines 25 - 31, The preview controls
and iframe lack accessible names; add explicit accessible labels to the
icon-only controls and the iframe: give the reload Button (onClick={reload}) an
aria-label="Reload preview" (or wrap the icon with a visually-hidden <span> for
screen readers), give the external link anchor or the Button with asChild an
aria-label="Open preview in new tab" (or include hidden text), and add a
descriptive title and/or aria-label to the iframe (e.g., title="Live preview of
user sandbox" and aria-label="Sandbox preview iframe") so assistive tech can
identify each control and the frame; update the elements referenced (the Button
with onClick={reload}, the Button asChild wrapping <a href={url} ...>, and the
iframe element) accordingly.

</Button>
</div>
</div>
<div className="relative w-full aspect-video bg-white">
{isLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-muted/20">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)}
<iframe
key={key}
src={url}
className="w-full h-full border-none"
sandbox="allow-scripts allow-same-origin allow-forms"
onLoad={() => setIsLoading(false)}
/>
</div>
</div>
)
}
51 changes: 51 additions & 0 deletions components/sandbox-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
'use client'

import { useStreamableValue, StreamableValue } from 'ai/rsc'
import { Section } from '@/components/section'
import { SandboxLogs } from './sandbox-logs'
import { SandboxPreview } from './sandbox-preview'
import { Terminal } from 'lucide-react'

interface LogEntry {
type: 'stdout' | 'stderr'
content: string
}

interface SandboxSectionProps {
logs: StreamableValue<LogEntry[]>
previewUrl?: string
exitCode?: number
error?: string
}

export function SandboxSection({ logs, previewUrl, exitCode, error }: SandboxSectionProps) {
const [data, errorFromStream] = useStreamableValue(logs)
const isExecuting = data === undefined

return (
<Section title="Sandbox" isCollapsed={false}>
<div className="space-y-4">
<SandboxLogs logs={data || []} isExecuting={isExecuting} />

{previewUrl && (
<div className="mt-4">
<h4 className="text-xs font-semibold uppercase text-muted-foreground mb-2 px-1">Live Preview</h4>
<SandboxPreview url={previewUrl} />
</div>
)}

{(error || !!errorFromStream) && (
<div className="p-3 text-xs bg-red-50 text-red-600 rounded border border-red-100 font-mono">
<strong>Error:</strong> {error || (errorFromStream as any)?.message || 'Execution failed'}
</div>
)}

{exitCode !== undefined && !previewUrl && (
<div className="text-[10px] text-muted-foreground font-mono px-1">
Process exited with code {exitCode}
</div>
)}
</div>
</Section>
)
}
7 changes: 6 additions & 1 deletion components/section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
MessageCircleMore,
Newspaper,
Repeat2,
Search
Search,
Terminal
} from 'lucide-react'
import React from 'react'
import { Separator } from './ui/separator'
Expand All @@ -19,6 +20,7 @@ type SectionProps = {
size?: 'sm' | 'md' | 'lg'
title?: string
separator?: boolean
isCollapsed?: boolean
}

export const Section: React.FC<SectionProps> = ({
Expand Down Expand Up @@ -49,6 +51,9 @@ export const Section: React.FC<SectionProps> = ({
case 'Follow-up':
icon = <MessageCircleMore size={18} className="mr-2" />
break
case 'Sandbox':
icon = <Terminal size={18} className="mr-2" />
break
default:
icon = <Search size={18} className="mr-2" />
}
Expand Down
15 changes: 11 additions & 4 deletions lib/agents/researcher.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,13 @@ Use these user-drawn areas/lines as primary areas of interest for your analysis
ONLY when the user explicitly provides one or more URLs and asks you to read, summarize, or extract content from them.
- **Never use** this tool proactively.

#### **3. Location, Geography, Navigation, and Mapping Queries**
#### **3. Code Execution and Data Transformation**
- **Tool**: \`sandbox\`
- **When to use**:
Use this to execute JavaScript or TypeScript code snippets, perform complex data transformations, generate dynamic HTML reports, or visualize data. The sandbox provides an isolated environment with network access and can host web servers for live previews.
- **Features**: Supports installing npm dependencies and provides a public preview URL if a web server (e.g., Express) is detected.

Comment on lines +50 to +55

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.

🧹 Nitpick | 🔵 Trivial

Consider documenting network access security implications.

The prompt states the sandbox provides "network access," which enables powerful data-fetching use cases but also introduces SSRF (Server-Side Request Forgery) risk. While this is inherent to the feature's design, consider documenting the security implications:

  • User code can make arbitrary HTTP requests to internal/external services
  • Could be used to scan internal networks or exfiltrate data
  • Could bypass IP allowlists or access localhost services

If the sandbox runs in a controlled environment (e.g., isolated network namespace, egress filtering), mention this in documentation. Otherwise, ensure users understand the security boundaries.

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

In `@lib/agents/researcher.tsx` around lines 50 - 55, Update the sandbox tool
description (the "sandbox" entry in lib/agents/researcher.tsx) to explicitly
document network-access security implications: state that user code can make
arbitrary HTTP requests (SSRF/exfiltration risk), potential to reach
internal/localhost services or bypass IP allowlists, and recommend mitigations
such as running sandboxes in an isolated network namespace, applying egress
filtering, and clearly defining allowed/forbidden endpoints; include a brief
guidance sentence for operators on when to enable the feature and where to
configure these controls.

#### **4. Location, Geography, Navigation, and Mapping Queries**
- **Tool**: \`geospatialQueryTool\` → **MUST be used (no exceptions)** for:
• Finding places, businesses, "near me", distances, directions
• Travel times, routes, traffic, map generation
Expand All @@ -68,9 +74,10 @@ Use these user-drawn areas/lines as primary areas of interest for your analysis

#### **Summary of Decision Flow**
1. User gave explicit URLs? → \`retrieve\`
2. Location/distance/direction/maps? → \`geospatialQueryTool\` (mandatory)
3. Everything else needing external data? → \`search\`
4. Otherwise → answer from knowledge
2. Need to execute code or transform data? → \`sandbox\`
3. Location/distance/direction/maps? → \`geospatialQueryTool\` (mandatory)
4. Everything else needing external data? → \`search\`
5. Otherwise → answer from knowledge

These rules override all previous instructions.

Expand Down
8 changes: 8 additions & 0 deletions lib/agents/tools/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { retrieveTool } from './retrieve'
import { searchTool } from './search'
import { videoSearchTool } from './video-search'
import { geospatialTool } from './geospatial' // Removed useGeospatialToolMcp import
import { sandboxTool } from './sandbox'

import { MapProvider } from '@/lib/store/settings'

Expand Down Expand Up @@ -38,5 +39,12 @@ export const getTools = ({ uiStream, fullResponse, mapProvider }: ToolProps) =>
})
}

if (process.env.VERCEL_TOKEN && process.env.VERCEL_TEAM_ID && process.env.VERCEL_PROJECT_ID) {
tools.sandbox = sandboxTool({
uiStream,
fullResponse
})
}

return tools
}
Loading