Skip to content

Commit 95ab2d9

Browse files
committed
feat: per-user 'computer' sandbox (bud parity) + IDE polish
Migrate from one Daytona sandbox per project to one sandbox PER USER, with each project as a /workspace/<slug> subfolder running its own dev server on its own per-project port — matching bud.app. Clean cut, no legacy/dual-mode. - tenancy: projectSandboxName -> userSandboxName (one sandbox/user) - projects.workspace_slug column (immutable per-project folder, unique/user) - per-project dev-server port + slot app-preview:<slug> (allocateProjectPort); general agent forced into /workspace/<slug> via runtime-context workspaceDir (tool cwd + dev-server cwd), mkdir at run start, system-prompt guidance - code-server: no project -> COMPUTER root (all projects); project -> scoped /workspace/<slug>; Files tab opens even with no project - concurrency limit counts distinct sandboxes (per-user model) - IDE: git UI off (uniform tree, no A/M/U), COMPUTER relabel symlink, ignoreRecommendations; single 'Working' timer (drop duplicate disclosure label) Verified live: two web projects share one user sandbox as distinct subfolders on distinct ports (5173/5174), each preview shows its own app, scoped computer. turbo build 22/22 green.
1 parent f2a5abd commit 95ab2d9

33 files changed

Lines changed: 2906 additions & 350 deletions

apps/agent-worker/src/agent-routing.ts

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
agentRunObjectName,
2929
isUuidRouteParam,
3030
legacyAgentRunObjectName,
31-
projectSandboxName,
31+
userSandboxName,
3232
} from "./tenancy";
3333

3434
const DO_FREE_TIER_DURATION_ERROR = "Exceeded allowed duration in Durable Objects free tier";
@@ -67,8 +67,11 @@ export async function sandboxForThread(
6767
threadId: string,
6868
): Promise<DurableObjectStub<ProjectSandbox>> {
6969
if (!isUuidRouteParam(threadId)) {
70-
return sandboxForLegacyThread(env, userId, threadId);
70+
return sandboxForLegacyThread(env, userId);
7171
}
72+
// The sandbox is per-user now: a project-less chat still resolves the same "computer" (its
73+
// project only scopes the code-server folder, handled by the ide route). We still verify the
74+
// thread belongs to the user before handing back a sandbox stub.
7275
const { db, close } = createDb(env.HYPERDRIVE);
7376
try {
7477
const thread = await withUserContext(db, UserId(userId), (tx) =>
@@ -77,24 +80,27 @@ export async function sandboxForThread(
7780
if (!thread) {
7881
throw new APIError(404, "not_found_thread", "Thread not found", { retriable: false });
7982
}
80-
if (!thread.projectId) {
81-
throw new APIError(404, "not_found_project", "This chat has no workspace yet", {
82-
hint: "Send a message to start the first run, which creates the project sandbox.",
83-
retriable: false,
84-
});
85-
}
86-
return sandboxForProject(env, userId, thread.projectId);
83+
return sandboxForUser(env, userId);
8784
} finally {
8885
await close();
8986
}
9087
}
9188

89+
// The per-user "computer" sandbox. `projectId` is accepted for call-site symmetry but no longer
90+
// keys the sandbox — every project shares the same per-user Daytona sandbox.
9291
export async function sandboxForProject(
9392
env: AgentEnv,
9493
userId: string,
95-
projectId: string,
94+
_projectId: string,
95+
): Promise<DurableObjectStub<ProjectSandbox>> {
96+
return sandboxForUser(env, userId);
97+
}
98+
99+
async function sandboxForUser(
100+
env: AgentEnv,
101+
userId: string,
96102
): Promise<DurableObjectStub<ProjectSandbox>> {
97-
const sandboxName = await projectSandboxName(userId, projectId);
103+
const sandboxName = await userSandboxName(userId);
98104
const sandbox = env.PROJECT_SANDBOX.get(env.PROJECT_SANDBOX.idFromName(sandboxName));
99105
await sandbox.registerOwner(userId, sandboxName);
100106
return sandbox;
@@ -185,6 +191,7 @@ export async function startAgentRun(
185191
model: body.model ?? run.modelId,
186192
modelExplicit: Boolean(body.model?.trim()),
187193
projectId: run.projectId,
194+
...(run.workspaceSlug ? { workspaceSlug: run.workspaceSlug } : {}),
188195
...(run.projectMode ? { projectMode: run.projectMode } : {}),
189196
...(policy.quotaWarning ? { quotaWarning: policy.quotaWarning } : {}),
190197
runId: run.runId,
@@ -380,9 +387,9 @@ export async function startLegacyThreadRun(
380387
body: CreateRun,
381388
): Promise<Response> {
382389
const runId = legacyAgentRunObjectName(userId, threadId);
383-
const projectId = await projectSandboxName(userId, threadId);
390+
const sandboxName = await userSandboxName(userId);
384391
const policy = await runEntitlementPolicy(env, userId);
385-
await sandboxForLegacyThread(env, userId, threadId);
392+
await sandboxForLegacyThread(env, userId);
386393
return fetchAgentRun(
387394
agentRunForLegacyThread(env, userId, threadId),
388395
"https://agent-run.internal/start",
@@ -397,9 +404,9 @@ export async function startLegacyThreadRun(
397404
disabledModels: [],
398405
messageText: extractRunMessageText(body),
399406
model: body.model,
400-
projectId,
407+
projectId: sandboxName,
401408
runId,
402-
sandboxName: projectId,
409+
sandboxName,
403410
threadId,
404411
userId,
405412
}),
@@ -495,15 +502,11 @@ export async function consumeTakeoverState(
495502
});
496503
}
497504

498-
async function sandboxForLegacyThread(
505+
function sandboxForLegacyThread(
499506
env: AgentEnv,
500507
userId: string,
501-
threadId: string,
502508
): Promise<DurableObjectStub<ProjectSandbox>> {
503-
const sandboxName = await projectSandboxName(userId, threadId);
504-
const sandbox = env.PROJECT_SANDBOX.get(env.PROJECT_SANDBOX.idFromName(sandboxName));
505-
await sandbox.registerOwner(userId, sandboxName);
506-
return sandbox;
509+
return sandboxForUser(env, userId);
507510
}
508511

509512
function agentRunForLegacyThread(

0 commit comments

Comments
 (0)