An open-source platform where users can instantly try any AI agent skill in a sandboxed environment. Built with Next.js 16, Convex, Clerk, and Daytona sandboxes.
- Dev server:
bun dev - Build:
bun run build - Lint:
bun run lint - Test:
bun run test(ornpx vitest run) - Type check:
npx tsc --noEmit - Convex dev:
npx convex dev --once - Convex prod deploy:
npx convex deploy --yes
Always run type check + tests before committing.
├── convex/ → Convex backend (schema, mutations, queries, crons)
├── src/
│ ├── app/ → Next.js App Router pages
│ │ ├── [...skillPath]/ → Skill page (launch + inline chat)
│ │ ├── api/hermes/ → Server-side proxy for Hermes Gateway API
│ │ ├── api/sandbox/ → Sandbox info + heartbeat API
│ │ ├── dashboard/ → User dashboard (sandbox + skill trials)
│ │ └── settings/ → API key settings
│ ├── components/
│ │ ├── chat/ → ChatPanel, useChat hook
│ │ └── ... → ConfigPanel, LaunchProgress, etc.
│ ├── hooks/ → useKeyStore, useHeartbeat
│ └── lib/
│ ├── sandbox/ → daytona.ts, hermes-api.ts, types.ts
│ ├── skill/ → resolver, parser, url-parser
│ └── providers/ → LLM provider registry
├── scripts/ → Build scripts (snapshot, etc.)
└── public/ → Static assets
Each user gets one persistent Hermes agent sandbox (Daytona). Skills accumulate on disk without cleanup. Hermes loads the requested skill per chat session.
active— sandbox running, accepting skill installscreating— cold create in progress (exclusive lock viaacquireCreateLock)stopped— Daytona auto-stopped after 30min idle
- Instant (~0ms): skill installed + config match + URL fresh + heartbeat recent → use stored URL
- Install (~3-5s): upload skill files to running sandbox, get fresh signed URL
- Cold create (~15-30s): create new Daytona sandbox from snapshot
getSandboxis a query (read-only), multiple tabs read simultaneously- Skill installs need no lock (concurrent uploads to separate directories)
- Cold create uses
acquireCreateLockmutation (atomic pending-placeholder) - Stale pending locks expire after 5 minutes
All chat API calls go through /api/hermes (POST) server-side route which proxies to the Gateway's /v1/chat/completions endpoint. The proxy adds X-Daytona-Skip-Preview-Warning header to bypass the Daytona preview warning page and validates that baseUrl matches Daytona host patterns only.
Chat sessions are stored in Convex (chatSessions table), not in the Gateway. Each skill trial creates a Convex session that persists messages across page reloads. Users can resume sessions from the dashboard via ?session= URL parameter.
This project uses Convex as its backend.
When working on Convex code, always read convex/_generated/ai/guidelines.md first for important guidelines on how to correctly use Convex APIs and patterns.
Convex agent skills for common tasks can be installed by running npx convex ai-files install.
sandboxes— one row per user's sandbox (poolState, configHash, installedSkills, gatewayUrl)skillTrials— records each skill launch (capped at 50 per query)chatSessions— persistent chat sessions (skillPath, title, model, messages, timestamps)apiKeys— encrypted user API keys (Clerk + AES)
- Dev:
tame-seahorse-513(set viaCONVEX_DEPLOYMENTin.env.local) - Prod:
acrobatic-malamute-199(auto-detected bynpx convex deploy) - After merging schema/function changes, always deploy to BOTH:
npx convex dev --once+npx convex deploy --yes
- Never store raw API keys in Convex — use SHA-256 hash for
configHash - Proxy routes validate
baseUrlagainst Daytona host regex before forwarding respondApprovalsurfaces errors (never silently swallow)- Signed preview URLs have 1h TTL; instant path checks 50min freshness + 30min heartbeat
- TypeScript strict mode, no semicolons in new code
- Minimal comments — only when non-obvious
- Use
bunfor package management (not npm/yarn) .gitignoreincludespackage-lock.json- Test files:
src/__tests__/mirrorssrc/structure, plusconvex/*.test.ts - Convex mutations: always verify
identity.tokenIdentifierownership - Use
sanitizeSkillDir(skillPath)(replaces/with--) for on-disk skill directories
- Convex codegen requires
CONVEX_DEPLOYMENTenv var:CONVEX_DEPLOYMENT="dev:tame-seahorse-513" npx convex codegen - Prod Convex needs
CLERK_JWT_ISSUER_DOMAINenv var set in dashboard EventSource(SSE) cannot send custom headers — that's why the server proxy exists- Daytona signed preview URLs embed auth in subdomain (
https://8787-{token}.proxy.daytona.work) autoLaunchLock(Map) prevents duplicate auto-launches within the same SPA sessioninstallSkillaccepts{ skipConfigWrite: true }when configHash matches to avoid concurrent config races