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
3 changes: 0 additions & 3 deletions .env.migrate.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,3 @@ SUPABASE_MIGRATION_EXPECTED_HOST=db.snqtclnmhcaupqynjyux.supabase.co
SUPABASE_MIGRATION_EXPECTED_DATABASE=postgres
SUPABASE_MIGRATION_EXPECTED_ROLE=postgres
SUPABASE_MIGRATION_EXPECTED_SYSTEM_IDENTIFIER=replace_with_pg_control_system_identifier

# Optional one-time protected migration attestation envelope.
CHEATCODE_MIGRATION_ATTESTATIONS=
3 changes: 3 additions & 0 deletions .github/workflows/static-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: Static Checks

on:
pull_request:
push:
branches:
- main

permissions:
contents: read
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ source tree on July 13, 2026; the repository now contains V2 code only.
| Layer | Choice |
|---|---|
| Backend | Cloudflare Workers + Durable Objects + Workflows |
| Frontend | Next.js 16.2.10 + React 19.2.7 + Tailwind 4.3.2 + shadcn 4.6.0 + AI Elements + Streamdown on Vercel |
| Frontend | Next.js 16.2.11 + React 19.2.7 + Tailwind 4.3.2 + shadcn 4.6.0 + AI Elements + Streamdown on Vercel |
| Agent framework | Mastra 1.51.0 on Vercel AI SDK v6.0.205 |
| Sandbox | Daytona per-user sandboxes via REST-over-fetch |
| Browser | Stagehand v3.7.0 LOCAL inside the Daytona sandbox snapshot |
Expand Down Expand Up @@ -234,7 +234,7 @@ The deleted `plan.md` is not authoritative and must not be restored. Base change

Specifically check before changing:
- `pnpm-workspace.yaml` and the lockfile for exact dependency versions
- `packages/db/src/schema`, `packages/db/drizzle`, and `infra/supabase/migrations` for Postgres behavior
- `packages/db/src/schema` and `packages/db/drizzle` for Postgres behavior
- Worker `wrangler.jsonc`, `apps/web/vercel.json`, and `.github/workflows` for deployment topology
- `biome.jsonc` and TypeScript configs for code-quality rules

Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Direct competitors: Manus (generalist async agent), HappyCapy (GUI workstation +
|---|---|
| Language | **TypeScript** everywhere. No Python in backend. Python lives only inside the Daytona sandbox. |
| Backend runtime | **Cloudflare Workers + Durable Objects + Workflows** |
| Frontend | **Next.js 16.2.10 + React 19.2.7 + Tailwind 4.3.2 + shadcn CLI 4.6.0 + AI Elements + Streamdown** on Vercel |
| 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 |
| Browser automation | **Stagehand v3.7.0 LOCAL mode** inside the Daytona sandbox image |
Expand Down
24 changes: 10 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,23 +202,21 @@ pnpm deadcode

## Database migrations

`scripts/migrate.ts` owns migration planning and execution. Every mutation
requires `--apply` and an explicit phase; `--phase=all` is read-only.
`scripts/migrate.ts` owns migration planning and execution. The repository keeps
one current-schema baseline plus future forward migrations in the Drizzle
journal; it does not retain the pre-launch migration archive.

```bash
pnpm db:migrate -- --dry-run --phase=all
pnpm db:migrate -- --apply --phase=pre-deploy
pnpm db:migrate -- --apply --phase=post-deploy
pnpm db:migrate -- --apply --phase=release-finalization
pnpm db:migrate -- --dry-run
pnpm db:migrate -- --apply
```

The migration command loads `.env.migrate` on an authorized operator
workstation, validates the administrative connection target and pinned database
identity before applying changes, and accepts protected process environment
values in automation. Migration credentials are never loaded by the app or
bound to a Worker. Apply expand
migrations before code that depends on them, and apply contractions only after
all deployed code is compatible with the contracted schema.
bound to a Worker. The runner verifies the exact source journal and final
production contract through the same pinned administrative session.

Configure the three Worker Hyperdrive bindings with the existing guarded helper:

Expand Down Expand Up @@ -260,11 +258,9 @@ deployed last. The independent preview proxy deploys only when its dependency
closure changed. If Cloudflare release metadata is unavailable or inconsistent,
the command safely redeploys the relevant set instead of guessing.

There is no second release orchestrator, compatibility deploy command, or hidden
workspace-reconciliation command. Schema migrations, Worker deployment, and
Vercel deployment are explicit operations; operators must sequence them using
the expand/contract rule above and verify Worker health and the production web
revision before applying destructive migrations.
Schema migrations, Worker deployment, and Vercel deployment are explicit
operations. Verify Worker health and the production web revision whenever a
release moves more than one surface.

Publish a new immutable Daytona snapshot after changing
`infra/containers/sandbox/` by dispatching the protected workflow from `main`:
Expand Down
55 changes: 19 additions & 36 deletions apps/agent-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,14 +196,15 @@ failed cleanup instead of silently leaking storage. Filesystem operations with a
path remain concurrent across unrelated projects, but arbitrary code, shell execution, and process
launch always take a non-exclusive global lease because path parsing cannot prove their runtime
filesystem scope. Project cleanup fences and drains that lease, terminates every managed and
same-user untracked sandbox process, and only then removes the folder. Account deletion destroys shared
sandbox state once and removes run Durable Objects in bounded pages. The account cleanup
RPC synchronously fences new sandbox work, drains operations that already started, records
final sandbox usage, clears the user's Daytona volume subpath, and deletes every validated
sandbox. A temporary durable tombstone makes an interrupted cleanup resume behind the same
fence. Once external cleanup succeeds, the configured 2026-07-15 Workers compatibility contract
lets one atomic `deleteAll()` remove that tombstone, owner keys, workspace SQLite schema, and
alarm so the object ceases to occupy storage.
same-user untracked sandbox process, and only then removes the folder. Account
deletion destroys shared sandbox state once and removes run Durable Objects in
bounded pages. The account cleanup RPC synchronously fences new sandbox work,
drains operations that already started, records final sandbox usage, clears the
user's Daytona volume subpath, and deletes every validated sandbox. A temporary
durable tombstone makes an interrupted cleanup resume behind the same fence.
Once external cleanup succeeds, one atomic `deleteAll()` removes the tombstone,
owner keys, workspace SQLite schema, and alarm so the object ceases to occupy
storage.

Constructors inspect existing identity and SQLite metadata without materializing an empty store.
An object with no registered owner absorbs late lease/alarm cleanup and rejects every other
Expand All @@ -221,38 +222,20 @@ project/thread soft-delete generation and verifies that every requested run belo
scope. The 30-second signature window is therefore safe to retry and cannot authorize stale or
cross-tenant destruction; no shared key or legacy signature fallback exists.

Workspace and sandbox releases use a separate signed internal RPC. For one exact
release SHA, the closed release gate and an in-memory mutation lease reject concurrent
workspace operations. Preparation stops affected processes, collision-checks and renames
Daytona folders, reconciles process and port state, and records only temporary KV evidence for
the canonical folders that existed. Finalization reloads the already-canonical Postgres
inventory and requires the same physical evidence before snapshot work begins. The release
workflow drains all AgentRuns before this phase, so no stale run can recreate a replaced path.
Generic Durable Object reconciliation deliberately runs first: it contracts the permanent
SQLite schema to the project tombstone table and removes the one-time transition and retired-slug
tables; prepare and finalize do not depend on either table. An owner with no materialized sandbox
state uses only the in-memory maintenance lease plus the temporary evidence key, so successful
reconciliation does not leave an empty SQLite store behind.

Finalization also reconciles the user's existing Daytona sandbox to the exact configured
snapshot. Volume-backed replacements mount the same isolated subpath and compare complete tree
digests. The one-time adoption of a local-disk sandbox creates a deterministic archive and copies
it through durable 8 MiB chunks; there is no total workspace-size cap. A candidate never carries
the canonical label while the source does. After digest verification the source is retired, the
candidate receives the full canonical label set, the Durable Object atomically adopts its exact
ID, and only then is the old sandbox deleted. Every boundary is retryable by the temporary
upgrade phase and deterministic candidate identity. Once final verification succeeds, both the
workspace-transition evidence and snapshot-upgrade state are deleted; an ambiguous response can
therefore retry against the canonical physical state without leaving cutover residue. Account
deletion clears the user's shared-volume subpath before deleting all exact owned sandboxes, so
persistent volume data does not outlive the account.
Every ProjectSandbox uses the one configured immutable Daytona snapshot and the
one configured shared workspace volume. Existing sandbox identity is accepted
only when its owner, canonical labels, snapshot, volume, and mount contract all
match. Mismatches fail closed instead of running a hidden migration. New
sandboxes mount the user's isolated volume subpath directly at `/workspace`.
Account deletion clears that subpath before deleting all exactly owned
sandboxes, so persistent volume data does not outlive the account.

Production binds `CHEATCODE_RELEASE_GATE` explicitly. `draining` rejects public
run, sandbox, preview, download, and deletion admission while allowing already
admitted AgentRun Workflow/DO callbacks, sandbox operations, and persistence to
finish. `closed` additionally fences those continuation paths and serves only
`/health` plus the exact signed canonical-workspace reconciliation RPC. Stable
drain proofs run at both gates before DDL.
`/health` plus the signed database-readiness RPC. Stable drain proofs run at
both gates before DDL.

Project ZIP generation and streaming share the exact
`PROJECT_ARCHIVE_MAX_OUTPUT_BYTES` contract from `@cheatcode/types` (640 MiB). The
Expand All @@ -276,7 +259,7 @@ pnpm --filter @cheatcode/agent-worker typecheck

- `CHEATCODE_ENVIRONMENT` (`production` in committed Wrangler config; local generated config overrides it)
- `CHEATCODE_RELEASE_SHA` (required for production deployments)
- `CHEATCODE_RELEASE_GATE` (`open` in source; coordinated releases inject `draining` and then `closed` until migration/reconciliation complete)
- `CHEATCODE_RELEASE_GATE` (`open` in source; coordinated releases inject `draining` and then `closed` until migration and database-readiness checks complete)
- `CF_VERSION_METADATA`
- `AGENT_RUN`
- `AGENT_RUN_WORKFLOW`
Expand Down
101 changes: 0 additions & 101 deletions apps/agent-worker/src/agent-api-system-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,14 @@ import {
findGeneratedOutput,
getProject,
isAgentStateDeletionAuthorized,
loadWorkspaceTransitionOwner,
withUserContext,
} from "@cheatcode/db";
import { resolveWorkerSecret, type WorkerSecret } from "@cheatcode/env";
import { APIError, readBoundedRequestText } from "@cheatcode/observability";
import {
InternalAgentStateDeleteBodySchema,
InternalStateDeleteResponseSchema,
InternalWorkspaceReconciliationBodySchema,
InternalWorkspaceReconciliationResponseSchema,
internalUserStateDeletePath,
internalUserWorkspaceReconciliationPath,
OutputIdSchema,
ProjectId,
UserId,
Expand Down Expand Up @@ -47,108 +43,11 @@ type AgentContext = Context<{ Bindings: AgentEnv }>;

export function registerAgentSystemHttpRoutes(app: Hono<{ Bindings: AgentEnv }>): void {
app.post("/internal/users/:userId/delete-state", deleteInternalUserState);
app.post("/internal/users/:userId/reconcile-workspaces", reconcileInternalUserWorkspaces);
app.post("/v1/outputs/:outputId/download-url", mintOutputDownloadUrl);
app.get("/v1/outputs/:outputId/download", downloadOutput);
app.post("/v1/projects/:projectId/download", downloadProjectArchive);
}

async function reconcileInternalUserWorkspaces(c: AgentContext): Promise<Response> {
if (c.env.CHEATCODE_RELEASE_GATE !== "closed") {
throw new APIError(
409,
"conflict_state_invalid",
"Workspace reconciliation requires the closed release gate",
{ retriable: false },
);
}
assertAgentInternalHostname(c.req.raw);
assertAgentLifecycleCapability(c.req.raw);
const userId = UserId(GatewayUserIdSchema.parse(c.req.param("userId")));
const rawBody = await readBoundedRequestText(
c.req.raw,
MAX_INTERNAL_MAINTENANCE_BODY_BYTES,
"Internal workspace reconciliation",
);
await verifyAgentLifecycleRequest({
expectedPathname: internalUserWorkspaceReconciliationPath(userId),
rawBody,
request: c.req.raw,
secrets: c.env,
});
const body = InternalWorkspaceReconciliationBodySchema.parse(
parseInternalMaintenanceJson(rawBody),
);
if (c.env.CHEATCODE_RELEASE_SHA !== body.releaseSha) {
throw new APIError(409, "conflict_state_invalid", "Agent release does not match transition", {
details: { actualReleaseSha: c.env.CHEATCODE_RELEASE_SHA ?? null },
retriable: false,
});
}
await assertWorkspaceTransitionInventory(c.env, userId, body);
const sandbox = await sandboxStubForUser(c.env, userId);
const result =
body.phase === "prepare"
? await sandbox.prepareWorkspaceTransition(body)
: await sandbox.finalizeWorkspaceTransition(body);
return c.json(InternalWorkspaceReconciliationResponseSchema.parse(result));
}

async function assertWorkspaceTransitionInventory(
env: AgentEnv,
userId: UserId,
body: z.infer<typeof InternalWorkspaceReconciliationBodySchema>,
): Promise<void> {
const { db, close } = createDb(env.HYPERDRIVE, {
audience: "app_agent",
signingSecret: env.DATABASE_CONTEXT_SIGNING_SECRET_AGENT,
});
try {
const owner = await withUserContext(db, userId, (transaction) =>
loadWorkspaceTransitionOwner(transaction, userId),
);
if (!owner || !workspaceInventoryMatches(owner.projects, body.projects, body.phase)) {
throw new APIError(
409,
"conflict_state_invalid",
"Postgres workspace inventory does not match transition",
{ retriable: false },
);
}
} finally {
await close();
}
}

function workspaceInventoryMatches(
actual: Array<{
canonicalWorkspaceSlug: string;
currentWorkspaceSlug: string;
projectId: string;
}>,
requested: Array<{
canonicalWorkspaceSlug: string;
currentWorkspaceSlug: string;
projectId: string;
}>,
phase: "finalize" | "prepare",
): boolean {
if (actual.length !== requested.length) {
return false;
}
const requestedById = new Map(requested.map((project) => [project.projectId, project]));
return actual.every((project) => {
const request = requestedById.get(project.projectId);
return (
request?.canonicalWorkspaceSlug === project.canonicalWorkspaceSlug &&
(phase === "finalize"
? project.currentWorkspaceSlug === request.canonicalWorkspaceSlug
: project.currentWorkspaceSlug === request.currentWorkspaceSlug ||
project.currentWorkspaceSlug === request.canonicalWorkspaceSlug)
);
});
}

async function deleteInternalUserState(c: AgentContext): Promise<Response> {
assertAgentInternalHostname(c.req.raw);
assertAgentLifecycleCapability(c.req.raw);
Expand Down
71 changes: 0 additions & 71 deletions apps/agent-worker/src/durable-object-storage.ts

This file was deleted.

Loading