From 998c1c616d5e39b2774686539588240237625f14 Mon Sep 17 00:00:00 2001 From: iamjr15 Date: Wed, 29 Jul 2026 15:38:00 +0530 Subject: [PATCH] refactor: consolidate packages, env schemas, gateway internals, health split --- .dependency-cruiser.cjs | 18 +- .env.example | 2 +- AGENTS.md | 4 +- CLAUDE.md | 12 +- README.md | 9 +- apps/agent-worker/README.md | 8 +- apps/agent-worker/package.json | 1 - .../src/agent-api-system-routes.ts | 4 +- apps/agent-worker/src/agent-env.ts | 4 +- .../agent-run-app-builder-scaffold.ts | 6 +- .../durable-objects/agent-run-app-builder.ts | 22 +- .../src/durable-objects/agent-run-env.ts | 3 +- .../project-sandbox-account-deletion.ts | 6 +- .../project-sandbox-content.ts | 2 +- .../project-sandbox-daytona-identity.ts | 2 +- .../durable-objects/project-sandbox-files.ts | 2 +- .../project-sandbox-lifecycle-support.ts | 13 +- .../project-sandbox-process-control.ts | 2 +- .../project-sandbox-process-support.ts | 2 +- .../project-sandbox-project-files.ts | 2 +- .../project-sandbox-provisioning.ts | 9 +- .../project-sandbox-runtime-handle.ts | 10 +- .../project-sandbox-runtime.ts | 2 +- apps/agent-worker/src/index.ts | 8 +- apps/agent-worker/src/sandbox-http-routes.ts | 11 - .../src/sandbox-terminal-http-routes.ts | 2 +- apps/agent-worker/src/user-skill-files.ts | 4 - .../src/user-skill-http-routes.ts | 8 - apps/agent-worker/src/user-skill-packages.ts | 7 +- apps/agent-worker/wrangler.jsonc | 6 +- apps/gateway-worker/README.md | 25 +- .../gateway-worker/src/account-http-routes.ts | 42 --- .../src/activity-http-routes.ts | 12 + apps/gateway-worker/src/activity-routes.ts | 24 +- apps/gateway-worker/src/agent-forwarding.ts | 8 +- apps/gateway-worker/src/agent-http-routes.ts | 4 +- apps/gateway-worker/src/authenticate.ts | 59 ++-- .../gateway-worker/src/billing-http-routes.ts | 2 + apps/gateway-worker/src/billing-routes.ts | 55 ++- apps/gateway-worker/src/core-http-routes.ts | 39 ++- apps/gateway-worker/src/cors.ts | 4 +- apps/gateway-worker/src/gateway-env.ts | 15 +- .../src/greeting-http-routes.ts | 12 + apps/gateway-worker/src/greeting-routes.ts | 17 +- apps/gateway-worker/src/index.ts | 31 +- .../src/integration-http-routes.ts | 36 +- apps/gateway-worker/src/integrations.ts | 4 +- .../gateway-worker/src/profile-http-routes.ts | 17 + apps/gateway-worker/src/profile-routes.ts | 8 +- .../gateway-worker/src/project-http-routes.ts | 71 ++-- apps/gateway-worker/src/project-routes.ts | 43 +-- .../src/provider-http-routes.ts | 20 +- apps/gateway-worker/src/rate-limit.ts | 48 ++- apps/gateway-worker/src/search-http-routes.ts | 17 + apps/gateway-worker/src/search-routes.ts | 18 +- apps/gateway-worker/src/skills-routes.ts | 14 +- apps/gateway-worker/src/telemetry-routes.ts | 15 +- apps/gateway-worker/wrangler.jsonc | 1 - apps/preview-proxy/README.md | 10 +- apps/preview-proxy/package.json | 3 +- apps/preview-proxy/src/env.ts | 101 ------ apps/preview-proxy/src/index.ts | 20 +- apps/preview-proxy/src/origin.ts | 3 +- apps/preview-proxy/src/proxy.ts | 2 +- apps/preview-proxy/wrangler.jsonc | 5 +- apps/web/README.md | 3 + apps/web/package.json | 6 +- apps/web/src/app/(app)/error.tsx | 2 +- apps/web/src/app/global-error.tsx | 2 +- apps/web/src/app/layout.tsx | 5 +- .../src/components/ai-elements/response.tsx | 2 +- apps/web/src/components/auth/auth-modal.tsx | 2 +- .../src/components/auth/auth-route-page.tsx | 2 +- .../billing/manage-subscription-dialog.tsx | 2 +- .../src/components/billing/upgrade-dialog.tsx | 2 +- .../src/components/chat/chat-context-view.tsx | 2 +- .../components/chat/folder-chats-search.tsx | 2 +- .../src/components/chat/message-activity.tsx | 2 +- .../components/chat/message-deliverables.tsx | 12 +- .../src/components/chat/message-list-view.tsx | 2 +- .../components/chat/message-part-blocks.tsx | 2 +- .../web/src/components/chat/message-parts.tsx | 2 +- .../components/chat/prompt-composer-view.tsx | 2 +- apps/web/src/components/chat/status-pill.tsx | 2 +- apps/web/src/components/composer/add-menu.tsx | 2 +- .../composer/composer-attachment-status.tsx | 2 +- .../composer/composer-context-chips.tsx | 2 +- .../components/composer/composer-popover.tsx | 2 +- .../components/composer/model-menu-view.tsx | 2 +- .../composer/project-picker-parts.tsx | 2 +- .../docs/cheatcode-101-sections.tsx | 2 +- .../home/home-composer-controls.tsx | 2 +- .../components/home/home-composer-intents.ts | 2 +- .../web/src/components/home/home-composer.tsx | 2 +- .../components/onboarding/onboarding-flow.tsx | 2 +- .../onboarding/onboarding-tools-step.tsx | 2 +- .../preview/computer-panel-tabs.tsx | 8 +- .../preview/computer-toggle-button.tsx | 2 +- .../preview/console-strip-header.tsx | 2 +- .../preview/preview-device-menu.tsx | 4 +- .../components/preview/preview-side-panel.tsx | 2 +- .../preview/preview-url-controls.tsx | 2 +- .../components/preview/sandbox-ide-tab.tsx | 2 +- .../search/command-palette-dialog.tsx | 2 +- .../settings/activity-year-chart-view.tsx | 2 +- .../settings/activity-year-chart.tsx | 2 +- .../components/settings/models-panel-list.tsx | 2 +- .../src/components/settings/models-panel.tsx | 2 +- .../src/components/settings/pricing-panel.tsx | 2 +- .../settings/provider-key-editor.tsx | 2 +- .../settings/sandbox-hours-meter-body.tsx | 2 +- .../settings/sandbox-hours-meter.tsx | 2 +- .../settings/use-personalization-form.tsx | 2 +- apps/web/src/components/shell/app-chrome.tsx | 2 +- .../components/shell/sidebar-chat-list.tsx | 2 +- .../shell/sidebar-expanded-account.tsx | 2 +- .../shell/sidebar-expanded-navigation.tsx | 2 +- .../shell/sidebar-expanded-settings.tsx | 2 +- .../shell/sidebar-item-controls.tsx | 2 +- .../shell/sidebar-list-controls.tsx | 2 +- .../components/shell/sidebar-project-list.tsx | 2 +- .../shell/sidebar-rail-more-menu.tsx | 2 +- .../skills/integration-skill-drawer.tsx | 2 +- .../skills/integration-skills-controls.tsx | 2 +- .../skills/integration-skills-grid.tsx | 2 +- .../components/skills/user-skill-drawer.tsx | 2 +- .../components/skills/user-skills-section.tsx | 2 +- .../src => apps/web/src/components/ui}/cn.ts | 0 .../web/src/components/ui}/confirm-dialog.tsx | 0 .../web/src/components/ui}/icons.ts | 0 .../web/src/components/ui}/index.ts | 0 apps/web/src/components/ui/recovery-card.tsx | 4 +- .../web/src/components/ui}/response.tsx | 0 .../workspace/workspace-route-state.tsx | 2 +- apps/web/src/lib/navigation/nav-model.ts | 2 +- apps/web/src/lib/ui/cn.ts | 2 +- apps/web/src/proxy.ts | 4 +- compose.yaml | 2 +- infra/containers/dev/Dockerfile | 4 - packages/agent-core/README.md | 3 +- packages/agent-core/package.json | 15 +- packages/agent-core/src/index.ts | 4 +- .../agent-core/src/mastra/agents/general.ts | 2 +- .../agent-core/src/mastra/composio-context.ts | 5 - packages/agent-core/src/mastra/context.ts | 30 ++ .../agent-core/src/mastra/research-context.ts | 2 - .../agent-core/src/mastra/system-prompt.ts | 95 +---- .../{tools => tool-defs}/browser-runtime.ts | 24 +- .../{tools => tool-defs}/browser-tools.ts | 4 +- .../mastra/{tools => tool-defs}/code-tools.ts | 4 +- .../{tools => tool-defs}/composio-tool.ts | 18 +- .../mastra/{tools => tool-defs}/data-tools.ts | 0 .../{tools => tool-defs}/document-tools.ts | 0 .../{tools => tool-defs}/media-tools.ts | 4 +- .../mastra/{tools => tool-defs}/registry.ts | 0 .../src/mastra/tool-defs/request-context.ts | 87 +++++ .../{tools => tool-defs}/research-tools.ts | 19 +- .../{tools => tool-defs}/skill-tools.ts | 2 +- .../tool-runtime-context.ts | 10 +- .../{tools => tool-defs}/tool-schemas.ts | 2 +- .../mastra/{tools => tool-defs}/tool-set.ts | 0 .../src/mastra/tools/request-context.ts | 121 ------- .../src/mastra/user-skill-runtime.ts | 63 ++++ .../src/tools/browser}/actions.ts | 30 +- .../src/tools/browser}/index.ts | 0 .../src/tools/browser}/runtime.ts | 6 +- .../src/tools/code}/daytona-client.ts | 0 .../src/tools/code}/files.ts | 22 +- .../src => agent-core/src/tools/code}/git.ts | 22 +- .../src/tools/code}/index.ts | 0 .../src/tools/code}/preview.ts | 17 +- .../src/tools/code}/run-code.ts | 4 +- .../src/tools/code}/shell.ts | 19 +- .../src/tools/code}/workspace-paths.ts | 0 packages/agent-core/src/tools/docs/execute.ts | 2 +- .../src/tools/research}/exa.ts | 0 .../src/tools/research}/firecrawl.ts | 0 .../src/tools/research}/index.ts | 0 .../src/tools/research}/output-limiter.ts | 0 .../src/tools/research}/provider-http.ts | 0 .../src/tools/research}/runtime.ts | 0 .../src/tools/research}/schemas.ts | 0 packages/auth/README.md | 4 +- packages/db/src/client.ts | 21 +- packages/db/src/index.ts | 2 +- packages/env/README.md | 12 +- packages/env/src/agent-worker.ts | 49 +++ packages/env/src/gateway-worker.ts | 52 +++ packages/env/src/index.ts | 12 +- packages/env/src/preview-proxy.ts | 105 ++++++ packages/env/src/web-config.ts | 8 +- packages/env/src/webhooks-worker.ts | 43 +++ packages/env/src/worker-shared.ts | 235 +++++++++++++ packages/env/src/worker.ts | 326 +----------------- packages/sandbox-contracts/README.md | 9 +- packages/sandbox-contracts/package.json | 1 - packages/sandbox-contracts/src/index.ts | 3 +- packages/sandbox-contracts/src/runtime.ts | 79 +++-- .../sandbox-contracts/src/sandbox-methods.ts | 67 ---- packages/tools-browser/README.md | 58 ---- packages/tools-browser/package.json | 28 -- packages/tools-browser/tsconfig.json | 9 - packages/tools-code/README.md | 68 ---- packages/tools-code/package.json | 29 -- packages/tools-code/tsconfig.json | 9 - packages/tools-research/README.md | 33 -- packages/tools-research/package.json | 27 -- packages/tools-research/tsconfig.json | 9 - packages/types/src/sandbox-wire.ts | 2 +- packages/ui/README.md | 21 -- packages/ui/package.json | 51 --- packages/ui/tsconfig.json | 10 - pnpm-lock.yaml | 124 +------ scripts/dev-worker-config.ts | 1 - 214 files changed, 1461 insertions(+), 1848 deletions(-) delete mode 100644 apps/agent-worker/src/sandbox-http-routes.ts delete mode 100644 apps/gateway-worker/src/account-http-routes.ts create mode 100644 apps/gateway-worker/src/activity-http-routes.ts create mode 100644 apps/gateway-worker/src/greeting-http-routes.ts create mode 100644 apps/gateway-worker/src/profile-http-routes.ts create mode 100644 apps/gateway-worker/src/search-http-routes.ts delete mode 100644 apps/preview-proxy/src/env.ts rename {packages/ui/src => apps/web/src/components/ui}/cn.ts (100%) rename {packages/ui/src => apps/web/src/components/ui}/confirm-dialog.tsx (100%) rename {packages/ui/src => apps/web/src/components/ui}/icons.ts (100%) rename {packages/ui/src => apps/web/src/components/ui}/index.ts (100%) rename {packages/ui/src => apps/web/src/components/ui}/response.tsx (100%) create mode 100644 packages/agent-core/src/mastra/context.ts delete mode 100644 packages/agent-core/src/mastra/research-context.ts rename packages/agent-core/src/mastra/{tools => tool-defs}/browser-runtime.ts (84%) rename packages/agent-core/src/mastra/{tools => tool-defs}/browser-tools.ts (98%) rename packages/agent-core/src/mastra/{tools => tool-defs}/code-tools.ts (99%) rename packages/agent-core/src/mastra/{tools => tool-defs}/composio-tool.ts (97%) rename packages/agent-core/src/mastra/{tools => tool-defs}/data-tools.ts (100%) rename packages/agent-core/src/mastra/{tools => tool-defs}/document-tools.ts (100%) rename packages/agent-core/src/mastra/{tools => tool-defs}/media-tools.ts (89%) rename packages/agent-core/src/mastra/{tools => tool-defs}/registry.ts (100%) create mode 100644 packages/agent-core/src/mastra/tool-defs/request-context.ts rename packages/agent-core/src/mastra/{tools => tool-defs}/research-tools.ts (94%) rename packages/agent-core/src/mastra/{tools => tool-defs}/skill-tools.ts (99%) rename packages/agent-core/src/mastra/{tools => tool-defs}/tool-runtime-context.ts (83%) rename packages/agent-core/src/mastra/{tools => tool-defs}/tool-schemas.ts (98%) rename packages/agent-core/src/mastra/{tools => tool-defs}/tool-set.ts (100%) delete mode 100644 packages/agent-core/src/mastra/tools/request-context.ts create mode 100644 packages/agent-core/src/mastra/user-skill-runtime.ts rename packages/{tools-browser/src => agent-core/src/tools/browser}/actions.ts (96%) rename packages/{tools-browser/src => agent-core/src/tools/browser}/index.ts (100%) rename packages/{tools-browser/src => agent-core/src/tools/browser}/runtime.ts (84%) rename packages/{tools-code/src => agent-core/src/tools/code}/daytona-client.ts (100%) rename packages/{tools-code/src => agent-core/src/tools/code}/files.ts (89%) rename packages/{tools-code/src => agent-core/src/tools/code}/git.ts (92%) rename packages/{tools-code/src => agent-core/src/tools/code}/index.ts (100%) rename packages/{tools-code/src => agent-core/src/tools/code}/preview.ts (94%) rename packages/{tools-code/src => agent-core/src/tools/code}/run-code.ts (92%) rename packages/{tools-code/src => agent-core/src/tools/code}/shell.ts (90%) rename packages/{tools-code/src => agent-core/src/tools/code}/workspace-paths.ts (100%) rename packages/{tools-research/src => agent-core/src/tools/research}/exa.ts (100%) rename packages/{tools-research/src => agent-core/src/tools/research}/firecrawl.ts (100%) rename packages/{tools-research/src => agent-core/src/tools/research}/index.ts (100%) rename packages/{tools-research/src => agent-core/src/tools/research}/output-limiter.ts (100%) rename packages/{tools-research/src => agent-core/src/tools/research}/provider-http.ts (100%) rename packages/{tools-research/src => agent-core/src/tools/research}/runtime.ts (100%) rename packages/{tools-research/src => agent-core/src/tools/research}/schemas.ts (100%) create mode 100644 packages/env/src/agent-worker.ts create mode 100644 packages/env/src/gateway-worker.ts create mode 100644 packages/env/src/preview-proxy.ts create mode 100644 packages/env/src/webhooks-worker.ts create mode 100644 packages/env/src/worker-shared.ts delete mode 100644 packages/sandbox-contracts/src/sandbox-methods.ts delete mode 100644 packages/tools-browser/README.md delete mode 100644 packages/tools-browser/package.json delete mode 100644 packages/tools-browser/tsconfig.json delete mode 100644 packages/tools-code/README.md delete mode 100644 packages/tools-code/package.json delete mode 100644 packages/tools-code/tsconfig.json delete mode 100644 packages/tools-research/README.md delete mode 100644 packages/tools-research/package.json delete mode 100644 packages/tools-research/tsconfig.json delete mode 100644 packages/ui/README.md delete mode 100644 packages/ui/package.json delete mode 100644 packages/ui/tsconfig.json diff --git a/.dependency-cruiser.cjs b/.dependency-cruiser.cjs index 67e22728..b8dbfe06 100644 --- a/.dependency-cruiser.cjs +++ b/.dependency-cruiser.cjs @@ -20,10 +20,20 @@ module.exports = { to: { path: "^apps/", pathNot: "^apps/$1/" }, }, { - name: "tool-domains-must-not-import-peer-tool-domains", + name: "exa-firecrawl-clients-only-in-research-tools", severity: "error", - from: { path: "^packages/(tools-[^/]+)/" }, - to: { path: "^packages/tools-[^/]+/", pathNot: "^packages/$1/" }, + from: { pathNot: "^packages/agent-core/src/tools/research/" }, + to: { + path: "^packages/agent-core/src/tools/research/(exa|firecrawl|provider-http)\\.ts$", + }, + }, + { + name: "browser-driver-only-in-browser-tools", + severity: "error", + from: { pathNot: "^packages/agent-core/src/tools/browser/" }, + to: { + path: "^packages/agent-core/src/tools/browser/(actions|runtime)\\.ts$", + }, }, { name: "database-must-not-import-billing-policy", @@ -37,7 +47,7 @@ module.exports = { from: { path: "^apps/web/" }, to: { // Dependency Cruiser evaluates resolved file paths, not package specifiers. - path: "^packages/(agent-core|auth|billing|byok|db|observability|tools-[^/]+)(/|$)", + path: "^packages/(agent-core|auth|billing|byok|db|observability)(/|$)", }, }, { diff --git a/.env.example b/.env.example index a1a44d2e..d0f560b6 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,7 @@ SUPABASE_WEBHOOKS_DATABASE_URL=postgresql://app_webhooks.snqtclnmhcaupqynjyux:re # Vercel environment uses the production Clerk instance. NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_replace_me CLERK_SECRET_KEY=sk_test_replace_me -# Optional only when receiving Clerk webhooks in local development. +# The seven optional Clerk/Composio/Daytona/Polar webhook, integration, and checkout keys may stay empty; affected local paths return 503. CLERK_WEBHOOK_SIGNING_SECRET= # Browser-visible local routing configuration; these values are not secrets. diff --git a/AGENTS.md b/AGENTS.md index 05e48094..a671bb07 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,15 +39,13 @@ apps/ Deployable services webhooks-worker/ Polar/Clerk/Composio webhooks + internal ops workflows packages/ Shared libraries - agent-core/ Mastra instance, workflows, and data/document/media tools - tools-{browser,code,research}/ Multi-consumer and isolated tool-domain packages + agent-core/ Mastra plus code/browser/research/data/document/media tools db/ Drizzle schema (per-domain) + queries + migrations byok/ Vault-backed BYOK key store skills/ Build-time skill bundler observability/ Structured logger + error handler + Analytics Engine emitters env/ t3-env + Zod types/ Zod schemas + branded IDs + InferAgentUIMessage - ui/ Shared Cheatcode UI primitives, icon barrel, AI response renderer skills/ 19 curated skills; _shared/office is vendored and not bundled infra/ Container images and Dockerfiles for local development and Daytona sandboxes diff --git a/CLAUDE.md b/CLAUDE.md index 5dc77a0e..9522bc30 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ Direct competitors: Manus (generalist async agent), HappyCapy (GUI workstation + | Backend runtime | **Cloudflare Workers + Durable Objects + Workflows** | | Frontend | **Next.js 16.2.11 + React 19.2.7 + Tailwind 4.3.2 + shadcn CLI 4.6.0 + AI Elements + Streamdown** on Vercel | | Agent framework | **Mastra 1.51.0** on top of **Vercel AI SDK v6.0.205** | -| Sandbox | **Daytona Sandboxes** via REST-over-fetch (no SDK in Workers; `packages/tools-code/src/daytona-client.ts`) — one persistent sandbox per user with isolated project folders | +| Sandbox | **Daytona Sandboxes** via REST-over-fetch (no SDK in Workers; `packages/agent-core/src/tools/code/daytona-client.ts`) — one persistent sandbox per user with isolated project folders | | Browser automation | **Stagehand v3.7.0 LOCAL mode** inside the Daytona sandbox image | | Database | **Supabase Postgres via Cloudflare Hyperdrive** + **Drizzle 0.45.2** (separate `app_gateway`, `app_agent`, and `app_webhooks` roles; no `service_role`) | | Auth | **Clerk 7.5.19** (Workers JWT verify) | @@ -37,10 +37,7 @@ apps/ webhooks-worker/ Clerk, Polar, Composio webhooks + internal ops workflows packages/ - agent-core/ Mastra agent, workflows, contexts, and data/document/media tools - tools-code/ Sandbox shell/file/git/runCode tools - tools-browser/ Stagehand LOCAL browser automation - tools-research/ Exa + Firecrawl + agent-core/ Mastra, workflows, contexts, and code/browser/research/data/docs/media tools db/ Drizzle schema (per-domain) + queries + migrations byok/ Vault-backed BYOK with provider validation skills/ Build-time skill bundler + runtime loader @@ -49,7 +46,6 @@ packages/ types/ Zod schemas + InferAgentUIMessage + branded IDs auth/ Clerk verifyToken helpers billing/ Polar SDK wrappers + entitlement checks - ui/ shared UI primitives, icon barrel, AI response renderer tsconfig/ Shared base/nextjs/worker/library configs skills/ 19 curated skills; _shared/office is vendored and not bundled @@ -109,7 +105,7 @@ removed V1 tree must not be restored or copied back as a testing surface. | Need | File | |---|---| -| Add a new tool | `packages/agent-core/src/tools//.ts` for data/docs/media; `packages/tools-/src/.ts` for browser/code/research | +| Add a new tool | `packages/agent-core/src/tools//.ts`; expose cross-package domains through a dedicated `@cheatcode/agent-core/tools/` subpath | | Add a new agent | `packages/agent-core/src/mastra/agents/.ts` | | Add a new workflow | `packages/agent-core/src/mastra/workflows/.ts` | | Add a new skill | `skills//SKILL.md` (+ optional `references/` / `assets/`) | @@ -158,7 +154,7 @@ The deleted `plan.md` is not authoritative and must not be restored. Use live so ## When stuck -- Sandbox not working? Start with `apps/agent-worker/src/durable-objects/project-sandbox-lifecycle.ts`, `apps/agent-worker/src/durable-objects/project-sandbox-runtime.ts`, and `packages/tools-code/src/daytona-client.ts`; check Daytona auth (`DAYTONA_API_KEY`), the configured snapshot and target, and toolbox/session requests. +- Sandbox not working? Start with `apps/agent-worker/src/durable-objects/project-sandbox-lifecycle.ts`, `apps/agent-worker/src/durable-objects/project-sandbox-runtime-handle.ts`, and `packages/agent-core/src/tools/code/daytona-client.ts`; check Daytona auth (`DAYTONA_API_KEY`), the configured snapshot and target, and toolbox/session requests. - Auth broken? `packages/auth/` — Clerk JWT verify pattern with `@clerk/backend`. - Skill not triggering? Inspect the bundled skill `description` first — it is the activation field, not the body. Use manual fixture review plus final UI diff --git a/README.md b/README.md index c9b30eeb..9cc760de 100644 --- a/README.md +++ b/README.md @@ -122,12 +122,13 @@ Expected local endpoints: - Web app: `http://localhost:3001` - Gateway and chained Workers: `http://127.0.0.1:8787` -- Gateway health: `http://127.0.0.1:8787/health` +- Gateway liveness: `http://127.0.0.1:8787/health/live` +- Gateway release convergence: `http://127.0.0.1:8787/health/release` - Wrangler inspector: `http://127.0.0.1:9239` -The Compose health check verifies both the web app's Cheatcode symbol asset and -the gateway's JSON health response. A healthy gateway also proves that the -service-bound agent and webhooks Workers are reachable. +The Compose health check uses cheap gateway liveness. Release verification uses +the convergence endpoint, which also proves that the service-bound agent and +webhooks Workers report the same SHA. ### Stop or reset the stack diff --git a/apps/agent-worker/README.md b/apps/agent-worker/README.md index 8920b969..81e2ed61 100644 --- a/apps/agent-worker/README.md +++ b/apps/agent-worker/README.md @@ -10,7 +10,7 @@ user header. The Worker implements the provider-neutral sandbox and artifact ports from `@cheatcode/sandbox-contracts`. Daytona control-plane and toolbox details remain -behind `@cheatcode/tools-code` and do not leak into peer tool packages. +behind `@cheatcode/agent-core/tools/code` and do not load the root Mastra surface. Generated artifacts use a crash-consistent Postgres/R2 protocol. Content determines the output UUID, object key, and SHA-256 metadata. The Worker durably reserves that identity, @@ -278,15 +278,15 @@ pnpm --filter @cheatcode/agent-worker typecheck must match the `app_agent` Supabase Vault HMAC secret) - `DAYTONA_API_KEY` - `DAYTONA_API_URL` -- `DAYTONA_TARGET` +- `DAYTONA_TARGET` (development override; defaults to `us`) - `DAYTONA_SANDBOX_SNAPSHOT` - `DAYTONA_WORKSPACE_VOLUME` (one shared environment volume; each user mounts only its sandbox-name subpath) - `PREVIEW_TOKEN_SECRET` - `COMPOSIO_API_KEY` - `DEEPSEEK_PLATFORM_API_KEY` - `OUTPUT_DOWNLOAD_SIGNING_SECRET` (Secrets Store binding) -- `OUTPUT_DOWNLOAD_BASE_URL` -- `PREVIEW_HOSTNAME` +- `OUTPUT_DOWNLOAD_BASE_URL` (development override; production defaults to the gateway origin) +- `PREVIEW_HOSTNAME` (development override; production derives the canonical app hostname) - `QUOTA_TRACKER` - `R2_AUDIT` - `R2_OUTPUTS` diff --git a/apps/agent-worker/package.json b/apps/agent-worker/package.json index be80adc5..57c44a9c 100644 --- a/apps/agent-worker/package.json +++ b/apps/agent-worker/package.json @@ -23,7 +23,6 @@ "@cheatcode/preview-bridge": "workspace:*", "@cheatcode/sandbox-contracts": "workspace:*", "@cheatcode/skills": "workspace:*", - "@cheatcode/tools-code": "workspace:*", "@cheatcode/types": "workspace:*", "ai": "catalog:", "hono": "catalog:", diff --git a/apps/agent-worker/src/agent-api-system-routes.ts b/apps/agent-worker/src/agent-api-system-routes.ts index e99c6765..9ebec273 100644 --- a/apps/agent-worker/src/agent-api-system-routes.ts +++ b/apps/agent-worker/src/agent-api-system-routes.ts @@ -1,5 +1,5 @@ import { findGeneratedOutput, getProject, withUserDb } from "@cheatcode/db"; -import { resolveWorkerSecret, type WorkerSecret } from "@cheatcode/env"; +import { previewHostnameForWorker, resolveWorkerSecret, type WorkerSecret } from "@cheatcode/env"; import { APIError } from "@cheatcode/observability"; import { OutputIdSchema, ProjectId, UserId } from "@cheatcode/types"; import { @@ -211,7 +211,7 @@ async function resolveOutputSigningSecret(secret: WorkerSecret): Promise; QUOTA_TRACKER: QuotaTrackerNamespace; R2_AUDIT: R2Bucket; diff --git a/apps/agent-worker/src/durable-objects/agent-run-app-builder-scaffold.ts b/apps/agent-worker/src/durable-objects/agent-run-app-builder-scaffold.ts index 8c4dd27e..db382a5c 100644 --- a/apps/agent-worker/src/durable-objects/agent-run-app-builder-scaffold.ts +++ b/apps/agent-worker/src/durable-objects/agent-run-app-builder-scaffold.ts @@ -1,6 +1,10 @@ +import { + executeShellExec, + executeShellTerminal, + executeWriteFile, +} from "@cheatcode/agent-core/tools/code"; import { APIError, type createLogger } from "@cheatcode/observability"; import type { CodeRuntimeContext } from "@cheatcode/sandbox-contracts"; -import { executeShellExec, executeShellTerminal, executeWriteFile } from "@cheatcode/tools-code"; import { appBuilderGlobalStylesSource, appBuilderLayoutSource, diff --git a/apps/agent-worker/src/durable-objects/agent-run-app-builder.ts b/apps/agent-worker/src/durable-objects/agent-run-app-builder.ts index 669d80c8..4df55127 100644 --- a/apps/agent-worker/src/durable-objects/agent-run-app-builder.ts +++ b/apps/agent-worker/src/durable-objects/agent-run-app-builder.ts @@ -1,3 +1,8 @@ +import { + executeShellExec, + executeShellTerminal, + executeStartDevServer, +} from "@cheatcode/agent-core/tools/code"; import { type AnalyticsBindings, APIError, @@ -5,11 +10,6 @@ import { emitUserEvent, } from "@cheatcode/observability"; import type { CodeRuntimeContext } from "@cheatcode/sandbox-contracts"; -import { - executeShellExec, - executeShellTerminal, - executeStartDevServer, -} from "@cheatcode/tools-code"; import type { ProjectMode } from "@cheatcode/types/api"; import type { UIMessageChunk } from "ai"; import { @@ -78,11 +78,7 @@ async function allocateAppPort( ): Promise { // Per-user sandbox: never fall back to a fixed shared port — two projects on the same fixed port // would fight over it (a rebuild's deleteProcessesOnPort would kill the other's dev server). If - // the allocator is unavailable, fail the dev-server start loudly instead of sharing a port. - if (!sandbox.allocateProjectPort) { - logger.error("app_port_alloc_missing_method", { slug }); - throw appPortAllocationError(slug); - } + // allocation fails, fail the dev-server start loudly instead of sharing a port. try { const port = await sandbox.allocateProjectPort({ projectId: slug, @@ -554,10 +550,6 @@ async function getSignedMetroUrl( logger: AgentRunLogger, port: number, ): Promise { - if (!sandbox.getSignedPreviewUrl) { - logger.warn("expo_signed_preview_unavailable"); - return null; - } try { const signed = await sandbox.getSignedPreviewUrl({ expiresInSeconds: SIGNED_PREVIEW_TTL_SECONDS, @@ -697,7 +689,7 @@ async function stopProjectPreview( slot: string, ): Promise { try { - await sandbox.killProcess?.({ processId: slot }); + await sandbox.killProcess({ processId: slot }); logger.info("sandbox_project_preview_stopped", { slot }); } catch (error) { logger.warn("sandbox_process_stop_failed", { diff --git a/apps/agent-worker/src/durable-objects/agent-run-env.ts b/apps/agent-worker/src/durable-objects/agent-run-env.ts index 596dde67..527ed763 100644 --- a/apps/agent-worker/src/durable-objects/agent-run-env.ts +++ b/apps/agent-worker/src/durable-objects/agent-run-env.ts @@ -6,6 +6,7 @@ import type { ProjectSandbox } from "./project-sandbox"; export interface AgentRunEnv extends AnalyticsBindings { AGENT_RUN_WORKFLOW: Workflow; + CHEATCODE_ENVIRONMENT: "development" | "production"; CHEATCODE_RELEASE_SHA?: string; COMPOSIO_API_KEY?: WorkerSecret; DATABASE_CONTEXT_SIGNING_SECRET_AGENT: WorkerSecret; @@ -13,7 +14,7 @@ export interface AgentRunEnv extends AnalyticsBindings { HYPERDRIVE: Hyperdrive; OUTPUT_DOWNLOAD_BASE_URL?: string; OUTPUT_DOWNLOAD_SIGNING_SECRET: WorkerSecret; - PREVIEW_HOSTNAME: string; + PREVIEW_HOSTNAME?: string; PROJECT_SANDBOX: DurableObjectNamespace; QUOTA_TRACKER: QuotaTrackerNamespace; R2_OUTPUTS: R2Bucket; diff --git a/apps/agent-worker/src/durable-objects/project-sandbox-account-deletion.ts b/apps/agent-worker/src/durable-objects/project-sandbox-account-deletion.ts index 96e2915f..d32f04d8 100644 --- a/apps/agent-worker/src/durable-objects/project-sandbox-account-deletion.ts +++ b/apps/agent-worker/src/durable-objects/project-sandbox-account-deletion.ts @@ -1,5 +1,9 @@ +import type { + DaytonaClient, + DaytonaSandbox, + SandboxDestroyResult, +} from "@cheatcode/agent-core/tools/code"; import { APIError } from "@cheatcode/observability"; -import type { DaytonaClient, DaytonaSandbox, SandboxDestroyResult } from "@cheatcode/tools-code"; import { z } from "zod"; import { clearWorkspaceCommand } from "./project-sandbox-cleanup-script"; import type { ProjectSandboxIdentityState } from "./project-sandbox-identity-state"; diff --git a/apps/agent-worker/src/durable-objects/project-sandbox-content.ts b/apps/agent-worker/src/durable-objects/project-sandbox-content.ts index 6354bd71..e83a13d8 100644 --- a/apps/agent-worker/src/durable-objects/project-sandbox-content.ts +++ b/apps/agent-worker/src/durable-objects/project-sandbox-content.ts @@ -1,3 +1,4 @@ +import { DaytonaApiError } from "@cheatcode/agent-core/tools/code"; import { APIError } from "@cheatcode/observability"; import type { SandboxDeleteFileResult, @@ -6,7 +7,6 @@ import type { SandboxSearchFilesResult, SandboxWriteFileResult, } from "@cheatcode/sandbox-contracts"; -import { DaytonaApiError } from "@cheatcode/tools-code"; import { metroForwardedHostFixScript } from "./expo-metro-forwarded-host"; import { CODE_SERVER_DISPLAY_DIR, diff --git a/apps/agent-worker/src/durable-objects/project-sandbox-daytona-identity.ts b/apps/agent-worker/src/durable-objects/project-sandbox-daytona-identity.ts index f46789db..be5b15af 100644 --- a/apps/agent-worker/src/durable-objects/project-sandbox-daytona-identity.ts +++ b/apps/agent-worker/src/durable-objects/project-sandbox-daytona-identity.ts @@ -1,4 +1,4 @@ -import type { DaytonaSandbox } from "@cheatcode/tools-code"; +import type { DaytonaSandbox } from "@cheatcode/agent-core/tools/code"; const APP_LABEL = "cheatcode"; diff --git a/apps/agent-worker/src/durable-objects/project-sandbox-files.ts b/apps/agent-worker/src/durable-objects/project-sandbox-files.ts index ef8009c7..9991b130 100644 --- a/apps/agent-worker/src/durable-objects/project-sandbox-files.ts +++ b/apps/agent-worker/src/durable-objects/project-sandbox-files.ts @@ -1,5 +1,5 @@ +import type { DaytonaClient, DaytonaFileInfo } from "@cheatcode/agent-core/tools/code"; import type { SandboxListFilesResult } from "@cheatcode/sandbox-contracts"; -import type { DaytonaClient, DaytonaFileInfo } from "@cheatcode/tools-code"; const MAX_LIST_FILE_ENTRIES = 1_000; diff --git a/apps/agent-worker/src/durable-objects/project-sandbox-lifecycle-support.ts b/apps/agent-worker/src/durable-objects/project-sandbox-lifecycle-support.ts index cf0e0c64..5b633b69 100644 --- a/apps/agent-worker/src/durable-objects/project-sandbox-lifecycle-support.ts +++ b/apps/agent-worker/src/durable-objects/project-sandbox-lifecycle-support.ts @@ -1,27 +1,32 @@ -import type { WorkerSecret } from "@cheatcode/env"; +import { DaytonaApiError, type DaytonaSandbox } from "@cheatcode/agent-core/tools/code"; +import { DEFAULT_DAYTONA_TARGET, type WorkerSecret } from "@cheatcode/env"; import { APIError } from "@cheatcode/observability"; -import { DaytonaApiError, type DaytonaSandbox } from "@cheatcode/tools-code"; import { z } from "zod"; import type { QuotaTrackerNamespace } from "../quota-tracker-binding"; export interface ProjectSandboxEnv { + CHEATCODE_ENVIRONMENT: "development" | "production"; CHEATCODE_RELEASE_SHA?: string; DATABASE_CONTEXT_SIGNING_SECRET_AGENT: WorkerSecret; DAYTONA_API_KEY: WorkerSecret; DAYTONA_API_URL: string; - DAYTONA_TARGET: string; + DAYTONA_TARGET?: string; DAYTONA_SANDBOX_SNAPSHOT: string; DAYTONA_WORKSPACE_VOLUME: string; HYPERDRIVE: Hyperdrive; DAYTONA_ORG_ID?: string; DAYTONA_PREVIEW_HOST_SUFFIXES?: string; PREVIEW_TOKEN_SECRET: WorkerSecret; - PREVIEW_HOSTNAME: string; + PREVIEW_HOSTNAME?: string; QUOTA_TRACKER: QuotaTrackerNamespace; R2_AUDIT: R2Bucket; R2_OUTPUTS: R2Bucket; } +export function daytonaTarget(env: Pick): string { + return env.DAYTONA_TARGET ?? DEFAULT_DAYTONA_TARGET; +} + export const ACCOUNT_DELETION_TOMBSTONE_KEY = "account_deletion_tombstone"; export const DAYTONA_ID_KEY = "daytona_sandbox_id"; export const RUN_LEASES_KEY = "run_leases"; diff --git a/apps/agent-worker/src/durable-objects/project-sandbox-process-control.ts b/apps/agent-worker/src/durable-objects/project-sandbox-process-control.ts index ecac63d6..e136bec8 100644 --- a/apps/agent-worker/src/durable-objects/project-sandbox-process-control.ts +++ b/apps/agent-worker/src/durable-objects/project-sandbox-process-control.ts @@ -1,5 +1,5 @@ +import type { DaytonaSessionExecResponse } from "@cheatcode/agent-core/tools/code"; import { APIError } from "@cheatcode/observability"; -import type { DaytonaSessionExecResponse } from "@cheatcode/tools-code"; import { WORKSPACE_DIR } from "./project-sandbox-content-support"; import { SANDBOX_PROCESS_TERMINATION_SCRIPT } from "./project-sandbox-process-cleanup"; import { diff --git a/apps/agent-worker/src/durable-objects/project-sandbox-process-support.ts b/apps/agent-worker/src/durable-objects/project-sandbox-process-support.ts index 4435d277..e7988d00 100644 --- a/apps/agent-worker/src/durable-objects/project-sandbox-process-support.ts +++ b/apps/agent-worker/src/durable-objects/project-sandbox-process-support.ts @@ -1,5 +1,5 @@ +import { type DaytonaSandbox, WorkspacePathSchema } from "@cheatcode/agent-core/tools/code"; import { APIError } from "@cheatcode/observability"; -import { type DaytonaSandbox, WorkspacePathSchema } from "@cheatcode/tools-code"; import { z } from "zod"; import type { ProjectStartProcessInputSchema } from "./project-sandbox-runtime"; diff --git a/apps/agent-worker/src/durable-objects/project-sandbox-project-files.ts b/apps/agent-worker/src/durable-objects/project-sandbox-project-files.ts index a48dd44d..1bc70bc2 100644 --- a/apps/agent-worker/src/durable-objects/project-sandbox-project-files.ts +++ b/apps/agent-worker/src/durable-objects/project-sandbox-project-files.ts @@ -1,6 +1,6 @@ +import { DaytonaApiError } from "@cheatcode/agent-core/tools/code"; import { workspacePathForSlug } from "@cheatcode/db"; import { APIError } from "@cheatcode/observability"; -import { DaytonaApiError } from "@cheatcode/tools-code"; import { PROJECT_FILE_MAX_CURRENT_FILES, type ProjectFile, diff --git a/apps/agent-worker/src/durable-objects/project-sandbox-provisioning.ts b/apps/agent-worker/src/durable-objects/project-sandbox-provisioning.ts index 2f6cdfa5..8943b6ea 100644 --- a/apps/agent-worker/src/durable-objects/project-sandbox-provisioning.ts +++ b/apps/agent-worker/src/durable-objects/project-sandbox-provisioning.ts @@ -1,10 +1,10 @@ -import { APIError } from "@cheatcode/observability"; import { DaytonaApiError, type DaytonaClient, type DaytonaSandbox, type DaytonaVolume, -} from "@cheatcode/tools-code"; +} from "@cheatcode/agent-core/tools/code"; +import { APIError } from "@cheatcode/observability"; import { canonicalSandboxLabels, isCanonicalSandbox, @@ -14,6 +14,7 @@ import { import { AUTO_ARCHIVE_MIN, DEFAULT_IDLE_STOP_MIN, + daytonaTarget, ENSURE_STARTED_ATTEMPTS, ENSURE_STARTED_DELAY_MS, isDaytonaNameConflictError, @@ -187,7 +188,7 @@ export class ProjectSandboxProvisioning { return isDesiredCanonicalSandbox(sandbox, { sandboxName: this.input.sandboxName(), snapshot: this.input.env.DAYTONA_SANDBOX_SNAPSHOT, - target: this.input.env.DAYTONA_TARGET, + target: daytonaTarget(this.input.env), volumeName: this.input.env.DAYTONA_WORKSPACE_VOLUME, }); } @@ -199,7 +200,7 @@ export class ProjectSandboxProvisioning { const created = await client.createSandbox({ name, snapshot: this.input.env.DAYTONA_SANDBOX_SNAPSHOT, - target: this.input.env.DAYTONA_TARGET, + target: daytonaTarget(this.input.env), user: "node", labels: canonicalSandboxLabels({ sandboxName: name, diff --git a/apps/agent-worker/src/durable-objects/project-sandbox-runtime-handle.ts b/apps/agent-worker/src/durable-objects/project-sandbox-runtime-handle.ts index 29c853cc..989f1e12 100644 --- a/apps/agent-worker/src/durable-objects/project-sandbox-runtime-handle.ts +++ b/apps/agent-worker/src/durable-objects/project-sandbox-runtime-handle.ts @@ -1,6 +1,6 @@ -import { PreviewHostnameSchema, resolveWorkerSecret } from "@cheatcode/env"; +import { DaytonaClient, type DaytonaSandbox } from "@cheatcode/agent-core/tools/code"; +import { previewHostnameForWorker, resolveWorkerSecret } from "@cheatcode/env"; import { APIError, createLogger } from "@cheatcode/observability"; -import { DaytonaClient, type DaytonaSandbox } from "@cheatcode/tools-code"; import { performAccountDeletion } from "./project-sandbox-account-deletion"; import { type SandboxExecAuditEntry, writeExecAudit } from "./project-sandbox-audit"; import { ProjectSandboxIdentityState } from "./project-sandbox-identity-state"; @@ -16,6 +16,7 @@ import { import { ACCOUNT_DELETION_TOMBSTONE_KEY, DAYTONA_ID_KEY, + daytonaTarget, type ProjectSandboxEnv, RUN_LEASES_KEY, runLeases, @@ -158,7 +159,8 @@ function runtimeHandle(state: RuntimeState): SandboxRuntime { meteringContext: () => Promise.resolve(meteringContext(state)), outputBucket: state.env.R2_OUTPUTS, ownerUserId: () => ownerUserId(state), - previewHostname: () => PreviewHostnameSchema.parse(state.env.PREVIEW_HOSTNAME), + previewHostname: () => + previewHostnameForWorker(state.env.CHEATCODE_ENVIRONMENT, state.env.PREVIEW_HOSTNAME), previewSecret: () => previewSecret(state), registerOwner: (userId, sandboxName) => state.identity.registerOwner(userId, sandboxName), restartSandboxForWorkspaceRecovery: (sandboxId) => @@ -235,7 +237,7 @@ async function ensureClient(state: RuntimeState): Promise { state.cache.client = new DaytonaClient({ apiKey, apiUrl: state.env.DAYTONA_API_URL, - target: state.env.DAYTONA_TARGET, + target: daytonaTarget(state.env), ...(state.env.DAYTONA_ORG_ID ? { organizationId: state.env.DAYTONA_ORG_ID } : {}), ...(state.env.DAYTONA_PREVIEW_HOST_SUFFIXES ? { previewHostSuffixes: state.env.DAYTONA_PREVIEW_HOST_SUFFIXES } diff --git a/apps/agent-worker/src/durable-objects/project-sandbox-runtime.ts b/apps/agent-worker/src/durable-objects/project-sandbox-runtime.ts index b8141caf..67cf55c5 100644 --- a/apps/agent-worker/src/durable-objects/project-sandbox-runtime.ts +++ b/apps/agent-worker/src/durable-objects/project-sandbox-runtime.ts @@ -1,5 +1,5 @@ +import { WorkspaceFilePathSchema, WorkspacePathSchema } from "@cheatcode/agent-core/tools/code"; import { EnvironmentVariablesSchema } from "@cheatcode/sandbox-contracts"; -import { WorkspaceFilePathSchema, WorkspacePathSchema } from "@cheatcode/tools-code"; import { ProjectId } from "@cheatcode/types"; import { PROJECT_FILE_MAX_BYTES, ProjectFileRelativePathSchema } from "@cheatcode/types/api"; import { z } from "zod"; diff --git a/apps/agent-worker/src/index.ts b/apps/agent-worker/src/index.ts index a4a2e279..639b6bd0 100644 --- a/apps/agent-worker/src/index.ts +++ b/apps/agent-worker/src/index.ts @@ -16,7 +16,9 @@ import { AgentRun } from "./durable-objects/agent-run"; import { AgentRunWorkflow } from "./durable-objects/agent-run-workflow"; import { ProjectSandbox } from "./durable-objects/project-sandbox"; import { formatAgentRouteError, toAgentRouteError } from "./error-handling"; -import { registerSandboxHttpRoutes } from "./sandbox-http-routes"; +import { registerProjectFileHttpRoutes } from "./project-file-http-routes"; +import { registerSandboxPreviewHttpRoutes } from "./sandbox-preview-http-routes"; +import { registerSandboxTerminalHttpRoutes } from "./sandbox-terminal-http-routes"; import { registerSkillRuntimeExecutionRoutes } from "./skill-runtime-execution-routes"; import { registerSkillRuntimeManagedRoutes } from "./skill-runtime-managed-routes"; import { registerUserSkillHttpRoutes } from "./user-skill-http-routes"; @@ -49,7 +51,9 @@ agentApp.get("/health", (c) => registerAgentSystemHttpRoutes(agentApp); registerAgentRunHttpRoutes(agentApp); -registerSandboxHttpRoutes(agentApp); +registerProjectFileHttpRoutes(agentApp); +registerSandboxPreviewHttpRoutes(agentApp); +registerSandboxTerminalHttpRoutes(agentApp); registerUserSkillHttpRoutes(agentApp); registerSkillRuntimeManagedRoutes(agentApp); registerSkillRuntimeExecutionRoutes(agentApp); diff --git a/apps/agent-worker/src/sandbox-http-routes.ts b/apps/agent-worker/src/sandbox-http-routes.ts deleted file mode 100644 index 715dd8dd..00000000 --- a/apps/agent-worker/src/sandbox-http-routes.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Hono } from "hono"; -import type { AgentEnv } from "./agent-env"; -import { registerProjectFileHttpRoutes } from "./project-file-http-routes"; -import { registerSandboxPreviewHttpRoutes } from "./sandbox-preview-http-routes"; -import { registerSandboxTerminalHttpRoutes } from "./sandbox-terminal-http-routes"; - -export function registerSandboxHttpRoutes(app: Hono<{ Bindings: AgentEnv }>): void { - registerProjectFileHttpRoutes(app); - registerSandboxPreviewHttpRoutes(app); - registerSandboxTerminalHttpRoutes(app); -} diff --git a/apps/agent-worker/src/sandbox-terminal-http-routes.ts b/apps/agent-worker/src/sandbox-terminal-http-routes.ts index 80ea428a..90891c31 100644 --- a/apps/agent-worker/src/sandbox-terminal-http-routes.ts +++ b/apps/agent-worker/src/sandbox-terminal-http-routes.ts @@ -1,6 +1,6 @@ +import { resolveProjectWorkspacePath } from "@cheatcode/agent-core/tools/code"; import { workspacePathForSlug } from "@cheatcode/db"; import { readJsonRequest } from "@cheatcode/observability"; -import { resolveProjectWorkspacePath } from "@cheatcode/tools-code"; import { SandboxConsoleQuerySchema, SandboxConsoleSnapshotSchema, diff --git a/apps/agent-worker/src/user-skill-files.ts b/apps/agent-worker/src/user-skill-files.ts index b620769c..fcfc175b 100644 --- a/apps/agent-worker/src/user-skill-files.ts +++ b/apps/agent-worker/src/user-skill-files.ts @@ -58,9 +58,6 @@ export async function writeUserSkillMirror( sandbox: SandboxLike, skill: UserSkillRecord, ): Promise { - if (typeof sandbox.writeFile !== "function") { - throw new Error("Sandbox does not support custom skill mirrors."); - } const path = userSkillFilePath(skill.name); await sandbox.writeFile({ content: await serializeUserSkillMarkdown(skill), @@ -182,7 +179,6 @@ async function userSkillRevision( } async function readUserSkillMirror(sandbox: SandboxLike, name: string): Promise { - if (typeof sandbox.readFile !== "function") return null; const file = await sandbox .readFile({ encoding: "utf8", path: userSkillFilePath(name) }) .catch(() => null); diff --git a/apps/agent-worker/src/user-skill-http-routes.ts b/apps/agent-worker/src/user-skill-http-routes.ts index 685e9246..5dad02c5 100644 --- a/apps/agent-worker/src/user-skill-http-routes.ts +++ b/apps/agent-worker/src/user-skill-http-routes.ts @@ -87,14 +87,6 @@ async function removeSkillPackageFiles( skill: UserSkillRecord, ): Promise { const sandbox = await sandboxForUser(env, userId); - if (!sandbox.deleteFile) { - throw new APIError( - 503, - "unavailable_maintenance", - "The skill workspace cannot be cleaned up right now", - { retriable: true }, - ); - } await Promise.all([ deleteUserSkillPackage(env.R2_OUTPUTS, userId, skill.id), sandbox.deleteFile({ path: userSkillDirectoryPath(skill.name), recursive: true }), diff --git a/apps/agent-worker/src/user-skill-packages.ts b/apps/agent-worker/src/user-skill-packages.ts index 37c77b24..03aa4e9d 100644 --- a/apps/agent-worker/src/user-skill-packages.ts +++ b/apps/agent-worker/src/user-skill-packages.ts @@ -175,7 +175,6 @@ export async function collectUserSkillPackageFromSandbox( encoding: "utf8", path: "SKILL.md", }; - if (!sandbox.listFiles || !sandbox.readFile) return [fallback]; const directory = sourceSlug ? `/workspace/.cheatcode/skills/${sourceSlug}` : userSkillDirectoryPath(skill.name); @@ -194,7 +193,6 @@ export async function writeUserSkillPackageMirror( skill: Pick, packageValue: UserSkillPackage, ): Promise { - if (!sandbox.writeFile) throw new Error("Sandbox does not support custom skill packages."); const directory = userSkillDirectoryPath(skill.name); const previous = await readMirrorManifest(sandbox, directory); if (previous?.revision === packageValue.revision) return userSkillFilePath(skill.name); @@ -332,7 +330,6 @@ async function readPackageCandidates( sandbox: SandboxLike, candidates: Array<{ absolutePath: string; path: string }>, ): Promise { - if (!sandbox.readFile) return []; const files: UserSkillPackageFile[] = []; for (const candidate of candidates) { const encoding = packageFileEncoding(candidate.path); @@ -353,7 +350,6 @@ async function readMirrorManifest( sandbox: SandboxLike, directory: string, ): Promise | null> { - if (!sandbox.readFile) return null; const file = await sandbox .readFile({ encoding: "utf8", path: `${directory}/${MIRROR_MANIFEST_FILE}` }) .catch(() => null); @@ -374,7 +370,7 @@ async function deleteStalePackageFiles( previous: z.infer | null, next: UserSkillPackage, ): Promise { - if (!sandbox.deleteFile || !previous) return; + if (!previous) return; const nextPaths = new Set(next.files.map((file) => file.path)); for (const path of previous.files) { if (!nextPaths.has(path)) { @@ -388,7 +384,6 @@ async function writeMirrorManifest( directory: string, packageValue: UserSkillPackage, ): Promise { - if (!sandbox.writeFile) return; await sandbox.writeFile({ content: `${JSON.stringify( { diff --git a/apps/agent-worker/wrangler.jsonc b/apps/agent-worker/wrangler.jsonc index 8f7bf8e3..1a0e9b09 100644 --- a/apps/agent-worker/wrangler.jsonc +++ b/apps/agent-worker/wrangler.jsonc @@ -13,12 +13,8 @@ "CHEATCODE_ENVIRONMENT": "production", "DAYTONA_API_URL": "https://app.daytona.io/api", "DAYTONA_ORG_ID": "300a0c8b-dc3b-4c5f-b31c-0f9e0344d00d", - "DAYTONA_PREVIEW_HOST_SUFFIXES": "daytonaproxy01.net,proxy.daytona.work", - "DAYTONA_TARGET": "us", "DAYTONA_SANDBOX_SNAPSHOT": "cheatcode-sandbox-viewer-bundle-66d3adbe9083-30338274543", - "DAYTONA_WORKSPACE_VOLUME": "cheatcode-workspaces-production", - "OUTPUT_DOWNLOAD_BASE_URL": "https://gateway.trycheatcode.com", - "PREVIEW_HOSTNAME": "trycheatcode.com" + "DAYTONA_WORKSPACE_VOLUME": "cheatcode-workspaces-production" }, "secrets_store_secrets": [ { diff --git a/apps/gateway-worker/README.md b/apps/gateway-worker/README.md index b954378a..02395a58 100644 --- a/apps/gateway-worker/README.md +++ b/apps/gateway-worker/README.md @@ -7,6 +7,10 @@ when webhooks have not run yet, rate-limits requests, and forwards agent work to The bootstrap reads one canonical Clerk identity snapshot including `updated_at`; the database compare-and-swap prevents a slower Backend API response or delayed webhook from regressing a newer email, display name, or avatar. +One database handle is created lazily on first access and reused for the whole +`/v1` request, then closed after the response handler completes. Liveness and +public forwarding paths that do not resolve user or route state never open +Hyperdrive. Provider key writes validate each supported BYOK provider through `packages/byok` before calling the Vault-backed RPC. Invalid keys are rejected @@ -30,6 +34,9 @@ soft `record` metering for sandbox-hours. Limit synchronization carries the entitlement row's `updatedAt` version, and the Durable Object ignores older writes so a stale KV or Worker request cannot overwrite a newer plan. Request rate-limit headers remain the canonical live rate-limit state. +Gateway-native buckets use Hono's registered route path; the resulting key +format replaces the former duplicated literals and old Durable Object buckets +age out naturally. Forwarded-route costs remain owned by the shared manifest. Billing routes create Polar checkout/portal sessions and manage end-of-period cancellation/reactivation through `/v1/billing/state`, `/v1/billing/cancel`, @@ -43,7 +50,8 @@ Run creation requires the current Clerk primary email to be verified before the request is forwarded to `agent-worker`, so authenticated but unverified users do not spawn Daytona sandbox work. JWT verification also requires `azp` to match one of the exact HTTP(S) origins in -`CLERK_AUTHORIZED_PARTIES`; production is pinned to `https://trycheatcode.com`. +`CLERK_AUTHORIZED_PARTIES`; production derives that party from the canonical +Cheatcode application origin. Resolved Clerk Backend API keys fail closed unless they are `sk_live_` in production or `sk_test_` in laptop development. @@ -92,10 +100,12 @@ Production deploys bind an immutable `CHEATCODE_RELEASE_SHA` into every affected Worker. Each Durable Object initializes one current SQLite contract and validates that exact contract before using existing storage. -The gateway health response exposes its own release SHA and the release SHAs -reported by its agent and webhook service bindings. Deployments publish the -gateway last so public traffic observes only a backend set built from the same -reviewed revision. SQLite schema validation remains synchronous. +`GET /health/live` is a cheap gateway-only liveness response. +`GET /health/release` exposes the gateway release SHA plus the release SHAs +reported by its agent and webhook service bindings, and returns 503 while they +converge. Deployments publish the gateway last so public traffic observes only a +backend set built from the same reviewed revision. SQLite schema validation +remains synchronous. `IdempotencyStore`, `RateLimiter`, and `QuotaTracker` each own one exact SQLite schema. New objects initialize that schema directly; existing objects must @@ -138,8 +148,9 @@ pnpm --filter @cheatcode/gateway-worker typecheck - `HYPERDRIVE` (dedicated config whose database login is exactly `app_gateway`) - `DATABASE_CONTEXT_SIGNING_SECRET_GATEWAY` (role-specific Secrets Store binding; must match the `app_gateway` Supabase Vault HMAC secret) -- `CLERK_SECRET_KEY` or `CLERK_JWT_KEY` -- `CLERK_AUTHORIZED_PARTIES` (comma-separated exact HTTP(S) origins) +- `CLERK_SECRET_KEY` +- `CLERK_AUTHORIZED_PARTIES` (generated development-only comma-separated exact + HTTP(S) origins; production derives the canonical application origin) - `POLAR_ACCESS_TOKEN` - `POLAR_SERVER` (`production` by default; set `sandbox` only with sandbox credentials/products) - `POLAR_PRODUCT_ID_PRO`, `POLAR_PRODUCT_ID_PREMIUM` diff --git a/apps/gateway-worker/src/account-http-routes.ts b/apps/gateway-worker/src/account-http-routes.ts deleted file mode 100644 index 25028a3f..00000000 --- a/apps/gateway-worker/src/account-http-routes.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { getActivityHistoryRoute } from "./activity-routes"; -import { authenticate, readRequiredSecret } from "./authenticate"; -import { myUsageRoute } from "./billing-routes"; -import type { GatewayApp } from "./gateway-env"; -import { greetingRoute } from "./greeting-routes"; -import { getMyProfileRoute, updateMyProfileRoute } from "./profile-routes"; -import { rateLimit } from "./rate-limit"; -import { listRecentThreadsRoute, searchWorkspaceRoute } from "./search-routes"; - -export function registerAccountHttpRoutes(app: GatewayApp): void { - app.get("/v1/me/profile", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/me/profile"); - return getMyProfileRoute(c.env, c.executionCtx, userId); - }); - app.patch("/v1/me/profile", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "PATCH /v1/me/profile"); - return updateMyProfileRoute(c.env, c.executionCtx, c.req.raw, userId); - }); - app.get("/v1/me/usage", (c) => myUsageRoute(c, { authenticate, readRequiredSecret })); - app.get("/v1/activity", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/activity"); - return getActivityHistoryRoute(c.env, c.executionCtx, c.req.raw, userId); - }); - app.get("/v1/search", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/search"); - return searchWorkspaceRoute(c.env, c.executionCtx, c.req.raw, userId); - }); - app.get("/v1/threads", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/threads"); - return listRecentThreadsRoute(c.env, c.executionCtx, c.req.raw, userId); - }); - app.get("/v1/greeting", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/greeting"); - return greetingRoute(c.env, c.executionCtx, c.req.raw, userId); - }); -} diff --git a/apps/gateway-worker/src/activity-http-routes.ts b/apps/gateway-worker/src/activity-http-routes.ts new file mode 100644 index 00000000..cc15f6c6 --- /dev/null +++ b/apps/gateway-worker/src/activity-http-routes.ts @@ -0,0 +1,12 @@ +import { getActivityHistoryRoute } from "./activity-routes"; +import { authenticate } from "./authenticate"; +import { type GatewayApp, requestDatabase } from "./gateway-env"; +import { rateLimit } from "./rate-limit"; + +export function registerActivityHttpRoutes(app: GatewayApp): void { + app.get("/v1/activity", async (c) => { + const userId = await authenticate(c); + await rateLimit(c, userId); + return getActivityHistoryRoute(requestDatabase(c), c.env, c.req.raw, userId); + }); +} diff --git a/apps/gateway-worker/src/activity-routes.ts b/apps/gateway-worker/src/activity-routes.ts index 72f34845..91a6a044 100644 --- a/apps/gateway-worker/src/activity-routes.ts +++ b/apps/gateway-worker/src/activity-routes.ts @@ -1,6 +1,7 @@ import { type AgentRunStartPointRange, type Database, + type DatabaseHandle, listAgentRunStartPoints, withUserDb, } from "@cheatcode/db"; @@ -15,7 +16,6 @@ import { import { QUOTA_FEATURES } from "@cheatcode/types/quota"; import type { z } from "zod"; import type { QuotaTracker } from "./durable-objects/quota-tracker"; -import type { WaitUntilContext } from "./wait-until-context"; export interface ActivityRouteEnv { DATABASE_CONTEXT_SIGNING_SECRET_GATEWAY: WorkerSecret; @@ -30,26 +30,18 @@ const RUN_POINT_DISPLAY_CAP = 2_000; const RUN_POINT_REQUEST_LIMIT = RUN_POINT_DISPLAY_CAP + 1; export async function getActivityHistoryRoute( + database: DatabaseHandle, env: ActivityRouteEnv, - ctx: WaitUntilContext, request: Request, userId: UserId, ): Promise { const query = parseActivityQuery(request); - return withUserDb( - env, - userId, - async ({ transaction }) => { - const sandboxHours = await listSandboxHourHistory(env, userId, query.days); - const response = await transaction((tx) => buildActivityResponse(tx, userId, query.days)); - response.sandboxHours = sandboxHours; - return Response.json(ActivityHistoryResponseSchema.parse(response)); - }, - (handle) => { - ctx.waitUntil(handle.close()); - return Promise.resolve(); - }, - ); + return withUserDb(database, userId, async ({ transaction }) => { + const sandboxHours = await listSandboxHourHistory(env, userId, query.days); + const response = await transaction((tx) => buildActivityResponse(tx, userId, query.days)); + response.sandboxHours = sandboxHours; + return Response.json(ActivityHistoryResponseSchema.parse(response)); + }); } async function buildActivityResponse( diff --git a/apps/gateway-worker/src/agent-forwarding.ts b/apps/gateway-worker/src/agent-forwarding.ts index f90c3baf..8aea52ea 100644 --- a/apps/gateway-worker/src/agent-forwarding.ts +++ b/apps/gateway-worker/src/agent-forwarding.ts @@ -1,7 +1,7 @@ import { type AgentForwardRoute, agentForwardRouteKey } from "@cheatcode/types/internal"; import { authenticate } from "./authenticate"; import type { GatewayContext } from "./gateway-env"; -import { rateLimit, rateLimitPublic, withRateLimitHeaders } from "./rate-limit"; +import { rateLimitForwarded, rateLimitPublicForwarded, withRateLimitHeaders } from "./rate-limit"; const PUBLIC_CREDENTIAL_HEADERS = [ "Authorization", @@ -54,9 +54,9 @@ export async function forwardAgentRequest( route: AgentForwardRoute, validate?: (c: GatewayContext) => void, ): Promise { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); + const userId = await authenticate(c); const routeKey = agentForwardRouteKey(route); - const headers = await rateLimit(c, userId, routeKey, route.rateLimitCost); + const headers = await rateLimitForwarded(c, userId, routeKey, route.rateLimitCost); validate?.(c); const forwarded = agentServiceRequest(c.req.raw, userId); return withRateLimitHeaders(await c.env.AGENT.fetch(forwarded), headers); @@ -67,6 +67,6 @@ export async function forwardPublicAgentRequest( route: AgentForwardRoute, ): Promise { const routeKey = agentForwardRouteKey(route); - const headers = await rateLimitPublic(c, routeKey, "publicRead", route.rateLimitCost); + const headers = await rateLimitPublicForwarded(c, routeKey, "publicRead", route.rateLimitCost); return withRateLimitHeaders(await c.env.AGENT.fetch(agentServiceRequest(c.req.raw)), headers); } diff --git a/apps/gateway-worker/src/agent-http-routes.ts b/apps/gateway-worker/src/agent-http-routes.ts index 1bb9ac5b..da34ac29 100644 --- a/apps/gateway-worker/src/agent-http-routes.ts +++ b/apps/gateway-worker/src/agent-http-routes.ts @@ -27,8 +27,8 @@ export function registerAgentHttpRoutes(app: GatewayApp): void { } async function createRunRoute(c: GatewayContext): Promise { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - const rateLimitHeaders = await rateLimit(c, userId, "POST /v1/threads/:threadId/runs"); + const userId = await authenticate(c); + const rateLimitHeaders = await rateLimit(c, userId); await requireVerifiedClerkEmail(c.req.raw, c.env); const prepared = await prepareIdempotentRunRequest(c.env, c.req.raw, userId); if (prepared.replay) { diff --git a/apps/gateway-worker/src/authenticate.ts b/apps/gateway-worker/src/authenticate.ts index 4a4f959f..337cddce 100644 --- a/apps/gateway-worker/src/authenticate.ts +++ b/apps/gateway-worker/src/authenticate.ts @@ -9,12 +9,15 @@ import { resolveInternalUserId, syncClerkUser, UserDeletionBlockedError, - withDatabase, } from "@cheatcode/db"; -import { resolveWorkerSecret, type WorkerSecret } from "@cheatcode/env"; +import { + PRODUCTION_CLERK_AUTHORIZED_PARTIES, + resolveWorkerSecret, + type WorkerSecret, +} from "@cheatcode/env"; import { APIError } from "@cheatcode/observability"; import type { UserId } from "@cheatcode/types"; -import type { WaitUntilContext } from "./wait-until-context"; +import { type GatewayContext, requestDatabase } from "./gateway-env"; /** * Narrow env surface the auth helpers depend on. `GatewayEnv` structurally @@ -23,49 +26,29 @@ import type { WaitUntilContext } from "./wait-until-context"; export interface AuthEnv { CHEATCODE_ENVIRONMENT: "development" | "production"; CLERK_AUTHORIZED_PARTIES?: string; - CLERK_JWT_KEY?: WorkerSecret; CLERK_SECRET_KEY?: WorkerSecret; DATABASE_CONTEXT_SIGNING_SECRET_GATEWAY: WorkerSecret; HYPERDRIVE: Hyperdrive; } -export async function authenticate( - request: Request, - env: AuthEnv, - ctx: WaitUntilContext, -): Promise { - const { secretKey, verificationOptions } = await clerkVerification(env); - const session = await verifyClerkBearerToken(request, verificationOptions); - return withDatabase( - env, - ({ db }) => resolveOrSyncClerkUser(db, session.clerkUserId, secretKey), - (handle) => { - ctx.waitUntil(handle.close()); - return Promise.resolve(); - }, - ); +export async function authenticate(c: GatewayContext): Promise { + const { secretKey, verificationOptions } = await clerkVerification(c.env); + const session = await verifyClerkBearerToken(c.req.raw, verificationOptions); + return resolveOrSyncClerkUser(requestDatabase(c).db, session.clerkUserId, secretKey); } async function clerkVerification(env: AuthEnv) { - const jwtKey = await readOptionalSecret(env.CLERK_JWT_KEY, "CLERK_JWT_KEY"); const secretKey = await readOptionalClerkSecret(env); - if (!jwtKey && !secretKey) { + if (!secretKey) { throw new APIError(503, "unavailable_maintenance", "Clerk verification is not configured", { - hint: "Set CLERK_JWT_KEY or CLERK_SECRET_KEY in the gateway Worker environment.", + hint: "Set CLERK_SECRET_KEY in the gateway Worker environment.", retriable: false, }); } - const verificationOptions: { - authorizedParties: string[]; - jwtKey?: string; - secretKey?: string; - } = { authorizedParties: clerkAuthorizedParties(env) }; - if (jwtKey) { - verificationOptions.jwtKey = jwtKey; - } - if (secretKey) { - verificationOptions.secretKey = secretKey; - } + const verificationOptions = { + authorizedParties: clerkAuthorizedParties(env), + secretKey, + }; return { secretKey, verificationOptions }; } @@ -156,15 +139,13 @@ export async function requireVerifiedClerkEmail(request: Request, env: AuthEnv): export function clerkAuthorizedParties( env: Pick, ): string[] { + if (env.CHEATCODE_ENVIRONMENT === "production") { + return [...PRODUCTION_CLERK_AUTHORIZED_PARTIES]; + } const configured = env.CLERK_AUTHORIZED_PARTIES?.split(",") .map((value) => value.trim()) .filter(Boolean); - const parties = - configured && configured.length > 0 - ? configured - : env.CHEATCODE_ENVIRONMENT === "production" - ? ["https://trycheatcode.com"] - : ["http://localhost:3001"]; + const parties = configured && configured.length > 0 ? configured : ["http://localhost:3001"]; if (parties.length > 16 || parties.some((value) => !isExactHttpOrigin(value))) { throw new APIError(503, "unavailable_maintenance", "Clerk authorized parties are invalid", { hint: "Configure CLERK_AUTHORIZED_PARTIES as comma-separated exact HTTP(S) origins.", diff --git a/apps/gateway-worker/src/billing-http-routes.ts b/apps/gateway-worker/src/billing-http-routes.ts index a7e61c03..76f90e9d 100644 --- a/apps/gateway-worker/src/billing-http-routes.ts +++ b/apps/gateway-worker/src/billing-http-routes.ts @@ -6,6 +6,7 @@ import { billingPortalRoute, billingReactivateRoute, billingStateRoute, + myUsageRoute, } from "./billing-routes"; import type { GatewayApp } from "./gateway-env"; @@ -18,4 +19,5 @@ export function registerBillingHttpRoutes(app: GatewayApp): void { app.get("/v1/billing/state", (c) => billingStateRoute(c, billingDependencies)); app.post("/v1/billing/cancel", (c) => billingCancelRoute(c, billingDependencies)); app.post("/v1/billing/reactivate", (c) => billingReactivateRoute(c, billingDependencies)); + app.get("/v1/me/usage", (c) => myUsageRoute(c, billingDependencies)); } diff --git a/apps/gateway-worker/src/billing-routes.ts b/apps/gateway-worker/src/billing-routes.ts index 0d517eff..5ad14e89 100644 --- a/apps/gateway-worker/src/billing-routes.ts +++ b/apps/gateway-worker/src/billing-routes.ts @@ -15,7 +15,7 @@ import { updateUserPolarCustomerId, withUserDb, } from "@cheatcode/db"; -import type { WorkerSecret } from "@cheatcode/env"; +import { PRODUCTION_APP_ORIGIN, type WorkerSecret } from "@cheatcode/env"; import { APIError, readJsonRequest } from "@cheatcode/observability"; import { BILLING_TIERS, @@ -32,13 +32,11 @@ import { SandboxUsageSummaryResponseSchema, type UserId, } from "@cheatcode/types"; -import type { Context } from "hono"; import { resolveCorsOrigin } from "./cors"; -import type { GatewayEnv } from "./gateway-env"; +import { type GatewayContext, type GatewayEnv, requestDatabase } from "./gateway-env"; import { resolveEntitlement } from "./limits"; import { rateLimit } from "./rate-limit"; import { buildSandboxUsageSummary } from "./usage-summary"; -import type { WaitUntilContext } from "./wait-until-context"; const POLAR_PRODUCT_ID_ENV = { premium: "POLAR_PRODUCT_ID_PREMIUM", @@ -46,13 +44,12 @@ const POLAR_PRODUCT_ID_ENV = { } as const satisfies Record; const BILLING_REQUEST_MAX_BYTES = 8 * 1024; -const PRODUCTION_WEB_ORIGIN = "https://trycheatcode.com"; const LOCAL_WEB_ORIGIN = "http://localhost:3001"; -type BillingContext = Context<{ Bindings: GatewayEnv }>; +type BillingContext = GatewayContext; export interface BillingRouteDeps { - authenticate: (request: Request, env: GatewayEnv, ctx: WaitUntilContext) => Promise; + authenticate: (c: GatewayContext) => Promise; readRequiredSecret: (secret: WorkerSecret | undefined, name: string) => Promise; } @@ -60,9 +57,9 @@ export async function billingStateRoute( c: BillingContext, deps: BillingRouteDeps, ): Promise { - const userId = await deps.authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/billing/state"); - return withUserDb(c.env, userId, async ({ transaction }) => { + const userId = await deps.authenticate(c); + await rateLimit(c, userId); + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { const entitlement = await transaction((tx) => findEntitlementByUserId(tx, userId)); return c.json(BillingStateResponseSchema.parse(billingStateFromEntitlement(entitlement))); }); @@ -72,8 +69,8 @@ export async function billingCheckoutRoute( c: BillingContext, deps: BillingRouteDeps, ): Promise { - const userId = await deps.authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "POST /v1/billing/checkout"); + const userId = await deps.authenticate(c); + await rateLimit(c, userId); const parsedInput = BillingCheckoutSchema.safeParse( await readJsonRequest(c.req.raw, BILLING_REQUEST_MAX_BYTES, "Billing payload"), ); @@ -85,7 +82,7 @@ export async function billingCheckoutRoute( } const accessToken = await deps.readRequiredSecret(c.env.POLAR_ACCESS_TOKEN, "POLAR_ACCESS_TOKEN"); const productId = polarProductIdForTier(c.env, parsedInput.data.tier); - return withUserDb(c.env, userId, async ({ transaction }) => { + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { const user = await requireBillingUser(transaction, userId); const redirect = checkoutRedirectUrls(c, parsedInput.data.returnPath); const url = await createCheckoutUrl({ @@ -113,7 +110,7 @@ function checkoutRedirectUrls( function billingWebOrigin(c: BillingContext): string { if (c.env.CHEATCODE_ENVIRONMENT === "production") { - return PRODUCTION_WEB_ORIGIN; + return PRODUCTION_APP_ORIGIN; } const requestOrigin = c.req.header("Origin"); return resolveCorsOrigin(requestOrigin, "development") ?? LOCAL_WEB_ORIGIN; @@ -123,18 +120,18 @@ export async function billingCatalogRoute( c: BillingContext, deps: BillingRouteDeps, ): Promise { - const userId = await deps.authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/billing/catalog"); - return withUserDb(c.env, userId, async ({ transaction }) => { + const userId = await deps.authenticate(c); + await rateLimit(c, userId); + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { const entitlement = await resolveEntitlement(c.env, transaction, userId); return c.json(BillingCatalogResponseSchema.parse(buildBillingCatalog(c.env, entitlement.tier))); }); } export async function myUsageRoute(c: BillingContext, deps: BillingRouteDeps): Promise { - const userId = await deps.authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/me/usage"); - return withUserDb(c.env, userId, async ({ transaction }) => { + const userId = await deps.authenticate(c); + await rateLimit(c, userId); + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { const summary = await buildSandboxUsageSummary(c.env, transaction, userId); return c.json(SandboxUsageSummaryResponseSchema.parse(summary)); }); @@ -144,10 +141,10 @@ export async function billingPortalRoute( c: BillingContext, deps: BillingRouteDeps, ): Promise { - const userId = await deps.authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "POST /v1/billing/portal"); + const userId = await deps.authenticate(c); + await rateLimit(c, userId); const accessToken = await deps.readRequiredSecret(c.env.POLAR_ACCESS_TOKEN, "POLAR_ACCESS_TOKEN"); - return withUserDb(c.env, userId, async ({ transaction }) => { + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { const user = await requireBillingUser(transaction, userId); const customerId = user.polarCustomerId ?? @@ -182,8 +179,8 @@ export async function billingCancelRoute( c: BillingContext, deps: BillingRouteDeps, ): Promise { - const userId = await deps.authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "POST /v1/billing/cancel"); + const userId = await deps.authenticate(c); + await rateLimit(c, userId); const parsedInput = BillingCancelSchema.safeParse( await readJsonRequest(c.req.raw, BILLING_REQUEST_MAX_BYTES, "Billing payload"), ); @@ -194,7 +191,7 @@ export async function billingCancelRoute( }); } const accessToken = await deps.readRequiredSecret(c.env.POLAR_ACCESS_TOKEN, "POLAR_ACCESS_TOKEN"); - return withUserDb(c.env, userId, async ({ transaction }) => { + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { const entitlement = await loadSubscriptionEntitlement(transaction, userId); const result = await cancelSubscriptionAtPeriodEnd({ accessToken, @@ -214,10 +211,10 @@ export async function billingReactivateRoute( c: BillingContext, deps: BillingRouteDeps, ): Promise { - const userId = await deps.authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "POST /v1/billing/reactivate"); + const userId = await deps.authenticate(c); + await rateLimit(c, userId); const accessToken = await deps.readRequiredSecret(c.env.POLAR_ACCESS_TOKEN, "POLAR_ACCESS_TOKEN"); - return withUserDb(c.env, userId, async ({ transaction }) => { + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { const entitlement = await loadSubscriptionEntitlement(transaction, userId); const result = await reactivateSubscription({ accessToken, diff --git a/apps/gateway-worker/src/core-http-routes.ts b/apps/gateway-worker/src/core-http-routes.ts index a8fa520a..03673a22 100644 --- a/apps/gateway-worker/src/core-http-routes.ts +++ b/apps/gateway-worker/src/core-http-routes.ts @@ -3,12 +3,11 @@ import type { UserId } from "@cheatcode/types"; import { AGENT_FORWARD_ROUTES } from "@cheatcode/types/internal"; import { forwardAgentRequest, forwardPublicAgentRequest } from "./agent-forwarding"; import { authenticate } from "./authenticate"; -import type { GatewayApp, GatewayEnv } from "./gateway-env"; +import { type GatewayApp, type GatewayContext, requestDatabase } from "./gateway-env"; import { rateLimit, rateLimitPublic, withRateLimitHeaders } from "./rate-limit"; import { readDownstreamReleaseHealth } from "./release-health"; import { listUserSkillsRoute } from "./skills-routes"; import { clientErrorRoute, clientUserEventRoute, vitalsRoute } from "./telemetry-routes"; -import type { WaitUntilContext } from "./wait-until-context"; export function registerCoreHttpRoutes(app: GatewayApp): void { registerHealthRoute(app); @@ -18,8 +17,16 @@ export function registerCoreHttpRoutes(app: GatewayApp): void { } function registerHealthRoute(app: GatewayApp): void { - app.get("/health", async (c) => { - const headers = await rateLimitPublic(c, "GET /health", "publicRead"); + app.get("/health/live", (c) => + c.json({ + ok: true, + releaseSha: c.env.CHEATCODE_RELEASE_SHA ?? "development", + versionId: c.env.CF_VERSION_METADATA?.id ?? null, + worker: "gateway", + }), + ); + app.get("/health/release", async (c) => { + const headers = await rateLimitPublic(c, "publicRead"); const releaseSha = c.env.CHEATCODE_RELEASE_SHA ?? "development"; const [{ health: agent }, { health: webhooks }] = await Promise.all([ readDownstreamReleaseHealth(c.env, "agent"), @@ -50,16 +57,16 @@ function registerHealthRoute(app: GatewayApp): void { function registerTelemetryRoutes(app: GatewayApp): void { app.post("/v1/client-error", async (c) => { - await rateLimitPublic(c, "POST /v1/client-error", "publicWrite"); + await rateLimitPublic(c, "publicWrite"); return clientErrorRoute(c, optionalTelemetryUser); }); app.post("/v1/vitals", async (c) => { - await rateLimitPublic(c, "POST /v1/vitals", "publicWrite"); + await rateLimitPublic(c, "publicWrite"); return vitalsRoute(c); }); app.post("/v1/user-events", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "POST /v1/user-events"); + const userId = await authenticate(c); + await rateLimit(c, userId); return clientUserEventRoute(c, async () => userId); }); } @@ -76,24 +83,20 @@ function registerOutputRoute(app: GatewayApp): void { function registerSkillRoutes(app: GatewayApp): void { app.get("/v1/skills", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/skills"); - return listUserSkillsRoute(c.env, c.executionCtx, userId); + const userId = await authenticate(c); + await rateLimit(c, userId); + return listUserSkillsRoute(requestDatabase(c), userId); }); const route = AGENT_FORWARD_ROUTES.core.deleteUserSkill; app.on(route.method, route.path, (c) => forwardAgentRequest(c, route)); } -async function optionalTelemetryUser( - request: Request, - env: GatewayEnv, - ctx: WaitUntilContext, -): Promise { - if (!request.headers.has("Authorization")) { +async function optionalTelemetryUser(c: GatewayContext): Promise { + if (!c.req.raw.headers.has("Authorization")) { return "anonymous"; } try { - return await authenticate(request, env, ctx); + return await authenticate(c); } catch { return "anonymous"; } diff --git a/apps/gateway-worker/src/cors.ts b/apps/gateway-worker/src/cors.ts index faed3c3a..749edfa0 100644 --- a/apps/gateway-worker/src/cors.ts +++ b/apps/gateway-worker/src/cors.ts @@ -1,5 +1,7 @@ +import { PRODUCTION_APP_ORIGIN } from "@cheatcode/env"; + // Supported production web origins. -const PRODUCTION_ORIGINS = new Set(["https://trycheatcode.com"]); +const PRODUCTION_ORIGINS = new Set([PRODUCTION_APP_ORIGIN]); const LOCAL_ORIGIN = /^http:\/\/(localhost|127\.0\.0\.1|\[::1\]):\d+$/; export function resolveCorsOrigin( diff --git a/apps/gateway-worker/src/gateway-env.ts b/apps/gateway-worker/src/gateway-env.ts index 05c421f6..4a66e006 100644 --- a/apps/gateway-worker/src/gateway-env.ts +++ b/apps/gateway-worker/src/gateway-env.ts @@ -1,3 +1,4 @@ +import type { DatabaseHandle } from "@cheatcode/db"; import type { CloudflareVersionMetadata, WorkerSecret } from "@cheatcode/env"; import type { AnalyticsBindings } from "@cheatcode/observability"; import type { ResourceDeletionServiceBinding } from "@cheatcode/types/internal"; @@ -13,7 +14,6 @@ export interface GatewayEnv extends AnalyticsBindings, IdempotencyBindings { CHEATCODE_ENVIRONMENT: "development" | "production"; CHEATCODE_RELEASE_SHA?: string; CLERK_AUTHORIZED_PARTIES?: string; - CLERK_JWT_KEY?: WorkerSecret; CLERK_SECRET_KEY?: WorkerSecret; COMPOSIO_API_KEY?: WorkerSecret; COMPOSIO_AUTH_CONFIGS?: WorkerSecret; @@ -32,5 +32,14 @@ export interface GatewayEnv extends AnalyticsBindings, IdempotencyBindings { WEBHOOKS: Fetcher; } -export type GatewayApp = Hono<{ Bindings: GatewayEnv }>; -export type GatewayContext = Context<{ Bindings: GatewayEnv }>; +interface GatewayVariables { + database: () => DatabaseHandle; +} + +export type GatewayHonoEnv = { Bindings: GatewayEnv; Variables: GatewayVariables }; +export type GatewayApp = Hono; +export type GatewayContext = Context; + +export function requestDatabase(c: GatewayContext): DatabaseHandle { + return c.get("database")(); +} diff --git a/apps/gateway-worker/src/greeting-http-routes.ts b/apps/gateway-worker/src/greeting-http-routes.ts new file mode 100644 index 00000000..ace511b9 --- /dev/null +++ b/apps/gateway-worker/src/greeting-http-routes.ts @@ -0,0 +1,12 @@ +import { authenticate } from "./authenticate"; +import { type GatewayApp, requestDatabase } from "./gateway-env"; +import { greetingRoute } from "./greeting-routes"; +import { rateLimit } from "./rate-limit"; + +export function registerGreetingHttpRoutes(app: GatewayApp): void { + app.get("/v1/greeting", async (c) => { + const userId = await authenticate(c); + await rateLimit(c, userId); + return greetingRoute(requestDatabase(c), c.executionCtx, c.req.raw, userId); + }); +} diff --git a/apps/gateway-worker/src/greeting-routes.ts b/apps/gateway-worker/src/greeting-routes.ts index 169b2de8..d1e49641 100644 --- a/apps/gateway-worker/src/greeting-routes.ts +++ b/apps/gateway-worker/src/greeting-routes.ts @@ -1,5 +1,4 @@ -import { sumWorkedMinutesToday, withUserDb } from "@cheatcode/db"; -import type { WorkerSecret } from "@cheatcode/env"; +import { type DatabaseHandle, sumWorkedMinutesToday, withUserDb } from "@cheatcode/db"; import { createLogger, readBoundedResponseJson, @@ -10,11 +9,6 @@ import { type GreetingResponse, GreetingResponseSchema } from "@cheatcode/types/ import { z } from "zod"; import type { WaitUntilContext } from "./wait-until-context"; -export interface GreetingRouteEnv { - DATABASE_CONTEXT_SIGNING_SECRET_GATEWAY: WorkerSecret; - HYPERDRIVE: Hyperdrive; -} - const OPEN_METEO_FORECAST_URL = "https://api.open-meteo.com/v1/forecast"; const WEATHER_FETCH_TIMEOUT_MS = 1_500; const WEATHER_CACHE_MAX_AGE_SECONDS = 900; @@ -57,14 +51,14 @@ interface ResolvedGeo { * falls back to a time-only greeting. Never returns a server clock. */ export async function greetingRoute( - env: GreetingRouteEnv, + database: DatabaseHandle, ctx: WaitUntilContext, request: Request, userId: UserId, ): Promise { const geo = resolveGeo(request); const weather = await resolveWeather(ctx, geo); - const workedMinutesToday = await resolveWorkedMinutesToday(env, ctx, userId, geo.timezone); + const workedMinutesToday = await resolveWorkedMinutesToday(database, userId, geo.timezone); const response: GreetingResponse = { city: geo.city, timezone: geo.timezone, @@ -80,12 +74,11 @@ export async function greetingRoute( * failing the greeting — mirrors the weather path's resilience. */ async function resolveWorkedMinutesToday( - env: GreetingRouteEnv, - _ctx: WaitUntilContext, + database: DatabaseHandle, userId: UserId, timezone: string | null, ): Promise { - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { try { return await transaction((tx) => sumWorkedMinutesToday(tx, userId, timezone ?? "UTC")); } catch (error) { diff --git a/apps/gateway-worker/src/index.ts b/apps/gateway-worker/src/index.ts index 2de69d2d..c0f9a022 100644 --- a/apps/gateway-worker/src/index.ts +++ b/apps/gateway-worker/src/index.ts @@ -1,4 +1,5 @@ -import { GatewayWorkerEnvSchema } from "@cheatcode/env"; +import { createDatabaseHandle, type DatabaseHandle } from "@cheatcode/db"; +import { GatewayWorkerEnvSchema, PRODUCTION_APP_ORIGIN } from "@cheatcode/env"; import { APIError, createPerformanceMetricMiddleware, @@ -11,7 +12,7 @@ import { Hono } from "hono"; import { cors } from "hono/cors"; import { routePath } from "hono/route"; import { secureHeaders } from "hono/secure-headers"; -import { registerAccountHttpRoutes } from "./account-http-routes"; +import { registerActivityHttpRoutes } from "./activity-http-routes"; import { registerAgentHttpRoutes } from "./agent-http-routes"; import { registerBillingHttpRoutes } from "./billing-http-routes"; import { registerCoreHttpRoutes } from "./core-http-routes"; @@ -20,12 +21,15 @@ import { IdempotencyStore } from "./durable-objects/idempotency"; import { QuotaTracker } from "./durable-objects/quota-tracker"; import { RateLimiter } from "./durable-objects/rate-limiter"; import { formatGatewayRouteError } from "./error-handling"; -import type { GatewayContext, GatewayEnv } from "./gateway-env"; +import type { GatewayContext, GatewayEnv, GatewayHonoEnv } from "./gateway-env"; +import { registerGreetingHttpRoutes } from "./greeting-http-routes"; import { registerIntegrationHttpRoutes } from "./integration-http-routes"; import { resolveLocalPreviewRoute } from "./local-preview-routing"; +import { registerProfileHttpRoutes } from "./profile-http-routes"; import { registerProjectHttpRoutes } from "./project-http-routes"; import { registerProviderHttpRoutes } from "./provider-http-routes"; import { withRateLimitErrorHeaders } from "./rate-limit"; +import { registerSearchHttpRoutes } from "./search-http-routes"; export { IdempotencyStore, QuotaTracker, RateLimiter }; @@ -44,7 +48,7 @@ const GATEWAY_SECURITY_HEADERS = { connectSrc: [ "'self'", "https://gateway.trycheatcode.com", - "https://trycheatcode.com", + PRODUCTION_APP_ORIGIN, "http://localhost:3001", "http://localhost:8787", "ws://localhost:8787", @@ -66,7 +70,7 @@ const GATEWAY_SECURITY_HEADERS = { xFrameOptions: "DENY", }; -const gatewayApp = new Hono<{ Bindings: GatewayEnv }>(); +const gatewayApp = new Hono(); // Answer route errors in-band: cors() stages its headers on the context before // the route runs and secureHeaders() applies after it, so the error response @@ -85,6 +89,18 @@ gatewayApp.onError((error, context) => { }); gatewayApp.use("*", secureHeaders(GATEWAY_SECURITY_HEADERS)); +gatewayApp.use("/v1/*", async (c, next) => { + let handle: DatabaseHandle | undefined; + c.set("database", () => { + handle ??= createDatabaseHandle(c.env); + return handle; + }); + try { + await next(); + } finally { + await handle?.close(); + } +}); gatewayApp.use( "/v1/*", cors({ @@ -108,7 +124,10 @@ gatewayApp.use( ); registerCoreHttpRoutes(gatewayApp); -registerAccountHttpRoutes(gatewayApp); +registerActivityHttpRoutes(gatewayApp); +registerProfileHttpRoutes(gatewayApp); +registerSearchHttpRoutes(gatewayApp); +registerGreetingHttpRoutes(gatewayApp); registerProjectHttpRoutes(gatewayApp); registerProviderHttpRoutes(gatewayApp); registerIntegrationHttpRoutes(gatewayApp); diff --git a/apps/gateway-worker/src/integration-http-routes.ts b/apps/gateway-worker/src/integration-http-routes.ts index 3cb6c42d..6f986b21 100644 --- a/apps/gateway-worker/src/integration-http-routes.ts +++ b/apps/gateway-worker/src/integration-http-routes.ts @@ -1,6 +1,6 @@ import { withUserDb } from "@cheatcode/db"; import { authenticate } from "./authenticate"; -import type { GatewayApp } from "./gateway-env"; +import { type GatewayApp, requestDatabase } from "./gateway-env"; import { connectIntegration, deleteIntegrationAccount, @@ -14,31 +14,31 @@ import { rateLimit } from "./rate-limit"; export function registerIntegrationHttpRoutes(app: GatewayApp): void { app.get("/v1/integrations", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/integrations"); - return withUserDb(c.env, userId, async ({ transaction }) => { + const userId = await authenticate(c); + await rateLimit(c, userId); + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { const integrations = await listIntegrationSummaries(transaction, c.env, userId); return c.json(integrations); }); }); app.get("/v1/integrations/catalog", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/integrations/catalog"); - return withUserDb(c.env, userId, async ({ transaction }) => { + const userId = await authenticate(c); + await rateLimit(c, userId); + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { const catalog = await getIntegrationCatalog(transaction, c.env, userId); return c.json(catalog); }); }); app.get("/v1/integrations/:name/tools", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/integrations/:name/tools"); + const userId = await authenticate(c); + await rateLimit(c, userId); return c.json(await listToolkitActions(c.env, parseIntegrationName(c.req.param("name")))); }); app.post("/v1/integrations/:name/connect", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "POST /v1/integrations/:name/connect"); + const userId = await authenticate(c); + await rateLimit(c, userId); const integration = parseIntegrationName(c.req.param("name")); - return withUserDb(c.env, userId, ({ transaction }) => + return withUserDb(requestDatabase(c), userId, ({ transaction }) => connectIntegration({ env: c.env, integration, request: c.req.raw, transaction, userId }), ); }); @@ -47,10 +47,10 @@ export function registerIntegrationHttpRoutes(app: GatewayApp): void { function registerIntegrationAccountRoutes(app: GatewayApp): void { app.post("/v1/integrations/:name/accounts/:connectionId/default", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "POST /v1/integrations/:name/accounts/:connectionId/default"); + const userId = await authenticate(c); + await rateLimit(c, userId); const integration = parseIntegrationName(c.req.param("name")); - return withUserDb(c.env, userId, async ({ transaction }) => { + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { await makeIntegrationAccountDefault({ composioConnectionId: parseComposioConnectionId(c.req.param("connectionId")), integration, @@ -61,10 +61,10 @@ function registerIntegrationAccountRoutes(app: GatewayApp): void { }); }); app.delete("/v1/integrations/:name/accounts/:connectionId", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "DELETE /v1/integrations/:name/accounts/:connectionId"); + const userId = await authenticate(c); + await rateLimit(c, userId); const integration = parseIntegrationName(c.req.param("name")); - return withUserDb(c.env, userId, async ({ transaction }) => { + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { await deleteIntegrationAccount({ composioConnectionId: parseComposioConnectionId(c.req.param("connectionId")), env: c.env, diff --git a/apps/gateway-worker/src/integrations.ts b/apps/gateway-worker/src/integrations.ts index 9d47974e..85a4900a 100644 --- a/apps/gateway-worker/src/integrations.ts +++ b/apps/gateway-worker/src/integrations.ts @@ -11,7 +11,7 @@ import { upsertUserIntegration, upsertUserIntegrations, } from "@cheatcode/db"; -import { resolveWorkerSecret, type WorkerSecret } from "@cheatcode/env"; +import { PRODUCTION_APP_ORIGIN, resolveWorkerSecret, type WorkerSecret } from "@cheatcode/env"; import { APIError, createLogger, safeErrorTelemetry } from "@cheatcode/observability"; import { type IntegrationName, IntegrationNameSchema, type UserId } from "@cheatcode/types"; import { @@ -569,7 +569,7 @@ function resolveCallbackUrl( const origin = trustedAppOrigin(request.headers.get("Origin"), environment) ?? trustedAppOrigin(request.headers.get("Referer"), environment) ?? - "https://trycheatcode.com"; + PRODUCTION_APP_ORIGIN; const callback = new URL("/skills", origin); callback.searchParams.set("toolkit", integration); return callback.toString(); diff --git a/apps/gateway-worker/src/profile-http-routes.ts b/apps/gateway-worker/src/profile-http-routes.ts new file mode 100644 index 00000000..f45a6155 --- /dev/null +++ b/apps/gateway-worker/src/profile-http-routes.ts @@ -0,0 +1,17 @@ +import { authenticate } from "./authenticate"; +import { type GatewayApp, requestDatabase } from "./gateway-env"; +import { getMyProfileRoute, updateMyProfileRoute } from "./profile-routes"; +import { rateLimit } from "./rate-limit"; + +export function registerProfileHttpRoutes(app: GatewayApp): void { + app.get("/v1/me/profile", async (c) => { + const userId = await authenticate(c); + await rateLimit(c, userId); + return getMyProfileRoute(requestDatabase(c), c.executionCtx, userId); + }); + app.patch("/v1/me/profile", async (c) => { + const userId = await authenticate(c); + await rateLimit(c, userId); + return updateMyProfileRoute(requestDatabase(c), c.env, c.executionCtx, c.req.raw, userId); + }); +} diff --git a/apps/gateway-worker/src/profile-routes.ts b/apps/gateway-worker/src/profile-routes.ts index 7c50079e..2adcc997 100644 --- a/apps/gateway-worker/src/profile-routes.ts +++ b/apps/gateway-worker/src/profile-routes.ts @@ -1,5 +1,6 @@ import { updateClerkUserPublicMetadata, verifyClerkBearerToken } from "@cheatcode/auth"; import { + type DatabaseHandle, getUserProfile, type UpsertUserProfileInput, type UserProfileRecord, @@ -34,17 +35,18 @@ export interface ProfileRouteEnv { const MAX_PROFILE_REQUEST_BYTES = 32 * 1024; export async function getMyProfileRoute( - env: ProfileRouteEnv, + database: DatabaseHandle, _ctx: WaitUntilContext, userId: UserId, ): Promise { - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const record = await transaction((tx) => getUserProfile(tx, userId)); return Response.json(UserProfileSchema.parse(profileResponse(record))); }); } export async function updateMyProfileRoute( + database: DatabaseHandle, env: ProfileRouteEnv, _ctx: WaitUntilContext, request: Request, @@ -57,7 +59,7 @@ export async function updateMyProfileRoute( throw invalidRequestBody("Invalid profile payload", parsed.error); } const body = parsed.data; - const result = await withUserDb(env, userId, ({ transaction }) => + const result = await withUserDb(database, userId, ({ transaction }) => transaction((tx) => upsertUserProfile(tx, buildProfilePatch(userId, body))), ); if (body.onboardingCompleted === true) { diff --git a/apps/gateway-worker/src/project-http-routes.ts b/apps/gateway-worker/src/project-http-routes.ts index e8daab58..8f376d2a 100644 --- a/apps/gateway-worker/src/project-http-routes.ts +++ b/apps/gateway-worker/src/project-http-routes.ts @@ -1,7 +1,7 @@ import { AGENT_FORWARD_ROUTES } from "@cheatcode/types/internal"; import { forwardAgentRequest } from "./agent-forwarding"; import { authenticate } from "./authenticate"; -import type { GatewayApp } from "./gateway-env"; +import { type GatewayApp, requestDatabase } from "./gateway-env"; import { createChatRoute, createProjectRoute, @@ -28,33 +28,33 @@ export function registerProjectHttpRoutes(app: GatewayApp): void { function registerProjectCollectionRoutes(app: GatewayApp): void { app.get("/v1/projects", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/projects"); - return listProjectsRoute(c.env, c.executionCtx, c.req.raw, userId); + const userId = await authenticate(c); + await rateLimit(c, userId); + return listProjectsRoute(requestDatabase(c), c.executionCtx, c.req.raw, userId); }); app.post("/v1/projects", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "POST /v1/projects"); - return createProjectRoute(c.env, c.executionCtx, c.req.raw, userId); + const userId = await authenticate(c); + await rateLimit(c, userId); + return createProjectRoute(requestDatabase(c), c.executionCtx, c.req.raw, userId); }); } function registerProjectItemRoutes(app: GatewayApp): void { app.get("/v1/projects/:projectId", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/projects/:projectId"); + const userId = await authenticate(c); + await rateLimit(c, userId); return getProjectRoute( - c.env, + requestDatabase(c), c.executionCtx, parseProjectParam(c.req.param("projectId")), userId, ); }); app.patch("/v1/projects/:projectId", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "PATCH /v1/projects/:projectId"); + const userId = await authenticate(c); + await rateLimit(c, userId); return updateProjectRoute( - c.env, + requestDatabase(c), c.executionCtx, c.req.raw, parseProjectParam(c.req.param("projectId")), @@ -62,10 +62,11 @@ function registerProjectItemRoutes(app: GatewayApp): void { ); }); app.delete("/v1/projects/:projectId", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "DELETE /v1/projects/:projectId"); + const userId = await authenticate(c); + await rateLimit(c, userId); return deleteProjectRoute( c.env, + requestDatabase(c), c.executionCtx, parseProjectParam(c.req.param("projectId")), userId, @@ -78,10 +79,10 @@ function registerProjectRelatedRoutes(app: GatewayApp): void { app.on(route.method, route.path, (c) => forwardAgentRequest(c, route)); } app.get("/v1/projects/:projectId/threads", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/projects/:projectId/threads"); + const userId = await authenticate(c); + await rateLimit(c, userId); return listProjectThreadsRoute( - c.env, + requestDatabase(c), c.executionCtx, c.req.raw, parseProjectParam(c.req.param("projectId")), @@ -92,20 +93,25 @@ function registerProjectRelatedRoutes(app: GatewayApp): void { function registerThreadRoutes(app: GatewayApp): void { app.post("/v1/threads", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "POST /v1/threads"); - return createChatRoute(c.env, c.executionCtx, c.req.raw, userId); + const userId = await authenticate(c); + await rateLimit(c, userId); + return createChatRoute(requestDatabase(c), c.executionCtx, c.req.raw, userId); }); app.get("/v1/threads/:threadId", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/threads/:threadId"); - return getThreadRoute(c.env, c.executionCtx, parseThreadParam(c.req.param("threadId")), userId); + const userId = await authenticate(c); + await rateLimit(c, userId); + return getThreadRoute( + requestDatabase(c), + c.executionCtx, + parseThreadParam(c.req.param("threadId")), + userId, + ); }); app.patch("/v1/threads/:threadId", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "PATCH /v1/threads/:threadId"); + const userId = await authenticate(c); + await rateLimit(c, userId); return updateThreadRoute( - c.env, + requestDatabase(c), c.executionCtx, c.req.raw, parseThreadParam(c.req.param("threadId")), @@ -113,20 +119,21 @@ function registerThreadRoutes(app: GatewayApp): void { ); }); app.delete("/v1/threads/:threadId", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "DELETE /v1/threads/:threadId"); + const userId = await authenticate(c); + await rateLimit(c, userId); return deleteThreadRoute( c.env, + requestDatabase(c), c.executionCtx, parseThreadParam(c.req.param("threadId")), userId, ); }); app.get("/v1/threads/:threadId/messages", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/threads/:threadId/messages"); + const userId = await authenticate(c); + await rateLimit(c, userId); return listThreadMessagesRoute( - c.env, + requestDatabase(c), c.executionCtx, c.req.raw, parseThreadParam(c.req.param("threadId")), diff --git a/apps/gateway-worker/src/project-routes.ts b/apps/gateway-worker/src/project-routes.ts index 3f33cb57..7092beec 100644 --- a/apps/gateway-worker/src/project-routes.ts +++ b/apps/gateway-worker/src/project-routes.ts @@ -3,6 +3,7 @@ import { beginThreadDeletion, createProject, createThread, + type DatabaseHandle, getProject, getProjectWriteState, getThread, @@ -76,13 +77,13 @@ const PageCursorSchema = z.discriminatedUnion("kind", [ type PageCursorKind = "messages" | "projects" | "threads"; export async function listProjectsRoute( - env: ProjectRouteEnv, + database: DatabaseHandle, _ctx: WaitUntilContext, request: Request, userId: UserId, ): Promise { const pagination = parsePagination(request, "projects"); - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const projects = await transaction((tx) => listProjects(tx, { ...(pagination.cursor ? { cursor: pagination.cursor } : {}), @@ -101,7 +102,7 @@ export async function listProjectsRoute( } export async function createProjectRoute( - env: ProjectRouteEnv, + database: DatabaseHandle, _ctx: WaitUntilContext, request: Request, userId: UserId, @@ -112,7 +113,7 @@ export async function createProjectRoute( if (!parsedInput.success) { throw invalidRequestBody("Invalid project payload", parsedInput.error); } - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const project = await transaction(async (tx) => { await enforceActiveProjectLimit(tx, userId); return createProject(tx, { @@ -132,12 +133,12 @@ export async function createProjectRoute( } export async function getProjectRoute( - env: ProjectRouteEnv, + database: DatabaseHandle, _ctx: WaitUntilContext, projectId: ProjectIdType, userId: UserId, ): Promise { - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const project = await transaction((tx) => getProject(tx, { projectId, userId })); if (!project) { throw projectNotFound(); @@ -147,7 +148,7 @@ export async function getProjectRoute( } export async function updateProjectRoute( - env: ProjectRouteEnv, + database: DatabaseHandle, _ctx: WaitUntilContext, request: Request, projectId: ProjectIdType, @@ -159,7 +160,7 @@ export async function updateProjectRoute( if (!parsedInput.success) { throw invalidRequestBody("Invalid project update payload", parsedInput.error); } - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const input = parsedInput.data; const project = await transaction((tx) => updateWritableProject(tx, projectId, userId, { @@ -177,11 +178,12 @@ export async function updateProjectRoute( export async function deleteProjectRoute( env: ProjectRouteEnv, + database: DatabaseHandle, _ctx: WaitUntilContext, projectId: ProjectIdType, userId: UserId, ): Promise { - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const deletion = await transaction((tx) => beginProjectDeletion(tx, { projectId, userId })); if (deletion.type === "not-found") { throw projectNotFound(); @@ -204,14 +206,14 @@ export async function deleteProjectRoute( } export async function listProjectThreadsRoute( - env: ProjectRouteEnv, + database: DatabaseHandle, _ctx: WaitUntilContext, request: Request, projectId: ProjectIdType, userId: UserId, ): Promise { const pagination = parsePagination(request, "threads"); - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const threadRows = await transaction(async (tx) => { await requireProject(tx, projectId, userId); return listProjectThreads(tx, { @@ -235,7 +237,7 @@ export async function listProjectThreadsRoute( * `projectId` set it's the deliberate "add a chat to an existing project" grouping. */ export async function createChatRoute( - env: ProjectRouteEnv, + database: DatabaseHandle, _ctx: WaitUntilContext, request: Request, userId: UserId, @@ -247,7 +249,7 @@ export async function createChatRoute( throw invalidRequestBody("Invalid thread payload", parsedInput.error); } const input = parsedInput.data; - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const thread = await transaction((tx) => createThreadForRequest(tx, input, userId)); return Response.json(ThreadSchema.parse(threadResponse(thread)), { status: 201 }); }); @@ -293,12 +295,12 @@ function hasLaunchIntent(launchIntent: NonNullable { - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const thread = await transaction((tx) => getThread(tx, { threadId, userId })); if (!thread) { throw threadNotFound(); @@ -308,7 +310,7 @@ export async function getThreadRoute( } export async function updateThreadRoute( - env: ProjectRouteEnv, + database: DatabaseHandle, _ctx: WaitUntilContext, request: Request, threadId: ThreadIdType, @@ -320,7 +322,7 @@ export async function updateThreadRoute( if (!parsedInput.success) { throw invalidRequestBody("Invalid thread update payload", parsedInput.error); } - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const thread = await transaction((tx) => updateThread(tx, { threadId, title: parsedInput.data.title, userId }), ); @@ -333,11 +335,12 @@ export async function updateThreadRoute( export async function deleteThreadRoute( env: ProjectRouteEnv, + database: DatabaseHandle, _ctx: WaitUntilContext, threadId: ThreadIdType, userId: UserId, ): Promise { - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const deleted = await transaction((tx) => beginThreadDeletion(tx, { threadId, userId })); if (deleted.type === "not-found") { throw threadNotFound(); @@ -360,14 +363,14 @@ export async function deleteThreadRoute( } export async function listThreadMessagesRoute( - env: ProjectRouteEnv, + database: DatabaseHandle, _ctx: WaitUntilContext, request: Request, threadId: ThreadIdType, userId: UserId, ): Promise { const pagination = parsePagination(request, "messages"); - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const rows = await transaction(async (tx) => { await requireThread(tx, threadId, userId); return listThreadMessages(tx, { diff --git a/apps/gateway-worker/src/provider-http-routes.ts b/apps/gateway-worker/src/provider-http-routes.ts index c51cc707..b71cf364 100644 --- a/apps/gateway-worker/src/provider-http-routes.ts +++ b/apps/gateway-worker/src/provider-http-routes.ts @@ -8,16 +8,16 @@ import { lockUserProviderKeyMutations, withUserDb } from "@cheatcode/db"; import { APIError, emitUserEvent, readJsonRequest } from "@cheatcode/observability"; import { ProviderSchema, UpsertProviderKeySchema } from "@cheatcode/types/api"; import { authenticate } from "./authenticate"; -import type { GatewayApp, GatewayContext } from "./gateway-env"; +import { type GatewayApp, type GatewayContext, requestDatabase } from "./gateway-env"; import { rateLimit } from "./rate-limit"; const MAX_PROVIDER_KEY_REQUEST_BYTES = 32 * 1024; export function registerProviderHttpRoutes(app: GatewayApp): void { app.get("/v1/provider-keys", async (c) => { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "GET /v1/provider-keys"); - return withUserDb(c.env, userId, async ({ transaction }) => { + const userId = await authenticate(c); + await rateLimit(c, userId); + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { return c.json(await transaction((tx) => listProviderKeys(tx))); }); }); @@ -26,8 +26,8 @@ export function registerProviderHttpRoutes(app: GatewayApp): void { } async function upsertProviderKey(c: GatewayContext): Promise { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "POST /v1/provider-keys"); + const userId = await authenticate(c); + await rateLimit(c, userId); const parsedInput = UpsertProviderKeySchema.safeParse( await readJsonRequest(c.req.raw, MAX_PROVIDER_KEY_REQUEST_BYTES, "Provider key request"), ); @@ -39,7 +39,7 @@ async function upsertProviderKey(c: GatewayContext): Promise { } const input = parsedInput.data; await validateProviderKey(input.provider, input.key); - return withUserDb(c.env, userId, async ({ transaction }) => { + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { const result = await transaction(async (tx) => { await lockUserProviderKeyMutations(tx, userId); const existingKeys = await listProviderKeys(tx); @@ -61,8 +61,8 @@ async function upsertProviderKey(c: GatewayContext): Promise { } async function deleteProviderKeyRoute(c: GatewayContext): Promise { - const userId = await authenticate(c.req.raw, c.env, c.executionCtx); - await rateLimit(c, userId, "DELETE /v1/provider-keys/:provider"); + const userId = await authenticate(c); + await rateLimit(c, userId); const parsedProvider = ProviderSchema.safeParse(c.req.param("provider")); if (!parsedProvider.success) { throw new APIError(400, "invalid_path_param", "Invalid provider", { @@ -70,7 +70,7 @@ async function deleteProviderKeyRoute(c: GatewayContext): Promise { retriable: false, }); } - return withUserDb(c.env, userId, async ({ transaction }) => { + return withUserDb(requestDatabase(c), userId, async ({ transaction }) => { await transaction(async (tx) => { await lockUserProviderKeyMutations(tx, userId); await deleteProviderKey(tx, parsedProvider.data); diff --git a/apps/gateway-worker/src/rate-limit.ts b/apps/gateway-worker/src/rate-limit.ts index e5f40f47..7810fa07 100644 --- a/apps/gateway-worker/src/rate-limit.ts +++ b/apps/gateway-worker/src/rate-limit.ts @@ -1,5 +1,7 @@ import { APIError, createLogger, safeErrorTelemetry } from "@cheatcode/observability"; import type { UserId } from "@cheatcode/types"; +import type { Context } from "hono"; +import { routePath } from "hono/route"; import type { RateLimitResult } from "./durable-objects/rate-limit-contract"; import type { RateLimiter } from "./durable-objects/rate-limiter"; @@ -57,8 +59,25 @@ const RATE_LIMIT_POLICIES = { export async function rateLimit( c: RateLimitContext, userId: UserId, +): Promise { + const route = nativeRoutePath(c); + const policy = policyForRoute(route); + return consumeRateLimit( + c, + { + durableObjectName: `ratelimit:${userId.slice(0, 8)}`, + key: `user:${userId}:${route}`, + }, + route, + policy, + ); +} + +export async function rateLimitForwarded( + c: RateLimitContext, + userId: UserId, route: string, - cost?: number, + cost: number, ): Promise { const policy = policyForRoute(route); return consumeRateLimit( @@ -73,10 +92,30 @@ export async function rateLimit( } export async function rateLimitPublic( + c: PublicRateLimitContext, + policyName: PublicRateLimitPolicyName, +): Promise { + const route = nativeRoutePath(c); + const addressHash = await publicClientAddressHash(c.req.raw); + return consumeRateLimit( + c, + { + durableObjectName: `ratelimit:public:${addressHash.slice( + 0, + PUBLIC_RATE_LIMIT_SHARD_PREFIX_LENGTH, + )}`, + key: `public:${addressHash}:${route}`, + }, + route, + RATE_LIMIT_POLICIES[policyName], + ); +} + +export async function rateLimitPublicForwarded( c: PublicRateLimitContext, route: string, policyName: PublicRateLimitPolicyName, - cost?: number, + cost: number, ): Promise { const addressHash = await publicClientAddressHash(c.req.raw); return consumeRateLimit( @@ -93,6 +132,11 @@ export async function rateLimitPublic( ); } +function nativeRoutePath(c: RateLimitContext): string { + const context = c as Context; + return `${context.req.method} ${routePath(context, -1)}`; +} + async function consumeRateLimit( c: RateLimitContext, subject: RateLimitSubject, diff --git a/apps/gateway-worker/src/search-http-routes.ts b/apps/gateway-worker/src/search-http-routes.ts new file mode 100644 index 00000000..1437ca6b --- /dev/null +++ b/apps/gateway-worker/src/search-http-routes.ts @@ -0,0 +1,17 @@ +import { authenticate } from "./authenticate"; +import { type GatewayApp, requestDatabase } from "./gateway-env"; +import { rateLimit } from "./rate-limit"; +import { listRecentThreadsRoute, searchWorkspaceRoute } from "./search-routes"; + +export function registerSearchHttpRoutes(app: GatewayApp): void { + app.get("/v1/search", async (c) => { + const userId = await authenticate(c); + await rateLimit(c, userId); + return searchWorkspaceRoute(requestDatabase(c), c.req.raw, userId); + }); + app.get("/v1/threads", async (c) => { + const userId = await authenticate(c); + await rateLimit(c, userId); + return listRecentThreadsRoute(requestDatabase(c), c.req.raw, userId); + }); +} diff --git a/apps/gateway-worker/src/search-routes.ts b/apps/gateway-worker/src/search-routes.ts index a3f1ef2c..65ddba5a 100644 --- a/apps/gateway-worker/src/search-routes.ts +++ b/apps/gateway-worker/src/search-routes.ts @@ -1,11 +1,11 @@ import { + type DatabaseHandle, listRecentThreads, searchWorkspace, type WorkspaceSearchRecord, type WorkspaceThreadSearchRecord, withUserDb, } from "@cheatcode/db"; -import type { WorkerSecret } from "@cheatcode/env"; import { APIError, createLogger } from "@cheatcode/observability"; import type { UserId } from "@cheatcode/types"; import { @@ -16,22 +16,15 @@ import { type SearchResult, } from "@cheatcode/types/api"; import type { z } from "zod"; -import type { WaitUntilContext } from "./wait-until-context"; - -export interface SearchRouteEnv { - DATABASE_CONTEXT_SIGNING_SECRET_GATEWAY: WorkerSecret; - HYPERDRIVE: Hyperdrive; -} export async function searchWorkspaceRoute( - env: SearchRouteEnv, - _ctx: WaitUntilContext, + database: DatabaseHandle, request: Request, userId: UserId, ): Promise { const query = parseSearchQuery(request); const startedAt = performance.now(); - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const records = await transaction((tx) => searchWorkspace(tx, userId, { limit: query.limit, q: query.q }), ); @@ -44,13 +37,12 @@ export async function searchWorkspaceRoute( /** `GET /v1/threads?limit=N` — the user's recent chats (threads) across all projects. */ export async function listRecentThreadsRoute( - env: SearchRouteEnv, - _ctx: WaitUntilContext, + database: DatabaseHandle, request: Request, userId: UserId, ): Promise { const limit = parseRecentThreadsLimit(request); - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const records = await transaction((tx) => listRecentThreads(tx, userId, limit)); const response = RecentThreadsResponseSchema.parse({ threads: records.map(toThreadResult), diff --git a/apps/gateway-worker/src/skills-routes.ts b/apps/gateway-worker/src/skills-routes.ts index 1d5bea74..2116f36c 100644 --- a/apps/gateway-worker/src/skills-routes.ts +++ b/apps/gateway-worker/src/skills-routes.ts @@ -1,8 +1,11 @@ -import { listUserSkillSummaries, type UserSkillSummaryRecord, withUserDb } from "@cheatcode/db"; +import { + type DatabaseHandle, + listUserSkillSummaries, + type UserSkillSummaryRecord, + withUserDb, +} from "@cheatcode/db"; import type { UserId } from "@cheatcode/types"; import { MAX_USER_SKILLS, UserSkillSchema, UserSkillsResponseSchema } from "@cheatcode/types/api"; -import type { GatewayEnv } from "./gateway-env"; -import type { WaitUntilContext } from "./wait-until-context"; function skillSummary(record: UserSkillSummaryRecord): unknown { return UserSkillSchema.parse({ @@ -18,11 +21,10 @@ function skillSummary(record: UserSkillSummaryRecord): unknown { /** `GET /v1/skills` — the caller's custom skills (body-less summaries). */ export async function listUserSkillsRoute( - env: GatewayEnv, - _ctx: WaitUntilContext, + database: DatabaseHandle, userId: UserId, ): Promise { - return withUserDb(env, userId, async ({ transaction }) => { + return withUserDb(database, userId, async ({ transaction }) => { const rows = await transaction((tx) => listUserSkillSummaries(tx, userId, MAX_USER_SKILLS)); return Response.json(UserSkillsResponseSchema.parse({ skills: rows.map(skillSummary) })); }); diff --git a/apps/gateway-worker/src/telemetry-routes.ts b/apps/gateway-worker/src/telemetry-routes.ts index ac604c93..09016d4a 100644 --- a/apps/gateway-worker/src/telemetry-routes.ts +++ b/apps/gateway-worker/src/telemetry-routes.ts @@ -14,17 +14,10 @@ import { type UserId, WebVitalsBodySchema, } from "@cheatcode/types"; -import type { Context } from "hono"; import type { z } from "zod"; -import type { GatewayEnv } from "./gateway-env"; -import type { WaitUntilContext } from "./wait-until-context"; +import type { GatewayContext } from "./gateway-env"; -type GatewayContext = Context<{ Bindings: GatewayEnv }>; -type TelemetryUserResolver = ( - request: Request, - env: GatewayEnv, - ctx: WaitUntilContext, -) => Promise; +type TelemetryUserResolver = (c: GatewayContext) => Promise; const MAX_TELEMETRY_BODY_CHARS = 16 * 1024; const MAX_TELEMETRY_BODY_BYTES = 64 * 1024; @@ -38,7 +31,7 @@ export async function clientErrorRoute( if (!parsed.success) { throw invalidTelemetryPayload(parsed.error); } - const userId = await resolveTelemetryUser(c.req.raw, c.env, c.executionCtx); + const userId = await resolveTelemetryUser(c); const route = pathFromUrl(parsed.data.url) ?? "/v1/client-error"; const telemetry = safeErrorTelemetry({ name: parsed.data.type ?? "FrontendError" }); createLogger({ @@ -85,7 +78,7 @@ export async function clientUserEventRoute( if (!parsed.success) { throw invalidTelemetryPayload(parsed.error); } - const userId = await resolveTelemetryUser(c.req.raw, c.env, c.executionCtx); + const userId = await resolveTelemetryUser(c); if (userId === "anonymous") { throw new APIError(401, "permission_denied", "Authentication is required for user events", { retriable: false, diff --git a/apps/gateway-worker/wrangler.jsonc b/apps/gateway-worker/wrangler.jsonc index 5a9d2be7..3abb26a7 100644 --- a/apps/gateway-worker/wrangler.jsonc +++ b/apps/gateway-worker/wrangler.jsonc @@ -11,7 +11,6 @@ }, "vars": { "CHEATCODE_ENVIRONMENT": "production", - "CLERK_AUTHORIZED_PARTIES": "https://trycheatcode.com", "POLAR_PRODUCT_ID_PRO": "c76a8fa5-ac08-4333-9cee-aa7a8a3d959c", "POLAR_PRODUCT_ID_PREMIUM": "11e9b294-73e0-47f6-943c-314e53b63e6e" }, diff --git a/apps/preview-proxy/README.md b/apps/preview-proxy/README.md index 651065a9..d1426652 100644 --- a/apps/preview-proxy/README.md +++ b/apps/preview-proxy/README.md @@ -167,21 +167,21 @@ non-namespaced browser cookie before the upstream request, removes upstream `Domain` attributes, uses a host-only partitioned session cookie, rejects sibling preview origins, and sends `Origin-Agent-Cluster: ?1`. The gateway authenticates API calls with bearer tokens rather than ambient cookies. DNS, the wildcard -Worker route, and Worker-side `PREVIEW_HOSTNAME` must still be released together; -the web app derives the owned production apex from its deployment identity. +Worker route, and shared production-origin constant must still be released together; +the Worker and web app derive the owned production apex from that constant. ## Env / secrets | Binding | Type | Source | | --------------------- | --------------- | --------------------------------------- | -| `CHEATCODE_APP_ORIGIN` | var | exact trusted Vercel/local app origin | +| `CHEATCODE_APP_ORIGIN` | derived/local var | canonical production origin or explicit loopback origin | | `CHEATCODE_ENVIRONMENT` | var | `wrangler.jsonc` (`production`) | | `CHEATCODE_RELEASE_SHA` | release var | Cloudflare deployment workflow | | `CF_VERSION_METADATA` | version metadata | Cloudflare runtime | | `DAYTONA_API_URL` | var | `wrangler.jsonc` (`https://app.daytona.io/api`) | -| `DAYTONA_PREVIEW_HOST_SUFFIXES` | var | allowlisted Daytona preview apexes | +| `DAYTONA_PREVIEW_HOST_SUFFIXES` | optional local var | defaults to the built-in Daytona preview apexes | | `DAYTONA_API_KEY` | Secrets Store | `daytona-api-key` | -| `PREVIEW_HOSTNAME` | var | preview apex shared with agent-worker | +| `PREVIEW_HOSTNAME` | derived/local var | canonical production hostname or explicit local apex | | `PREVIEW_TOKEN_SECRET`| Secrets Store | `preview-token-secret` | Secrets bind from store `ba25994718db4707ab99a498e22eb5a6` (shared with diff --git a/apps/preview-proxy/package.json b/apps/preview-proxy/package.json index 6796a775..d7316110 100644 --- a/apps/preview-proxy/package.json +++ b/apps/preview-proxy/package.json @@ -15,8 +15,7 @@ "@cheatcode/env": "workspace:*", "@cheatcode/observability": "workspace:*", "@cheatcode/preview-bridge": "workspace:*", - "@cheatcode/types": "workspace:*", - "zod": "catalog:" + "@cheatcode/types": "workspace:*" }, "devDependencies": { "@cheatcode/tsconfig": "workspace:*", diff --git a/apps/preview-proxy/src/env.ts b/apps/preview-proxy/src/env.ts deleted file mode 100644 index 8814d145..00000000 --- a/apps/preview-proxy/src/env.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { - type CloudflareVersionMetadata, - PreviewHostnameSchema, - WorkerReleaseBindingsSchema, - type WorkerSecret, - WorkerSecretSchema, -} from "@cheatcode/env"; -import type { AnalyticsBindings } from "@cheatcode/observability"; -import { z } from "zod"; - -/** - * Trust-boundary schema for the preview-proxy worker env. Validates only the - * three values the proxy depends on; Analytics Engine bindings are optional and - * null-checked at emit time, so they are intentionally not required here. - */ -export const PreviewProxyEnvSchema = z - .object({ - ...WorkerReleaseBindingsSchema, - CHEATCODE_APP_ORIGIN: z.string().url(), - DAYTONA_API_KEY: WorkerSecretSchema, - DAYTONA_API_URL: z.string().url(), - DAYTONA_PREVIEW_HOST_SUFFIXES: z.string().min(1).max(1_024).optional(), - PREVIEW_HOSTNAME: PreviewHostnameSchema, - PREVIEW_TOKEN_SECRET: WorkerSecretSchema, - }) - .superRefine((env, context) => { - if (env.CHEATCODE_ENVIRONMENT === "production" && !env.CHEATCODE_RELEASE_SHA) { - context.addIssue({ - code: "custom", - message: "Production Workers require an immutable release SHA.", - path: ["CHEATCODE_RELEASE_SHA"], - }); - } - if ( - env.CHEATCODE_ENVIRONMENT === "production" && - env.CHEATCODE_APP_ORIGIN !== "https://trycheatcode.com" - ) { - context.addIssue({ - code: "custom", - message: "Production previews require the canonical Vercel application origin", - path: ["CHEATCODE_APP_ORIGIN"], - }); - } - if (!isExactAppOrigin(env.CHEATCODE_APP_ORIGIN, env.CHEATCODE_ENVIRONMENT)) { - context.addIssue({ - code: "custom", - message: "Preview application origin must be an exact trusted HTTP(S) origin", - path: ["CHEATCODE_APP_ORIGIN"], - }); - } - if (env.CHEATCODE_ENVIRONMENT === "production" && env.PREVIEW_HOSTNAME.includes(":")) { - context.addIssue({ - code: "custom", - message: "Production preview hostname must be a DNS hostname without a port", - path: ["PREVIEW_HOSTNAME"], - }); - } - if (env.CHEATCODE_ENVIRONMENT === "production" && env.PREVIEW_HOSTNAME !== "trycheatcode.com") { - context.addIssue({ - code: "custom", - message: "Production previews require the owned trycheatcode.com wildcard route", - path: ["PREVIEW_HOSTNAME"], - }); - } - }); - -export interface PreviewProxyEnv extends AnalyticsBindings { - CF_VERSION_METADATA?: CloudflareVersionMetadata; - CHEATCODE_APP_ORIGIN: string; - CHEATCODE_ENVIRONMENT: "development" | "production"; - CHEATCODE_RELEASE_SHA?: string; - DAYTONA_API_KEY: WorkerSecret; - DAYTONA_API_URL: string; - DAYTONA_PREVIEW_HOST_SUFFIXES?: string; - PREVIEW_HOSTNAME: string; - PREVIEW_TOKEN_SECRET: WorkerSecret; -} - -function isExactAppOrigin(value: string, environment: "development" | "production"): boolean { - try { - const url = new URL(value); - if ( - url.origin !== value || - url.pathname !== "/" || - url.search || - url.hash || - url.username || - url.password - ) { - return false; - } - if (environment === "production") { - return url.protocol === "https:"; - } - return ( - url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1") - ); - } catch { - return false; - } -} diff --git a/apps/preview-proxy/src/index.ts b/apps/preview-proxy/src/index.ts index 9393df2b..3187c1b3 100644 --- a/apps/preview-proxy/src/index.ts +++ b/apps/preview-proxy/src/index.ts @@ -1,6 +1,10 @@ -import { resolveWorkerSecret } from "@cheatcode/env"; -import { APIError, createWorkerRuntime, routeName } from "@cheatcode/observability"; -import { type PreviewProxyEnv, PreviewProxyEnvSchema } from "./env"; +import { type PreviewProxyEnv, PreviewProxyEnvSchema, resolveWorkerSecret } from "@cheatcode/env"; +import { + type AnalyticsBindings, + APIError, + createWorkerRuntime, + routeName, +} from "@cheatcode/observability"; import { type PreviewTarget, parsePreviewHost } from "./host"; import { PREVIEW_SESSION_PATH, @@ -221,7 +225,9 @@ function safeHeaderOrigin(value: string | null): string { } } -const previewProxyHandler = createWorkerRuntime({ +type PreviewProxyRuntimeEnv = PreviewProxyEnv & AnalyticsBindings; + +const previewProxyHandler = createWorkerRuntime({ errorCategory: WORKER_NAME, errorLogFields: ({ apiError, request }) => ({ httpStatus: apiError.status, @@ -229,10 +235,10 @@ const previewProxyHandler = createWorkerRuntime { - PreviewProxyEnvSchema.parse(env); + const parsedEnv = PreviewProxyEnvSchema.parse(env); return withPreviewSecurityHeaders( - await handlePreviewRequest(request, env), - env.CHEATCODE_APP_ORIGIN, + await handlePreviewRequest(request, parsedEnv), + parsedEnv.CHEATCODE_APP_ORIGIN, ); }, formatError: ({ apiError, env, requestId: id }) => diff --git a/apps/preview-proxy/src/origin.ts b/apps/preview-proxy/src/origin.ts index 2176134d..a6e409b2 100644 --- a/apps/preview-proxy/src/origin.ts +++ b/apps/preview-proxy/src/origin.ts @@ -1,10 +1,9 @@ -import { resolveWorkerSecret } from "@cheatcode/env"; +import { type PreviewProxyEnv, resolveWorkerSecret } from "@cheatcode/env"; import { APIError, readBoundedResponseText } from "@cheatcode/observability"; import { parseDaytonaPreviewHostSuffixes, parseDaytonaPreviewLink, } from "@cheatcode/types/daytona-preview"; -import type { PreviewProxyEnv } from "./env"; import type { PreviewTarget } from "./host"; /** diff --git a/apps/preview-proxy/src/proxy.ts b/apps/preview-proxy/src/proxy.ts index cbc209ae..2c9b285e 100644 --- a/apps/preview-proxy/src/proxy.ts +++ b/apps/preview-proxy/src/proxy.ts @@ -1,3 +1,4 @@ +import type { PreviewProxyEnv } from "@cheatcode/env"; import { readBoundedResponseText } from "@cheatcode/observability"; import { CODE_SERVER_PORT, @@ -5,7 +6,6 @@ import { isCodeServerWorkbenchHtml, MAX_CODE_SERVER_HTML_BYTES, } from "@cheatcode/preview-bridge"; -import type { PreviewProxyEnv } from "./env"; import type { PreviewTarget } from "./host"; import { type PreviewOrigin, diff --git a/apps/preview-proxy/wrangler.jsonc b/apps/preview-proxy/wrangler.jsonc index b492d072..d7ddbefc 100644 --- a/apps/preview-proxy/wrangler.jsonc +++ b/apps/preview-proxy/wrangler.jsonc @@ -14,11 +14,8 @@ } ], "vars": { - "CHEATCODE_APP_ORIGIN": "https://trycheatcode.com", "CHEATCODE_ENVIRONMENT": "production", - "DAYTONA_API_URL": "https://app.daytona.io/api", - "DAYTONA_PREVIEW_HOST_SUFFIXES": "daytonaproxy01.net,proxy.daytona.work", - "PREVIEW_HOSTNAME": "trycheatcode.com" + "DAYTONA_API_URL": "https://app.daytona.io/api" }, "secrets_store_secrets": [ { diff --git a/apps/web/README.md b/apps/web/README.md index e7526732..3d6a6f2d 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -26,6 +26,9 @@ does not create or wake Daytona merely because the user opens it. Framework app only. +Web-owned UI primitives, the icon barrel file, confirm dialog, and AI response +renderer live in `src/components/ui/`. + ## Code Checks ```bash diff --git a/apps/web/package.json b/apps/web/package.json index 3dca48be..2fc3e16b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -18,17 +18,20 @@ "@cheatcode/env": "workspace:*", "@cheatcode/skills": "workspace:*", "@cheatcode/types": "workspace:*", - "@cheatcode/ui": "workspace:*", "@clerk/nextjs": "catalog:", "@next/env": "catalog:", "@clerk/ui": "catalog:", "@hookform/resolvers": "catalog:", "@tanstack/react-query": "catalog:", "@tanstack/react-virtual": "catalog:", + "@streamdown/code": "catalog:", + "@streamdown/math": "catalog:", + "@streamdown/mermaid": "catalog:", "ai": "catalog:", "cmdk": "catalog:", "geist": "catalog:", "katex": "catalog:", + "lucide-react": "catalog:", "next": "catalog:", "next-themes": "catalog:", "nuqs": "catalog:", @@ -37,6 +40,7 @@ "react-dom": "catalog:", "react-hook-form": "catalog:", "sonner": "catalog:", + "streamdown": "catalog:", "web-vitals": "catalog:", "zod": "catalog:", "zustand": "catalog:" diff --git a/apps/web/src/app/(app)/error.tsx b/apps/web/src/app/(app)/error.tsx index 09d90210..2d24c35f 100644 --- a/apps/web/src/app/(app)/error.tsx +++ b/apps/web/src/app/(app)/error.tsx @@ -1,7 +1,7 @@ "use client"; -import { SquareAsterisk } from "@cheatcode/ui"; import { useEffect } from "react"; +import { SquareAsterisk } from "@/components/ui"; import { RecoveryCard } from "@/components/ui/recovery-card"; import { reportClientError } from "@/lib/error-reporter"; diff --git a/apps/web/src/app/global-error.tsx b/apps/web/src/app/global-error.tsx index e6884b72..3cc45817 100644 --- a/apps/web/src/app/global-error.tsx +++ b/apps/web/src/app/global-error.tsx @@ -1,7 +1,7 @@ "use client"; -import { SquareAsterisk } from "@cheatcode/ui"; import { useEffect } from "react"; +import { SquareAsterisk } from "@/components/ui"; import { RecoveryCard } from "@/components/ui/recovery-card"; import { reportClientError } from "@/lib/error-reporter"; diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx index 7cc7205b..357183ae 100644 --- a/apps/web/src/app/layout.tsx +++ b/apps/web/src/app/layout.tsx @@ -1,4 +1,5 @@ import { env } from "@cheatcode/env/web"; +import { PRODUCTION_APP_ORIGIN } from "@cheatcode/env/web-config"; import { ClerkProvider } from "@clerk/nextjs"; import { ui } from "@clerk/ui"; import { GeistMono } from "geist/font/mono"; @@ -13,14 +14,14 @@ import { Providers } from "./providers"; export const metadata: Metadata = { title: "Cheatcode", description: "AI agents that build, research, and ship.", - metadataBase: new URL("https://trycheatcode.com"), + metadataBase: new URL(PRODUCTION_APP_ORIGIN), openGraph: { description: "Your keys. Your models. Your sandbox.", images: [{ alt: "Cheatcode", height: 630, url: "/opengraph-image", width: 1200 }], siteName: "Cheatcode", title: "Cheatcode", type: "website", - url: "https://trycheatcode.com", + url: PRODUCTION_APP_ORIGIN, }, twitter: { card: "summary_large_image", diff --git a/apps/web/src/components/ai-elements/response.tsx b/apps/web/src/components/ai-elements/response.tsx index 5a8bd92c..50f75fae 100644 --- a/apps/web/src/components/ai-elements/response.tsx +++ b/apps/web/src/components/ai-elements/response.tsx @@ -1,3 +1,3 @@ "use client"; -export { Response } from "@cheatcode/ui"; +export { Response } from "@/components/ui"; diff --git a/apps/web/src/components/auth/auth-modal.tsx b/apps/web/src/components/auth/auth-modal.tsx index bda30fb1..6ad79eb0 100644 --- a/apps/web/src/components/auth/auth-modal.tsx +++ b/apps/web/src/components/auth/auth-modal.tsx @@ -1,8 +1,8 @@ "use client"; -import { ModalShell, X } from "@cheatcode/ui"; import { SignIn, SignUp } from "@clerk/nextjs"; import { useSyncExternalStore } from "react"; +import { ModalShell, X } from "@/components/ui"; import { clerkAuthAppearance } from "./clerk-auth-appearance"; export type AuthMode = "sign-in" | "sign-up"; diff --git a/apps/web/src/components/auth/auth-route-page.tsx b/apps/web/src/components/auth/auth-route-page.tsx index 1f2e87e9..a641773f 100644 --- a/apps/web/src/components/auth/auth-route-page.tsx +++ b/apps/web/src/components/auth/auth-route-page.tsx @@ -1,10 +1,10 @@ "use client"; -import { Monitor } from "@cheatcode/ui"; import { useAuth } from "@clerk/nextjs"; import { useRouter } from "next/navigation"; import { useSyncExternalStore } from "react"; import { AuthModal, type AuthMode } from "@/components/auth/auth-modal"; +import { Monitor } from "@/components/ui"; import { CheatcodeMark } from "@/components/ui/cheatcode-mark"; import { safeLocalRedirect } from "@/lib/navigation/safe-local-redirect"; diff --git a/apps/web/src/components/billing/manage-subscription-dialog.tsx b/apps/web/src/components/billing/manage-subscription-dialog.tsx index 4722eb02..4c689a70 100644 --- a/apps/web/src/components/billing/manage-subscription-dialog.tsx +++ b/apps/web/src/components/billing/manage-subscription-dialog.tsx @@ -6,10 +6,10 @@ import type { BillingStateResponse, BillingSubscriptionActionResponse, } from "@cheatcode/types"; -import { ChevronDown, CreditCard, Loader2, ModalShell } from "@cheatcode/ui"; import { type QueryClient, useMutation, useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; import { toast } from "sonner"; +import { ChevronDown, CreditCard, Loader2, ModalShell } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { RecoveryCard } from "@/components/ui/recovery-card"; import { diff --git a/apps/web/src/components/billing/upgrade-dialog.tsx b/apps/web/src/components/billing/upgrade-dialog.tsx index 231c4935..ffcb541c 100644 --- a/apps/web/src/components/billing/upgrade-dialog.tsx +++ b/apps/web/src/components/billing/upgrade-dialog.tsx @@ -2,9 +2,9 @@ import type { PlanSummary } from "@cheatcode/types"; import { PaidBillingTierSchema } from "@cheatcode/types"; -import { CreditCard, Loader2, ModalShell } from "@cheatcode/ui"; import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; +import { CreditCard, Loader2, ModalShell } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { RecoveryCard } from "@/components/ui/recovery-card"; import { requestCheckout } from "@/lib/api/billing"; diff --git a/apps/web/src/components/chat/chat-context-view.tsx b/apps/web/src/components/chat/chat-context-view.tsx index f61fbeb7..4365e866 100644 --- a/apps/web/src/components/chat/chat-context-view.tsx +++ b/apps/web/src/components/chat/chat-context-view.tsx @@ -1,9 +1,9 @@ "use client"; import type { ProjectSummary } from "@cheatcode/types/api"; -import { Clock3, Loader2, Plus, X } from "@cheatcode/ui"; import type { ChatContextController } from "@/components/chat/chat-context-controller"; import { FolderChatsSearch } from "@/components/chat/folder-chats-search"; +import { Clock3, Loader2, Plus, X } from "@/components/ui"; import { CheatcodeTooltip } from "@/components/ui/cheatcode-tooltip"; import type { ChatWorkspaceTab } from "@/lib/store/chat-tabs-store"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/chat/folder-chats-search.tsx b/apps/web/src/components/chat/folder-chats-search.tsx index 25e8ca89..f117b5b8 100644 --- a/apps/web/src/components/chat/folder-chats-search.tsx +++ b/apps/web/src/components/chat/folder-chats-search.tsx @@ -1,7 +1,6 @@ "use client"; import type { ProjectSummary, Thread } from "@cheatcode/types/api"; -import { Search } from "@cheatcode/ui"; import { useAuth } from "@clerk/nextjs"; import { useInfiniteQuery } from "@tanstack/react-query"; import { type ReactNode, type RefObject, useEffect, useRef, useState } from "react"; @@ -10,6 +9,7 @@ import { filterFolderThreads, folderChatResultStatus, } from "@/components/chat/chat-context-model"; +import { Search } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { type CursorPage, listProjectThreadsPage } from "@/lib/api/project-thread"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/chat/message-activity.tsx b/apps/web/src/components/chat/message-activity.tsx index ab5bef84..6e84b709 100644 --- a/apps/web/src/components/chat/message-activity.tsx +++ b/apps/web/src/components/chat/message-activity.tsx @@ -1,9 +1,9 @@ "use client"; import type { CheatcodeUIMessage } from "@cheatcode/types"; -import { ChevronDown } from "@cheatcode/ui"; import { useState } from "react"; import { Response as MarkdownResponse } from "@/components/ai-elements/response"; +import { ChevronDown } from "@/components/ui"; import { cn } from "@/lib/ui/cn"; const MAX_TOOL_STRING_LENGTH = 600; diff --git a/apps/web/src/components/chat/message-deliverables.tsx b/apps/web/src/components/chat/message-deliverables.tsx index 537859bb..78754227 100644 --- a/apps/web/src/components/chat/message-deliverables.tsx +++ b/apps/web/src/components/chat/message-deliverables.tsx @@ -1,5 +1,10 @@ "use client"; +import { useAuth } from "@clerk/nextjs"; +import { useState } from "react"; +import { toast } from "sonner"; +import { formatBytes } from "@/components/chat/message-deliverable-model"; +import type { ArtifactData } from "@/components/chat/message-parts.types"; import { Code, Download, @@ -8,12 +13,7 @@ import { Image as ImageIcon, Presentation, Video, -} from "@cheatcode/ui"; -import { useAuth } from "@clerk/nextjs"; -import { useState } from "react"; -import { toast } from "sonner"; -import { formatBytes } from "@/components/chat/message-deliverable-model"; -import type { ArtifactData } from "@/components/chat/message-parts.types"; +} from "@/components/ui"; import { createOutputDownloadUrl } from "@/lib/api/outputs"; type GetToken = () => Promise; diff --git a/apps/web/src/components/chat/message-list-view.tsx b/apps/web/src/components/chat/message-list-view.tsx index 36ae300a..90614b6a 100644 --- a/apps/web/src/components/chat/message-list-view.tsx +++ b/apps/web/src/components/chat/message-list-view.tsx @@ -1,7 +1,6 @@ "use client"; import type { CheatcodeUIMessage } from "@cheatcode/types"; -import { ArrowDown } from "@cheatcode/ui"; import type { ReactVirtualizer } from "@tanstack/react-virtual"; import { MessageParts } from "@/components/chat/message-parts"; import type { MessageTurn } from "@/components/chat/message-turns"; @@ -11,6 +10,7 @@ import type { MessageScrollState, OlderMessagesLoadResult, } from "@/components/chat/use-message-list-scroll"; +import { ArrowDown } from "@/components/ui"; import { useElapsedSeconds } from "@/lib/hooks/use-elapsed-seconds"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/chat/message-part-blocks.tsx b/apps/web/src/components/chat/message-part-blocks.tsx index 5d00d84e..60563a16 100644 --- a/apps/web/src/components/chat/message-part-blocks.tsx +++ b/apps/web/src/components/chat/message-part-blocks.tsx @@ -1,4 +1,4 @@ -import { RotateCw } from "@cheatcode/ui"; +import { RotateCw } from "@/components/ui"; export function ErrorRecoveryBlock({ message, diff --git a/apps/web/src/components/chat/message-parts.tsx b/apps/web/src/components/chat/message-parts.tsx index 5513545f..79d4ce3e 100644 --- a/apps/web/src/components/chat/message-parts.tsx +++ b/apps/web/src/components/chat/message-parts.tsx @@ -5,7 +5,6 @@ import { type ModelFallbackData, reconstructedTranscriptUIMessage, } from "@cheatcode/types"; -import { FileText, Loader2, Puzzle } from "@cheatcode/ui"; import { useAuth } from "@clerk/nextjs"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import Link from "next/link"; @@ -30,6 +29,7 @@ import { formatUnknown, isHiddenTranscriptPart, } from "@/components/chat/message-timeline"; +import { FileText, Loader2, Puzzle } from "@/components/ui"; import { openUserSkill } from "@/lib/api/skills"; import { useAppStore } from "@/lib/store/app-store"; diff --git a/apps/web/src/components/chat/prompt-composer-view.tsx b/apps/web/src/components/chat/prompt-composer-view.tsx index f58ce015..a0d7ac67 100644 --- a/apps/web/src/components/chat/prompt-composer-view.tsx +++ b/apps/web/src/components/chat/prompt-composer-view.tsx @@ -1,7 +1,6 @@ "use client"; import { USER_MESSAGE_MAX_CHARACTERS } from "@cheatcode/types/api"; -import { ArrowUp, Paperclip, Square } from "@cheatcode/ui"; import type { PromptComposerController } from "@/components/chat/prompt-composer-controller"; import { ComposerAttachmentStatus } from "@/components/composer/composer-attachment-status"; import { ComposerContextChips } from "@/components/composer/composer-context-chips"; @@ -9,6 +8,7 @@ import { COMPOSER_TEXTAREA_CLASS, ComposerFrame } from "@/components/composer/co import { ComposerPopover } from "@/components/composer/composer-popover"; import { ModelMenu } from "@/components/composer/model-menu"; import { ProjectPicker } from "@/components/composer/project-picker"; +import { ArrowUp, Paperclip, Square } from "@/components/ui"; import { CheatcodeTooltip } from "@/components/ui/cheatcode-tooltip"; import { PROMPT_ATTACHMENT_ACCEPT } from "@/lib/input/prompt-attachments"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/chat/status-pill.tsx b/apps/web/src/components/chat/status-pill.tsx index 3e3cafd3..83a82b98 100644 --- a/apps/web/src/components/chat/status-pill.tsx +++ b/apps/web/src/components/chat/status-pill.tsx @@ -1,4 +1,4 @@ -import { Loader2 } from "@cheatcode/ui"; +import { Loader2 } from "@/components/ui"; export type RunStatus = "error" | "ready" | "streaming" | "submitted"; diff --git a/apps/web/src/components/composer/add-menu.tsx b/apps/web/src/components/composer/add-menu.tsx index b181d989..154ab0bc 100644 --- a/apps/web/src/components/composer/add-menu.tsx +++ b/apps/web/src/components/composer/add-menu.tsx @@ -1,8 +1,8 @@ "use client"; -import { Paperclip, Plus } from "@cheatcode/ui"; import { useAddMenuController } from "@/components/composer/add-menu-controller"; import { RepoImportControl } from "@/components/composer/repo-import-control"; +import { Paperclip, Plus } from "@/components/ui"; import { CheatcodeTooltip } from "@/components/ui/cheatcode-tooltip"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/composer/composer-attachment-status.tsx b/apps/web/src/components/composer/composer-attachment-status.tsx index a73bff74..7fdeba3a 100644 --- a/apps/web/src/components/composer/composer-attachment-status.tsx +++ b/apps/web/src/components/composer/composer-attachment-status.tsx @@ -1,6 +1,6 @@ "use client"; -import { Loader2, Paperclip } from "@cheatcode/ui"; +import { Loader2, Paperclip } from "@/components/ui"; import { cn } from "@/lib/ui/cn"; export interface ComposerAttachmentStatusState { diff --git a/apps/web/src/components/composer/composer-context-chips.tsx b/apps/web/src/components/composer/composer-context-chips.tsx index 09aeba88..e3011647 100644 --- a/apps/web/src/components/composer/composer-context-chips.tsx +++ b/apps/web/src/components/composer/composer-context-chips.tsx @@ -1,7 +1,7 @@ "use client"; import type { IntegrationName } from "@cheatcode/types"; -import { Link as LinkIcon, X } from "@cheatcode/ui"; +import { Link as LinkIcon, X } from "@/components/ui"; import { CheatcodeMark } from "@/components/ui/cheatcode-mark"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/composer/composer-popover.tsx b/apps/web/src/components/composer/composer-popover.tsx index ae65c877..58a2eaa8 100644 --- a/apps/web/src/components/composer/composer-popover.tsx +++ b/apps/web/src/components/composer/composer-popover.tsx @@ -1,9 +1,9 @@ "use client"; import type { IntegrationName } from "@cheatcode/types"; -import { FileArchive, FileText, Folder } from "@cheatcode/ui"; import { useId } from "react"; import { IntegrationBrandLogo } from "@/components/skills/integration-brand-logo"; +import { FileArchive, FileText, Folder } from "@/components/ui"; import { CheatcodeMark } from "@/components/ui/cheatcode-mark"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/composer/model-menu-view.tsx b/apps/web/src/components/composer/model-menu-view.tsx index 34978545..8667f00f 100644 --- a/apps/web/src/components/composer/model-menu-view.tsx +++ b/apps/web/src/components/composer/model-menu-view.tsx @@ -1,9 +1,9 @@ "use client"; -import { Check, ChevronDown, SlidersHorizontal } from "@cheatcode/ui"; import Link from "next/link"; import type { ModelMenuController } from "@/components/composer/model-menu-controller"; import { modelMenuLabel, providerIconClassName } from "@/components/composer/model-menu-model"; +import { Check, ChevronDown, SlidersHorizontal } from "@/components/ui"; import { CheatcodeTooltip } from "@/components/ui/cheatcode-tooltip"; import { ProviderMark } from "@/components/ui/provider-mark"; import type { AgentModelOption } from "@/lib/agent-models"; diff --git a/apps/web/src/components/composer/project-picker-parts.tsx b/apps/web/src/components/composer/project-picker-parts.tsx index 66cebee2..022be3f5 100644 --- a/apps/web/src/components/composer/project-picker-parts.tsx +++ b/apps/web/src/components/composer/project-picker-parts.tsx @@ -1,7 +1,6 @@ "use client"; import type { ProjectSummary } from "@cheatcode/types/api"; -import { ConfirmDialog, ModalShell, Plus, Search, X } from "@cheatcode/ui"; import { type FocusEvent, type KeyboardEvent, @@ -20,6 +19,7 @@ import { ProjectMoreIcon, ProjectTrashIcon, } from "@/components/composer/project-picker-icons"; +import { ConfirmDialog, ModalShell, Plus, Search, X } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { CheatcodeTooltip } from "@/components/ui/cheatcode-tooltip"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/docs/cheatcode-101-sections.tsx b/apps/web/src/components/docs/cheatcode-101-sections.tsx index 7a760a34..8a207a89 100644 --- a/apps/web/src/components/docs/cheatcode-101-sections.tsx +++ b/apps/web/src/components/docs/cheatcode-101-sections.tsx @@ -1,8 +1,8 @@ "use client"; -import { ArrowUpRight, ChevronDown, Play } from "@cheatcode/ui"; import { useEffect, useState } from "react"; import { PromptLaunchButton } from "@/components/navigation/prompt-launch-button"; +import { ArrowUpRight, ChevronDown, Play } from "@/components/ui"; import type { Cheatcode101Block, Cheatcode101Faq, diff --git a/apps/web/src/components/home/home-composer-controls.tsx b/apps/web/src/components/home/home-composer-controls.tsx index 76b1b36f..e555d8df 100644 --- a/apps/web/src/components/home/home-composer-controls.tsx +++ b/apps/web/src/components/home/home-composer-controls.tsx @@ -1,12 +1,12 @@ "use client"; -import { Puzzle, X } from "@cheatcode/ui"; import { type ComposerIntent, type IntentId, QUICK_ACTION_PRIMARY_INTENTS, QUICK_ACTION_SECONDARY_INTENTS, } from "@/components/home/home-composer-intents"; +import { Puzzle, X } from "@/components/ui"; import { cn } from "@/lib/ui/cn"; const SKILL_CREATOR_SUGGESTIONS = [ diff --git a/apps/web/src/components/home/home-composer-intents.ts b/apps/web/src/components/home/home-composer-intents.ts index c93e9386..10cb861f 100644 --- a/apps/web/src/components/home/home-composer-intents.ts +++ b/apps/web/src/components/home/home-composer-intents.ts @@ -1,6 +1,6 @@ -import { Globe, Smartphone, Star, TrendingUp } from "@cheatcode/ui"; import type { ComponentType } from "react"; import { skillSurface } from "@/components/home/use-initial-skill"; +import { Globe, Smartphone, Star, TrendingUp } from "@/components/ui"; import { CheatcodeMark } from "@/components/ui/cheatcode-mark"; export type IntentId = "data" | "mobile-app" | "research" | "slides" | "web-app"; diff --git a/apps/web/src/components/home/home-composer.tsx b/apps/web/src/components/home/home-composer.tsx index 4127a901..bf6467e3 100644 --- a/apps/web/src/components/home/home-composer.tsx +++ b/apps/web/src/components/home/home-composer.tsx @@ -2,7 +2,6 @@ import type { IntegrationName } from "@cheatcode/types"; import { type ProjectSummary, USER_MESSAGE_MAX_CHARACTERS } from "@cheatcode/types/api"; -import { ArrowUp, X } from "@cheatcode/ui"; import type { ChangeEvent, KeyboardEvent, RefObject } from "react"; import { createPortal } from "react-dom"; import { AuthModal } from "@/components/auth/auth-modal"; @@ -29,6 +28,7 @@ import { type HomeComposerProps, useHomeComposerController, } from "@/components/home/use-home-composer-controller"; +import { ArrowUp, X } from "@/components/ui"; import { CheatcodeMark } from "@/components/ui/cheatcode-mark"; import { PROMPT_ATTACHMENT_ACCEPT } from "@/lib/input/prompt-attachments"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/onboarding/onboarding-flow.tsx b/apps/web/src/components/onboarding/onboarding-flow.tsx index 8997f684..1f0e1a5f 100644 --- a/apps/web/src/components/onboarding/onboarding-flow.tsx +++ b/apps/web/src/components/onboarding/onboarding-flow.tsx @@ -1,6 +1,6 @@ "use client"; -import { Sparkles as SparklesIcon } from "@cheatcode/ui"; +import { Sparkles as SparklesIcon } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { RecoveryCard } from "@/components/ui/recovery-card"; import { createOnboardingStepProps, renderOnboardingStep } from "./onboarding-step-router"; diff --git a/apps/web/src/components/onboarding/onboarding-tools-step.tsx b/apps/web/src/components/onboarding/onboarding-tools-step.tsx index 1ada16af..be44c4de 100644 --- a/apps/web/src/components/onboarding/onboarding-tools-step.tsx +++ b/apps/web/src/components/onboarding/onboarding-tools-step.tsx @@ -1,7 +1,6 @@ "use client"; import type { IntegrationName } from "@cheatcode/types"; -import { Check } from "@cheatcode/ui"; import { useAuth } from "@clerk/nextjs"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { type ReactNode, useState } from "react"; @@ -13,6 +12,7 @@ import { SlackLogo, Sparkle, } from "@/components/onboarding/onboarding-icons"; +import { Check } from "@/components/ui"; import { connectIntegration, INTEGRATIONS_QUERY, listIntegrations } from "@/lib/api/integrations"; import { OnboardingActions, diff --git a/apps/web/src/components/preview/computer-panel-tabs.tsx b/apps/web/src/components/preview/computer-panel-tabs.tsx index 28fe7e28..24949244 100644 --- a/apps/web/src/components/preview/computer-panel-tabs.tsx +++ b/apps/web/src/components/preview/computer-panel-tabs.tsx @@ -1,5 +1,8 @@ "use client"; +import { useAuth } from "@clerk/nextjs"; +import { type CSSProperties, useEffect, useRef, useState } from "react"; +import { toast } from "sonner"; import { Download, Inbox, @@ -8,10 +11,7 @@ import { Monitor, MoreHorizontal, Play, -} from "@cheatcode/ui"; -import { useAuth } from "@clerk/nextjs"; -import { type CSSProperties, useEffect, useRef, useState } from "react"; -import { toast } from "sonner"; +} from "@/components/ui"; import { CheatcodeTooltip } from "@/components/ui/cheatcode-tooltip"; import { downloadProjectArchive } from "@/lib/api/project-thread"; import type { PreviewTab } from "@/lib/store/app-store"; diff --git a/apps/web/src/components/preview/computer-toggle-button.tsx b/apps/web/src/components/preview/computer-toggle-button.tsx index 6b30706c..d42be8e6 100644 --- a/apps/web/src/components/preview/computer-toggle-button.tsx +++ b/apps/web/src/components/preview/computer-toggle-button.tsx @@ -1,7 +1,7 @@ "use client"; -import { Monitor } from "@cheatcode/ui"; import type { ButtonHTMLAttributes } from "react"; +import { Monitor } from "@/components/ui"; import { cn } from "@/lib/ui/cn"; /** The shared Cheatcode-parity Computer pill: light at rest, dark only while open. */ diff --git a/apps/web/src/components/preview/console-strip-header.tsx b/apps/web/src/components/preview/console-strip-header.tsx index c3911e87..4a1292c9 100644 --- a/apps/web/src/components/preview/console-strip-header.tsx +++ b/apps/web/src/components/preview/console-strip-header.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChevronDown, Plus, X } from "@cheatcode/ui"; +import { ChevronDown, Plus, X } from "@/components/ui"; import { CheatcodeTooltip } from "@/components/ui/cheatcode-tooltip"; import type { ConsoleTab } from "@/lib/preview/console-terminal.types"; import { consoleTabLabel } from "@/lib/preview/console-terminal-state"; diff --git a/apps/web/src/components/preview/preview-device-menu.tsx b/apps/web/src/components/preview/preview-device-menu.tsx index 60e7a9c2..d55667a1 100644 --- a/apps/web/src/components/preview/preview-device-menu.tsx +++ b/apps/web/src/components/preview/preview-device-menu.tsx @@ -1,9 +1,9 @@ "use client"; -import type { LucideIcon } from "@cheatcode/ui"; -import { Monitor, Smartphone, Tablet } from "@cheatcode/ui"; import type { RefObject } from "react"; import { useEffect, useRef, useState } from "react"; +import type { LucideIcon } from "@/components/ui"; +import { Monitor, Smartphone, Tablet } from "@/components/ui"; import type { PreviewDevice } from "@/lib/store/app-store"; import { useAppStore } from "@/lib/store/app-store"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/preview/preview-side-panel.tsx b/apps/web/src/components/preview/preview-side-panel.tsx index a3fd4dc5..e6298571 100644 --- a/apps/web/src/components/preview/preview-side-panel.tsx +++ b/apps/web/src/components/preview/preview-side-panel.tsx @@ -1,10 +1,10 @@ "use client"; import type { ProjectSummary } from "@cheatcode/types/api"; -import { Monitor, Smartphone } from "@cheatcode/ui"; import { useAuth } from "@clerk/nextjs"; import { QRCodeSVG } from "qrcode.react"; import { Activity, type ReactNode, useEffect } from "react"; +import { Monitor, Smartphone } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { CheatcodeTooltip } from "@/components/ui/cheatcode-tooltip"; import { RecoveryCard } from "@/components/ui/recovery-card"; diff --git a/apps/web/src/components/preview/preview-url-controls.tsx b/apps/web/src/components/preview/preview-url-controls.tsx index adc343b4..c91fc949 100644 --- a/apps/web/src/components/preview/preview-url-controls.tsx +++ b/apps/web/src/components/preview/preview-url-controls.tsx @@ -1,6 +1,6 @@ "use client"; -import { ChevronLeft, ChevronRight, ExternalLink, RefreshCw } from "@cheatcode/ui"; +import { ChevronLeft, ChevronRight, ExternalLink, RefreshCw } from "@/components/ui"; import { buildPreviewIframeSrc } from "@/lib/preview/url-bar"; import { useAppStore } from "@/lib/store/app-store"; diff --git a/apps/web/src/components/preview/sandbox-ide-tab.tsx b/apps/web/src/components/preview/sandbox-ide-tab.tsx index 54d51efd..bae3e36b 100644 --- a/apps/web/src/components/preview/sandbox-ide-tab.tsx +++ b/apps/web/src/components/preview/sandbox-ide-tab.tsx @@ -1,10 +1,10 @@ "use client"; -import { FolderOpen } from "@cheatcode/ui"; import { useAuth } from "@clerk/nextjs"; import { useQuery } from "@tanstack/react-query"; import { useTheme } from "next-themes"; import { type RefObject, useEffect, useRef, useState } from "react"; +import { FolderOpen } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { CheatcodeTooltip } from "@/components/ui/cheatcode-tooltip"; import { RecoveryCard } from "@/components/ui/recovery-card"; diff --git a/apps/web/src/components/search/command-palette-dialog.tsx b/apps/web/src/components/search/command-palette-dialog.tsx index 9dbd10a0..aede1ea0 100644 --- a/apps/web/src/components/search/command-palette-dialog.tsx +++ b/apps/web/src/components/search/command-palette-dialog.tsx @@ -1,5 +1,5 @@ -import { ModalShell, Search } from "@cheatcode/ui"; import { Command } from "cmdk"; +import { ModalShell, Search } from "@/components/ui"; import { CommandPaletteResults } from "./command-palette-results"; import type { useCommandPalette } from "./use-command-palette"; diff --git a/apps/web/src/components/settings/activity-year-chart-view.tsx b/apps/web/src/components/settings/activity-year-chart-view.tsx index 7bcd72b9..8ead8af4 100644 --- a/apps/web/src/components/settings/activity-year-chart-view.tsx +++ b/apps/web/src/components/settings/activity-year-chart-view.tsx @@ -1,8 +1,8 @@ "use client"; import type { ActivityRunPoint, SandboxHourPoint } from "@cheatcode/types/api"; -import { ChevronDown } from "@cheatcode/ui"; import type { ReactNode } from "react"; +import { ChevronDown } from "@/components/ui"; import { ActivityHourAxis, ActivitySvg, ActivityTooltip } from "./activity-year-graphics"; import { formatRunCount, diff --git a/apps/web/src/components/settings/activity-year-chart.tsx b/apps/web/src/components/settings/activity-year-chart.tsx index 5957f681..71e3d413 100644 --- a/apps/web/src/components/settings/activity-year-chart.tsx +++ b/apps/web/src/components/settings/activity-year-chart.tsx @@ -1,8 +1,8 @@ "use client"; import type { ActivityRunPoint, SandboxHourPoint } from "@cheatcode/types/api"; -import { ChartNoAxesCombined } from "@cheatcode/ui"; import { useEffect, useMemo, useState } from "react"; +import { ChartNoAxesCombined } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { RecoveryCard } from "@/components/ui/recovery-card"; import { useActivityQuery } from "@/lib/hooks/use-billing"; diff --git a/apps/web/src/components/settings/models-panel-list.tsx b/apps/web/src/components/settings/models-panel-list.tsx index f0cc7f80..2e3b06ad 100644 --- a/apps/web/src/components/settings/models-panel-list.tsx +++ b/apps/web/src/components/settings/models-panel-list.tsx @@ -1,5 +1,5 @@ -import { ChevronDown, Loader2 } from "@cheatcode/ui"; import type { ReactNode } from "react"; +import { ChevronDown, Loader2 } from "@/components/ui"; import { ProviderMark } from "@/components/ui/provider-mark"; import { cn } from "@/lib/ui/cn"; import { diff --git a/apps/web/src/components/settings/models-panel.tsx b/apps/web/src/components/settings/models-panel.tsx index fbe08e15..1efcd9c2 100644 --- a/apps/web/src/components/settings/models-panel.tsx +++ b/apps/web/src/components/settings/models-panel.tsx @@ -1,6 +1,6 @@ "use client"; -import { Brain } from "@cheatcode/ui"; +import { Brain } from "@/components/ui"; import { RecoveryCard } from "@/components/ui/recovery-card"; import { ModelsCatalog } from "./models-panel-list"; import { SETTINGS_KEY_PROVIDERS } from "./models-panel-model"; diff --git a/apps/web/src/components/settings/pricing-panel.tsx b/apps/web/src/components/settings/pricing-panel.tsx index d79221e0..69e1cef4 100644 --- a/apps/web/src/components/settings/pricing-panel.tsx +++ b/apps/web/src/components/settings/pricing-panel.tsx @@ -2,12 +2,12 @@ import type { BillingTier, PlanSummary } from "@cheatcode/types"; import { PaidBillingTierSchema } from "@cheatcode/types"; -import { CreditCard, Loader2 } from "@cheatcode/ui"; import { useAuth } from "@clerk/nextjs"; import { useMutation } from "@tanstack/react-query"; import { useState } from "react"; import { toast } from "sonner"; import { ManageSubscriptionDialog } from "@/components/billing/manage-subscription-dialog"; +import { CreditCard, Loader2 } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { RecoveryCard } from "@/components/ui/recovery-card"; import { requestCheckout } from "@/lib/api/billing"; diff --git a/apps/web/src/components/settings/provider-key-editor.tsx b/apps/web/src/components/settings/provider-key-editor.tsx index dc80ed3f..d2bca73b 100644 --- a/apps/web/src/components/settings/provider-key-editor.tsx +++ b/apps/web/src/components/settings/provider-key-editor.tsx @@ -1,7 +1,7 @@ import type { Provider, ProviderKeySummary } from "@cheatcode/types/api"; -import { Check, Eye, EyeOff, Loader2 } from "@cheatcode/ui"; import type { FormEventHandler } from "react"; import type { FieldError, UseFormRegister } from "react-hook-form"; +import { Check, Eye, EyeOff, Loader2 } from "@/components/ui"; import { PROVIDER_META, type ProviderKeyEditorStatus, diff --git a/apps/web/src/components/settings/sandbox-hours-meter-body.tsx b/apps/web/src/components/settings/sandbox-hours-meter-body.tsx index 08312ab7..ca6868aa 100644 --- a/apps/web/src/components/settings/sandbox-hours-meter-body.tsx +++ b/apps/web/src/components/settings/sandbox-hours-meter-body.tsx @@ -1,9 +1,9 @@ "use client"; import type { SandboxUsageSummaryResponse, SandboxUsageWarnLevel } from "@cheatcode/types"; -import { ArrowUpRight } from "@cheatcode/ui"; import { ManageSubscriptionDialog } from "@/components/billing/manage-subscription-dialog"; import { UpgradeDialog } from "@/components/billing/upgrade-dialog"; +import { ArrowUpRight } from "@/components/ui"; import { formatHoursTotal, formatHoursUsed } from "@/lib/hooks/use-billing"; import { cn } from "@/lib/ui/cn"; import { useSandboxHoursMeter } from "./use-sandbox-hours-meter"; diff --git a/apps/web/src/components/settings/sandbox-hours-meter.tsx b/apps/web/src/components/settings/sandbox-hours-meter.tsx index 43a1ed6e..5fb6db1a 100644 --- a/apps/web/src/components/settings/sandbox-hours-meter.tsx +++ b/apps/web/src/components/settings/sandbox-hours-meter.tsx @@ -1,6 +1,6 @@ "use client"; -import { Clock3 } from "@cheatcode/ui"; +import { Clock3 } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { RecoveryCard } from "@/components/ui/recovery-card"; import { useSandboxUsageQuery } from "@/lib/hooks/use-billing"; diff --git a/apps/web/src/components/settings/use-personalization-form.tsx b/apps/web/src/components/settings/use-personalization-form.tsx index 3d845eed..29c72852 100644 --- a/apps/web/src/components/settings/use-personalization-form.tsx +++ b/apps/web/src/components/settings/use-personalization-form.tsx @@ -1,9 +1,9 @@ "use client"; import type { UpdateUserProfile, UserProfile } from "@cheatcode/types"; -import { Loader2 } from "@cheatcode/ui"; import { useState } from "react"; import { toast } from "sonner"; +import { Loader2 } from "@/components/ui"; import { useUpdateProfileMutation } from "@/lib/hooks/use-profile"; export interface PersonalizationFormState { diff --git a/apps/web/src/components/shell/app-chrome.tsx b/apps/web/src/components/shell/app-chrome.tsx index 5e5fddf1..4f95c9ea 100644 --- a/apps/web/src/components/shell/app-chrome.tsx +++ b/apps/web/src/components/shell/app-chrome.tsx @@ -1,11 +1,11 @@ "use client"; -import { Plus } from "@cheatcode/ui"; import Link from "next/link"; import { usePathname } from "next/navigation"; import { type ReactNode, Suspense, useEffect, useRef } from "react"; import { AppSidebar } from "@/components/shell/app-sidebar"; import { SidebarContentFrame } from "@/components/shell/sidebar-content-frame"; +import { Plus } from "@/components/ui"; import { useAppStore } from "@/lib/store/app-store"; const MOBILE_ROUTE_TITLES = { diff --git a/apps/web/src/components/shell/sidebar-chat-list.tsx b/apps/web/src/components/shell/sidebar-chat-list.tsx index d0d33369..df698c3c 100644 --- a/apps/web/src/components/shell/sidebar-chat-list.tsx +++ b/apps/web/src/components/shell/sidebar-chat-list.tsx @@ -1,6 +1,5 @@ "use client"; -import { Loader2, MoreVertical, Pencil, Trash2 } from "@cheatcode/ui"; import { useAuth } from "@clerk/nextjs"; import { type QueryClient, useMutation, useQueryClient } from "@tanstack/react-query"; import Link from "next/link"; @@ -18,6 +17,7 @@ import { useSidebarInlineMenu, } from "@/components/shell/sidebar-list-controls"; import { SidebarListLoading } from "@/components/shell/sidebar-list-loading"; +import { Loader2, MoreVertical, Pencil, Trash2 } from "@/components/ui"; import { deleteThread, updateThread } from "@/lib/api/project-thread"; import { useChatTabsStore } from "@/lib/store/chat-tabs-store"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/shell/sidebar-expanded-account.tsx b/apps/web/src/components/shell/sidebar-expanded-account.tsx index dcaab950..822482f6 100644 --- a/apps/web/src/components/shell/sidebar-expanded-account.tsx +++ b/apps/web/src/components/shell/sidebar-expanded-account.tsx @@ -1,12 +1,12 @@ "use client"; -import { Search } from "@cheatcode/ui"; import Image from "next/image"; import Link from "next/link"; import type { AuthMode } from "@/components/auth/auth-modal"; import { OPEN_COMMAND_PALETTE_EVENT } from "@/components/search/command-palette-event"; import { SidebarPanelToggleIcon } from "@/components/shell/sidebar-nav-icons"; import { isExternalHref } from "@/components/shell/sidebar-navigation-model"; +import { Search } from "@/components/ui"; import { CheatcodeMark } from "@/components/ui/cheatcode-mark"; import { CheatcodeTooltip } from "@/components/ui/cheatcode-tooltip"; import { formatHoursUsed, useSandboxUsageQuery } from "@/lib/hooks/use-billing"; diff --git a/apps/web/src/components/shell/sidebar-expanded-navigation.tsx b/apps/web/src/components/shell/sidebar-expanded-navigation.tsx index d7fbf974..3e0c03a1 100644 --- a/apps/web/src/components/shell/sidebar-expanded-navigation.tsx +++ b/apps/web/src/components/shell/sidebar-expanded-navigation.tsx @@ -1,6 +1,5 @@ "use client"; -import { ChevronDown } from "@cheatcode/ui"; import Link from "next/link"; import type { ReactNode } from "react"; import type { @@ -20,6 +19,7 @@ import { WORKSPACE_SECTION_NAV, } from "@/components/shell/sidebar-navigation-model"; import { ProjectList } from "@/components/shell/sidebar-project-list"; +import { ChevronDown } from "@/components/ui"; import { isNavItemActive, type NavItem } from "@/lib/navigation/nav-model"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/shell/sidebar-expanded-settings.tsx b/apps/web/src/components/shell/sidebar-expanded-settings.tsx index 1761518c..40ab86ae 100644 --- a/apps/web/src/components/shell/sidebar-expanded-settings.tsx +++ b/apps/web/src/components/shell/sidebar-expanded-settings.tsx @@ -1,6 +1,5 @@ "use client"; -import { LifeBuoy, MoreVertical } from "@cheatcode/ui"; import Link from "next/link"; import { isExternalHref } from "@/components/shell/sidebar-navigation-model"; import { @@ -11,6 +10,7 @@ import { SidebarUsageIcon, } from "@/components/shell/sidebar-settings-icons"; import { useSidebarTheme } from "@/components/shell/sidebar-theme"; +import { LifeBuoy, MoreVertical } from "@/components/ui"; import { cn } from "@/lib/ui/cn"; const SETTINGS_MENU_LINKS = [ diff --git a/apps/web/src/components/shell/sidebar-item-controls.tsx b/apps/web/src/components/shell/sidebar-item-controls.tsx index 91ae5fd1..ec814080 100644 --- a/apps/web/src/components/shell/sidebar-item-controls.tsx +++ b/apps/web/src/components/shell/sidebar-item-controls.tsx @@ -1,7 +1,7 @@ "use client"; -import { Loader2, ModalShell } from "@cheatcode/ui"; import { type KeyboardEvent, useEffect, useId, useRef, useState } from "react"; +import { Loader2, ModalShell } from "@/components/ui"; import { cn } from "@/lib/ui/cn"; interface SidebarInlineRenameInputProps { diff --git a/apps/web/src/components/shell/sidebar-list-controls.tsx b/apps/web/src/components/shell/sidebar-list-controls.tsx index 78c93514..97d12c02 100644 --- a/apps/web/src/components/shell/sidebar-list-controls.tsx +++ b/apps/web/src/components/shell/sidebar-list-controls.tsx @@ -1,8 +1,8 @@ "use client"; -import type { LucideIcon } from "@cheatcode/ui"; import type { ReactNode } from "react"; import { useEffect, useRef } from "react"; +import type { LucideIcon } from "@/components/ui"; import { cn } from "@/lib/ui/cn"; export function SidebarInlineActions({ children, open }: { children: ReactNode; open: boolean }) { diff --git a/apps/web/src/components/shell/sidebar-project-list.tsx b/apps/web/src/components/shell/sidebar-project-list.tsx index c782bda3..3d9fcb49 100644 --- a/apps/web/src/components/shell/sidebar-project-list.tsx +++ b/apps/web/src/components/shell/sidebar-project-list.tsx @@ -1,6 +1,5 @@ "use client"; -import { MoreHorizontal, Pencil, Trash2 } from "@cheatcode/ui"; import { useAuth } from "@clerk/nextjs"; import { type QueryClient, useMutation, useQueryClient } from "@tanstack/react-query"; import Link from "next/link"; @@ -19,6 +18,7 @@ import { useSidebarInlineMenu, } from "@/components/shell/sidebar-list-controls"; import { SidebarListLoading } from "@/components/shell/sidebar-list-loading"; +import { MoreHorizontal, Pencil, Trash2 } from "@/components/ui"; import { CheatcodeTooltip } from "@/components/ui/cheatcode-tooltip"; import { createChat, deleteProject } from "@/lib/api/project-thread"; import { useChatTabsStore } from "@/lib/store/chat-tabs-store"; diff --git a/apps/web/src/components/shell/sidebar-rail-more-menu.tsx b/apps/web/src/components/shell/sidebar-rail-more-menu.tsx index 57e9327c..a382398f 100644 --- a/apps/web/src/components/shell/sidebar-rail-more-menu.tsx +++ b/apps/web/src/components/shell/sidebar-rail-more-menu.tsx @@ -1,6 +1,5 @@ "use client"; -import { CreditCard, LifeBuoy, type LucideIcon, MoreVertical, TrendingUp } from "@cheatcode/ui"; import Link from "next/link"; import { type RefObject, useEffect, useId, useRef, useState } from "react"; import { FOOTER_NAV, isExternalHref } from "@/components/shell/sidebar-navigation-model"; @@ -11,6 +10,7 @@ import { SidebarSystemThemeIcon, } from "@/components/shell/sidebar-settings-icons"; import { useSidebarTheme } from "@/components/shell/sidebar-theme"; +import { CreditCard, LifeBuoy, type LucideIcon, MoreVertical, TrendingUp } from "@/components/ui"; import { CheatcodeTooltip } from "@/components/ui/cheatcode-tooltip"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/skills/integration-skill-drawer.tsx b/apps/web/src/components/skills/integration-skill-drawer.tsx index 4294f056..938d3cda 100644 --- a/apps/web/src/components/skills/integration-skill-drawer.tsx +++ b/apps/web/src/components/skills/integration-skill-drawer.tsx @@ -2,11 +2,11 @@ import type { IntegrationName } from "@cheatcode/types"; import type { IntegrationAccount, ToolkitAction, ToolkitCatalogEntry } from "@cheatcode/types/api"; -import { FileText, Loader2, ModalShell, Pencil, Plus, Search, Trash2, X } from "@cheatcode/ui"; import { useAuth } from "@clerk/nextjs"; import { useQuery } from "@tanstack/react-query"; import { PromptLaunchButton } from "@/components/navigation/prompt-launch-button"; import { IntegrationBrandLogo } from "@/components/skills/integration-brand-logo"; +import { FileText, Loader2, ModalShell, Pencil, Plus, Search, Trash2, X } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { fetchToolkitActions } from "@/lib/api/integrations"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/skills/integration-skills-controls.tsx b/apps/web/src/components/skills/integration-skills-controls.tsx index 8d6216eb..1f69f978 100644 --- a/apps/web/src/components/skills/integration-skills-controls.tsx +++ b/apps/web/src/components/skills/integration-skills-controls.tsx @@ -1,9 +1,9 @@ "use client"; import type { ToolkitCategory } from "@cheatcode/types/api"; -import { MoreVertical, Search } from "@cheatcode/ui"; import Link from "next/link"; import { useEffect, useRef, useState } from "react"; +import { MoreVertical, Search } from "@/components/ui"; import { cn } from "@/lib/ui/cn"; const ALL_CATEGORY = "all"; diff --git a/apps/web/src/components/skills/integration-skills-grid.tsx b/apps/web/src/components/skills/integration-skills-grid.tsx index 9beabcab..0d3993fd 100644 --- a/apps/web/src/components/skills/integration-skills-grid.tsx +++ b/apps/web/src/components/skills/integration-skills-grid.tsx @@ -1,9 +1,9 @@ import type { IntegrationName } from "@cheatcode/types"; import type { ToolkitCatalogEntry, UserSkill } from "@cheatcode/types/api"; -import { Loader2, Sparkles } from "@cheatcode/ui"; import { IntegrationBrandLogo } from "@/components/skills/integration-brand-logo"; import type { IntegrationDrawerHandlers } from "@/components/skills/integration-skill-drawer"; import { UserSkillCard, type UserSkillsCatalog } from "@/components/skills/user-skills-section"; +import { Loader2, Sparkles } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { RecoveryCard } from "@/components/ui/recovery-card"; import { cn } from "@/lib/ui/cn"; diff --git a/apps/web/src/components/skills/user-skill-drawer.tsx b/apps/web/src/components/skills/user-skill-drawer.tsx index bc455d18..d6f7aeb9 100644 --- a/apps/web/src/components/skills/user-skill-drawer.tsx +++ b/apps/web/src/components/skills/user-skill-drawer.tsx @@ -1,7 +1,7 @@ "use client"; import type { UserSkill } from "@cheatcode/types/api"; -import { Loader2, ModalShell, Trash2 } from "@cheatcode/ui"; +import { Loader2, ModalShell, Trash2 } from "@/components/ui"; import { CheatcodeMark } from "@/components/ui/cheatcode-mark"; export function UserSkillDrawer({ diff --git a/apps/web/src/components/skills/user-skills-section.tsx b/apps/web/src/components/skills/user-skills-section.tsx index 9f9eb9e5..89ac2d23 100644 --- a/apps/web/src/components/skills/user-skills-section.tsx +++ b/apps/web/src/components/skills/user-skills-section.tsx @@ -1,11 +1,11 @@ "use client"; import type { UserSkill } from "@cheatcode/types/api"; -import { BookOpen, Loader2, MoreVertical, Trash2 } from "@cheatcode/ui"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { KeyboardEvent, MouseEvent } from "react"; import { useEffect, useRef, useState } from "react"; import { toast } from "sonner"; +import { BookOpen, Loader2, MoreVertical, Trash2 } from "@/components/ui"; import { CheatcodeMark } from "@/components/ui/cheatcode-mark"; import { RecoveryCard } from "@/components/ui/recovery-card"; import { deleteUserSkill, listUserSkills, USER_SKILLS_QUERY } from "@/lib/api/skills"; diff --git a/packages/ui/src/cn.ts b/apps/web/src/components/ui/cn.ts similarity index 100% rename from packages/ui/src/cn.ts rename to apps/web/src/components/ui/cn.ts diff --git a/packages/ui/src/confirm-dialog.tsx b/apps/web/src/components/ui/confirm-dialog.tsx similarity index 100% rename from packages/ui/src/confirm-dialog.tsx rename to apps/web/src/components/ui/confirm-dialog.tsx diff --git a/packages/ui/src/icons.ts b/apps/web/src/components/ui/icons.ts similarity index 100% rename from packages/ui/src/icons.ts rename to apps/web/src/components/ui/icons.ts diff --git a/packages/ui/src/index.ts b/apps/web/src/components/ui/index.ts similarity index 100% rename from packages/ui/src/index.ts rename to apps/web/src/components/ui/index.ts diff --git a/apps/web/src/components/ui/recovery-card.tsx b/apps/web/src/components/ui/recovery-card.tsx index ec075430..ebf80530 100644 --- a/apps/web/src/components/ui/recovery-card.tsx +++ b/apps/web/src/components/ui/recovery-card.tsx @@ -1,6 +1,6 @@ -import type { LucideIcon } from "@cheatcode/ui"; -import { Loader2, RefreshCw } from "@cheatcode/ui"; import Link from "next/link"; +import type { LucideIcon } from "@/components/ui"; +import { Loader2, RefreshCw } from "@/components/ui"; import { cn } from "@/lib/ui/cn"; type RecoveryActionBase = { diff --git a/packages/ui/src/response.tsx b/apps/web/src/components/ui/response.tsx similarity index 100% rename from packages/ui/src/response.tsx rename to apps/web/src/components/ui/response.tsx diff --git a/apps/web/src/components/workspace/workspace-route-state.tsx b/apps/web/src/components/workspace/workspace-route-state.tsx index 27dfe9ed..92b8e10f 100644 --- a/apps/web/src/components/workspace/workspace-route-state.tsx +++ b/apps/web/src/components/workspace/workspace-route-state.tsx @@ -1,4 +1,4 @@ -import { MessageCircle } from "@cheatcode/ui"; +import { MessageCircle } from "@/components/ui"; import { CheatcodeLoader } from "@/components/ui/cheatcode-loader"; import { RecoveryCard } from "@/components/ui/recovery-card"; diff --git a/apps/web/src/lib/navigation/nav-model.ts b/apps/web/src/lib/navigation/nav-model.ts index c3761379..bb6ab828 100644 --- a/apps/web/src/lib/navigation/nav-model.ts +++ b/apps/web/src/lib/navigation/nav-model.ts @@ -1,4 +1,3 @@ -import { SlidersHorizontal } from "@cheatcode/ui"; import type { ComponentType } from "react"; import { SidebarModelsIcon, @@ -7,6 +6,7 @@ import { SidebarProjectsIcon, SidebarSkillsIcon, } from "@/components/shell/sidebar-nav-icons"; +import { SlidersHorizontal } from "@/components/ui"; import { CheatcodeMark } from "@/components/ui/cheatcode-mark"; type NavItemId = diff --git a/apps/web/src/lib/ui/cn.ts b/apps/web/src/lib/ui/cn.ts index 9d3e0bc3..fb5941d8 100644 --- a/apps/web/src/lib/ui/cn.ts +++ b/apps/web/src/lib/ui/cn.ts @@ -1 +1 @@ -export { cn } from "@cheatcode/ui"; +export { cn } from "@/components/ui"; diff --git a/apps/web/src/proxy.ts b/apps/web/src/proxy.ts index f341bc8d..eec2d086 100644 --- a/apps/web/src/proxy.ts +++ b/apps/web/src/proxy.ts @@ -1,9 +1,9 @@ import { env } from "@cheatcode/env/web"; +import { PRODUCTION_APP_ORIGIN } from "@cheatcode/env/web-config"; import { clerkMiddleware } from "@clerk/nextjs/server"; import { type NextRequest, NextResponse } from "next/server"; import { safeLocalRedirect } from "@/lib/navigation/safe-local-redirect"; -const PRODUCTION_WEB_ORIGIN = "https://trycheatcode.com"; const REJECTED_AUTHORIZED_PARTY = "https://invalid.invalid"; const AUTH_PATH_PATTERN = /^\/sign-(?:in|up)(?:\/|$)/u; const PROTECTED_PATH_PATTERN = @@ -57,7 +57,7 @@ function authorizedParty(request: NextRequest): string | null { const { hostname, origin, protocol } = request.nextUrl; if (env.VERCEL_ENV === "production") { const deploymentOrigin = env.VERCEL_URL ? `https://${env.VERCEL_URL}` : null; - return origin === PRODUCTION_WEB_ORIGIN || origin === deploymentOrigin ? origin : null; + return origin === PRODUCTION_APP_ORIGIN || origin === deploymentOrigin ? origin : null; } if (env.VERCEL_ENV === "preview") { const deploymentOrigin = env.VERCEL_URL ? `https://${env.VERCEL_URL}` : null; diff --git a/compose.yaml b/compose.yaml index 666e55d7..f615a35a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -54,7 +54,7 @@ services: - CMD - node - -e - - Promise.all([fetch('http://127.0.0.1:3001/cheatcode-symbol.png'),fetch('http://127.0.0.1:8787/health')]).then(async([web,gateway])=>{if(!web.ok||!web.headers.get('content-type')?.startsWith('image/png')||!gateway.ok||(await gateway.json()).ok!==true)process.exit(1)}).catch(()=>process.exit(1)) + - Promise.all([fetch('http://127.0.0.1:3001/cheatcode-symbol.png'),fetch('http://127.0.0.1:8787/health/live')]).then(async([web,gateway])=>{if(!web.ok||!web.headers.get('content-type')?.startsWith('image/png')||!gateway.ok||(await gateway.json()).ok!==true)process.exit(1)}).catch(()=>process.exit(1)) interval: 5s timeout: 5s retries: 24 diff --git a/infra/containers/dev/Dockerfile b/infra/containers/dev/Dockerfile index 09d0cbd7..b00a3c94 100644 --- a/infra/containers/dev/Dockerfile +++ b/infra/containers/dev/Dockerfile @@ -30,12 +30,8 @@ COPY packages/observability/package.json packages/observability/package.json COPY packages/preview-bridge/package.json packages/preview-bridge/package.json COPY packages/sandbox-contracts/package.json packages/sandbox-contracts/package.json COPY packages/skills/package.json packages/skills/package.json -COPY packages/tools-browser/package.json packages/tools-browser/package.json -COPY packages/tools-code/package.json packages/tools-code/package.json -COPY packages/tools-research/package.json packages/tools-research/package.json COPY packages/tsconfig/package.json packages/tsconfig/package.json COPY packages/types/package.json packages/types/package.json -COPY packages/ui/package.json packages/ui/package.json RUN --mount=type=cache,id=cheatcode-pnpm-store,target=/pnpm/store \ pnpm install --frozen-lockfile --ignore-scripts \ diff --git a/packages/agent-core/README.md b/packages/agent-core/README.md index a0911b0d..16486432 100644 --- a/packages/agent-core/README.md +++ b/packages/agent-core/README.md @@ -15,7 +15,8 @@ artifact-stream routing derive from the catalog's exact runtime traits, so a tool's registry and stream behavior move together. Sandbox and artifact capabilities cross tool-domain boundaries only through `@cheatcode/sandbox-contracts`; concrete code-tool executors remain in -`@cheatcode/tools-code`. +`src/tools/code` and are available to deployables through the +`@cheatcode/agent-core/tools/code` subpath. Single-consumer data, document, and media implementations live under `src/tools/`. Data tools profile and normalize bounded tabular inputs and render diff --git a/packages/agent-core/package.json b/packages/agent-core/package.json index 3e6ae926..7a047519 100644 --- a/packages/agent-core/package.json +++ b/packages/agent-core/package.json @@ -9,6 +9,18 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" + }, + "./tools/browser": { + "types": "./dist/tools/browser/index.d.ts", + "import": "./dist/tools/browser/index.js" + }, + "./tools/code": { + "types": "./dist/tools/code/index.d.ts", + "import": "./dist/tools/code/index.js" + }, + "./tools/research": { + "types": "./dist/tools/research/index.d.ts", + "import": "./dist/tools/research/index.js" } }, "scripts": { @@ -25,9 +37,6 @@ "@cheatcode/observability": "workspace:*", "@cheatcode/sandbox-contracts": "workspace:*", "@cheatcode/skills": "workspace:*", - "@cheatcode/tools-browser": "workspace:*", - "@cheatcode/tools-code": "workspace:*", - "@cheatcode/tools-research": "workspace:*", "@cheatcode/types": "workspace:*", "@google/genai": "catalog:", "@mastra/core": "catalog:", diff --git a/packages/agent-core/src/index.ts b/packages/agent-core/src/index.ts index 3fb59a41..f3de4d95 100644 --- a/packages/agent-core/src/index.ts +++ b/packages/agent-core/src/index.ts @@ -11,6 +11,7 @@ export type { ComposioQuotaMeter, ComposioQuotaResult, } from "./mastra/composio-context"; +export { createCodeRequestContext } from "./mastra/tool-defs/request-context"; export type { UserSkillCreateInput, UserSkillCreateResult, @@ -18,5 +19,4 @@ export type { UserSkillDefinition, UserSkillLoader, UserSkillRuntime, -} from "./mastra/system-prompt"; -export { createCodeRequestContext } from "./mastra/tools/request-context"; +} from "./mastra/user-skill-runtime"; diff --git a/packages/agent-core/src/mastra/agents/general.ts b/packages/agent-core/src/mastra/agents/general.ts index 58a35deb..6bdc0c67 100644 --- a/packages/agent-core/src/mastra/agents/general.ts +++ b/packages/agent-core/src/mastra/agents/general.ts @@ -24,7 +24,7 @@ import { OPENROUTER_API_KEY_CONTEXT_KEY, } from "../llm-context"; import { buildSystemPrompt, promptRuntimeContextFromRequestContext } from "../system-prompt"; -import { cheatcodeTools } from "../tools/tool-set"; +import { cheatcodeTools } from "../tool-defs/tool-set"; import { boundedProviderFetch } from "./provider-fetch"; export type { LlmProvider, LlmTransportSelection } from "../llm-context"; diff --git a/packages/agent-core/src/mastra/composio-context.ts b/packages/agent-core/src/mastra/composio-context.ts index 3f373008..5fa421c6 100644 --- a/packages/agent-core/src/mastra/composio-context.ts +++ b/packages/agent-core/src/mastra/composio-context.ts @@ -1,11 +1,6 @@ import { type IntegrationName, IntegrationNameSchema } from "@cheatcode/types/integrations"; import { z } from "zod/v4"; -export const COMPOSIO_API_KEY_CONTEXT_KEY = "composioApiKey"; -export const COMPOSIO_CONNECTED_ACCOUNTS_CONTEXT_KEY = "composioConnectedAccounts"; -export const COMPOSIO_QUOTA_METER_CONTEXT_KEY = "composioQuotaMeter"; -export const COMPOSIO_USER_ID_CONTEXT_KEY = "composioUserId"; - export type ComposioConnectedAccounts = Partial>; export interface ComposioQuotaResult { diff --git a/packages/agent-core/src/mastra/context.ts b/packages/agent-core/src/mastra/context.ts new file mode 100644 index 00000000..8a9f0cbe --- /dev/null +++ b/packages/agent-core/src/mastra/context.ts @@ -0,0 +1,30 @@ +export const CONTEXT = { + agentDisplayName: "agentDisplayName", + anthropicApiKey: "anthropicApiKey", + browserRunId: "browserRunId", + codeRuntime: "codeRuntime", + composioApiKey: "composioApiKey", + composioConnectedAccounts: "composioConnectedAccounts", + composioQuotaMeter: "composioQuotaMeter", + composioUserId: "composioUserId", + deepseekApiKey: "deepseekApiKey", + exaApiKey: "exaApiKey", + firecrawlApiKey: "firecrawlApiKey", + globalMemory: "globalMemory", + googleApiKey: "googleApiKey", + llmModelId: "llmModelId", + llmProvider: "llmProvider", + openaiApiKey: "openaiApiKey", + openrouterApiKey: "openrouterApiKey", + promptProjectMode: "promptProjectMode", + promptTaskMessage: "promptTaskMessage", + promptWorkspaceDir: "promptWorkspaceDir", + researchEvidenceCollector: "researchEvidenceCollector", + researchWorkflowActive: "researchWorkflowActive", + runIntent: "runIntent", + userSkillCreator: "userSkillCreator", + userSkillLoader: "userSkillLoader", + userSkills: "userSkills", +} as const satisfies Record; + +export type ContextKey = (typeof CONTEXT)[keyof typeof CONTEXT]; diff --git a/packages/agent-core/src/mastra/research-context.ts b/packages/agent-core/src/mastra/research-context.ts deleted file mode 100644 index 3139ae75..00000000 --- a/packages/agent-core/src/mastra/research-context.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const EXA_API_KEY_CONTEXT_KEY = "exaApiKey"; -export const FIRECRAWL_API_KEY_CONTEXT_KEY = "firecrawlApiKey"; diff --git a/packages/agent-core/src/mastra/system-prompt.ts b/packages/agent-core/src/mastra/system-prompt.ts index a5785149..ce923334 100644 --- a/packages/agent-core/src/mastra/system-prompt.ts +++ b/packages/agent-core/src/mastra/system-prompt.ts @@ -1,84 +1,7 @@ import { buildSystemPromptSection, getSkillByName, SKILLS } from "@cheatcode/skills"; import { MAX_USER_SKILLS, type RunIntent } from "@cheatcode/types/api"; - -export const AGENT_DISPLAY_NAME_CONTEXT_KEY = "agentDisplayName"; -export const GLOBAL_MEMORY_CONTEXT_KEY = "globalMemory"; -/** Run's project mode (app-builder / app-builder-mobile / general) — picks the domain prompt module. */ -export const PROMPT_PROJECT_MODE_CONTEXT_KEY = "promptProjectMode"; -/** Explicit UI-selected one-run mode; never inferred from user wording. */ -export const RUN_INTENT_CONTEXT_KEY = "runIntent"; -/** The user's message — classified to pick domain prompt modules on the general path. */ -export const PROMPT_TASK_MESSAGE_CONTEXT_KEY = "promptTaskMessage"; -/** The run's project folder (/workspace/) — told to the agent as its working directory. */ -export const PROMPT_WORKSPACE_DIR_CONTEXT_KEY = "promptWorkspaceDir"; -/** Bounded custom-skill metadata used to advertise skills in the system prompt. */ -export const USER_SKILLS_CONTEXT_KEY = "userSkills"; -/** Request-scoped capability that loads one custom skill body on demand. */ -export const USER_SKILL_LOADER_CONTEXT_KEY = "userSkillLoader"; -/** Request-scoped capability that atomically persists a Skill Creator package. */ -export const USER_SKILL_CREATOR_CONTEXT_KEY = "userSkillCreator"; - -/** Body-less custom-skill metadata carried on every run. */ -export interface UserSkillRuntime { - name: string; - description: string; - category?: string; -} - -/** A custom skill definition loaded only when `skill_invoke` selects it. */ -export interface UserSkillDefinition extends UserSkillRuntime { - body: string; - rootPath: string; -} - -/** Loads a single custom skill body from the request's user-scoped store. */ -export interface UserSkillLoader { - load(name: string): Promise; -} - -/** Validated metadata passed to the user-scoped Skill Creator persistence boundary. */ -export interface UserSkillCreateInput { - body: string; - category: string; - description: string; - name: string; - sourceSlug: string; - tags: string[]; -} - -/** Client-safe identity returned only after the complete skill package is durable. */ -export interface UserSkillCreateResult { - description: string; - filePath: string; - id: string; - name: string; - slug: string; -} - -/** Persists one authored skill package within the active user's run context. */ -export interface UserSkillCreator { - create(input: UserSkillCreateInput): Promise; -} - -export function userSkillLoaderFromRequestContext( - requestContext: { get(key: string): unknown } | undefined, -): UserSkillLoader | null { - const value = requestContext?.get(USER_SKILL_LOADER_CONTEXT_KEY); - if (value && typeof (value as UserSkillLoader).load === "function") { - return value as UserSkillLoader; - } - return null; -} - -export function userSkillCreatorFromRequestContext( - requestContext: { get(key: string): unknown } | undefined, -): UserSkillCreator | null { - const value = requestContext?.get(USER_SKILL_CREATOR_CONTEXT_KEY); - if (value && typeof (value as UserSkillCreator).create === "function") { - return value as UserSkillCreator; - } - return null; -} +import { CONTEXT } from "./context"; +import type { UserSkillRuntime } from "./user-skill-runtime"; interface RequestContextReader { get(key: string): unknown; @@ -101,7 +24,7 @@ export interface PromptRuntimeContext { function userSkillsFromRequestContext( requestContext: RequestContextReader | undefined, ): UserSkillRuntime[] { - const raw = requestContext?.get(USER_SKILLS_CONTEXT_KEY); + const raw = requestContext?.get(CONTEXT.userSkills); if (!Array.isArray(raw)) { return []; } @@ -128,11 +51,11 @@ export function promptRuntimeContextFromRequestContext( requestContext: RequestContextReader | undefined, ): PromptRuntimeContext { const context: PromptRuntimeContext = {}; - const agentDisplayName = trimmedContextValue(requestContext, AGENT_DISPLAY_NAME_CONTEXT_KEY); + const agentDisplayName = trimmedContextValue(requestContext, CONTEXT.agentDisplayName); if (agentDisplayName) { context.agentDisplayName = agentDisplayName; } - const globalMemory = trimmedContextValue(requestContext, GLOBAL_MEMORY_CONTEXT_KEY); + const globalMemory = trimmedContextValue(requestContext, CONTEXT.globalMemory); if (globalMemory) { context.globalMemory = globalMemory; } @@ -140,19 +63,19 @@ export function promptRuntimeContextFromRequestContext( if (userSkills.length > 0) { context.userSkills = userSkills; } - const projectMode = trimmedContextValue(requestContext, PROMPT_PROJECT_MODE_CONTEXT_KEY); + const projectMode = trimmedContextValue(requestContext, CONTEXT.promptProjectMode); if (projectMode) { context.projectMode = projectMode; } - const runIntent = trimmedContextValue(requestContext, RUN_INTENT_CONTEXT_KEY); + const runIntent = trimmedContextValue(requestContext, CONTEXT.runIntent); if (runIntent === "skill-creator") { context.runIntent = runIntent; } - const taskMessage = trimmedContextValue(requestContext, PROMPT_TASK_MESSAGE_CONTEXT_KEY); + const taskMessage = trimmedContextValue(requestContext, CONTEXT.promptTaskMessage); if (taskMessage) { context.taskMessage = taskMessage; } - const workspaceDir = trimmedContextValue(requestContext, PROMPT_WORKSPACE_DIR_CONTEXT_KEY); + const workspaceDir = trimmedContextValue(requestContext, CONTEXT.promptWorkspaceDir); if (workspaceDir) { context.workspaceDir = workspaceDir; } diff --git a/packages/agent-core/src/mastra/tools/browser-runtime.ts b/packages/agent-core/src/mastra/tool-defs/browser-runtime.ts similarity index 84% rename from packages/agent-core/src/mastra/tools/browser-runtime.ts rename to packages/agent-core/src/mastra/tool-defs/browser-runtime.ts index 96884468..dcb883fa 100644 --- a/packages/agent-core/src/mastra/tools/browser-runtime.ts +++ b/packages/agent-core/src/mastra/tool-defs/browser-runtime.ts @@ -1,33 +1,27 @@ import { APIError } from "@cheatcode/observability"; import { CodeRuntimeContextSchema } from "@cheatcode/sandbox-contracts"; -import type { BrowserProvider, BrowserRuntimeContext } from "@cheatcode/tools-browser"; +import type { BrowserProvider, BrowserRuntimeContext } from "../../tools/browser"; +import { CONTEXT } from "../context"; import { - ANTHROPIC_API_KEY_CONTEXT_KEY, DEFAULT_ANTHROPIC_MODEL_ID, DEFAULT_GOOGLE_MODEL_ID, DEFAULT_OPENAI_MODEL_ID, - GOOGLE_API_KEY_CONTEXT_KEY, - LLM_MODEL_ID_CONTEXT_KEY, - LLM_PROVIDER_CONTEXT_KEY, - OPENAI_API_KEY_CONTEXT_KEY, } from "../llm-context"; -export const BROWSER_RUN_ID_CONTEXT_KEY = "browserRunId"; - const BROWSER_PROVIDER_CONFIG: Record< BrowserProvider, { apiKeyContext: string; defaultModelId: string } > = { anthropic: { - apiKeyContext: ANTHROPIC_API_KEY_CONTEXT_KEY, + apiKeyContext: CONTEXT.anthropicApiKey, defaultModelId: DEFAULT_ANTHROPIC_MODEL_ID, }, google: { - apiKeyContext: GOOGLE_API_KEY_CONTEXT_KEY, + apiKeyContext: CONTEXT.googleApiKey, defaultModelId: DEFAULT_GOOGLE_MODEL_ID, }, openai: { - apiKeyContext: OPENAI_API_KEY_CONTEXT_KEY, + apiKeyContext: CONTEXT.openaiApiKey, defaultModelId: DEFAULT_OPENAI_MODEL_ID, }, }; @@ -44,8 +38,8 @@ export interface RequestContextReader { export function browserRuntimeFromRequestContext( requestContext: RequestContextReader, ): BrowserRuntimeContext { - const runtimeContext = CodeRuntimeContextSchema.parse(requestContext.get("codeRuntime")); - const runId = requestContext.get(BROWSER_RUN_ID_CONTEXT_KEY); + const runtimeContext = CodeRuntimeContextSchema.parse(requestContext.get(CONTEXT.codeRuntime)); + const runId = requestContext.get(CONTEXT.browserRunId); if (typeof runId !== "string" || runId.length === 0) { throw new APIError(500, "internal_error", "Browser runtime is missing its run identity.", { retriable: false, @@ -60,8 +54,8 @@ export function browserRuntimeFromRequestContext( } function browserCredentialFromRequestContext(requestContext: RequestContextReader) { - const requestedProvider = requestContext.get(LLM_PROVIDER_CONTEXT_KEY); - const modelId = requestContext.get(LLM_MODEL_ID_CONTEXT_KEY); + const requestedProvider = requestContext.get(CONTEXT.llmProvider); + const modelId = requestContext.get(CONTEXT.llmModelId); if (isBrowserProvider(requestedProvider)) { const config = BROWSER_PROVIDER_CONFIG[requestedProvider]; return providerCredential( diff --git a/packages/agent-core/src/mastra/tools/browser-tools.ts b/packages/agent-core/src/mastra/tool-defs/browser-tools.ts similarity index 98% rename from packages/agent-core/src/mastra/tools/browser-tools.ts rename to packages/agent-core/src/mastra/tool-defs/browser-tools.ts index 5630ced7..57d15334 100644 --- a/packages/agent-core/src/mastra/tools/browser-tools.ts +++ b/packages/agent-core/src/mastra/tool-defs/browser-tools.ts @@ -1,3 +1,4 @@ +import { createTool } from "@mastra/core/tools"; import { BrowserActInputSchema, BrowserActionsOutputSchema, @@ -11,8 +12,7 @@ import { executeBrowserOpen, executeBrowserScreenshot, inspectBrowserPage, -} from "@cheatcode/tools-browser"; -import { createTool } from "@mastra/core/tools"; +} from "../../tools/browser"; import { browserRuntimeFromContext } from "./tool-runtime-context"; export const mastraBrowserOpen = createTool({ diff --git a/packages/agent-core/src/mastra/tools/code-tools.ts b/packages/agent-core/src/mastra/tool-defs/code-tools.ts similarity index 99% rename from packages/agent-core/src/mastra/tools/code-tools.ts rename to packages/agent-core/src/mastra/tool-defs/code-tools.ts index 413464f2..448c6b34 100644 --- a/packages/agent-core/src/mastra/tools/code-tools.ts +++ b/packages/agent-core/src/mastra/tool-defs/code-tools.ts @@ -1,3 +1,4 @@ +import { createTool } from "@mastra/core/tools"; import { DeleteFileInputSchema, DeleteFileOutputSchema, @@ -40,8 +41,7 @@ import { ShellTerminalInputSchema, WriteFileInputSchema, WriteFileOutputSchema, -} from "@cheatcode/tools-code"; -import { createTool } from "@mastra/core/tools"; +} from "../../tools/code"; import { codeRuntimeFromContext, workspaceRuntimeFromContext } from "./tool-runtime-context"; import { startDevServerInputSchema, startDevServerOutputSchema } from "./tool-schemas"; diff --git a/packages/agent-core/src/mastra/tools/composio-tool.ts b/packages/agent-core/src/mastra/tool-defs/composio-tool.ts similarity index 97% rename from packages/agent-core/src/mastra/tools/composio-tool.ts rename to packages/agent-core/src/mastra/tool-defs/composio-tool.ts index 399ffce3..4cbfb6f8 100644 --- a/packages/agent-core/src/mastra/tools/composio-tool.ts +++ b/packages/agent-core/src/mastra/tool-defs/composio-tool.ts @@ -3,14 +3,8 @@ import { createLogger } from "@cheatcode/observability"; import { IntegrationNameSchema } from "@cheatcode/types/integrations"; import { createTool } from "@mastra/core/tools"; import { z } from "zod/v4"; -import { - COMPOSIO_API_KEY_CONTEXT_KEY, - COMPOSIO_CONNECTED_ACCOUNTS_CONTEXT_KEY, - COMPOSIO_QUOTA_METER_CONTEXT_KEY, - COMPOSIO_USER_ID_CONTEXT_KEY, - ComposioConnectedAccountsSchema, - type ComposioQuotaMeter, -} from "../composio-context"; +import { ComposioConnectedAccountsSchema, type ComposioQuotaMeter } from "../composio-context"; +import { CONTEXT } from "../context"; const MAX_COMPOSIO_ARGUMENTS_JSON_CHARS = 100_000; const MAX_COMPOSIO_OUTPUT_CHARS = 20_000; @@ -180,12 +174,12 @@ function requestContextFromToolContext(context: unknown): { get(key: string): un function composioRuntimeFromContext(context: unknown): ComposioRuntimeContext { const requestContext = requestContextFromToolContext(context); - const apiKey = requestContext.get(COMPOSIO_API_KEY_CONTEXT_KEY); - const userId = requestContext.get(COMPOSIO_USER_ID_CONTEXT_KEY); - const quotaMeter = requestContext.get(COMPOSIO_QUOTA_METER_CONTEXT_KEY); + const apiKey = requestContext.get(CONTEXT.composioApiKey); + const userId = requestContext.get(CONTEXT.composioUserId); + const quotaMeter = requestContext.get(CONTEXT.composioQuotaMeter); return { connectedAccounts: ComposioConnectedAccountsSchema.parse( - requestContext.get(COMPOSIO_CONNECTED_ACCOUNTS_CONTEXT_KEY) ?? {}, + requestContext.get(CONTEXT.composioConnectedAccounts) ?? {}, ), ...(typeof apiKey === "string" && apiKey.trim() ? { apiKey } : {}), ...(isComposioQuotaMeter(quotaMeter) ? { quotaMeter } : {}), diff --git a/packages/agent-core/src/mastra/tools/data-tools.ts b/packages/agent-core/src/mastra/tool-defs/data-tools.ts similarity index 100% rename from packages/agent-core/src/mastra/tools/data-tools.ts rename to packages/agent-core/src/mastra/tool-defs/data-tools.ts diff --git a/packages/agent-core/src/mastra/tools/document-tools.ts b/packages/agent-core/src/mastra/tool-defs/document-tools.ts similarity index 100% rename from packages/agent-core/src/mastra/tools/document-tools.ts rename to packages/agent-core/src/mastra/tool-defs/document-tools.ts diff --git a/packages/agent-core/src/mastra/tools/media-tools.ts b/packages/agent-core/src/mastra/tool-defs/media-tools.ts similarity index 89% rename from packages/agent-core/src/mastra/tools/media-tools.ts rename to packages/agent-core/src/mastra/tool-defs/media-tools.ts index 2fb8b877..4e50a2b7 100644 --- a/packages/agent-core/src/mastra/tools/media-tools.ts +++ b/packages/agent-core/src/mastra/tool-defs/media-tools.ts @@ -4,7 +4,7 @@ import { GenerateOrEditMediaInputSchema, GenerateOrEditMediaOutputSchema, } from "../../tools/media/schemas"; -import { GOOGLE_API_KEY_CONTEXT_KEY } from "../llm-context"; +import { CONTEXT } from "../context"; import { requestContextFromToolContext, workspaceRuntimeFromContext } from "./tool-runtime-context"; /** Generates or edits image/video artifacts with the user's request-scoped Google key. */ @@ -16,7 +16,7 @@ export const mastraGenerateOrEditMedia = createTool({ outputSchema: GenerateOrEditMediaOutputSchema, execute: async (input, context) => { const requestContext = requestContextFromToolContext(context); - const googleApiKey = requestContext.get(GOOGLE_API_KEY_CONTEXT_KEY); + const googleApiKey = requestContext.get(CONTEXT.googleApiKey); return executeGenerateOrEditMedia( GenerateOrEditMediaInputSchema.parse(input), await workspaceRuntimeFromContext(context), diff --git a/packages/agent-core/src/mastra/tools/registry.ts b/packages/agent-core/src/mastra/tool-defs/registry.ts similarity index 100% rename from packages/agent-core/src/mastra/tools/registry.ts rename to packages/agent-core/src/mastra/tool-defs/registry.ts diff --git a/packages/agent-core/src/mastra/tool-defs/request-context.ts b/packages/agent-core/src/mastra/tool-defs/request-context.ts new file mode 100644 index 00000000..598a5a85 --- /dev/null +++ b/packages/agent-core/src/mastra/tool-defs/request-context.ts @@ -0,0 +1,87 @@ +import type { CodeRuntimeContext } from "@cheatcode/sandbox-contracts"; +import type { RunIntent } from "@cheatcode/types/api"; +import { RequestContext } from "@mastra/core/request-context"; +import type { ComposioConnectedAccounts, ComposioQuotaMeter } from "../composio-context"; +import { CONTEXT, type ContextKey } from "../context"; +import type { LlmProvider } from "../llm-context"; +import type { UserSkillCreator, UserSkillLoader, UserSkillRuntime } from "../user-skill-runtime"; + +interface CodeRequestContextOptions { + agentDisplayName?: string | undefined; + anthropicApiKey?: string | undefined; + composioApiKey?: string | undefined; + composioConnectedAccounts?: ComposioConnectedAccounts | undefined; + composioQuotaMeter?: ComposioQuotaMeter | undefined; + composioUserId?: string | undefined; + deepseekApiKey?: string | undefined; + exaApiKey?: string | undefined; + firecrawlApiKey?: string | undefined; + globalMemory?: string | undefined; + googleApiKey?: string | undefined; + llmProvider?: LlmProvider | undefined; + modelId?: string | undefined; + openaiApiKey?: string | undefined; + openrouterApiKey?: string | undefined; + projectMode?: string | undefined; + runIntent?: RunIntent | undefined; + runId?: string | undefined; + taskMessage?: string | undefined; + userSkills?: UserSkillRuntime[] | undefined; + userSkillCreator?: UserSkillCreator | undefined; + userSkillLoader?: UserSkillLoader | undefined; +} + +export function createCodeRequestContext( + runtimeContext: CodeRuntimeContext, + options: CodeRequestContextOptions = {}, +): RequestContext { + const requestContext = new RequestContext(); + requestContext.set(CONTEXT.codeRuntime, runtimeContext); + setOptionalContextValue(requestContext, CONTEXT.promptWorkspaceDir, runtimeContext.workspaceDir); + for (const [key, value] of contextEntries(options)) { + setOptionalContextValue(requestContext, key, value); + } + return requestContext; +} + +function contextEntries( + options: CodeRequestContextOptions, +): ReadonlyArray { + return [ + [CONTEXT.llmProvider, options.llmProvider], + [CONTEXT.llmModelId, options.modelId], + [CONTEXT.agentDisplayName, options.agentDisplayName], + [CONTEXT.globalMemory, options.globalMemory], + [CONTEXT.promptProjectMode, options.projectMode], + [CONTEXT.runIntent, options.runIntent], + [CONTEXT.promptTaskMessage, options.taskMessage], + [CONTEXT.anthropicApiKey, options.anthropicApiKey], + [CONTEXT.composioApiKey, options.composioApiKey], + [CONTEXT.composioConnectedAccounts, options.composioConnectedAccounts], + [CONTEXT.composioQuotaMeter, options.composioQuotaMeter], + [CONTEXT.composioUserId, options.composioUserId], + [CONTEXT.openaiApiKey, options.openaiApiKey], + [CONTEXT.googleApiKey, options.googleApiKey], + [CONTEXT.openrouterApiKey, options.openrouterApiKey], + [CONTEXT.deepseekApiKey, options.deepseekApiKey], + [CONTEXT.exaApiKey, options.exaApiKey], + [CONTEXT.firecrawlApiKey, options.firecrawlApiKey], + [CONTEXT.browserRunId, options.runId], + [CONTEXT.userSkills, options.userSkills], + [CONTEXT.userSkillCreator, options.userSkillCreator], + [CONTEXT.userSkillLoader, options.userSkillLoader], + ]; +} + +function setOptionalContextValue( + requestContext: RequestContext, + key: ContextKey, + value: unknown, +): void { + if (typeof value === "string" && value.length === 0) { + return; + } + if (value !== undefined) { + requestContext.set(key, value); + } +} diff --git a/packages/agent-core/src/mastra/tools/research-tools.ts b/packages/agent-core/src/mastra/tool-defs/research-tools.ts similarity index 94% rename from packages/agent-core/src/mastra/tools/research-tools.ts rename to packages/agent-core/src/mastra/tool-defs/research-tools.ts index ee816d66..ee1aaaa5 100644 --- a/packages/agent-core/src/mastra/tools/research-tools.ts +++ b/packages/agent-core/src/mastra/tool-defs/research-tools.ts @@ -1,3 +1,4 @@ +import { createTool, type ToolExecutionContext } from "@mastra/core/tools"; import { ExaSearchInputSchema, ExaSearchOutputSchema, @@ -11,8 +12,8 @@ import { FirecrawlScrapeOutputSchema, FirecrawlSearchInputSchema, FirecrawlSearchOutputSchema, -} from "@cheatcode/tools-research"; -import { createTool, type ToolExecutionContext } from "@mastra/core/tools"; +} from "../../tools/research"; +import { CONTEXT } from "../context"; import { DeepResearchFanoutInputSchema, DeepResearchInputSchema, @@ -27,8 +28,6 @@ import { import { researchRuntimeFromContext } from "./tool-runtime-context"; import { workflowResultSchema } from "./tool-schemas"; -const RESEARCH_WORKFLOW_ACTIVE_CONTEXT_KEY = "researchWorkflowActive"; - type RequestContextReader = { get(key: string): unknown }; type MutableRequestContext = RequestContextReader & { delete(key: string): boolean; @@ -135,7 +134,7 @@ function requestContextFromUnknownToolContext(context: ToolExecutionContext): un } function researchWorkflowIsActive(requestContext: unknown): boolean { - return mutableRequestContext(requestContext)?.get(RESEARCH_WORKFLOW_ACTIVE_CONTEXT_KEY) === true; + return mutableRequestContext(requestContext)?.get(CONTEXT.researchWorkflowActive) === true; } function markResearchWorkflowActive(requestContext: unknown): () => void { @@ -143,15 +142,15 @@ function markResearchWorkflowActive(requestContext: unknown): () => void { if (!mutableContext) { return () => undefined; } - const hadPrevious = mutableContext.has(RESEARCH_WORKFLOW_ACTIVE_CONTEXT_KEY); - const previous = mutableContext.get(RESEARCH_WORKFLOW_ACTIVE_CONTEXT_KEY); - mutableContext.set(RESEARCH_WORKFLOW_ACTIVE_CONTEXT_KEY, true); + const hadPrevious = mutableContext.has(CONTEXT.researchWorkflowActive); + const previous = mutableContext.get(CONTEXT.researchWorkflowActive); + mutableContext.set(CONTEXT.researchWorkflowActive, true); return () => { if (hadPrevious) { - mutableContext.set(RESEARCH_WORKFLOW_ACTIVE_CONTEXT_KEY, previous); + mutableContext.set(CONTEXT.researchWorkflowActive, previous); return; } - mutableContext.delete(RESEARCH_WORKFLOW_ACTIVE_CONTEXT_KEY); + mutableContext.delete(CONTEXT.researchWorkflowActive); }; } diff --git a/packages/agent-core/src/mastra/tools/skill-tools.ts b/packages/agent-core/src/mastra/tool-defs/skill-tools.ts similarity index 99% rename from packages/agent-core/src/mastra/tools/skill-tools.ts rename to packages/agent-core/src/mastra/tool-defs/skill-tools.ts index 04b12df9..e023e94d 100644 --- a/packages/agent-core/src/mastra/tools/skill-tools.ts +++ b/packages/agent-core/src/mastra/tool-defs/skill-tools.ts @@ -3,7 +3,7 @@ import { createTool } from "@mastra/core/tools"; import { userSkillCreatorFromRequestContext, userSkillLoaderFromRequestContext, -} from "../system-prompt"; +} from "../user-skill-runtime"; import { requestContextFromToolContext } from "./tool-runtime-context"; import { skillCreateInputSchema, diff --git a/packages/agent-core/src/mastra/tools/tool-runtime-context.ts b/packages/agent-core/src/mastra/tool-defs/tool-runtime-context.ts similarity index 83% rename from packages/agent-core/src/mastra/tools/tool-runtime-context.ts rename to packages/agent-core/src/mastra/tool-defs/tool-runtime-context.ts index c019fad6..2ce2dcdb 100644 --- a/packages/agent-core/src/mastra/tools/tool-runtime-context.ts +++ b/packages/agent-core/src/mastra/tool-defs/tool-runtime-context.ts @@ -1,6 +1,6 @@ import { type CodeRuntimeContext, CodeRuntimeContextSchema } from "@cheatcode/sandbox-contracts"; -import { ResearchRuntimeContextSchema } from "@cheatcode/tools-research"; -import { EXA_API_KEY_CONTEXT_KEY, FIRECRAWL_API_KEY_CONTEXT_KEY } from "../research-context"; +import { ResearchRuntimeContextSchema } from "../../tools/research"; +import { CONTEXT } from "../context"; import { browserRuntimeFromRequestContext } from "./browser-runtime"; const requestContextReaderSchema = { @@ -28,7 +28,7 @@ export function requestContextFromToolContext(context: unknown): RequestContextR export function codeRuntimeFromContext(context: unknown): CodeRuntimeContext { const requestContext = requestContextFromToolContext(context); - return CodeRuntimeContextSchema.parse(requestContext.get("codeRuntime")); + return CodeRuntimeContextSchema.parse(requestContext.get(CONTEXT.codeRuntime)); } export async function workspaceRuntimeFromContext(context: unknown) { @@ -53,7 +53,7 @@ export function browserRuntimeFromContext(context: unknown) { export function researchRuntimeFromContext(context: unknown) { const requestContext = requestContextFromToolContext(context); return ResearchRuntimeContextSchema.parse({ - exaApiKey: requestContext.get(EXA_API_KEY_CONTEXT_KEY), - firecrawlApiKey: requestContext.get(FIRECRAWL_API_KEY_CONTEXT_KEY), + exaApiKey: requestContext.get(CONTEXT.exaApiKey), + firecrawlApiKey: requestContext.get(CONTEXT.firecrawlApiKey), }); } diff --git a/packages/agent-core/src/mastra/tools/tool-schemas.ts b/packages/agent-core/src/mastra/tool-defs/tool-schemas.ts similarity index 98% rename from packages/agent-core/src/mastra/tools/tool-schemas.ts rename to packages/agent-core/src/mastra/tool-defs/tool-schemas.ts index 1b509318..1572e173 100644 --- a/packages/agent-core/src/mastra/tools/tool-schemas.ts +++ b/packages/agent-core/src/mastra/tool-defs/tool-schemas.ts @@ -1,7 +1,7 @@ import { EnvironmentVariablesSchema } from "@cheatcode/sandbox-contracts"; import { SKILLS } from "@cheatcode/skills"; -import { WorkspacePathSchema } from "@cheatcode/tools-code"; import { z } from "zod/v4"; +import { WorkspacePathSchema } from "../../tools/code"; import { ResearchReportSchema } from "../workflows"; export const startDevServerInputSchema = z diff --git a/packages/agent-core/src/mastra/tools/tool-set.ts b/packages/agent-core/src/mastra/tool-defs/tool-set.ts similarity index 100% rename from packages/agent-core/src/mastra/tools/tool-set.ts rename to packages/agent-core/src/mastra/tool-defs/tool-set.ts diff --git a/packages/agent-core/src/mastra/tools/request-context.ts b/packages/agent-core/src/mastra/tools/request-context.ts deleted file mode 100644 index 261bcf21..00000000 --- a/packages/agent-core/src/mastra/tools/request-context.ts +++ /dev/null @@ -1,121 +0,0 @@ -import type { CodeRuntimeContext } from "@cheatcode/sandbox-contracts"; -import type { RunIntent } from "@cheatcode/types/api"; -import { RequestContext } from "@mastra/core/request-context"; -import { - COMPOSIO_API_KEY_CONTEXT_KEY, - COMPOSIO_CONNECTED_ACCOUNTS_CONTEXT_KEY, - COMPOSIO_QUOTA_METER_CONTEXT_KEY, - COMPOSIO_USER_ID_CONTEXT_KEY, - type ComposioConnectedAccounts, - type ComposioQuotaMeter, -} from "../composio-context"; -import { - ANTHROPIC_API_KEY_CONTEXT_KEY, - DEEPSEEK_API_KEY_CONTEXT_KEY, - GOOGLE_API_KEY_CONTEXT_KEY, - LLM_MODEL_ID_CONTEXT_KEY, - LLM_PROVIDER_CONTEXT_KEY, - type LlmProvider, - OPENAI_API_KEY_CONTEXT_KEY, - OPENROUTER_API_KEY_CONTEXT_KEY, -} from "../llm-context"; -import { EXA_API_KEY_CONTEXT_KEY, FIRECRAWL_API_KEY_CONTEXT_KEY } from "../research-context"; -import { - AGENT_DISPLAY_NAME_CONTEXT_KEY, - GLOBAL_MEMORY_CONTEXT_KEY, - PROMPT_PROJECT_MODE_CONTEXT_KEY, - PROMPT_TASK_MESSAGE_CONTEXT_KEY, - PROMPT_WORKSPACE_DIR_CONTEXT_KEY, - RUN_INTENT_CONTEXT_KEY, - USER_SKILL_CREATOR_CONTEXT_KEY, - USER_SKILL_LOADER_CONTEXT_KEY, - USER_SKILLS_CONTEXT_KEY, - type UserSkillCreator, - type UserSkillLoader, - type UserSkillRuntime, -} from "../system-prompt"; -import { BROWSER_RUN_ID_CONTEXT_KEY } from "./browser-runtime"; - -interface CodeRequestContextOptions { - agentDisplayName?: string | undefined; - anthropicApiKey?: string | undefined; - composioApiKey?: string | undefined; - composioConnectedAccounts?: ComposioConnectedAccounts | undefined; - composioQuotaMeter?: ComposioQuotaMeter | undefined; - composioUserId?: string | undefined; - deepseekApiKey?: string | undefined; - exaApiKey?: string | undefined; - firecrawlApiKey?: string | undefined; - globalMemory?: string | undefined; - googleApiKey?: string | undefined; - llmProvider?: LlmProvider | undefined; - modelId?: string | undefined; - openaiApiKey?: string | undefined; - openrouterApiKey?: string | undefined; - projectMode?: string | undefined; - runIntent?: RunIntent | undefined; - runId?: string | undefined; - taskMessage?: string | undefined; - userSkills?: UserSkillRuntime[] | undefined; - userSkillCreator?: UserSkillCreator | undefined; - userSkillLoader?: UserSkillLoader | undefined; -} - -export function createCodeRequestContext( - runtimeContext: CodeRuntimeContext, - options: CodeRequestContextOptions = {}, -): RequestContext { - const requestContext = new RequestContext(); - requestContext.set("codeRuntime", runtimeContext); - setOptionalContextValue( - requestContext, - PROMPT_WORKSPACE_DIR_CONTEXT_KEY, - runtimeContext.workspaceDir, - ); - for (const [key, value] of contextEntries(options)) { - setOptionalContextValue(requestContext, key, value); - } - return requestContext; -} - -function contextEntries( - options: CodeRequestContextOptions, -): ReadonlyArray { - return [ - [LLM_PROVIDER_CONTEXT_KEY, options.llmProvider], - [LLM_MODEL_ID_CONTEXT_KEY, options.modelId], - [AGENT_DISPLAY_NAME_CONTEXT_KEY, options.agentDisplayName], - [GLOBAL_MEMORY_CONTEXT_KEY, options.globalMemory], - [PROMPT_PROJECT_MODE_CONTEXT_KEY, options.projectMode], - [RUN_INTENT_CONTEXT_KEY, options.runIntent], - [PROMPT_TASK_MESSAGE_CONTEXT_KEY, options.taskMessage], - [ANTHROPIC_API_KEY_CONTEXT_KEY, options.anthropicApiKey], - [COMPOSIO_API_KEY_CONTEXT_KEY, options.composioApiKey], - [COMPOSIO_CONNECTED_ACCOUNTS_CONTEXT_KEY, options.composioConnectedAccounts], - [COMPOSIO_QUOTA_METER_CONTEXT_KEY, options.composioQuotaMeter], - [COMPOSIO_USER_ID_CONTEXT_KEY, options.composioUserId], - [OPENAI_API_KEY_CONTEXT_KEY, options.openaiApiKey], - [GOOGLE_API_KEY_CONTEXT_KEY, options.googleApiKey], - [OPENROUTER_API_KEY_CONTEXT_KEY, options.openrouterApiKey], - [DEEPSEEK_API_KEY_CONTEXT_KEY, options.deepseekApiKey], - [EXA_API_KEY_CONTEXT_KEY, options.exaApiKey], - [FIRECRAWL_API_KEY_CONTEXT_KEY, options.firecrawlApiKey], - [BROWSER_RUN_ID_CONTEXT_KEY, options.runId], - [USER_SKILLS_CONTEXT_KEY, options.userSkills], - [USER_SKILL_CREATOR_CONTEXT_KEY, options.userSkillCreator], - [USER_SKILL_LOADER_CONTEXT_KEY, options.userSkillLoader], - ]; -} - -function setOptionalContextValue( - requestContext: RequestContext, - key: string, - value: unknown, -): void { - if (typeof value === "string" && value.length === 0) { - return; - } - if (value !== undefined) { - requestContext.set(key, value); - } -} diff --git a/packages/agent-core/src/mastra/user-skill-runtime.ts b/packages/agent-core/src/mastra/user-skill-runtime.ts new file mode 100644 index 00000000..5b0c6d16 --- /dev/null +++ b/packages/agent-core/src/mastra/user-skill-runtime.ts @@ -0,0 +1,63 @@ +import { CONTEXT } from "./context"; + +/** Body-less custom-skill metadata carried on every run. */ +export interface UserSkillRuntime { + name: string; + description: string; + category?: string; +} + +/** A custom skill definition loaded only when `skill_invoke` selects it. */ +export interface UserSkillDefinition extends UserSkillRuntime { + body: string; + rootPath: string; +} + +/** Loads a single custom skill body from the request's user-scoped store. */ +export interface UserSkillLoader { + load(name: string): Promise; +} + +/** Validated metadata passed to the user-scoped Skill Creator persistence boundary. */ +export interface UserSkillCreateInput { + body: string; + category: string; + description: string; + name: string; + sourceSlug: string; + tags: string[]; +} + +/** Client-safe identity returned only after the complete skill package is durable. */ +export interface UserSkillCreateResult { + description: string; + filePath: string; + id: string; + name: string; + slug: string; +} + +/** Persists one authored skill package within the active user's run context. */ +export interface UserSkillCreator { + create(input: UserSkillCreateInput): Promise; +} + +export function userSkillLoaderFromRequestContext( + requestContext: { get(key: string): unknown } | undefined, +): UserSkillLoader | null { + const value = requestContext?.get(CONTEXT.userSkillLoader); + if (value && typeof (value as UserSkillLoader).load === "function") { + return value as UserSkillLoader; + } + return null; +} + +export function userSkillCreatorFromRequestContext( + requestContext: { get(key: string): unknown } | undefined, +): UserSkillCreator | null { + const value = requestContext?.get(CONTEXT.userSkillCreator); + if (value && typeof (value as UserSkillCreator).create === "function") { + return value as UserSkillCreator; + } + return null; +} diff --git a/packages/tools-browser/src/actions.ts b/packages/agent-core/src/tools/browser/actions.ts similarity index 96% rename from packages/tools-browser/src/actions.ts rename to packages/agent-core/src/tools/browser/actions.ts index e24c53ea..ef0656b0 100644 --- a/packages/tools-browser/src/actions.ts +++ b/packages/agent-core/src/tools/browser/actions.ts @@ -1,5 +1,5 @@ import { APIError } from "@cheatcode/observability"; -import { type ArtifactUploadResult, callSandboxMethod } from "@cheatcode/sandbox-contracts"; +import type { ArtifactUploadResult } from "@cheatcode/sandbox-contracts"; import { z } from "zod"; import type { BrowserRuntimeContext } from "./runtime"; import { BrowserRuntimeContextSchema } from "./runtime"; @@ -296,7 +296,7 @@ async function executeBrowserActions( async function readyBrowserDriver(runtimeContext: BrowserRuntimeContext) { const parsedRuntime = BrowserRuntimeContextSchema.parse(runtimeContext); - await callSandboxMethod(parsedRuntime.sandbox, "exec", { + await parsedRuntime.sandbox.exec({ command: [START_BROWSER_SCRIPT], timeoutMs: 30_000, }); @@ -340,14 +340,6 @@ async function signedBrowserDriverUrl( port: number, timeoutMs: number, ): Promise { - if (!runtimeContext.sandbox.getSignedPreviewUrl) { - throw new APIError( - 500, - "validation_tool_not_registered", - "Sandbox signed preview URLs are unavailable.", - { retriable: false }, - ); - } const signed = await runtimeContext.sandbox.getSignedPreviewUrl({ expiresInSeconds: Math.max(60, Math.ceil(timeoutMs / 1000) + 30), port, @@ -449,7 +441,7 @@ async function ensureBrowserDriverServer( } await stopBrowserDriverServer(runtimeContext, connection.processId); connection.port = await reserveBrowserDriverPort(runtimeContext, connection.processId); - await callSandboxMethod(runtimeContext.sandbox, "startProcess", { + await runtimeContext.sandbox.startProcess({ command: ["sudo", "-n", "-H", "-u", "cheatcode-browser", "--", DRIVER_LAUNCHER_PATH], cwd: "/workspace", processId: connection.processId, @@ -466,9 +458,11 @@ async function stopBrowserDriverServer( runtimeContext: BrowserRuntimeContext, processId: string, ): Promise { - await callSandboxMethod(runtimeContext.sandbox, "killProcess", { - processId, - }).catch(() => undefined); + await runtimeContext.sandbox + .killProcess({ + processId, + }) + .catch(() => undefined); } async function browserDriverServerIsHealthy( @@ -705,14 +699,6 @@ async function reserveBrowserDriverPort( runtimeContext: BrowserRuntimeContext, processId: string, ): Promise { - if (!runtimeContext.sandbox.allocateProcessPort) { - throw new APIError( - 500, - "validation_tool_not_registered", - "Sandbox process port allocator is missing.", - { retriable: false }, - ); - } return runtimeContext.sandbox.allocateProcessPort({ maxPort: DRIVER_PORT_MAX, minPort: DRIVER_PORT_BASE, diff --git a/packages/tools-browser/src/index.ts b/packages/agent-core/src/tools/browser/index.ts similarity index 100% rename from packages/tools-browser/src/index.ts rename to packages/agent-core/src/tools/browser/index.ts diff --git a/packages/tools-browser/src/runtime.ts b/packages/agent-core/src/tools/browser/runtime.ts similarity index 84% rename from packages/tools-browser/src/runtime.ts rename to packages/agent-core/src/tools/browser/runtime.ts index 55cea928..18d964b9 100644 --- a/packages/tools-browser/src/runtime.ts +++ b/packages/agent-core/src/tools/browser/runtime.ts @@ -7,6 +7,10 @@ import { import { z } from "zod"; export type BrowserProvider = "anthropic" | "google" | "openai"; +type BrowserSandbox = Pick< + SandboxLike, + "allocateProcessPort" | "exec" | "getSignedPreviewUrl" | "killProcess" | "startProcess" +>; interface BrowserCredential { apiKey: string; @@ -18,7 +22,7 @@ export interface BrowserRuntimeContext { artifacts?: ArtifactRuntime | undefined; credential: BrowserCredential; runId: string; - sandbox: SandboxLike; + sandbox: BrowserSandbox; } const BrowserCredentialSchema = z diff --git a/packages/tools-code/src/daytona-client.ts b/packages/agent-core/src/tools/code/daytona-client.ts similarity index 100% rename from packages/tools-code/src/daytona-client.ts rename to packages/agent-core/src/tools/code/daytona-client.ts diff --git a/packages/tools-code/src/files.ts b/packages/agent-core/src/tools/code/files.ts similarity index 89% rename from packages/tools-code/src/files.ts rename to packages/agent-core/src/tools/code/files.ts index 482f3565..ec09b77d 100644 --- a/packages/tools-code/src/files.ts +++ b/packages/agent-core/src/tools/code/files.ts @@ -1,5 +1,5 @@ import { APIError } from "@cheatcode/observability"; -import { callSandboxMethod, type getCodeRuntimeContext } from "@cheatcode/sandbox-contracts"; +import type { CodeRuntimeContextFor } from "@cheatcode/sandbox-contracts"; import { sandboxFileEntryShape } from "@cheatcode/types"; import { z } from "zod"; import { @@ -127,11 +127,11 @@ type DeleteFileOutput = z.infer; export async function executeReadFile( input: ReadFileInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"readFile">, ): Promise { const parsedInput = ReadFileInputSchema.parse(input); return ReadFileOutputSchema.parse( - await callSandboxMethod(runtimeContext.sandbox, "readFile", { + await runtimeContext.sandbox.readFile({ path: resolveProjectWorkspacePath(parsedInput.path, runtimeContext.workspaceDir), ...(parsedInput.encoding ? { encoding: parsedInput.encoding } : {}), }), @@ -140,11 +140,11 @@ export async function executeReadFile( export async function executeWriteFile( input: WriteFileInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"writeFile">, ): Promise { const parsedInput = WriteFileInputSchema.parse(input); const output = WriteFileOutputSchema.parse( - await callSandboxMethod(runtimeContext.sandbox, "writeFile", { + await runtimeContext.sandbox.writeFile({ path: resolveProjectWorkspacePath(parsedInput.path, runtimeContext.workspaceDir), content: parsedInput.content, encoding: parsedInput.encoding, @@ -162,11 +162,11 @@ export async function executeWriteFile( export async function executeListFiles( input: ListFilesInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"listFiles">, ): Promise { const parsedInput = ListFilesInputSchema.parse(input); return ListFilesOutputSchema.parse( - await callSandboxMethod(runtimeContext.sandbox, "listFiles", { + await runtimeContext.sandbox.listFiles({ path: resolveProjectWorkspacePath(parsedInput.path, runtimeContext.workspaceDir), includeHidden: parsedInput.includeHidden, recursive: parsedInput.recursive, @@ -176,11 +176,11 @@ export async function executeListFiles( export async function executeSearchFiles( input: SearchFilesInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"searchFiles">, ): Promise { const parsedInput = SearchFilesInputSchema.parse(input); return SearchFilesOutputSchema.parse( - await callSandboxMethod(runtimeContext.sandbox, "searchFiles", { + await runtimeContext.sandbox.searchFiles({ caseSensitive: parsedInput.caseSensitive, contextLines: parsedInput.contextLines, excludeDirs: parsedInput.excludeDirs, @@ -194,11 +194,11 @@ export async function executeSearchFiles( export async function executeDeleteFile( input: DeleteFileInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"deleteFile">, ): Promise { const parsedInput = DeleteFileInputSchema.parse(input); const output = DeleteFileOutputSchema.parse( - await callSandboxMethod(runtimeContext.sandbox, "deleteFile", { + await runtimeContext.sandbox.deleteFile({ path: resolveProjectWorkspacePath(parsedInput.path, runtimeContext.workspaceDir), recursive: parsedInput.recursive, }), diff --git a/packages/tools-code/src/git.ts b/packages/agent-core/src/tools/code/git.ts similarity index 92% rename from packages/tools-code/src/git.ts rename to packages/agent-core/src/tools/code/git.ts index 0de92e8b..5fbea0f7 100644 --- a/packages/tools-code/src/git.ts +++ b/packages/agent-core/src/tools/code/git.ts @@ -1,5 +1,5 @@ import { APIError } from "@cheatcode/observability"; -import type { getCodeRuntimeContext } from "@cheatcode/sandbox-contracts"; +import type { CodeRuntimeContextFor } from "@cheatcode/sandbox-contracts"; import { z } from "zod"; import { executeShellExec, ShellExecOutputSchema } from "./shell"; import { @@ -75,7 +75,7 @@ function isSafeCloneUrl(value: string): boolean { export async function executeGitStatus( input: GitStatusInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"exec">, ): Promise { return executeShellExec( { command: ["git", "status", "--short", "--branch"], cwd: input.cwd }, @@ -85,7 +85,7 @@ export async function executeGitStatus( export async function executeGitClone( input: GitCloneInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"exec">, ): Promise { const command = [ "git", @@ -102,7 +102,7 @@ export async function executeGitClone( /** Builds the exact add/commit sequence with repository-controlled hooks disabled. */ export function prepareGitCommit( input: GitCommitInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"exec">, ): PreparedGitCommit { const parsedInput = GitCommitInputSchema.parse(input); const gitSafetyConfig = ["-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null"]; @@ -118,7 +118,7 @@ export function prepareGitCommit( /** Executes only the prepared command sequence. */ export async function executePreparedGitCommit( prepared: PreparedGitCommit, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"exec">, ): Promise { await executeShellExec({ command: prepared.commands[0], cwd: prepared.cwd }, runtimeContext); return executeShellExec({ command: prepared.commands[1], cwd: prepared.cwd }, runtimeContext); @@ -127,7 +127,7 @@ export async function executePreparedGitCommit( /** Resolves the effective URL, branch, and source commit before execution. */ export async function prepareGitPush( input: GitPushInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"exec">, ): Promise { const parsedInput = GitPushInputSchema.parse(input); const cwd = resolveProjectWorkspacePath(parsedInput.cwd, runtimeContext.workspaceDir); @@ -154,7 +154,7 @@ export async function prepareGitPush( /** Executes only the prepared destination and commit. */ export async function executePreparedGitPush( prepared: PreparedGitPush, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"exec">, ): Promise { await assertNoPushUrlRewrites(prepared.cwd, runtimeContext); return executeShellExec( @@ -166,7 +166,7 @@ export async function executePreparedGitPush( async function resolvePushBranch( requestedBranch: string | undefined, cwd: string, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"exec">, ): Promise { const branch = requestedBranch ? requestedBranch @@ -189,7 +189,7 @@ async function resolvePushBranch( async function resolvePushUrl( remote: string, cwd: string, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"exec">, ): Promise { await assertNoPushUrlRewrites(cwd, runtimeContext); const output = await executeShellExec( @@ -210,7 +210,7 @@ async function resolvePushUrl( async function assertNoPushUrlRewrites( cwd: string, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"exec">, ): Promise { const output = await executeShellExec( { command: ["git", "config", "--includes", "--null", "--name-only", "--list"], cwd }, @@ -226,7 +226,7 @@ async function assertNoPushUrlRewrites( async function resolveSourceCommit( destinationRef: string, cwd: string, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"exec">, ): Promise { const output = await executeShellExec( { command: ["git", "rev-parse", "--verify", `${destinationRef}^{commit}`], cwd }, diff --git a/packages/tools-code/src/index.ts b/packages/agent-core/src/tools/code/index.ts similarity index 100% rename from packages/tools-code/src/index.ts rename to packages/agent-core/src/tools/code/index.ts diff --git a/packages/tools-code/src/preview.ts b/packages/agent-core/src/tools/code/preview.ts similarity index 94% rename from packages/tools-code/src/preview.ts rename to packages/agent-core/src/tools/code/preview.ts index c0611879..f929f196 100644 --- a/packages/tools-code/src/preview.ts +++ b/packages/agent-core/src/tools/code/preview.ts @@ -1,8 +1,7 @@ import { APIError, createLogger } from "@cheatcode/observability"; import { - callSandboxMethod, + type CodeRuntimeContextFor, EnvironmentVariablesSchema, - type getCodeRuntimeContext, type SandboxStartProcessInput, } from "@cheatcode/sandbox-contracts"; import { z } from "zod"; @@ -57,7 +56,7 @@ const APP_PREVIEW_SLOT_PREFIX = "app-preview:"; export async function executeStartDevServer( input: StartDevServerInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"allocateProjectPort" | "startProcess">, ): Promise { return executePreparedStartDevServer( await prepareStartDevServer(input, runtimeContext), @@ -68,7 +67,7 @@ export async function executeStartDevServer( /** Resolves dynamic port allocation and command normalization before execution. */ export async function prepareStartDevServer( input: StartDevServerInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"allocateProjectPort">, ): Promise { const parsedInput = StartDevServerInputSchema.parse(input); const cwd = resolveProjectWorkspacePath(parsedInput.cwd, runtimeContext.workspaceDir); @@ -97,9 +96,9 @@ export async function prepareStartDevServer( /** Executes only the fully resolved process plan. */ export async function executePreparedStartDevServer( prepared: PreparedStartDevServer, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"startProcess">, ): Promise { - const process = await callSandboxMethod(runtimeContext.sandbox, "startProcess", prepared.process); + const process = await runtimeContext.sandbox.startProcess(prepared.process); return StartDevServerOutputSchema.parse({ processId: process.id, pid: process.pid, @@ -174,15 +173,11 @@ function deriveWorkspaceSlug(cwd: string): string { // one port would kill each other's dev server via deleteProcessesOnPort — so if the allocator is // unavailable or errors, fail the dev-server start loudly instead of returning a shared port. async function allocateDevServerPort( - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"allocateProjectPort">, slug: string, isMobile: boolean, ): Promise { const logger = createLogger(); - if (!runtimeContext.sandbox.allocateProjectPort) { - logger.error("dev_server_port_alloc_missing", { slug }); - throw devServerPortAllocationError(slug); - } try { const port = await runtimeContext.sandbox.allocateProjectPort({ projectId: slug, diff --git a/packages/tools-code/src/run-code.ts b/packages/agent-core/src/tools/code/run-code.ts similarity index 92% rename from packages/tools-code/src/run-code.ts rename to packages/agent-core/src/tools/code/run-code.ts index 80b79b87..6f0ec328 100644 --- a/packages/tools-code/src/run-code.ts +++ b/packages/agent-core/src/tools/code/run-code.ts @@ -1,5 +1,5 @@ import { APIError } from "@cheatcode/observability"; -import type { getCodeRuntimeContext } from "@cheatcode/sandbox-contracts"; +import type { CodeRuntimeContextFor } from "@cheatcode/sandbox-contracts"; import { z } from "zod"; import { resolveProjectWorkspacePath } from "./workspace-paths"; @@ -26,7 +26,7 @@ export type RunCodeOutput = z.infer; export async function executeRunCode( input: RunCodeInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"runCode">, ): Promise { const parsedInput = RunCodeInputSchema.parse(input); const result = await runtimeContext.sandbox.runCode({ diff --git a/packages/tools-code/src/shell.ts b/packages/agent-core/src/tools/code/shell.ts similarity index 90% rename from packages/tools-code/src/shell.ts rename to packages/agent-core/src/tools/code/shell.ts index d718c439..c2614daa 100644 --- a/packages/tools-code/src/shell.ts +++ b/packages/agent-core/src/tools/code/shell.ts @@ -1,8 +1,7 @@ import { APIError } from "@cheatcode/observability"; import { - callSandboxMethod, + type CodeRuntimeContextFor, EnvironmentVariablesSchema, - type getCodeRuntimeContext, } from "@cheatcode/sandbox-contracts"; import { z } from "zod"; import { resolveProjectWorkspacePath, WorkspacePathSchema } from "./workspace-paths"; @@ -110,10 +109,10 @@ export type ShellTerminalInput = z.input; export async function executeShellExec( input: ShellExecInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"exec">, ): Promise { const parsedInput = ShellExecInputSchema.parse(input); - const result = await callSandboxMethod(runtimeContext.sandbox, "exec", { + const result = await runtimeContext.sandbox.exec({ command: parsedInput.command, cwd: resolveProjectWorkspacePath(parsedInput.cwd, runtimeContext.workspaceDir), ...(parsedInput.env ? { env: parsedInput.env } : {}), @@ -132,7 +131,7 @@ export async function executeShellExec( export async function executeShellStartProcess( input: ShellStartProcessInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"startProcess">, ): Promise { const parsedInput = ShellStartProcessInputSchema.parse(input); const waitForPort = parsedInput.waitForPort @@ -145,7 +144,7 @@ export async function executeShellStartProcess( } : undefined; return ShellProcessOutputSchema.parse( - await callSandboxMethod(runtimeContext.sandbox, "startProcess", { + await runtimeContext.sandbox.startProcess({ command: parsedInput.command, cwd: resolveProjectWorkspacePath(parsedInput.cwd, runtimeContext.workspaceDir), ...(parsedInput.env ? { env: parsedInput.env } : {}), @@ -161,11 +160,11 @@ export async function executeShellStartProcess( export async function executeShellKillProcess( input: ShellKillProcessInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"killProcess">, ): Promise { const parsedInput = ShellKillProcessInputSchema.parse(input); return ShellKillProcessOutputSchema.parse( - await callSandboxMethod(runtimeContext.sandbox, "killProcess", { + await runtimeContext.sandbox.killProcess({ processId: parsedInput.processId, }), ); @@ -173,11 +172,11 @@ export async function executeShellKillProcess( export async function executeShellTerminal( input: ShellTerminalInput, - runtimeContext: ReturnType, + runtimeContext: CodeRuntimeContextFor<"exec">, ): Promise { const parsedInput = ShellTerminalInputSchema.parse(input); return ShellExecOutputSchema.parse( - await callSandboxMethod(runtimeContext.sandbox, "exec", { + await runtimeContext.sandbox.exec({ command: ["sh", "-lc", parsedInput.command], cwd: resolveProjectWorkspacePath(parsedInput.cwd, runtimeContext.workspaceDir), timeoutMs: parsedInput.timeoutMs, diff --git a/packages/tools-code/src/workspace-paths.ts b/packages/agent-core/src/tools/code/workspace-paths.ts similarity index 100% rename from packages/tools-code/src/workspace-paths.ts rename to packages/agent-core/src/tools/code/workspace-paths.ts diff --git a/packages/agent-core/src/tools/docs/execute.ts b/packages/agent-core/src/tools/docs/execute.ts index fc7d2b59..4856b758 100644 --- a/packages/agent-core/src/tools/docs/execute.ts +++ b/packages/agent-core/src/tools/docs/execute.ts @@ -153,7 +153,7 @@ async function writeWorkspaceArtifact( try { // The run's own project folder is authoritative in the shared per-user sandbox — write the // live file there so it shows in THIS project's Files/Computer tab instead of leaking into a - // sibling project's /workspace/ (matches resolveWorkspaceDir in tools-code). + // sibling project's /workspace/ (matches the code tool's resolveWorkspaceDir). const directory = runtimeContext.workspaceDir ?? WORKSPACE_ROOT; await runtimeContext.sandbox.writeFile({ content: artifact.base64, diff --git a/packages/tools-research/src/exa.ts b/packages/agent-core/src/tools/research/exa.ts similarity index 100% rename from packages/tools-research/src/exa.ts rename to packages/agent-core/src/tools/research/exa.ts diff --git a/packages/tools-research/src/firecrawl.ts b/packages/agent-core/src/tools/research/firecrawl.ts similarity index 100% rename from packages/tools-research/src/firecrawl.ts rename to packages/agent-core/src/tools/research/firecrawl.ts diff --git a/packages/tools-research/src/index.ts b/packages/agent-core/src/tools/research/index.ts similarity index 100% rename from packages/tools-research/src/index.ts rename to packages/agent-core/src/tools/research/index.ts diff --git a/packages/tools-research/src/output-limiter.ts b/packages/agent-core/src/tools/research/output-limiter.ts similarity index 100% rename from packages/tools-research/src/output-limiter.ts rename to packages/agent-core/src/tools/research/output-limiter.ts diff --git a/packages/tools-research/src/provider-http.ts b/packages/agent-core/src/tools/research/provider-http.ts similarity index 100% rename from packages/tools-research/src/provider-http.ts rename to packages/agent-core/src/tools/research/provider-http.ts diff --git a/packages/tools-research/src/runtime.ts b/packages/agent-core/src/tools/research/runtime.ts similarity index 100% rename from packages/tools-research/src/runtime.ts rename to packages/agent-core/src/tools/research/runtime.ts diff --git a/packages/tools-research/src/schemas.ts b/packages/agent-core/src/tools/research/schemas.ts similarity index 100% rename from packages/tools-research/src/schemas.ts rename to packages/agent-core/src/tools/research/schemas.ts diff --git a/packages/auth/README.md b/packages/auth/README.md index 40e6cfe1..d76a9704 100644 --- a/packages/auth/README.md +++ b/packages/auth/README.md @@ -42,8 +42,8 @@ pnpm --filter @cheatcode/auth lint ## Env -Callers pass `CLERK_SECRET_KEY`, `CLERK_JWT_KEY`, or -`PREVIEW_TOKEN_SECRET` from their validated Worker env. Sandbox skill-runtime +Callers pass `CLERK_SECRET_KEY` or `PREVIEW_TOKEN_SECRET` from their validated +Worker env. Sandbox skill-runtime access uses random per-run opaque capabilities whose digests and exact scopes live on the tenant-scoped agent-run row shared by local and production Workers; this package owns their strict token format, generation, parsing, and diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts index 2d887f4e..fa13292d 100644 --- a/packages/db/src/client.ts +++ b/packages/db/src/client.ts @@ -71,6 +71,11 @@ function createDb( return { db, close: () => closeDatabase(db, pool) }; } +/** Creates a request-scoped database handle that the caller must close. */ +export function createDatabaseHandle(env: DatabaseEnvironment): DatabaseHandle { + return createDb(env.HYPERDRIVE, databaseContextConfig(env)); +} + export async function withUserContext( source: UserContextSource, internalUserId: UserId, @@ -99,7 +104,7 @@ export async function withDatabase( fn: (handle: DatabaseHandle) => Promise, closeHandle: CloseDatabaseHandle = closeDatabaseHandle, ): Promise { - const handle = createDb(env.HYPERDRIVE, databaseContextConfig(env)); + const handle = createDatabaseHandle(env); try { return await fn(handle); } finally { @@ -108,13 +113,19 @@ export async function withDatabase( } export async function withUserDb( - env: DatabaseEnvironment, + source: DatabaseEnvironment | DatabaseHandle, userId: UserId, fn: (session: UserDatabaseSession) => Promise, closeHandle?: CloseDatabaseHandle, ): Promise { + if (isDatabaseHandle(source)) { + return fn({ + handle: source, + transaction: (operation) => withUserContext(source, userId, operation), + }); + } return withDatabase( - env, + source, (handle) => fn({ handle, @@ -124,6 +135,10 @@ export async function withUserDb( ); } +function isDatabaseHandle(source: DatabaseEnvironment | DatabaseHandle): source is DatabaseHandle { + return "db" in source && "close" in source; +} + function databaseContextConfig(env: DatabaseEnvironment): DatabaseContextConfig { if ("DATABASE_CONTEXT_SIGNING_SECRET_AGENT" in env) { return { diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 50a707e4..51eaaf3d 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -39,7 +39,7 @@ export type { UserContextDatabase, UserDatabaseSession, } from "./client"; -export { withDatabase, withUserContext, withUserDb } from "./client"; +export { createDatabaseHandle, withDatabase, withUserContext, withUserDb } from "./client"; export type { UserIntegrationRecord } from "./integrations"; export { deleteUserIntegrationAccount, diff --git a/packages/env/README.md b/packages/env/README.md index 2f029ee6..358d0441 100644 --- a/packages/env/README.md +++ b/packages/env/README.md @@ -9,6 +9,7 @@ read raw `process.env` values only to pass them into the pure `./web-config` val - `./web-config`: pure Vercel target and public web build validators used by both `next.config.ts` and `./web` - `./worker`: Cloudflare Worker binding schemas and Secrets Store resolution helpers +- `./preview-proxy`: the strict preview-proxy schema - `./migrate`: administrative database identity pins ## Code Checks @@ -29,8 +30,9 @@ Gateway, release-SHA, deployment-target, and Clerk publishable-key validation has one canonical implementation in `./web-config`. The framework config reads the raw build environment and delegates every parse and invariant to that module; missing public build values fail instead of receiving compatibility defaults. -The preview apex is not an environment variable: it is `localhost` for local -builds and the owned `trycheatcode.com` apex for Vercel builds. +The canonical production app origin is exported from the shared config layer. +Production preview hostname, preview app origin, and Clerk authorized parties +derive from it; local development supplies explicit loopback overrides. Because Next runs from `apps/web`, its config uses Next's official `@next/env` loader to read the repository-root `.env.local`, then immediately removes every loaded value outside the explicit web allowlist. Local builds therefore keep one @@ -43,9 +45,9 @@ remote Vercel targets so local development and optimized local QA can reach Wran `VERCEL_TARGET_ENV` selects the remote build-time validation branch for prebuilt artifacts. `VERCEL_URL` remains optional during that build and becomes mandatory only when the actual `VERCEL_ENV` is `production` or `preview` at runtime. -`PREVIEW_HOSTNAME` is required by both preview-producing Workers, normalized to a -multi-label DNS hostname (or `localhost:8787` in development), and production rejects a -port-bearing value. Moving previews to a different registrable site requires an atomic +`PREVIEW_HOSTNAME` is an explicit local override normalized to a multi-label DNS +hostname (or `localhost:8787` in development); production derives the canonical +hostname and rejects a port-bearing or different value. Moving previews to a different registrable site requires an atomic DNS/Worker/web-release change; there is no browser environment override or legacy hostname fallback. diff --git a/packages/env/src/agent-worker.ts b/packages/env/src/agent-worker.ts new file mode 100644 index 00000000..017535cf --- /dev/null +++ b/packages/env/src/agent-worker.ts @@ -0,0 +1,49 @@ +import { z } from "zod"; +import { + AnalyticsBindingsSchema, + DEFAULT_DAYTONA_TARGET, + DurableObjectNamespaceBindingSchema, + HyperdriveSchema, + KvNamespaceBindingSchema, + OptionalWorkerSecretSchema, + PreviewHostnameSchema, + R2BucketBindingSchema, + requireProductionDaytonaOrg, + requireProductionPreviewHostname, + requireProductionReleaseSha, + WorkerReleaseBindingsSchema, + WorkerSecretSchema, + WorkflowBindingSchema, +} from "./worker-shared"; + +export const AgentWorkerEnvSchema = z + .object({ + ...AnalyticsBindingsSchema, + ...WorkerReleaseBindingsSchema, + AGENT_RUN: DurableObjectNamespaceBindingSchema, + AGENT_RUN_WORKFLOW: WorkflowBindingSchema, + COMPOSIO_API_KEY: OptionalWorkerSecretSchema, + DATABASE_CONTEXT_SIGNING_SECRET_AGENT: WorkerSecretSchema, + DAYTONA_API_KEY: WorkerSecretSchema, + DAYTONA_API_URL: z.string().url(), + DAYTONA_ORG_ID: z.string().uuid().optional(), + DAYTONA_PREVIEW_HOST_SUFFIXES: z.string().min(1).max(1_024).optional(), + DAYTONA_SANDBOX_SNAPSHOT: z.string().min(1), + DAYTONA_TARGET: z.string().min(1).default(DEFAULT_DAYTONA_TARGET), + DAYTONA_WORKSPACE_VOLUME: z.string().min(1).max(100), + DEEPSEEK_PLATFORM_API_KEY: OptionalWorkerSecretSchema, + HYPERDRIVE: HyperdriveSchema, + OUTPUT_DOWNLOAD_BASE_URL: z.string().url().optional(), + OUTPUT_DOWNLOAD_SIGNING_SECRET: WorkerSecretSchema, + PREVIEW_HOSTNAME: PreviewHostnameSchema.optional(), + PREVIEW_TOKEN_SECRET: WorkerSecretSchema, + PROJECT_SANDBOX: DurableObjectNamespaceBindingSchema, + QUOTA_TRACKER: DurableObjectNamespaceBindingSchema, + R2_AUDIT: R2BucketBindingSchema, + R2_OUTPUTS: R2BucketBindingSchema, + SANDBOX_STATE: KvNamespaceBindingSchema.optional(), + }) + .strict() + .superRefine(requireProductionReleaseSha) + .superRefine(requireProductionDaytonaOrg) + .superRefine(requireProductionPreviewHostname); diff --git a/packages/env/src/gateway-worker.ts b/packages/env/src/gateway-worker.ts new file mode 100644 index 00000000..ba725746 --- /dev/null +++ b/packages/env/src/gateway-worker.ts @@ -0,0 +1,52 @@ +import { z } from "zod"; +import { PRODUCTION_APP_ORIGIN } from "./web-config"; +import { + AnalyticsBindingsSchema, + DurableObjectNamespaceBindingSchema, + FetcherBindingSchema, + HyperdriveSchema, + KvNamespaceBindingSchema, + OptionalWorkerSecretSchema, + requireProductionReleaseSha, + WorkerReleaseBindingsSchema, + WorkerSecretSchema, +} from "./worker-shared"; + +export const GatewayWorkerEnvSchema = z + .object({ + ...AnalyticsBindingsSchema, + ...WorkerReleaseBindingsSchema, + AGENT: FetcherBindingSchema, + CLERK_AUTHORIZED_PARTIES: z.string().trim().min(1).max(2_048).optional(), + CLERK_SECRET_KEY: OptionalWorkerSecretSchema, + COMPOSIO_API_KEY: OptionalWorkerSecretSchema, + COMPOSIO_AUTH_CONFIGS: OptionalWorkerSecretSchema, + DATABASE_CONTEXT_SIGNING_SECRET_GATEWAY: WorkerSecretSchema, + ENTITLEMENTS_CACHE: KvNamespaceBindingSchema, + HYPERDRIVE: HyperdriveSchema, + IDEMPOTENCY: DurableObjectNamespaceBindingSchema, + POLAR_ACCESS_TOKEN: OptionalWorkerSecretSchema, + POLAR_PRODUCT_ID_PRO: z.string().min(1).optional(), + POLAR_PRODUCT_ID_PREMIUM: z.string().min(1).optional(), + POLAR_SERVER: z.enum(["production", "sandbox"]).optional(), + PREVIEW_PROXY: FetcherBindingSchema.optional(), + QUOTA_TRACKER: DurableObjectNamespaceBindingSchema, + RATE_LIMITER: DurableObjectNamespaceBindingSchema, + RESOURCE_DELETION: FetcherBindingSchema, + WEBHOOKS: FetcherBindingSchema, + }) + .strict() + .superRefine(requireProductionReleaseSha) + .superRefine((env, context) => { + if ( + env.CHEATCODE_ENVIRONMENT === "production" && + env.CLERK_AUTHORIZED_PARTIES !== undefined && + env.CLERK_AUTHORIZED_PARTIES !== PRODUCTION_APP_ORIGIN + ) { + context.addIssue({ + code: "custom", + message: "Production Clerk authorized parties derive from the canonical app origin", + path: ["CLERK_AUTHORIZED_PARTIES"], + }); + } + }); diff --git a/packages/env/src/index.ts b/packages/env/src/index.ts index 4702fdd2..b442576f 100644 --- a/packages/env/src/index.ts +++ b/packages/env/src/index.ts @@ -1,10 +1,14 @@ -export type { CloudflareVersionMetadata, WorkerSecret } from "./worker"; +export { + PRODUCTION_APP_ORIGIN, + PRODUCTION_CLERK_AUTHORIZED_PARTIES, +} from "./web-config"; +export type { CloudflareVersionMetadata, PreviewProxyEnv, WorkerSecret } from "./worker"; export { AgentWorkerEnvSchema, + DEFAULT_DAYTONA_TARGET, GatewayWorkerEnvSchema, - PreviewHostnameSchema, + PreviewProxyEnvSchema, + previewHostnameForWorker, resolveWorkerSecret, WebhooksWorkerEnvSchema, - WorkerReleaseBindingsSchema, - WorkerSecretSchema, } from "./worker"; diff --git a/packages/env/src/preview-proxy.ts b/packages/env/src/preview-proxy.ts new file mode 100644 index 00000000..0e71ed53 --- /dev/null +++ b/packages/env/src/preview-proxy.ts @@ -0,0 +1,105 @@ +import { z } from "zod"; +import { PRODUCTION_APP_HOSTNAME, PRODUCTION_APP_ORIGIN } from "./web-config"; +import { + AnalyticsBindingsSchema, + PreviewHostnameSchema, + requireProductionPreviewHostname, + requireProductionReleaseSha, + WorkerReleaseBindingsSchema, + WorkerSecretSchema, +} from "./worker-shared"; + +export const PreviewProxyEnvSchema = z + .object({ + ...AnalyticsBindingsSchema, + ...WorkerReleaseBindingsSchema, + CHEATCODE_APP_ORIGIN: z.string().url().optional(), + DAYTONA_API_KEY: WorkerSecretSchema, + DAYTONA_API_URL: z.string().url(), + DAYTONA_PREVIEW_HOST_SUFFIXES: z.string().min(1).max(1_024).optional(), + PREVIEW_HOSTNAME: PreviewHostnameSchema.optional(), + PREVIEW_TOKEN_SECRET: WorkerSecretSchema, + }) + .strict() + .transform((env, context) => { + const appOrigin = + env.CHEATCODE_APP_ORIGIN ?? + (env.CHEATCODE_ENVIRONMENT === "production" ? PRODUCTION_APP_ORIGIN : undefined); + const previewHostname = + env.PREVIEW_HOSTNAME ?? + (env.CHEATCODE_ENVIRONMENT === "production" ? PRODUCTION_APP_HOSTNAME : undefined); + if (!appOrigin) { + context.addIssue({ + code: "custom", + message: "Preview application origin is required", + path: ["CHEATCODE_APP_ORIGIN"], + }); + return z.NEVER; + } + if (!previewHostname) { + context.addIssue({ + code: "custom", + message: "Preview hostname is required", + path: ["PREVIEW_HOSTNAME"], + }); + return z.NEVER; + } + return { + ...env, + CHEATCODE_APP_ORIGIN: appOrigin, + PREVIEW_HOSTNAME: previewHostname, + }; + }) + .superRefine(requireProductionReleaseSha) + .superRefine((env, context) => { + if (!isExactAppOrigin(env.CHEATCODE_APP_ORIGIN, env.CHEATCODE_ENVIRONMENT)) { + context.addIssue({ + code: "custom", + message: "Preview application origin must be an exact trusted HTTP(S) origin", + path: ["CHEATCODE_APP_ORIGIN"], + }); + } + if ( + env.CHEATCODE_ENVIRONMENT === "production" && + env.CHEATCODE_APP_ORIGIN !== PRODUCTION_APP_ORIGIN + ) { + context.addIssue({ + code: "custom", + message: "Production previews derive from the canonical application origin", + path: ["CHEATCODE_APP_ORIGIN"], + }); + } + requireProductionPreviewHostname( + { + CHEATCODE_ENVIRONMENT: env.CHEATCODE_ENVIRONMENT, + PREVIEW_HOSTNAME: env.PREVIEW_HOSTNAME, + }, + context, + ); + }); + +export type PreviewProxyEnv = z.output; + +function isExactAppOrigin(value: string, environment: "development" | "production"): boolean { + try { + const url = new URL(value); + if ( + url.origin !== value || + url.pathname !== "/" || + url.search || + url.hash || + url.username || + url.password + ) { + return false; + } + if (environment === "production") { + return url.protocol === "https:"; + } + return ( + url.protocol === "http:" && (url.hostname === "localhost" || url.hostname === "127.0.0.1") + ); + } catch { + return false; + } +} diff --git a/packages/env/src/web-config.ts b/packages/env/src/web-config.ts index cb9b0609..a362b331 100644 --- a/packages/env/src/web-config.ts +++ b/packages/env/src/web-config.ts @@ -1,8 +1,12 @@ import { z } from "zod"; +/** Canonical public application origin for every production surface. */ +export const PRODUCTION_APP_ORIGIN = "https://trycheatcode.com"; +export const PRODUCTION_APP_HOSTNAME = new URL(PRODUCTION_APP_ORIGIN).hostname; +export const PRODUCTION_CLERK_AUTHORIZED_PARTIES = [PRODUCTION_APP_ORIGIN] as const; + const PRODUCTION_CLERK_FRONTEND_HOSTNAME = "clerk.trycheatcode.com"; const PRODUCTION_GATEWAY_ORIGIN = "https://gateway.trycheatcode.com"; -const PRODUCTION_PREVIEW_HOSTNAME = "trycheatcode.com"; const LOCAL_PREVIEW_HOSTNAME = "localhost"; const VercelEnvironmentSchema = z.enum(["development", "preview", "production"]); const OptionalVercelEnvironmentSchema = VercelEnvironmentSchema.optional(); @@ -143,7 +147,7 @@ export function parseWebBuildEnvironment(input: WebBuildEnvironmentInput) { } export function previewHostnameForDeployment(deployment: WebDeployment): string { - return deployment.isVercelDeployment ? PRODUCTION_PREVIEW_HOSTNAME : LOCAL_PREVIEW_HOSTNAME; + return deployment.isVercelDeployment ? PRODUCTION_APP_HOSTNAME : LOCAL_PREVIEW_HOSTNAME; } function clerkPublishableConfigurationSchema(deployment: WebDeployment) { diff --git a/packages/env/src/webhooks-worker.ts b/packages/env/src/webhooks-worker.ts new file mode 100644 index 00000000..7cddbbbb --- /dev/null +++ b/packages/env/src/webhooks-worker.ts @@ -0,0 +1,43 @@ +import { z } from "zod"; +import { + AnalyticsBindingsSchema, + DurableObjectNamespaceBindingSchema, + FetcherBindingSchema, + HyperdriveSchema, + KvNamespaceBindingSchema, + OptionalWorkerSecretSchema, + R2BucketBindingSchema, + requireProductionReleaseSha, + WorkerReleaseBindingsSchema, + WorkerSecretSchema, + WorkflowBindingSchema, +} from "./worker-shared"; + +export const WebhooksWorkerEnvSchema = z + .object({ + ...AnalyticsBindingsSchema, + ...WorkerReleaseBindingsSchema, + AGENT_LIFECYCLE: FetcherBindingSchema, + CLERK_WEBHOOK_SIGNING_SECRET: OptionalWorkerSecretSchema, + COMPOSIO_API_KEY: OptionalWorkerSecretSchema, + COMPOSIO_WEBHOOK_SECRET: OptionalWorkerSecretSchema, + DAILY_MAINTENANCE_WORKFLOW: WorkflowBindingSchema, + DATABASE_CONTEXT_SIGNING_SECRET_WEBHOOKS: WorkerSecretSchema, + DAYTONA_WEBHOOK_SIGNING_SECRET: WorkerSecretSchema, + ENTITLEMENTS_CACHE: KvNamespaceBindingSchema, + HYPERDRIVE: HyperdriveSchema, + POLAR_ACCESS_TOKEN: OptionalWorkerSecretSchema, + POLAR_PRODUCT_ID_PREMIUM: z.string().min(1).optional(), + POLAR_PRODUCT_ID_PRO: z.string().min(1).optional(), + POLAR_SERVER: z.enum(["production", "sandbox"]).optional(), + POLAR_WEBHOOK_SECRET: OptionalWorkerSecretSchema, + QUOTA_TRACKER: DurableObjectNamespaceBindingSchema, + R2_OUTPUTS: R2BucketBindingSchema, + RESOURCE_DELETION_WORKFLOW: WorkflowBindingSchema, + SANDBOX_STATE: KvNamespaceBindingSchema.optional(), + USER_DELETION_WORKFLOW: WorkflowBindingSchema, + WEBHOOK_IDEMPOTENCY: DurableObjectNamespaceBindingSchema, + WEBHOOK_WORKFLOW: WorkflowBindingSchema, + }) + .strict() + .superRefine(requireProductionReleaseSha); diff --git a/packages/env/src/worker-shared.ts b/packages/env/src/worker-shared.ts new file mode 100644 index 00000000..0e2dfd2b --- /dev/null +++ b/packages/env/src/worker-shared.ts @@ -0,0 +1,235 @@ +import { z } from "zod"; +import { PRODUCTION_APP_HOSTNAME } from "./web-config"; + +export type WorkerSecret = string | SecretsStoreSecret; +export const DEFAULT_DAYTONA_TARGET = "us"; + +export interface CloudflareVersionMetadata { + id: string; + tag: string; + timestamp: string; +} + +function hasBindingMethods(value: unknown, methods: readonly string[]): value is object { + if (value === null || (typeof value !== "object" && typeof value !== "function")) { + return false; + } + try { + return methods.every((method) => typeof Reflect.get(value, method) === "function"); + } catch { + return false; + } +} + +function isSecretsStoreSecret(value: unknown): value is SecretsStoreSecret { + return hasBindingMethods(value, ["get"]); +} + +function isAnalyticsDatasetBinding(value: unknown): boolean { + return hasBindingMethods(value, ["writeDataPoint"]); +} + +function isFetcherBinding(value: unknown): value is Fetcher { + return hasBindingMethods(value, ["fetch"]); +} + +function isKvNamespaceBinding(value: unknown): value is KVNamespace { + return hasBindingMethods(value, ["get", "put", "delete", "list"]); +} + +function isDurableObjectNamespaceBinding(value: unknown): value is DurableObjectNamespace { + return hasBindingMethods(value, [ + "newUniqueId", + "idFromName", + "idFromString", + "get", + "getByName", + "jurisdiction", + ]); +} + +function isR2BucketBinding(value: unknown): value is R2Bucket { + return hasBindingMethods(value, [ + "head", + "get", + "put", + "delete", + "list", + "createMultipartUpload", + "resumeMultipartUpload", + ]); +} + +function isWorkflowBinding(value: unknown): value is Workflow { + return hasBindingMethods(value, ["get", "create", "createBatch"]); +} + +export const WorkerSecretSchema = z.union([ + z.string().min(1), + z.custom(isSecretsStoreSecret), +]); + +export const OptionalWorkerSecretSchema = z.preprocess( + (value) => (value === "" ? undefined : value), + WorkerSecretSchema.optional(), +); + +const AnalyticsDatasetBindingSchema = z.custom<{ + writeDataPoint(point: { blobs?: string[]; doubles?: number[]; indexes?: string[] }): void; +}>(isAnalyticsDatasetBinding, "Expected a Cloudflare Analytics Engine dataset binding"); +export const DurableObjectNamespaceBindingSchema = z.custom( + isDurableObjectNamespaceBinding, + "Expected a Cloudflare Durable Object namespace binding", +); +export const FetcherBindingSchema = z.custom( + isFetcherBinding, + "Expected a Cloudflare service binding", +); +export const KvNamespaceBindingSchema = z.custom( + isKvNamespaceBinding, + "Expected a Cloudflare KV namespace binding", +); +export const R2BucketBindingSchema = z.custom( + isR2BucketBinding, + "Expected a Cloudflare R2 bucket binding", +); +export const WorkflowBindingSchema = z.custom>( + isWorkflowBinding, + "Expected a Cloudflare Workflow binding", +); + +export const PreviewHostnameSchema = z + .string() + .trim() + .toLowerCase() + .refine( + (hostname) => + hostname === "localhost" || + hostname === "localhost:8787" || + isMultiLabelDnsHostname(hostname), + "Preview hostname must be localhost, localhost:8787, or a multi-label DNS hostname", + ); + +export const HyperdriveSchema = z + .object({ + connectionString: z.string().min(1), + }) + .passthrough(); + +export const AnalyticsBindingsSchema = { + AGENT_METRICS: AnalyticsDatasetBindingSchema.optional(), + ERROR_EVENTS: AnalyticsDatasetBindingSchema.optional(), + PERFORMANCE_METRICS: AnalyticsDatasetBindingSchema.optional(), + USER_EVENTS: AnalyticsDatasetBindingSchema.optional(), +} as const; + +export const WorkerReleaseBindingsSchema = { + CF_VERSION_METADATA: z + .object({ + id: z.string().min(1), + tag: z.string(), + timestamp: z.string().min(1), + }) + .passthrough() + .optional(), + CHEATCODE_ENVIRONMENT: z.enum(["development", "production"]), + CHEATCODE_RELEASE_SHA: z + .string() + .regex(/^[0-9a-f]{40}$/u) + .optional(), +} as const; + +export interface WorkerReleaseIdentity { + CHEATCODE_ENVIRONMENT: "development" | "production"; + CHEATCODE_RELEASE_SHA?: string | undefined; +} + +export function requireProductionReleaseSha( + bindings: WorkerReleaseIdentity, + context: z.RefinementCtx, +): void { + if (bindings.CHEATCODE_ENVIRONMENT === "production" && !bindings.CHEATCODE_RELEASE_SHA) { + context.addIssue({ + code: "custom", + message: "Production Workers require an immutable release SHA.", + path: ["CHEATCODE_RELEASE_SHA"], + }); + } +} + +export function requireProductionDaytonaOrg( + bindings: WorkerReleaseIdentity & { DAYTONA_ORG_ID?: string | undefined }, + context: z.RefinementCtx, +): void { + if (bindings.CHEATCODE_ENVIRONMENT === "production" && !bindings.DAYTONA_ORG_ID) { + context.addIssue({ + code: "custom", + message: "Production sandbox Workers require the pinned Daytona organization ID.", + path: ["DAYTONA_ORG_ID"], + }); + } +} + +export async function resolveWorkerSecret( + secret: WorkerSecret | undefined, +): Promise { + if (!secret) { + return undefined; + } + if (typeof secret === "string") { + return secret; + } + return secret.get(); +} + +export function requireProductionPreviewHostname( + bindings: { + CHEATCODE_ENVIRONMENT: "development" | "production"; + PREVIEW_HOSTNAME?: string | undefined; + }, + context: z.RefinementCtx, +): void { + if (bindings.CHEATCODE_ENVIRONMENT !== "production") { + return; + } + const previewHostname = bindings.PREVIEW_HOSTNAME ?? PRODUCTION_APP_HOSTNAME; + if (previewHostname.includes(":")) { + context.addIssue({ + code: "custom", + message: "Production preview hostname must be a DNS hostname without a port", + path: ["PREVIEW_HOSTNAME"], + }); + } + if (previewHostname !== PRODUCTION_APP_HOSTNAME) { + context.addIssue({ + code: "custom", + message: "Production previews require the owned trycheatcode.com wildcard route", + path: ["PREVIEW_HOSTNAME"], + }); + } +} + +export function previewHostnameForWorker( + environment: "development" | "production", + configured: string | undefined, +): string { + const hostname = PreviewHostnameSchema.parse( + configured ?? (environment === "production" ? PRODUCTION_APP_HOSTNAME : undefined), + ); + if (environment === "production" && hostname !== PRODUCTION_APP_HOSTNAME) { + throw new Error("Production preview hostname must derive from the canonical app origin"); + } + return hostname; +} + +function isMultiLabelDnsHostname(hostname: string): boolean { + if (hostname.length > 253 || !hostname.includes(".")) { + return false; + } + return hostname + .split(".") + .every( + (label) => + label.length >= 1 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label), + ); +} diff --git a/packages/env/src/worker.ts b/packages/env/src/worker.ts index 7bd941d0..b568d00f 100644 --- a/packages/env/src/worker.ts +++ b/packages/env/src/worker.ts @@ -1,315 +1,11 @@ -import { z } from "zod"; - -export type WorkerSecret = string | SecretsStoreSecret; - -export interface CloudflareVersionMetadata { - id: string; - tag: string; - timestamp: string; -} - -interface AnalyticsDatasetBinding { - writeDataPoint(point: { blobs?: string[]; doubles?: number[]; indexes?: string[] }): void; -} - -function hasBindingMethods(value: unknown, methods: readonly string[]): value is object { - if (value === null || (typeof value !== "object" && typeof value !== "function")) { - return false; - } - try { - return methods.every((method) => typeof Reflect.get(value, method) === "function"); - } catch { - return false; - } -} - -function isSecretsStoreSecret(value: unknown): value is SecretsStoreSecret { - return hasBindingMethods(value, ["get"]); -} - -function isAnalyticsDatasetBinding(value: unknown): value is AnalyticsDatasetBinding { - return hasBindingMethods(value, ["writeDataPoint"]); -} - -function isFetcherBinding(value: unknown): value is Fetcher { - return hasBindingMethods(value, ["fetch"]); -} - -function isKvNamespaceBinding(value: unknown): value is KVNamespace { - return hasBindingMethods(value, ["get", "put", "delete", "list"]); -} - -function isDurableObjectNamespaceBinding(value: unknown): value is DurableObjectNamespace { - return hasBindingMethods(value, [ - "newUniqueId", - "idFromName", - "idFromString", - "get", - "getByName", - "jurisdiction", - ]); -} - -function isR2BucketBinding(value: unknown): value is R2Bucket { - return hasBindingMethods(value, [ - "head", - "get", - "put", - "delete", - "list", - "createMultipartUpload", - "resumeMultipartUpload", - ]); -} - -function isWorkflowBinding(value: unknown): value is Workflow { - return hasBindingMethods(value, ["get", "create", "createBatch"]); -} - -export const WorkerSecretSchema = z.union([ - z.string().min(1), - z.custom(isSecretsStoreSecret), -]); - -const OptionalWorkerSecretSchema = z.preprocess( - (value) => (value === "" ? undefined : value), - WorkerSecretSchema.optional(), -); - -const AnalyticsDatasetBindingSchema = z.custom( - isAnalyticsDatasetBinding, - "Expected a Cloudflare Analytics Engine dataset binding", -); -const DurableObjectNamespaceBindingSchema = z.custom( - isDurableObjectNamespaceBinding, - "Expected a Cloudflare Durable Object namespace binding", -); -const FetcherBindingSchema = z.custom( - isFetcherBinding, - "Expected a Cloudflare service binding", -); -const KvNamespaceBindingSchema = z.custom( - isKvNamespaceBinding, - "Expected a Cloudflare KV namespace binding", -); -const R2BucketBindingSchema = z.custom( - isR2BucketBinding, - "Expected a Cloudflare R2 bucket binding", -); -const WorkflowBindingSchema = z.custom>( - isWorkflowBinding, - "Expected a Cloudflare Workflow binding", -); - -export const PreviewHostnameSchema = z - .string() - .trim() - .toLowerCase() - .refine( - (hostname) => - hostname === "localhost" || - hostname === "localhost:8787" || - isMultiLabelDnsHostname(hostname), - "Preview hostname must be localhost, localhost:8787, or a multi-label DNS hostname", - ); - -const HyperdriveSchema = z - .object({ - connectionString: z.string().min(1), - }) - .passthrough(); - -const AnalyticsBindingsSchema = { - AGENT_METRICS: AnalyticsDatasetBindingSchema.optional(), - ERROR_EVENTS: AnalyticsDatasetBindingSchema.optional(), - PERFORMANCE_METRICS: AnalyticsDatasetBindingSchema.optional(), - USER_EVENTS: AnalyticsDatasetBindingSchema.optional(), -} as const; - -export const WorkerReleaseBindingsSchema = { - CF_VERSION_METADATA: z - .object({ - id: z.string().min(1), - tag: z.string(), - timestamp: z.string().min(1), - }) - .passthrough() - .optional(), - CHEATCODE_ENVIRONMENT: z.enum(["development", "production"]), - CHEATCODE_RELEASE_SHA: z - .string() - .regex(/^[0-9a-f]{40}$/u) - .optional(), -} as const; - -interface WorkerReleaseIdentity { - CHEATCODE_ENVIRONMENT: "development" | "production"; - CHEATCODE_RELEASE_SHA?: string | undefined; -} - -function requireProductionReleaseSha( - bindings: WorkerReleaseIdentity, - context: z.RefinementCtx, -): void { - if (bindings.CHEATCODE_ENVIRONMENT === "production" && !bindings.CHEATCODE_RELEASE_SHA) { - context.addIssue({ - code: "custom", - message: "Production Workers require an immutable release SHA.", - path: ["CHEATCODE_RELEASE_SHA"], - }); - } -} - -function requireProductionDaytonaOrg( - bindings: WorkerReleaseIdentity & { DAYTONA_ORG_ID?: string | undefined }, - context: z.RefinementCtx, -): void { - if (bindings.CHEATCODE_ENVIRONMENT === "production" && !bindings.DAYTONA_ORG_ID) { - context.addIssue({ - code: "custom", - message: "Production sandbox Workers require the pinned Daytona organization ID.", - path: ["DAYTONA_ORG_ID"], - }); - } -} - -export const GatewayWorkerEnvSchema = z - .object({ - ...AnalyticsBindingsSchema, - ...WorkerReleaseBindingsSchema, - AGENT: FetcherBindingSchema, - CLERK_AUTHORIZED_PARTIES: z.string().trim().min(1).max(2_048).optional(), - CLERK_JWT_KEY: OptionalWorkerSecretSchema, - CLERK_SECRET_KEY: OptionalWorkerSecretSchema, - COMPOSIO_API_KEY: OptionalWorkerSecretSchema, - COMPOSIO_AUTH_CONFIGS: OptionalWorkerSecretSchema, - DATABASE_CONTEXT_SIGNING_SECRET_GATEWAY: WorkerSecretSchema, - ENTITLEMENTS_CACHE: KvNamespaceBindingSchema, - HYPERDRIVE: HyperdriveSchema, - IDEMPOTENCY: DurableObjectNamespaceBindingSchema, - POLAR_ACCESS_TOKEN: OptionalWorkerSecretSchema, - // Polar product ids per paid tier — non-secret config bound as plain wrangler vars - // (not secrets). Optional so a tier without a Polar product simply 503s on checkout. - POLAR_PRODUCT_ID_PRO: z.string().min(1).optional(), - POLAR_PRODUCT_ID_PREMIUM: z.string().min(1).optional(), - POLAR_SERVER: z.enum(["production", "sandbox"]).optional(), - PREVIEW_PROXY: FetcherBindingSchema.optional(), - QUOTA_TRACKER: DurableObjectNamespaceBindingSchema, - RATE_LIMITER: DurableObjectNamespaceBindingSchema, - RESOURCE_DELETION: FetcherBindingSchema, - WEBHOOKS: FetcherBindingSchema, - }) - .strict() - .superRefine(requireProductionReleaseSha); - -export const AgentWorkerEnvSchema = z - .object({ - ...AnalyticsBindingsSchema, - ...WorkerReleaseBindingsSchema, - AGENT_RUN: DurableObjectNamespaceBindingSchema, - AGENT_RUN_WORKFLOW: WorkflowBindingSchema, - // Secret-store-bound and resolved request-scoped in the ProjectSandbox DO. - DAYTONA_API_KEY: WorkerSecretSchema, - DAYTONA_API_URL: z.string().url(), - DAYTONA_TARGET: z.string().min(1).default("us"), - DAYTONA_SANDBOX_SNAPSHOT: z.string().min(1), - DAYTONA_WORKSPACE_VOLUME: z.string().min(1).max(100), - DAYTONA_ORG_ID: z.string().uuid().optional(), - DAYTONA_PREVIEW_HOST_SUFFIXES: z.string().min(1).max(1_024).optional(), - // Shared HMAC secret for the preview-proxy access-token contract. - PREVIEW_TOKEN_SECRET: WorkerSecretSchema, - COMPOSIO_API_KEY: OptionalWorkerSecretSchema, - DATABASE_CONTEXT_SIGNING_SECRET_AGENT: WorkerSecretSchema, - // Platform-provided DeepSeek key for the free tier (Secrets-Store-bound, resolved - // request-scoped in the AgentRun DO via resolveWorkerSecret()). - DEEPSEEK_PLATFORM_API_KEY: OptionalWorkerSecretSchema, - HYPERDRIVE: HyperdriveSchema, - OUTPUT_DOWNLOAD_BASE_URL: z.string().url().optional(), - OUTPUT_DOWNLOAD_SIGNING_SECRET: WorkerSecretSchema, - PREVIEW_HOSTNAME: PreviewHostnameSchema, - PROJECT_SANDBOX: DurableObjectNamespaceBindingSchema, - QUOTA_TRACKER: DurableObjectNamespaceBindingSchema, - R2_AUDIT: R2BucketBindingSchema, - R2_OUTPUTS: R2BucketBindingSchema, - SANDBOX_STATE: KvNamespaceBindingSchema.optional(), - }) - .strict() - .superRefine(requireProductionReleaseSha) - .superRefine(requireProductionDaytonaOrg) - .superRefine(requireProductionPreviewHostname); - -export const WebhooksWorkerEnvSchema = z - .object({ - ...AnalyticsBindingsSchema, - ...WorkerReleaseBindingsSchema, - AGENT_LIFECYCLE: FetcherBindingSchema, - CLERK_WEBHOOK_SIGNING_SECRET: OptionalWorkerSecretSchema, - COMPOSIO_API_KEY: OptionalWorkerSecretSchema, - COMPOSIO_WEBHOOK_SECRET: OptionalWorkerSecretSchema, - DATABASE_CONTEXT_SIGNING_SECRET_WEBHOOKS: WorkerSecretSchema, - DAILY_MAINTENANCE_WORKFLOW: WorkflowBindingSchema, - DAYTONA_WEBHOOK_SIGNING_SECRET: WorkerSecretSchema, - ENTITLEMENTS_CACHE: KvNamespaceBindingSchema, - HYPERDRIVE: HyperdriveSchema, - POLAR_ACCESS_TOKEN: OptionalWorkerSecretSchema, - POLAR_PRODUCT_ID_PRO: z.string().min(1).optional(), - POLAR_PRODUCT_ID_PREMIUM: z.string().min(1).optional(), - POLAR_SERVER: z.enum(["production", "sandbox"]).optional(), - POLAR_WEBHOOK_SECRET: OptionalWorkerSecretSchema, - QUOTA_TRACKER: DurableObjectNamespaceBindingSchema, - R2_OUTPUTS: R2BucketBindingSchema, - RESOURCE_DELETION_WORKFLOW: WorkflowBindingSchema, - SANDBOX_STATE: KvNamespaceBindingSchema.optional(), - USER_DELETION_WORKFLOW: WorkflowBindingSchema, - WEBHOOK_IDEMPOTENCY: DurableObjectNamespaceBindingSchema, - WEBHOOK_WORKFLOW: WorkflowBindingSchema, - }) - .strict() - .superRefine(requireProductionReleaseSha); - -export async function resolveWorkerSecret( - secret: WorkerSecret | undefined, -): Promise { - if (!secret) { - return undefined; - } - if (typeof secret === "string") { - return secret; - } - return secret.get(); -} - -function requireProductionPreviewHostname( - bindings: { CHEATCODE_ENVIRONMENT: "development" | "production"; PREVIEW_HOSTNAME: string }, - context: z.RefinementCtx, -): void { - if (bindings.CHEATCODE_ENVIRONMENT !== "production") { - return; - } - if (bindings.PREVIEW_HOSTNAME.includes(":")) { - context.addIssue({ - code: "custom", - message: "Production preview hostname must be a DNS hostname without a port", - path: ["PREVIEW_HOSTNAME"], - }); - } - if (bindings.PREVIEW_HOSTNAME !== "trycheatcode.com") { - context.addIssue({ - code: "custom", - message: "Production previews require the owned trycheatcode.com wildcard route", - path: ["PREVIEW_HOSTNAME"], - }); - } -} - -function isMultiLabelDnsHostname(hostname: string): boolean { - if (hostname.length > 253 || !hostname.includes(".")) { - return false; - } - return hostname - .split(".") - .every( - (label) => - label.length >= 1 && label.length <= 63 && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label), - ); -} +export { AgentWorkerEnvSchema } from "./agent-worker"; +export { GatewayWorkerEnvSchema } from "./gateway-worker"; +export { type PreviewProxyEnv, PreviewProxyEnvSchema } from "./preview-proxy"; +export { WebhooksWorkerEnvSchema } from "./webhooks-worker"; +export { + type CloudflareVersionMetadata, + DEFAULT_DAYTONA_TARGET, + previewHostnameForWorker, + resolveWorkerSecret, + type WorkerSecret, +} from "./worker-shared"; diff --git a/packages/sandbox-contracts/README.md b/packages/sandbox-contracts/README.md index 0eb4b394..2f8b05e3 100644 --- a/packages/sandbox-contracts/README.md +++ b/packages/sandbox-contracts/README.md @@ -2,11 +2,11 @@ Provider-neutral ports shared by sandbox consumers and implementations. This package owns only structural runtime contracts, their trust-boundary validators, -and the generic sandbox method dispatcher. Daytona lifecycle and REST client -types remain in `@cheatcode/tools-code`. +and narrowed tool-facing method sets. Daytona REST client types live behind +`@cheatcode/agent-core/tools/code`. `ArtifactRuntimeSchema` requires an object with a callable `put` method, while -`SandboxLikeSchema` requires a callable `runCode` method. Both schemas preserve +`SandboxLikeSchema` requires the complete callable sandbox surface. Both schemas preserve the original object identity so Durable Object stubs and request-scoped runtime objects are not cloned or stripped during validation. @@ -16,9 +16,8 @@ objects are not cloned or stripped during validation. - `ArtifactRuntime`, artifact upload types, `ArtifactRuntimeSchema`, and the canonical `ArtifactKind` type re-exported from `@cheatcode/types/artifacts`; upload results expose durable output identity and presentation metadata, not R2 locators or expiring capabilities -- `CodeRuntimeContext`, `CodeRuntimeContextSchema`, and `getCodeRuntimeContext` +- `CodeRuntimeContext`, `CodeRuntimeContextFor`, and `CodeRuntimeContextSchema` - `EnvironmentVariablesSchema` -- `callSandboxMethod` Long-running processes require a caller-owned stable `processId`. The sandbox uses that identity as an idempotency slot for replacement, inspection, cleanup, and bounded record reaping; anonymous diff --git a/packages/sandbox-contracts/package.json b/packages/sandbox-contracts/package.json index 7f5becd1..70f99ad0 100644 --- a/packages/sandbox-contracts/package.json +++ b/packages/sandbox-contracts/package.json @@ -17,7 +17,6 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@cheatcode/observability": "workspace:*", "@cheatcode/types": "workspace:*", "zod": "catalog:" }, diff --git a/packages/sandbox-contracts/src/index.ts b/packages/sandbox-contracts/src/index.ts index e25cd799..6bff0917 100644 --- a/packages/sandbox-contracts/src/index.ts +++ b/packages/sandbox-contracts/src/index.ts @@ -4,6 +4,7 @@ export type { ArtifactUploadInput, ArtifactUploadResult, CodeRuntimeContext, + CodeRuntimeContextFor, SandboxDeleteFileResult, SandboxExecResult, SandboxKillProcessResult, @@ -22,7 +23,5 @@ export { ArtifactRuntimeSchema, CodeRuntimeContextSchema, EnvironmentVariablesSchema, - getCodeRuntimeContext, SandboxLikeSchema, } from "./runtime"; -export { callSandboxMethod } from "./sandbox-methods"; diff --git a/packages/sandbox-contracts/src/runtime.ts b/packages/sandbox-contracts/src/runtime.ts index bf489461..3807ed8e 100644 --- a/packages/sandbox-contracts/src/runtime.ts +++ b/packages/sandbox-contracts/src/runtime.ts @@ -16,7 +16,7 @@ export const EnvironmentVariablesSchema = z `At most ${MAX_ENVIRONMENT_VARIABLES} environment variables are allowed.`, ); -export interface SandboxRunCodeInput { +interface SandboxRunCodeInput { code: string; cwd?: string; env?: Record; @@ -31,7 +31,7 @@ export interface SandboxRunCodeResult { success: boolean; } -export interface SandboxExecInput { +interface SandboxExecInput { command: string[]; cwd?: string; env?: Record; @@ -40,7 +40,7 @@ export interface SandboxExecInput { export type SandboxExecResult = SandboxExecResultBase; -export interface SandboxReadFileInput { +interface SandboxReadFileInput { encoding?: "utf8" | "base64"; path: string; } @@ -52,7 +52,7 @@ export interface SandboxReadFileResult { size?: number; } -export interface SandboxWriteFileInput { +interface SandboxWriteFileInput { content: string; encoding?: "utf8" | "base64"; path: string; @@ -63,7 +63,7 @@ export interface SandboxWriteFileResult { success: boolean; } -export interface SandboxListFilesInput { +interface SandboxListFilesInput { includeHidden?: boolean; path: string; recursive?: boolean; @@ -74,7 +74,7 @@ export interface SandboxListFilesResult { path: string; } -export interface SandboxSearchFilesInput { +interface SandboxSearchFilesInput { caseSensitive?: boolean; contextLines?: number; excludeDirs?: string[]; @@ -99,7 +99,7 @@ export interface SandboxSearchFilesResult { truncated?: boolean; } -export interface SandboxDeleteFileInput { +interface SandboxDeleteFileInput { path: string; recursive?: boolean; } @@ -132,7 +132,7 @@ export interface SandboxProcessResult { status: string; } -export interface SandboxKillProcessInput { +interface SandboxKillProcessInput { processId: string; } @@ -170,20 +170,20 @@ interface SandboxAllocateProcessPortInput { } export interface SandboxLike { - allocateProjectPort?(input: SandboxAllocateProjectPortInput): Promise; - allocateProcessPort?(input: SandboxAllocateProcessPortInput): Promise; - deleteFile?(input: SandboxDeleteFileInput): Promise; - ensureReady?(): Promise; - exec?(input: SandboxExecInput): Promise; - getSignedPreviewUrl?(input: SandboxSignedPreviewUrlInput): Promise; - killAllProcesses?(): Promise; - killProcess?(input: SandboxKillProcessInput): Promise; - listFiles?(input: SandboxListFilesInput): Promise; - readFile?(input: SandboxReadFileInput): Promise; + allocateProjectPort(input: SandboxAllocateProjectPortInput): Promise; + allocateProcessPort(input: SandboxAllocateProcessPortInput): Promise; + deleteFile(input: SandboxDeleteFileInput): Promise; + ensureReady(): Promise; + exec(input: SandboxExecInput): Promise; + getSignedPreviewUrl(input: SandboxSignedPreviewUrlInput): Promise; + killAllProcesses(): Promise; + killProcess(input: SandboxKillProcessInput): Promise; + listFiles(input: SandboxListFilesInput): Promise; + readFile(input: SandboxReadFileInput): Promise; runCode(input: SandboxRunCodeInput): Promise; - searchFiles?(input: SandboxSearchFilesInput): Promise; - startProcess?(input: SandboxStartProcessInput): Promise; - writeFile?(input: SandboxWriteFileInput): Promise; + searchFiles(input: SandboxSearchFilesInput): Promise; + startProcess(input: SandboxStartProcessInput): Promise; + writeFile(input: SandboxWriteFileInput): Promise; } export interface ArtifactUploadInput { @@ -213,6 +213,14 @@ export interface CodeRuntimeContext { workspaceDir?: string | undefined; } +/** Narrows a tool's sandbox dependency to only the methods it invokes. */ +export type CodeRuntimeContextFor = Omit< + CodeRuntimeContext, + "sandbox" +> & { + sandbox: Pick; +}; + export interface WorkspaceBinding { projectId: string; workspaceDir: string; @@ -228,7 +236,7 @@ export const ArtifactRuntimeSchema = z.custom( export const SandboxLikeSchema = z.custom( isSandboxLike, - "Expected a sandbox runtime with a callable runCode method", + "Expected a complete sandbox runtime", ); export const CodeRuntimeContextSchema = z @@ -240,22 +248,27 @@ export const CodeRuntimeContextSchema = z }) .strict(); -const ObjectWithRuntimeContextSchema = z - .object({ - runtimeContext: CodeRuntimeContextSchema, - }) - .passthrough(); - -export function getCodeRuntimeContext(options: unknown): CodeRuntimeContext { - return ObjectWithRuntimeContextSchema.parse(options).runtimeContext; -} - function isArtifactRuntime(value: unknown): value is ArtifactRuntime { return hasCallableMethod(value, "put"); } function isSandboxLike(value: unknown): value is SandboxLike { - return hasCallableMethod(value, "runCode"); + return [ + "allocateProjectPort", + "allocateProcessPort", + "deleteFile", + "ensureReady", + "exec", + "getSignedPreviewUrl", + "killAllProcesses", + "killProcess", + "listFiles", + "readFile", + "runCode", + "searchFiles", + "startProcess", + "writeFile", + ].every((method) => hasCallableMethod(value, method)); } function hasCallableMethod(value: unknown, method: string): boolean { diff --git a/packages/sandbox-contracts/src/sandbox-methods.ts b/packages/sandbox-contracts/src/sandbox-methods.ts deleted file mode 100644 index 565ec2ee..00000000 --- a/packages/sandbox-contracts/src/sandbox-methods.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { APIError } from "@cheatcode/observability"; -import type { - SandboxDeleteFileInput, - SandboxExecInput, - SandboxKillProcessInput, - SandboxLike, - SandboxListFilesInput, - SandboxReadFileInput, - SandboxRunCodeInput, - SandboxRunCodeResult, - SandboxSearchFilesInput, - SandboxStartProcessInput, - SandboxWriteFileInput, -} from "./runtime"; - -type SandboxMethodName = - | "deleteFile" - | "exec" - | "killProcess" - | "listFiles" - | "readFile" - | "runCode" - | "searchFiles" - | "startProcess" - | "writeFile"; - -type SandboxMethodInput = { - deleteFile: SandboxDeleteFileInput; - exec: SandboxExecInput; - killProcess: SandboxKillProcessInput; - listFiles: SandboxListFilesInput; - readFile: SandboxReadFileInput; - runCode: SandboxRunCodeInput; - searchFiles: SandboxSearchFilesInput; - startProcess: SandboxStartProcessInput; - writeFile: SandboxWriteFileInput; -}; - -type SandboxMethodOutput = { - deleteFile: Awaited>>; - exec: Awaited>>; - killProcess: Awaited>>; - listFiles: Awaited>>; - readFile: Awaited>>; - runCode: SandboxRunCodeResult; - searchFiles: Awaited>>; - startProcess: Awaited>>; - writeFile: Awaited>>; -}; - -export async function callSandboxMethod( - sandbox: SandboxLike, - name: Name, - input: SandboxMethodInput[Name], -): Promise { - const method = sandbox[name]; - if (typeof method !== "function") { - throw new APIError(500, "validation_tool_not_registered", `Sandbox method ${name} is missing`, { - hint: "Update the ProjectSandbox Durable Object before using this tool.", - retriable: false, - }); - } - const typedMethod = method as ( - value: SandboxMethodInput[Name], - ) => Promise; - return typedMethod(input); -} diff --git a/packages/tools-browser/README.md b/packages/tools-browser/README.md deleted file mode 100644 index f58b3903..00000000 --- a/packages/tools-browser/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# @cheatcode/tools-browser - -Stagehand-backed browser automation tools that run inside the user's Daytona sandbox. -The product path starts a persistent local-only driver process at -`127.0.0.1` on a run-scoped dynamically allocated port inside the sandbox, so -`open`, `act`, `observe`, `extract`, and `screenshot` share the same Stagehand -page state. The driver listens only inside the sandbox and is never exposed as -a user-facing preview. -The driver health check includes the configured model and a non-secret BYOK -fingerprint prefix so the Worker restarts the process when a user changes keys -without logging or returning plaintext credentials. The driver captures its -request-scoped model configuration and clears provider-key environment variables -before Chromium is launched, so browser child processes do not inherit BYOK values. -Driver responses are byte-bounded and strictly projected before entering model -context. Structured observations and extractions are capped, while screenshots -are uploaded to the run artifact store and returned as metadata instead of -inline base64. - -`open` canonicalizes sandbox loopback navigation to `127.0.0.1`. Before an -`act`, the Worker reads the exact active-page URL; only the active project's -durably allocated preview port is eligible for autonomous interaction. Every act -is bound to that URL and origin, checked again inside the driver, and confined by -a context-wide exact-origin CDP request interceptor and connection guard for the -duration of the action. The guard covers current and newly attached pages, frames, -popups, workers, service workers, redirects, WebSockets, WebTransport, and peer -connections. A page change before execution, an origin change during execution, -or an isolation setup failure fails closed. The driver explicitly enables -flattened CDP auto-attach with new targets paused; page/frame policies are queued -before resume, while auxiliary and unknown network targets are closed. Target -isolation is idempotent, interception work drains to quiescence, and any guarded -action/setup/cleanup failure discards the browser instance so a paused or partially -guarded target cannot leak into a later action. - -The package depends on the provider-neutral sandbox and artifact ports from -`@cheatcode/sandbox-contracts`; it does not depend on another tool domain. - -## Public exports - -- `executeBrowserOpen` -- `executeBrowserAct` -- `executeBrowserObserve` -- `executeBrowserExtract` -- `executeBrowserScreenshot` -- `inspectBrowserPage` -- the matching Mastra input/output schemas -- `BrowserProvider` and `BrowserRuntimeContext` - -## Code Checks - -```bash -pnpm --filter @cheatcode/tools-browser typecheck -``` - -## Env - -None directly. Provider keys are delivered request-scoped through the driver's -bounded one-shot bootstrap input, removed from process environment variables -before Chromium starts, and never logged by the Worker. diff --git a/packages/tools-browser/package.json b/packages/tools-browser/package.json deleted file mode 100644 index a9ff4ea6..00000000 --- a/packages/tools-browser/package.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "@cheatcode/tools-browser", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "tsc -p tsconfig.json", - "lint": "biome check .", - "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "dependencies": { - "@cheatcode/observability": "workspace:*", - "@cheatcode/sandbox-contracts": "workspace:*", - "zod": "catalog:" - }, - "devDependencies": { - "@cheatcode/tsconfig": "workspace:*", - "typescript": "catalog:" - } -} diff --git a/packages/tools-browser/tsconfig.json b/packages/tools-browser/tsconfig.json deleted file mode 100644 index 9119e809..00000000 --- a/packages/tools-browser/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig/library.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "lib": ["ES2023", "DOM"] - }, - "include": ["src/**/*"] -} diff --git a/packages/tools-code/README.md b/packages/tools-code/README.md deleted file mode 100644 index e33e9b7e..00000000 --- a/packages/tools-code/README.md +++ /dev/null @@ -1,68 +0,0 @@ -# @cheatcode/tools-code - -Deterministic code-execution primitives backed by the user's Daytona sandbox. -Tool-facing file and cwd inputs are constrained to `/workspace`. `executeRunCode` -uses Daytona process execution with `python3 -c` or -`node --input-type=module -e`; it does not write temporary runtime files. -Mastra tool registration and top-level tool descriptions live in -`@cheatcode/agent-core`; field-level descriptions stay with the canonical input -schemas in this package so validation and the LLM contract cannot drift. -Daytona JSON, error, file, and accumulated-log responses are streamed through -endpoint-specific byte boundaries before buffering in a Worker isolate. -Sandbox control-plane responses retain and validate identity-critical name, label, -snapshot, target, runtime-user, and volume-mount fields so lifecycle code cannot accidentally trust a -different or stale resource after projecting the provider payload. -Sandbox discovery exhausts Daytona's current `{ items, nextCursor }` response instead of trusting -the first page, and file listings require the canonical RFC 3339 `modifiedAt` field rather than -accepting the deprecated `modTime` representation or fabricating timestamps. -The same REST-only client resolves/creates shared Daytona volumes, validates their current -provider shape and readiness, supplies `volumeId`/`mountPath`/`subpath` only at sandbox creation, -and replaces complete sandbox label sets during release-scoped promotion. Snapshot replacement -does not use the Daytona SDK or assume an in-place snapshot mutation API. -When a run supplies `/workspace/`, every file, shell, code-execution, -Git, and dev-server path is confined lexically to that project root; a bare -`/workspace` is remapped to it and sibling-project paths are rejected. -Every long-running process start also requires a stable caller-selected process ID. Repeating that -ID replaces the same managed slot instead of growing orphan session metadata. -Dev-server startup has an explicit prepare/execute boundary: dynamic project-port -allocation and Expo command normalization happen during preparation, then the -resolved command, cwd, environment key names, port, and process policy can be -displayed and hash-bound before that exact prepared plan executes. -Git push uses the same boundary: preparation resolves exactly one credential-free -HTTPS push URL, a normalized local branch, its immutable commit OID, and the -destination ref. Execution uses that exact URL and OID-to-ref refspec, so changes -to `.git/config` or the local branch after preparation cannot redirect the push. -Preparation rejects `insteadOf` and `pushInsteadOf` configuration, and execution -rechecks that invariant immediately before push, preventing the prepared URL from -being redirected through repository, global, system, or included Git config. -Git commit is prepared as an exact `add` plus `commit` sequence. Hooks and -filesystem monitors are disabled for that sequence. - -## Public exports - -- `executeRunCode` -- file executors: `executeReadFile`, `executeWriteFile`, `executeListFiles`, - `executeSearchFiles`, `executeDeleteFile` -- shell executors: `executeShellExec`, `executeShellStartProcess`, - `executeShellKillProcess`, `executeShellTerminal` -- preview executors: `prepareStartDevServer`, `executePreparedStartDevServer`, - and `executeStartDevServer` -- git executors: `executeGitStatus`, `executeGitClone`, `prepareGitCommit`, - `executePreparedGitCommit`, `prepareGitPush`, and `executePreparedGitPush` -- input/output schemas consumed by Mastra tool definitions -- canonical `WorkspacePathSchema` and `WorkspaceFilePathSchema` trust-boundary - validators plus `resolveProjectWorkspacePath` -- `DaytonaClient` and the Daytona response types consumed by `agent-worker`; - neutral sandbox and artifact ports live in `@cheatcode/sandbox-contracts`, while - shared Daytona preview URL validation lives in - `@cheatcode/types/daytona-preview` - -## Code Checks - -```bash -pnpm --filter @cheatcode/tools-code typecheck -``` - -## Env - -None directly. Tools use the request-scoped sandbox from Mastra runtime context. diff --git a/packages/tools-code/package.json b/packages/tools-code/package.json deleted file mode 100644 index 1ede0663..00000000 --- a/packages/tools-code/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "@cheatcode/tools-code", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "tsc -p tsconfig.json", - "lint": "biome check .", - "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "dependencies": { - "@cheatcode/observability": "workspace:*", - "@cheatcode/sandbox-contracts": "workspace:*", - "@cheatcode/types": "workspace:*", - "zod": "catalog:" - }, - "devDependencies": { - "@cheatcode/tsconfig": "workspace:*", - "typescript": "catalog:" - } -} diff --git a/packages/tools-code/tsconfig.json b/packages/tools-code/tsconfig.json deleted file mode 100644 index 9119e809..00000000 --- a/packages/tools-code/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig/library.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "lib": ["ES2023", "DOM"] - }, - "include": ["src/**/*"] -} diff --git a/packages/tools-research/README.md b/packages/tools-research/README.md deleted file mode 100644 index 72bf1025..00000000 --- a/packages/tools-research/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# @cheatcode/tools-research - -Research tool adapters for Cheatcode agents. - -## Exports - -- `executeExaSearch` -- `executeFirecrawlScrape` -- `executeFirecrawlSearch` -- `executeFirecrawlExtract` -- Zod input/output schemas for each tool - -## Runtime - -Tools receive provider keys through `ResearchRuntimeContext`. Workers must source -keys through `packages/byok`; this package never reads environment variables and -never logs provider keys. Each executor accepts the caller's `AbortSignal` as -its optional third argument. - -Provider calls use Workers-native `fetch` with whole-request timeouts composed -with the active tool's cancellation signal and stream-bounded JSON reads. Exa -and Firecrawl documents are normalized to strict output schemas; unknown -provider fields, embedded screenshot data, and oversized content are omitted or -rejected before a result enters the model context. Firecrawl extraction polling -has both a total wall-time boundary and a finite attempt count, and both its HTTP -requests and poll delays stop immediately when the caller cancels. - -## Code Checks - -```bash -pnpm --filter @cheatcode/tools-research typecheck -pnpm --filter @cheatcode/tools-research lint -``` diff --git a/packages/tools-research/package.json b/packages/tools-research/package.json deleted file mode 100644 index bbcc2e36..00000000 --- a/packages/tools-research/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "@cheatcode/tools-research", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.js" - } - }, - "scripts": { - "build": "tsc -p tsconfig.json", - "lint": "biome check .", - "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "dependencies": { - "@cheatcode/observability": "workspace:*", - "zod": "catalog:" - }, - "devDependencies": { - "@cheatcode/tsconfig": "workspace:*", - "typescript": "catalog:" - } -} diff --git a/packages/tools-research/tsconfig.json b/packages/tools-research/tsconfig.json deleted file mode 100644 index 9119e809..00000000 --- a/packages/tools-research/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../tsconfig/library.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "lib": ["ES2023", "DOM"] - }, - "include": ["src/**/*"] -} diff --git a/packages/types/src/sandbox-wire.ts b/packages/types/src/sandbox-wire.ts index 04ca3c6f..16bc8cdb 100644 --- a/packages/types/src/sandbox-wire.ts +++ b/packages/types/src/sandbox-wire.ts @@ -18,7 +18,7 @@ type SandboxFileEntryShapeWithPath> = Omit< /** * Builds every sandbox file-entry schema from one field definition. The list - * order option preserves the existing tools-code JSON Schema serialization. + * order option preserves the existing code-tool JSON Schema serialization. */ export function sandboxFileEntryShape>( path: PathSchema, diff --git a/packages/ui/README.md b/packages/ui/README.md deleted file mode 100644 index 124ab17f..00000000 --- a/packages/ui/README.md +++ /dev/null @@ -1,21 +0,0 @@ -# @cheatcode/ui - -Shared UI primitives for the Vercel-hosted Next.js app. - -## Public exports - -- `cn`: class name composition helper -- `icons.ts`: constrained Lucide icon barrel used by product screens -- `Response`: Streamdown-powered AI response renderer with lazy math and Mermaid plugins - -The web app keeps app-local `@/components/ui/*` and `@/lib/ui/*` facades where they provide product-specific composition; shared primitives are exported from this package. - -## Code Checks - -```bash -pnpm --filter @cheatcode/ui typecheck -``` - -## Env - -None. diff --git a/packages/ui/package.json b/packages/ui/package.json deleted file mode 100644 index 9d6cf182..00000000 --- a/packages/ui/package.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "name": "@cheatcode/ui", - "private": true, - "version": "0.0.0", - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - }, - "./cn": { - "types": "./dist/cn.d.ts", - "default": "./dist/cn.js" - }, - "./confirm-dialog": { - "types": "./dist/confirm-dialog.d.ts", - "default": "./dist/confirm-dialog.js" - }, - "./icons": { - "types": "./dist/icons.d.ts", - "default": "./dist/icons.js" - }, - "./response": { - "types": "./dist/response.d.ts", - "default": "./dist/response.js" - } - }, - "scripts": { - "build": "tsc -p tsconfig.json", - "lint": "biome check .", - "typecheck": "tsc -p tsconfig.json --noEmit" - }, - "dependencies": { - "@streamdown/code": "catalog:", - "@streamdown/math": "catalog:", - "@streamdown/mermaid": "catalog:", - "lucide-react": "catalog:", - "streamdown": "catalog:" - }, - "devDependencies": { - "@cheatcode/tsconfig": "workspace:*", - "@types/react": "catalog:", - "react": "catalog:", - "typescript": "catalog:" - }, - "peerDependencies": { - "react": "catalog:" - } -} diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json deleted file mode 100644 index ae4d5590..00000000 --- a/packages/ui/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../tsconfig/library.json", - "compilerOptions": { - "outDir": "dist", - "rootDir": "src", - "jsx": "react-jsx", - "lib": ["ES2023", "DOM", "DOM.Iterable"] - }, - "include": ["src/**/*"] -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58c15925..5c411500 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -289,9 +289,6 @@ importers: '@cheatcode/skills': specifier: workspace:* version: link:../../packages/skills - '@cheatcode/tools-code': - specifier: workspace:* - version: link:../../packages/tools-code '@cheatcode/types': specifier: workspace:* version: link:../../packages/types @@ -384,9 +381,6 @@ importers: '@cheatcode/types': specifier: workspace:* version: link:../../packages/types - zod: - specifier: 'catalog:' - version: 4.4.3 devDependencies: '@cheatcode/tsconfig': specifier: workspace:* @@ -415,9 +409,6 @@ importers: '@cheatcode/types': specifier: workspace:* version: link:../../packages/types - '@cheatcode/ui': - specifier: workspace:* - version: link:../../packages/ui '@clerk/nextjs': specifier: 'catalog:' version: 7.5.19(next@16.2.11(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -430,6 +421,15 @@ importers: '@next/env': specifier: 'catalog:' version: 16.2.11 + '@streamdown/code': + specifier: 'catalog:' + version: 1.1.1(react@19.2.7) + '@streamdown/math': + specifier: 'catalog:' + version: 1.0.2(react@19.2.7)(supports-color@10.2.2) + '@streamdown/mermaid': + specifier: 'catalog:' + version: 1.0.2(react@19.2.7) '@tanstack/react-query': specifier: 'catalog:' version: 5.101.2(react@19.2.7) @@ -448,6 +448,9 @@ importers: katex: specifier: 'catalog:' version: 0.17.0 + lucide-react: + specifier: 'catalog:' + version: 1.24.0(react@19.2.7) next: specifier: 'catalog:' version: 16.2.11(@babel/core@7.29.7(supports-color@10.2.2))(@opentelemetry/api@1.9.1)(babel-plugin-macros@3.1.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) @@ -472,6 +475,9 @@ importers: sonner: specifier: 'catalog:' version: 2.0.7(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + streamdown: + specifier: 'catalog:' + version: 2.5.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2) web-vitals: specifier: 'catalog:' version: 5.3.0 @@ -588,15 +594,6 @@ importers: '@cheatcode/skills': specifier: workspace:* version: link:../skills - '@cheatcode/tools-browser': - specifier: workspace:* - version: link:../tools-browser - '@cheatcode/tools-code': - specifier: workspace:* - version: link:../tools-code - '@cheatcode/tools-research': - specifier: workspace:* - version: link:../tools-research '@cheatcode/types': specifier: workspace:* version: link:../types @@ -806,9 +803,6 @@ importers: packages/sandbox-contracts: dependencies: - '@cheatcode/observability': - specifier: workspace:* - version: link:../observability '@cheatcode/types': specifier: workspace:* version: link:../types @@ -838,63 +832,6 @@ importers: specifier: 'catalog:' version: 6.0.3 - packages/tools-browser: - dependencies: - '@cheatcode/observability': - specifier: workspace:* - version: link:../observability - '@cheatcode/sandbox-contracts': - specifier: workspace:* - version: link:../sandbox-contracts - zod: - specifier: 'catalog:' - version: 4.4.3 - devDependencies: - '@cheatcode/tsconfig': - specifier: workspace:* - version: link:../tsconfig - typescript: - specifier: 'catalog:' - version: 6.0.3 - - packages/tools-code: - dependencies: - '@cheatcode/observability': - specifier: workspace:* - version: link:../observability - '@cheatcode/sandbox-contracts': - specifier: workspace:* - version: link:../sandbox-contracts - '@cheatcode/types': - specifier: workspace:* - version: link:../types - zod: - specifier: 'catalog:' - version: 4.4.3 - devDependencies: - '@cheatcode/tsconfig': - specifier: workspace:* - version: link:../tsconfig - typescript: - specifier: 'catalog:' - version: 6.0.3 - - packages/tools-research: - dependencies: - '@cheatcode/observability': - specifier: workspace:* - version: link:../observability - zod: - specifier: 'catalog:' - version: 4.4.3 - devDependencies: - '@cheatcode/tsconfig': - specifier: workspace:* - version: link:../tsconfig - typescript: - specifier: 'catalog:' - version: 6.0.3 - packages/tsconfig: devDependencies: next: @@ -917,37 +854,6 @@ importers: specifier: 'catalog:' version: 6.0.3 - packages/ui: - dependencies: - '@streamdown/code': - specifier: 'catalog:' - version: 1.1.1(react@19.2.7) - '@streamdown/math': - specifier: 'catalog:' - version: 1.0.2(react@19.2.7)(supports-color@10.2.2) - '@streamdown/mermaid': - specifier: 'catalog:' - version: 1.0.2(react@19.2.7) - lucide-react: - specifier: 'catalog:' - version: 1.24.0(react@19.2.7) - streamdown: - specifier: 'catalog:' - version: 2.5.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(supports-color@10.2.2) - devDependencies: - '@cheatcode/tsconfig': - specifier: workspace:* - version: link:../tsconfig - '@types/react': - specifier: 'catalog:' - version: 19.2.17 - react: - specifier: 'catalog:' - version: 19.2.7 - typescript: - specifier: 'catalog:' - version: 6.0.3 - packages: '@a2a-js/sdk@0.3.14': diff --git a/scripts/dev-worker-config.ts b/scripts/dev-worker-config.ts index d9079ad0..ab358dcb 100644 --- a/scripts/dev-worker-config.ts +++ b/scripts/dev-worker-config.ts @@ -38,7 +38,6 @@ const PRODUCTION_SUPABASE_TARGET = { const LOCAL_WORKER_SECRET_BINDINGS: Record = { "wrangler.jsonc": [ - "CLERK_JWT_KEY", "CLERK_SECRET_KEY", "COMPOSIO_API_KEY", "COMPOSIO_AUTH_CONFIGS",