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
12 changes: 6 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# Run `pnpm dev:setup` to create the permission-restricted .env.local file. These
# neutral placeholders document the contract; do not hand-edit pooler URLs when
# neutral placeholders document the contract; do not hand-edit runtime URLs when
# the wizard can assemble and validate them for you.

# Supabase session pooler only: *.pooler.supabase.com, port 5432, /postgres,
# and the role-qualified username <role>.<project-ref>. A free dedicated
# Supabase Direct endpoint only: db.<project-ref>.supabase.co, port 5432,
# /postgres, and the least-privilege role username. Hyperdrive owns pooling. A free dedicated
# Supabase project is sufficient. Administrative credentials stay in
# .env.migrate and never enter the application environment.
SUPABASE_GATEWAY_DATABASE_URL=postgresql://app_gateway.<project-ref>:<generated-role-password>@<region-pooler-host>.pooler.supabase.com:5432/postgres?sslmode=require&uselibpqcompat=true
SUPABASE_AGENT_DATABASE_URL=postgresql://app_agent.<project-ref>:<generated-role-password>@<region-pooler-host>.pooler.supabase.com:5432/postgres?sslmode=require&uselibpqcompat=true
SUPABASE_WEBHOOKS_DATABASE_URL=postgresql://app_webhooks.<project-ref>:<generated-role-password>@<region-pooler-host>.pooler.supabase.com:5432/postgres?sslmode=require&uselibpqcompat=true
SUPABASE_GATEWAY_DATABASE_URL=postgresql://app_gateway:<generated-role-password>@db.<project-ref>.supabase.co:5432/postgres?sslmode=require&uselibpqcompat=true
SUPABASE_AGENT_DATABASE_URL=postgresql://app_agent:<generated-role-password>@db.<project-ref>.supabase.co:5432/postgres?sslmode=require&uselibpqcompat=true
SUPABASE_WEBHOOKS_DATABASE_URL=postgresql://app_webhooks:<generated-role-password>@db.<project-ref>.supabase.co:5432/postgres?sslmode=require&uselibpqcompat=true

# Clerk development instance. Production keys are rejected locally. Configure
# the Clerk session token to expose metadata={{user.public_metadata}}.
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/deploy-cloudflare.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
run: >-
pnpm exec turbo run build
--filter=@cheatcode/agent-worker
--filter=@cheatcode/artifact-worker
--filter=@cheatcode/webhooks-worker
--filter=@cheatcode/gateway-worker
--filter=@cheatcode/preview-proxy
Expand All @@ -58,6 +59,7 @@ jobs:
run: |
set -Eeuo pipefail
configs=(
apps/artifact-worker/wrangler.jsonc
apps/agent-worker/wrangler.jsonc
apps/webhooks-worker/wrangler.jsonc
apps/preview-proxy/wrangler.jsonc
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/static-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,9 @@ jobs:
if: needs.changes.outputs.root_code == 'true'
run: pnpm typecheck:scripts

- name: Check Worker performance budgets
run: pnpm worker:performance-budgets

- name: Check affected architecture boundaries
if: steps.workspace-scope.outputs.directories != '[]'
env:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ node_modules/
.wrangler/
.next/
.vercel/
.terraform/
*.tfstate
*.tfstate.*
dist/
.qa/
*.tsbuildinfo
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,9 @@ different cookie site from `localhost`.

Supabase is the only supported Postgres topology. Cheatcode does not start a
local database, use Supabase Storage or Realtime, or expose runtime Workers to
an administrative credential. Each Worker connects through the shared Supabase
session pooler with its own `app_gateway`, `app_agent`, or `app_webhooks` login.
an administrative credential. Each database-backed Worker connects through
Hyperdrive to the Supabase Direct endpoint with its own `app_gateway`,
`app_agent`, or `app_webhooks` login.

Before setup, publish an immutable Daytona snapshot from
[`infra/containers/sandbox`](infra/containers/sandbox). The protected workflow
Expand All @@ -120,7 +121,7 @@ or follow the container README to build the same sandbox image in your own
Daytona environment. Enter the resulting immutable snapshot name in
`DAYTONA_SANDBOX_SNAPSHOT`; setup will not inherit the hosted project's value.

Production deployments use Vercel for `apps/web` and Cloudflare for the four
Production deployments use Vercel for `apps/web` and Cloudflare for the five
Workers. Review each app's `wrangler.jsonc`, `apps/web/vercel.json`, and the
deployment workflows before changing that topology. Runtime provider keys are
BYOK and must continue through `packages/byok`.
Expand Down
16 changes: 9 additions & 7 deletions apps/agent-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,13 @@ the exact R2 object again before returning. Terminal run
persistence records upload quiescence only after the Workflow-owned tool steps have settled, while
deletion RPCs terminate the run's Workflow before removing its durable state.

Artifact messages persist only the output UUID and presentation metadata. The authenticated
`POST /v1/outputs/:outputId/download-url` path rechecks tenant ownership, retention, and R2
existence before minting a one-hour HMAC capability; the public signed download route is only the
streaming second hop. That route forwards single HTTP byte ranges to R2 and returns `206` metadata,
so browser media previews seek and start without downloading the entire artifact. Expiring
capabilities and internal R2 keys are never stored in transcripts or returned by artifact tools.
Artifact messages persist only the output UUID and presentation metadata. The lightweight
Artifact Worker owns the authenticated `POST /v1/outputs/:outputId/download-url` path and public
signed streaming hop; it rechecks the same tenant ownership, retention, and R2 existence through
the `app_agent` database role before minting a one-hour HMAC capability. The streaming hop forwards
single HTTP byte ranges to R2 and returns `206` metadata so browser media previews seek and start
without downloading the entire artifact. Expiring capabilities and internal R2 keys are never
stored in transcripts or returned by artifact tools.

Browser screenshots use the same crash-consistent R2 persistence but are classified as internal
tool evidence. Their reserved filenames keep them out of project-file and slash-command catalogs,
Expand Down Expand Up @@ -337,7 +338,8 @@ and it does not apply per-run or daily dollar caps. Provider usage remains an
opaque SDK concern.

AgentRun writes Workers Analytics Engine agent-run metrics on terminal statuses and emits
a first-visible-chunk TTFT performance metric. Run
separate first-status, first-model-text, final-token, run-completion, and Workflow-acceptance
performance metrics. The legacy TTFT column aliases first model text only. Run
admission events carry the planned logical model, while stream-attempt/completion events carry
the resolved logical model. A failure before any stream attempt keeps planned attribution instead;
provider-local transport IDs remain structured-log context. R2-backed artifact
Expand Down
174 changes: 2 additions & 172 deletions apps/agent-worker/src/agent-api-system-routes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { findGeneratedOutput, getProject, withUserDb } from "@cheatcode/db";
import { previewHostnameForWorker, resolveWorkerSecret, type WorkerSecret } from "@cheatcode/env";
import { getProject, withUserDb } from "@cheatcode/db";
import { APIError } from "@cheatcode/observability";
import {
OutputIdSchema,
type ProjectId,
toProjectId,
toUserId,
type UserId,
} from "@cheatcode/types";
import { type ProjectId, toProjectId, toUserId, type UserId } from "@cheatcode/types";
import {
AGENT_FORWARD_ROUTES,
InternalAgentStateDeleteBodySchema,
Expand All @@ -25,25 +18,13 @@ import {
sandboxStubForUser,
} from "./agent-routing";
import { isAgentStateDeletionAuthorized } from "./agent-state-deletion-policy";
import {
createOutputDownloadCapability,
OutputDownloadQuerySchema,
verifySignedOutputDownload,
} from "./output-download";
import { readGatewayUserId } from "./tenancy";

const RUN_STATE_DELETE_CONCURRENCY = 16;
type AgentContext = Context<{ Bindings: AgentEnv }>;

export function registerAgentSystemHttpRoutes(app: Hono<{ Bindings: AgentEnv }>): void {
const coreRoutes = AGENT_FORWARD_ROUTES.core;
const projectRoute = AGENT_FORWARD_ROUTES.project.downloadProject;
app.on(
coreRoutes.mintOutputDownloadUrl.method,
coreRoutes.mintOutputDownloadUrl.path,
mintOutputDownloadUrl,
);
app.on(coreRoutes.downloadOutput.method, coreRoutes.downloadOutput.path, downloadOutput);
app.on(projectRoute.method, projectRoute.path, downloadProjectArchive);
}

Expand Down Expand Up @@ -125,157 +106,6 @@ function deletedStateResult(): InternalStateDeleteResponse {
return InternalStateDeleteResponseSchema.parse({ ok: true });
}

async function mintOutputDownloadUrl(c: AgentContext): Promise<Response> {
const outputId = parseOutputId(c.req.param("outputId"));
const userId = toUserId(readGatewayUserId(c.req.raw.headers));
const output = await findDownloadableOutput(c.env, outputId, userId);
if (!(await c.env.R2_OUTPUTS.head(output.r2Key))) {
throw new APIError(404, "resource_output_not_found", "Output object not found", {
retriable: false,
});
}
const capability = await createOutputDownloadCapability({
baseUrl: outputDownloadBaseUrl(c.env),
outputId,
secret: await resolveOutputSigningSecret(c.env.OUTPUT_DOWNLOAD_SIGNING_SECRET),
userId,
});
const response = c.json(capability);
response.headers.set("Cache-Control", "private, max-age=0, no-store");
response.headers.set("Referrer-Policy", "no-referrer");
return response;
}

async function downloadOutput(c: AgentContext): Promise<Response> {
const outputId = parseOutputId(c.req.param("outputId"));
const query = parseOutputDownloadQuery(c);
const isValid = await verifySignedOutputDownload({
expires: query.expires,
outputId,
secret: await resolveOutputSigningSecret(c.env.OUTPUT_DOWNLOAD_SIGNING_SECRET),
signature: query.sig,
userId: query.userId,
});
if (!isValid) {
throw new APIError(403, "permission_access_denied", "Invalid or expired output download URL", {
retriable: false,
});
}
const output = await findDownloadableOutput(c.env, outputId, query.userId);
const object = await c.env.R2_OUTPUTS.get(output.r2Key, {
range: c.req.raw.headers,
});
if (!object?.body) {
throw new APIError(404, "resource_output_not_found", "Output object not found", {
retriable: false,
});
}
const headers = outputDownloadHeaders(output, object);
return new Response(object.body, {
headers,
status: object.range ? 206 : 200,
});
}

function outputDownloadHeaders(
output: { filename: string; mimeType: string },
object: R2ObjectBody,
): Headers {
const headers = new Headers({
"Accept-Ranges": "bytes",
"Cache-Control": "private, max-age=0, no-store",
"Content-Disposition": downloadContentDisposition(output.filename),
"Content-Type": output.mimeType,
"Cross-Origin-Resource-Policy": "cross-origin",
ETag: object.httpEtag,
"Referrer-Policy": "no-referrer",
"X-Content-Type-Options": "nosniff",
});
const range = resolveOutputRange(object.range, object.size);
headers.set("Content-Length", String(range?.length ?? object.size));
if (range) {
headers.set("Content-Range", `bytes ${range.offset}-${range.end}/${object.size}`);
}
return headers;
}

function resolveOutputRange(
range: R2Range | undefined,
objectSize: number,
): { end: number; length: number; offset: number } | undefined {
if (!range) return undefined;
if ("suffix" in range && typeof range.suffix === "number") {
const length = Math.min(range.suffix, objectSize);
const offset = objectSize - length;
return { end: objectSize - 1, length, offset };
}
const offset = "offset" in range && typeof range.offset === "number" ? range.offset : 0;
const length = "length" in range && typeof range.length === "number" ? range.length : undefined;
const boundedLength = Math.min(length ?? objectSize - offset, objectSize - offset);
return { end: offset + boundedLength - 1, length: boundedLength, offset };
}

function parseOutputId(value: string | undefined): string {
const parsed = OutputIdSchema.safeParse(value);
if (!parsed.success) {
throw new APIError(400, "request_path_param_invalid", "Invalid output id", {
details: { issues: parsed.error.issues.map((issue) => issue.message) },
retriable: false,
});
}
return parsed.data;
}

function parseOutputDownloadQuery(c: AgentContext): z.infer<typeof OutputDownloadQuerySchema> {
const parsed = OutputDownloadQuerySchema.safeParse({
expires: c.req.query("expires"),
sig: c.req.query("sig"),
userId: c.req.query("userId"),
});
if (!parsed.success) {
throw new APIError(400, "request_query_param_invalid", "Invalid output download signature", {
details: { issues: parsed.error.issues.map((issue) => issue.message) },
retriable: false,
});
}
return parsed.data;
}

async function findDownloadableOutput(env: AgentEnv, outputId: string, userId: UserId) {
return withUserDb(env, userId, async ({ transaction }) => {
const output = await transaction((tx) => findGeneratedOutput(tx, { outputId, userId }));
if (!output) {
throw new APIError(404, "resource_output_not_found", "Output not found", {
retriable: false,
});
}
return output;
});
}

async function resolveOutputSigningSecret(secret: WorkerSecret): Promise<string | undefined> {
try {
return await resolveWorkerSecret(secret);
} catch {
throw new APIError(
503,
"service_maintenance_unavailable",
"Output signing secret is unavailable",
{
retriable: true,
},
);
}
}

function outputDownloadBaseUrl(env: AgentEnv): string | undefined {
const previewHostname = previewHostnameForWorker(env.CHEATCODE_ENVIRONMENT, env.PREVIEW_HOSTNAME);
if (previewHostname === "localhost:8787" || previewHostname === "127.0.0.1:8787") {
return `http://${previewHostname}`;
}
return env.OUTPUT_DOWNLOAD_BASE_URL;
}

async function downloadProjectArchive(c: AgentContext): Promise<Response> {
const parsedProjectId = z.string().uuid().safeParse(c.req.param("projectId"));
if (!parsedProjectId.success) {
Expand Down
2 changes: 0 additions & 2 deletions apps/agent-worker/src/agent-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ export interface AgentEnv extends AnalyticsBindings {
DAYTONA_WORKSPACE_VOLUME: string;
HYPERDRIVE: Hyperdrive;
MORPH_API_KEY: WorkerSecret;
OUTPUT_DOWNLOAD_BASE_URL?: string;
OUTPUT_DOWNLOAD_SIGNING_SECRET: WorkerSecret;
PREVIEW_TOKEN_SECRET: WorkerSecret;
PREVIEW_HOSTNAME?: string;
PROJECT_SANDBOX: DurableObjectNamespace<ProjectSandbox>;
Expand Down
9 changes: 7 additions & 2 deletions apps/agent-worker/src/agent-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { toAgentRunId, toProjectId, toThreadId, toUserId, type UserId } from "@c
import type { CreateRun, ProjectSummary } from "@cheatcode/types/api";
import { QUOTA_FEATURES } from "@cheatcode/types/quota";
import type { AgentEnv } from "./agent-env";
import { durableObjectLocationHint } from "./durable-object-location";
import type { AgentRun } from "./durable-objects/agent-run";
import { type StartRunInput, StartRunInputSchema } from "./durable-objects/agent-run-schemas";
import type { ProjectSandbox } from "./durable-objects/project-sandbox";
Expand Down Expand Up @@ -63,7 +64,9 @@ export async function sandboxForUser(
async function sandboxIdentityForUser(env: AgentEnv, userId: string) {
const sandboxName = await userSandboxName(userId);
return {
sandbox: env.PROJECT_SANDBOX.get(env.PROJECT_SANDBOX.idFromName(sandboxName)),
sandbox: env.PROJECT_SANDBOX.get(env.PROJECT_SANDBOX.idFromName(sandboxName), {
locationHint: durableObjectLocationHint(env.DAYTONA_TARGET),
}),
sandboxName,
};
}
Expand Down Expand Up @@ -159,7 +162,9 @@ export async function requireProjectAccess(
}

export function agentRunForRunId(env: AgentEnv, runId: string): DurableObjectStub<AgentRun> {
return env.AGENT_RUN.get(env.AGENT_RUN.idFromName(runId));
return env.AGENT_RUN.get(env.AGENT_RUN.idFromName(runId), {
locationHint: durableObjectLocationHint(env.DAYTONA_TARGET),
});
}

interface StartAgentRunInput {
Expand Down
11 changes: 11 additions & 0 deletions apps/agent-worker/src/durable-object-location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DEFAULT_DAYTONA_TARGET } from "@cheatcode/env";

/** Best-effort first-instantiation hint aligned with the configured sandbox region. */
export function durableObjectLocationHint(target: string | undefined): DurableObjectLocationHint {
const normalized = (target ?? DEFAULT_DAYTONA_TARGET).trim().toLowerCase();
if (/^(?:eu|europe|eu-)/u.test(normalized)) return "weur";
if (/^(?:apac|asia|sg|singapore|jp|japan|kr|korea|in|india)/u.test(normalized)) return "apac";
if (/^(?:au|australia|oc)/u.test(normalized)) return "oc";
if (/^(?:me|middle-east)/u.test(normalized)) return "me";
return "enam";
}
3 changes: 1 addition & 2 deletions apps/agent-worker/src/durable-objects/agent-run-env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,10 @@ export interface AgentRunEnv extends AnalyticsBindings {
CHEATCODE_RELEASE_SHA?: string;
COMPOSIO_API_KEY?: WorkerSecret;
DATABASE_CONTEXT_SIGNING_SECRET_AGENT: WorkerSecret;
DAYTONA_TARGET?: string;
DEEPSEEK_PLATFORM_API_KEY?: WorkerSecret;
HYPERDRIVE: Hyperdrive;
MORPH_API_KEY: WorkerSecret;
OUTPUT_DOWNLOAD_BASE_URL?: string;
OUTPUT_DOWNLOAD_SIGNING_SECRET: WorkerSecret;
PREVIEW_HOSTNAME?: string;
PROJECT_SANDBOX: DurableObjectNamespace<ProjectSandbox>;
QUOTA_TRACKER: QuotaTrackerNamespace;
Expand Down
4 changes: 2 additions & 2 deletions apps/agent-worker/src/durable-objects/agent-run-output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "../streaming/ui-message-stream";
import { emitRunAbandoned } from "./agent-run-abandonment";
import type { AgentRunEnv } from "./agent-run-env";
import { emitFirstVisibleChunkMetric } from "./agent-run-performance";
import { emitRunChunkPerformanceMetrics } from "./agent-run-performance";
import {
appendAgentRunMessagePart,
appendAgentRunMessagePartOnce,
Expand Down Expand Up @@ -123,7 +123,7 @@ export class AgentRunOutput {

private broadcast(chunk: UIMessageChunk, seq: number): void {
const sequencedChunk = { chunk, seq };
emitFirstVisibleChunkMetric(this.options.ctx, this.options.env, chunk);
emitRunChunkPerformanceMetrics(this.options.ctx, this.options.env, chunk);
for (const subscriber of [...this.subscribers]) {
if ((subscriber.controller.desiredSize ?? 1) <= 0) {
this.errorSubscriber(subscriber, new Error("Agent stream subscriber fell behind."));
Expand Down
Loading