Skip to content

Commit bea3b58

Browse files
NathanFlurryclaude
andcommitted
fix(foundry): use $HOME instead of hardcoded /home/sandbox for sandbox repo paths
E2B sandboxes run as `user` (home: /home/user), not `sandbox`, so `mkdir -p /home/sandbox` fails with "Permission denied". Replace all hardcoded `/home/sandbox` paths with `$HOME` resolved at shell runtime inside the sandbox, and dynamically resolve the repo CWD via the sandbox actor so it works across providers (E2B, local Docker, Daytona). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ffb9f10 commit bea3b58

2 files changed

Lines changed: 65 additions & 19 deletions

File tree

foundry/packages/backend/src/actors/sandbox/index.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@ import { logActorWarning, resolveErrorMessage } from "../logging.js";
1313
import { expectQueueResponse } from "../../services/queue.js";
1414
import { resolveSandboxProviderId } from "../../sandbox-config.js";
1515

16-
const SANDBOX_REPO_CWD = "/home/sandbox/repo";
16+
/**
17+
* Default repo CWD inside the sandbox. The actual path is resolved dynamically
18+
* via `$HOME/repo` because different sandbox providers run as different users
19+
* (e.g. E2B uses `/home/user`, local Docker uses `/home/sandbox`).
20+
*/
21+
const DEFAULT_SANDBOX_REPO_CWD = "/home/user/repo";
1722
const DEFAULT_LOCAL_SANDBOX_IMAGE = "rivetdev/sandbox-agent:foundry-base-latest";
1823
const DEFAULT_LOCAL_SANDBOX_PORT = 2468;
1924
const dockerClient = new Dockerode({ socketPath: "/var/run/docker.sock" });
@@ -297,6 +302,43 @@ async function listWorkspaceModelGroupsForSandbox(c: any): Promise<WorkspaceMode
297302

298303
const baseActions = baseTaskSandbox.config.actions as Record<string, (c: any, ...args: any[]) => Promise<any>>;
299304

305+
// ---------------------------------------------------------------------------
306+
// Dynamic repo CWD resolution
307+
// ---------------------------------------------------------------------------
308+
309+
let cachedRepoCwd: string | null = null;
310+
311+
/**
312+
* Resolve the repo CWD inside the sandbox by querying `$HOME`.
313+
* Different providers run as different users (E2B: `/home/user`, local Docker:
314+
* `/home/sandbox`), so the path must be resolved dynamically. The result is
315+
* cached for the lifetime of this sandbox actor instance.
316+
*/
317+
async function resolveRepoCwd(c: any): Promise<string> {
318+
if (cachedRepoCwd) return cachedRepoCwd;
319+
320+
try {
321+
const result = await baseActions.runProcess(c, {
322+
command: "bash",
323+
args: ["-lc", "echo $HOME"],
324+
cwd: "/",
325+
timeoutMs: 10_000,
326+
});
327+
const home = (result.stdout ?? result.result ?? "").trim();
328+
if (home && home.startsWith("/")) {
329+
cachedRepoCwd = `${home}/repo`;
330+
return cachedRepoCwd;
331+
}
332+
} catch (error) {
333+
logActorWarning("taskSandbox", "failed to resolve $HOME, using default", {
334+
error: resolveErrorMessage(error),
335+
});
336+
}
337+
338+
cachedRepoCwd = DEFAULT_SANDBOX_REPO_CWD;
339+
return cachedRepoCwd;
340+
}
341+
300342
// ---------------------------------------------------------------------------
301343
// Queue names for sandbox actor
302344
// ---------------------------------------------------------------------------
@@ -528,8 +570,9 @@ export const taskSandbox = actor({
528570
}
529571
},
530572

531-
async repoCwd(): Promise<{ cwd: string }> {
532-
return { cwd: SANDBOX_REPO_CWD };
573+
async repoCwd(c: any): Promise<{ cwd: string }> {
574+
const resolved = await resolveRepoCwd(c);
575+
return { cwd: resolved };
533576
},
534577

535578
// Long-running action — kept as direct action to avoid blocking the
@@ -600,4 +643,4 @@ export const taskSandbox = actor({
600643
run: workflow(runSandboxWorkflow),
601644
});
602645

603-
export { SANDBOX_REPO_CWD };
646+
export { DEFAULT_SANDBOX_REPO_CWD, resolveRepoCwd };

foundry/packages/backend/src/actors/task/workspace.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// @ts-nocheck
22
import { randomUUID } from "node:crypto";
3-
import { basename, dirname } from "node:path";
3+
import { basename } from "node:path";
44
import { asc, eq } from "drizzle-orm";
55
import {
66
DEFAULT_WORKSPACE_MODEL_GROUPS,
@@ -11,7 +11,6 @@ import {
1111
import { getActorRuntimeContext } from "../context.js";
1212
import { getOrCreateOrganization, getOrCreateTaskSandbox, getOrCreateUser, getTaskSandbox, selfTask } from "../handles.js";
1313
import { logActorInfo, logActorWarning, resolveErrorMessage } from "../logging.js";
14-
import { SANDBOX_REPO_CWD } from "../sandbox/index.js";
1514
import { resolveSandboxProviderId } from "../../sandbox-config.js";
1615
import { getBetterAuthService } from "../../services/better-auth.js";
1716
import { resolveOrganizationGithubAuth } from "../../services/github-auth.js";
@@ -183,9 +182,9 @@ async function injectGitCredentials(sandbox: any, login: string, email: string,
183182
"set -euo pipefail",
184183
`git config --global user.name ${JSON.stringify(login)}`,
185184
`git config --global user.email ${JSON.stringify(email)}`,
186-
`git config --global credential.helper 'store --file=/home/sandbox/.git-token'`,
187-
`printf '%s\\n' ${JSON.stringify(`https://${login}:${token}@github.com`)} > /home/sandbox/.git-token`,
188-
`chmod 600 /home/sandbox/.git-token`,
185+
`git config --global credential.helper 'store --file=$HOME/.git-token'`,
186+
`printf '%s\\n' ${JSON.stringify(`https://${login}:${token}@github.com`)} > $HOME/.git-token`,
187+
`chmod 600 $HOME/.git-token`,
189188
];
190189
const result = await sandbox.runProcess({
191190
command: "bash",
@@ -576,6 +575,10 @@ async function getTaskSandboxRuntime(
576575
const sandbox = await getOrCreateTaskSandbox(c, c.state.organizationId, sandboxId, {});
577576
const actorId = typeof sandbox.resolve === "function" ? await sandbox.resolve().catch(() => null) : null;
578577
const switchTarget = sandboxProviderId === "local" ? `sandbox://local/${sandboxId}` : `sandbox://e2b/${sandboxId}`;
578+
579+
// Resolve the actual repo CWD from the sandbox's $HOME (differs by provider).
580+
const repoCwdResult = await sandbox.repoCwd();
581+
const cwd = repoCwdResult?.cwd ?? "$HOME/repo";
579582
const now = Date.now();
580583

581584
await c.db
@@ -585,7 +588,7 @@ async function getTaskSandboxRuntime(
585588
sandboxProviderId,
586589
sandboxActorId: typeof actorId === "string" ? actorId : null,
587590
switchTarget,
588-
cwd: SANDBOX_REPO_CWD,
591+
cwd,
589592
createdAt: now,
590593
updatedAt: now,
591594
})
@@ -595,7 +598,7 @@ async function getTaskSandboxRuntime(
595598
sandboxProviderId,
596599
sandboxActorId: typeof actorId === "string" ? actorId : null,
597600
switchTarget,
598-
cwd: SANDBOX_REPO_CWD,
601+
cwd,
599602
updatedAt: now,
600603
},
601604
})
@@ -606,7 +609,7 @@ async function getTaskSandboxRuntime(
606609
.set({
607610
activeSandboxId: sandboxId,
608611
activeSwitchTarget: switchTarget,
609-
activeCwd: SANDBOX_REPO_CWD,
612+
activeCwd: cwd,
610613
updatedAt: now,
611614
})
612615
.where(eq(taskRuntime.id, 1))
@@ -617,7 +620,7 @@ async function getTaskSandboxRuntime(
617620
sandboxId,
618621
sandboxProviderId,
619622
switchTarget,
620-
cwd: SANDBOX_REPO_CWD,
623+
cwd,
621624
};
622625
}
623626

@@ -648,15 +651,15 @@ async function ensureSandboxRepo(c: any, sandbox: any, record: any, opts?: { ski
648651
logActorInfo("task.sandbox", "resolveAuth+metadata", { durationMs: Math.round(performance.now() - t0) });
649652

650653
const baseRef = metadata.defaultBranch ?? "main";
651-
const sandboxRepoRoot = dirname(SANDBOX_REPO_CWD);
654+
// Use $HOME inside the shell script so the path resolves correctly regardless
655+
// of which user the sandbox runs as (E2B: "user", local Docker: "sandbox").
652656
const script = [
653657
"set -euo pipefail",
654-
`mkdir -p ${JSON.stringify(sandboxRepoRoot)}`,
658+
'REPO_DIR="$HOME/repo"',
659+
'mkdir -p "$HOME"',
655660
"git config --global credential.helper '!f() { echo username=x-access-token; echo password=${GH_TOKEN:-$GITHUB_TOKEN}; }; f'",
656-
`if [ ! -d ${JSON.stringify(`${SANDBOX_REPO_CWD}/.git`)} ]; then rm -rf ${JSON.stringify(SANDBOX_REPO_CWD)} && git clone ${JSON.stringify(
657-
metadata.remoteUrl,
658-
)} ${JSON.stringify(SANDBOX_REPO_CWD)}; fi`,
659-
`cd ${JSON.stringify(SANDBOX_REPO_CWD)}`,
661+
`if [ ! -d "$REPO_DIR/.git" ]; then rm -rf "$REPO_DIR" && git clone ${JSON.stringify(metadata.remoteUrl)} "$REPO_DIR"; fi`,
662+
'cd "$REPO_DIR"',
660663
"git fetch origin --prune",
661664
`if git show-ref --verify --quiet refs/remotes/origin/${JSON.stringify(record.branchName).slice(1, -1)}; then target_ref=${JSON.stringify(
662665
`origin/${record.branchName}`,

0 commit comments

Comments
 (0)