Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions .dependency-cruiser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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)(/|$)",
},
},
{
Expand Down
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 1 addition & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 4 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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/<domain>/<tool>.ts` for data/docs/media; `packages/tools-<domain>/src/<tool>.ts` for browser/code/research |
| Add a new tool | `packages/agent-core/src/tools/<domain>/<tool>.ts`; expose cross-package domains through a dedicated `@cheatcode/agent-core/tools/<domain>` subpath |
| Add a new agent | `packages/agent-core/src/mastra/agents/<name>.ts` |
| Add a new workflow | `packages/agent-core/src/mastra/workflows/<name>.ts` |
| Add a new skill | `skills/<name>/SKILL.md` (+ optional `references/` / `assets/`) |
Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions apps/agent-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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`
Expand Down
1 change: 0 additions & 1 deletion apps/agent-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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:",
Expand Down
4 changes: 2 additions & 2 deletions apps/agent-worker/src/agent-api-system-routes.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -211,7 +211,7 @@ async function resolveOutputSigningSecret(secret: WorkerSecret): Promise<string
}

function outputDownloadBaseUrl(env: AgentEnv): string | undefined {
const previewHostname = env.PREVIEW_HOSTNAME.trim();
const previewHostname = previewHostnameForWorker(env.CHEATCODE_ENVIRONMENT, env.PREVIEW_HOSTNAME);
if (previewHostname === "localhost:8787" || previewHostname === "127.0.0.1:8787") {
return `http://${previewHostname}`;
}
Expand Down
4 changes: 2 additions & 2 deletions apps/agent-worker/src/agent-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ export interface AgentEnv extends AnalyticsBindings {
DAYTONA_API_URL: string;
DAYTONA_ORG_ID?: string;
DAYTONA_PREVIEW_HOST_SUFFIXES?: string;
DAYTONA_TARGET: string;
DAYTONA_TARGET?: string;
DAYTONA_WORKSPACE_VOLUME: string;
HYPERDRIVE: Hyperdrive;
OUTPUT_DOWNLOAD_BASE_URL?: string;
OUTPUT_DOWNLOAD_SIGNING_SECRET: WorkerSecret;
PREVIEW_TOKEN_SECRET: WorkerSecret;
PREVIEW_HOSTNAME: string;
PREVIEW_HOSTNAME?: string;
PROJECT_SANDBOX: DurableObjectNamespace<ProjectSandbox>;
QUOTA_TRACKER: QuotaTrackerNamespace;
R2_AUDIT: R2Bucket;
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
22 changes: 7 additions & 15 deletions apps/agent-worker/src/durable-objects/agent-run-app-builder.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import {
executeShellExec,
executeShellTerminal,
executeStartDevServer,
} from "@cheatcode/agent-core/tools/code";
import {
type AnalyticsBindings,
APIError,
type createLogger,
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 {
Expand Down Expand Up @@ -78,11 +78,7 @@ async function allocateAppPort(
): Promise<number> {
// 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,
Expand Down Expand Up @@ -554,10 +550,6 @@ async function getSignedMetroUrl(
logger: AgentRunLogger,
port: number,
): Promise<string | null> {
if (!sandbox.getSignedPreviewUrl) {
logger.warn("expo_signed_preview_unavailable");
return null;
}
try {
const signed = await sandbox.getSignedPreviewUrl({
expiresInSeconds: SIGNED_PREVIEW_TTL_SECONDS,
Expand Down Expand Up @@ -697,7 +689,7 @@ async function stopProjectPreview(
slot: string,
): Promise<void> {
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", {
Expand Down
3 changes: 2 additions & 1 deletion apps/agent-worker/src/durable-objects/agent-run-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@ import type { ProjectSandbox } from "./project-sandbox";

export interface AgentRunEnv extends AnalyticsBindings {
AGENT_RUN_WORKFLOW: Workflow<AgentRunWorkflowPayload>;
CHEATCODE_ENVIRONMENT: "development" | "production";
CHEATCODE_RELEASE_SHA?: string;
COMPOSIO_API_KEY?: WorkerSecret;
DATABASE_CONTEXT_SIGNING_SECRET_AGENT: WorkerSecret;
DEEPSEEK_PLATFORM_API_KEY?: WorkerSecret;
HYPERDRIVE: Hyperdrive;
OUTPUT_DOWNLOAD_BASE_URL?: string;
OUTPUT_DOWNLOAD_SIGNING_SECRET: WorkerSecret;
PREVIEW_HOSTNAME: string;
PREVIEW_HOSTNAME?: string;
PROJECT_SANDBOX: DurableObjectNamespace<ProjectSandbox>;
QUOTA_TRACKER: QuotaTrackerNamespace;
R2_OUTPUTS: R2Bucket;
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { DaytonaApiError } from "@cheatcode/agent-core/tools/code";
import { APIError } from "@cheatcode/observability";
import type {
SandboxDeleteFileResult,
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { DaytonaSandbox } from "@cheatcode/tools-code";
import type { DaytonaSandbox } from "@cheatcode/agent-core/tools/code";

const APP_LABEL = "cheatcode";

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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<ProjectSandboxEnv, "DAYTONA_TARGET">): 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";
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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";

Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Loading