Improve Daytona sandbox provisioning and frontend UI

Refactor git clone script in Daytona provider to use cleaner shell logic for GitHub token authentication and branch checkout. Add support for private repository clones with token-based auth. Improve Daytona provider error handling and git configuration setup.

Frontend improvements include enhanced dev panel, workspace dashboard, sidebar navigation, and UI components for better task/session management. Update interest manager and backend client to support improved session state handling.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Nathan Flurry 2026-03-13 23:06:24 -07:00
parent 8fb19b50da
commit 098b8113f3
19 changed files with 394 additions and 130 deletions

View file

@ -437,7 +437,6 @@ async function hydrateTaskIndexMutation(c: any, _cmd?: HydrateTaskIndexCommand):
}
async function createTaskMutation(c: any, cmd: CreateTaskCommand): Promise<TaskRecord> {
const localPath = await ensureProjectReady(c);
const onBranch = cmd.onBranch?.trim() || null;
const initialBranchName = onBranch;
const initialTitle = onBranch ? deriveFallbackTitle(cmd.task, cmd.explicitTitle ?? undefined) : null;
@ -463,7 +462,6 @@ async function createTaskMutation(c: any, cmd: CreateTaskCommand): Promise<TaskR
repoId: c.state.repoId,
taskId,
repoRemote: c.state.remoteUrl,
repoLocalPath: localPath,
branchName: initialBranchName,
title: initialTitle,
task: cmd.task,
@ -954,7 +952,7 @@ export async function runProjectWorkflow(ctx: any): Promise<void> {
if (msg.name === "project.command.createTask") {
const result = await loopCtx.step({
name: "project-create-task",
timeout: 12 * 60_000,
timeout: 60_000,
run: async () => createTaskMutation(loopCtx, msg.body as CreateTaskCommand),
});
await msg.complete(result);
@ -1020,7 +1018,7 @@ export const projectActions = {
return expectQueueResponse<TaskRecord>(
await self.send(projectWorkflowQueueName("project.command.createTask"), cmd, {
wait: true,
timeout: 12 * 60_000,
timeout: 60_000,
}),
);
},

View file

@ -41,7 +41,7 @@ export interface TaskInput {
repoId: string;
taskId: string;
repoRemote: string;
repoLocalPath: string;
repoLocalPath?: string;
branchName: string | null;
title: string | null;
task: string;

View file

@ -7,7 +7,6 @@ import { getOrCreateTaskStatusSync, getOrCreateProject, getOrCreateWorkspace, ge
import { resolveWorkspaceGithubAuth } from "../../services/github-auth.js";
import { task as taskTable, taskRuntime, taskWorkbenchSessions } from "./db/schema.js";
import { getCurrentRecord } from "./workflow/common.js";
import { taskWorkflowQueueName } from "./workflow/queue.js";
const STATUS_SYNC_INTERVAL_MS = 1_000;
@ -599,7 +598,13 @@ export async function ensureWorkbenchSeeded(c: any): Promise<any> {
function buildSessionSummary(record: any, meta: any): any {
const derivedSandboxSessionId = meta.sandboxSessionId ?? (meta.status === "pending_provision" && record.activeSessionId ? record.activeSessionId : null);
const sessionStatus =
meta.status === "ready" && derivedSandboxSessionId ? activeSessionStatus(record, derivedSandboxSessionId) : meta.status === "error" ? "error" : "idle";
meta.status === "pending_provision" || meta.status === "pending_session_create"
? meta.status
: meta.status === "ready" && derivedSandboxSessionId
? activeSessionStatus(record, derivedSandboxSessionId)
: meta.status === "error"
? "error"
: "ready";
let thinkingSinceMs = meta.thinkingSinceMs ?? null;
let unread = Boolean(meta.unread);
if (thinkingSinceMs && sessionStatus !== "running") {
@ -617,6 +622,7 @@ function buildSessionSummary(record: any, meta: any): any {
thinkingSinceMs: sessionStatus === "running" ? thinkingSinceMs : null,
unread,
created: Boolean(meta.created || derivedSandboxSessionId),
errorMessage: meta.errorMessage ?? null,
};
}
@ -633,6 +639,7 @@ function buildSessionDetailFromMeta(record: any, meta: any): any {
thinkingSinceMs: summary.thinkingSinceMs,
unread: summary.unread,
created: summary.created,
errorMessage: summary.errorMessage,
draft: {
text: meta.draftText ?? "",
attachments: Array.isArray(meta.draftAttachments) ? meta.draftAttachments : [],
@ -655,7 +662,7 @@ export async function buildTaskSummary(c: any): Promise<any> {
id: c.state.taskId,
repoId: c.state.repoId,
title: record.title ?? "New Task",
status: record.status === "archived" ? "archived" : record.status === "running" ? "running" : record.status === "idle" ? "idle" : "new",
status: record.status ?? "new",
repoName: repoLabelFromRemote(c.state.repoRemote),
updatedAtMs: record.updatedAt,
branch: record.branchName,
@ -837,14 +844,6 @@ export async function renameWorkbenchBranch(c: any, value: string): Promise<void
export async function createWorkbenchSession(c: any, model?: string): Promise<{ tabId: string }> {
let record = await ensureWorkbenchSeeded(c);
if (!record.activeSandboxId) {
// Fire-and-forget: enqueue provisioning without waiting to avoid self-deadlock
// (this handler already runs inside the task workflow loop, so wait:true would deadlock).
const providerId = record.providerId ?? c.state.providerId ?? getActorRuntimeContext().providers.defaultProviderId();
await selfTask(c).send(taskWorkflowQueueName("task.command.provision"), { providerId }, { wait: false });
throw new Error("sandbox is provisioning — retry shortly");
}
if (record.activeSessionId) {
const existingSessions = await listSessionMetaRows(c);
if (existingSessions.length === 0) {
@ -1216,9 +1215,16 @@ export async function publishWorkbenchPr(c: any): Promise<void> {
if (!record.branchName) {
throw new Error("cannot publish PR without a branch");
}
let repoLocalPath = c.state.repoLocalPath;
if (!repoLocalPath) {
const project = await getOrCreateProject(c, c.state.workspaceId, c.state.repoId, c.state.repoRemote);
const result = await project.ensure({ remoteUrl: c.state.repoRemote });
repoLocalPath = result.localPath;
c.state.repoLocalPath = repoLocalPath;
}
const { driver } = getActorRuntimeContext();
const auth = await resolveWorkspaceGithubAuth(c, c.state.workspaceId);
const created = await driver.github.createPr(c.state.repoLocalPath, record.branchName, record.title ?? c.state.task, undefined, {
const created = await driver.github.createPr(repoLocalPath, record.branchName, record.title ?? c.state.task, undefined, {
githubToken: auth?.githubToken ?? null,
});
await c.db

View file

@ -76,6 +76,7 @@ const commandHandlers: Record<TaskQueueName, WorkflowHandler> = {
"task.command.provision": async (loopCtx, msg) => {
const body = msg.body;
await loopCtx.removed("init-failed", "step");
await loopCtx.removed("init-failed-v2", "step");
try {
await loopCtx.step("init-ensure-name", async () => initEnsureNameActivity(loopCtx));
await loopCtx.step("init-assert-name", async () => initAssertNameActivity(loopCtx));
@ -107,7 +108,7 @@ const commandHandlers: Record<TaskQueueName, WorkflowHandler> = {
await loopCtx.step("init-complete", async () => initCompleteActivity(loopCtx, body, sandbox, session));
await msg.complete({ ok: true });
} catch (error) {
await loopCtx.step("init-failed-v2", async () => initFailedActivity(loopCtx, error));
await loopCtx.step("init-failed-v3", async () => initFailedActivity(loopCtx, error));
await msg.complete({
ok: false,
error: resolveErrorMessage(error),

View file

@ -178,8 +178,16 @@ export async function initEnsureNameActivity(loopCtx: any): Promise<void> {
const { driver } = getActorRuntimeContext();
const auth = await resolveWorkspaceGithubAuth(loopCtx, loopCtx.state.workspaceId);
let repoLocalPath = loopCtx.state.repoLocalPath;
if (!repoLocalPath) {
const project = await getOrCreateProject(loopCtx, loopCtx.state.workspaceId, loopCtx.state.repoId, loopCtx.state.repoRemote);
const result = await project.ensure({ remoteUrl: loopCtx.state.repoRemote });
repoLocalPath = result.localPath;
loopCtx.state.repoLocalPath = repoLocalPath;
}
try {
await driver.git.fetch(loopCtx.state.repoLocalPath, { githubToken: auth?.githubToken ?? null });
await driver.git.fetch(repoLocalPath, { githubToken: auth?.githubToken ?? null });
} catch (error) {
logActorWarning("task.init", "fetch before naming failed", {
workspaceId: loopCtx.state.workspaceId,
@ -188,7 +196,7 @@ export async function initEnsureNameActivity(loopCtx: any): Promise<void> {
error: resolveErrorMessage(error),
});
}
const remoteBranches = (await driver.git.listRemoteBranches(loopCtx.state.repoLocalPath, { githubToken: auth?.githubToken ?? null })).map(
const remoteBranches = (await driver.git.listRemoteBranches(repoLocalPath, { githubToken: auth?.githubToken ?? null })).map(
(branch: any) => branch.branchName,
);

View file

@ -370,7 +370,6 @@ async function createTaskMutation(c: any, input: CreateTaskInput): Promise<TaskR
.run();
const project = await getOrCreateProject(c, c.state.workspaceId, repoId, remoteUrl);
await project.ensure({ remoteUrl });
const created = await project.createTask({
task: input.task,
@ -457,7 +456,7 @@ export async function runWorkspaceWorkflow(ctx: any): Promise<void> {
if (msg.name === "workspace.command.createTask") {
const result = await loopCtx.step({
name: "workspace-create-task",
timeout: 12 * 60_000,
timeout: 60_000,
run: async () => createTaskMutation(loopCtx, msg.body as CreateTaskInput),
});
await msg.complete(result);
@ -547,7 +546,7 @@ export const workspaceActions = {
return expectQueueResponse<TaskRecord>(
await self.send(workspaceWorkflowQueueName("workspace.command.createTask"), input, {
wait: true,
timeout: 12 * 60_000,
timeout: 60_000,
}),
);
},

View file

@ -17,6 +17,9 @@ import type {
} from "../provider-api/index.js";
import type { DaytonaDriver } from "../../driver.js";
import { Image } from "@daytonaio/sdk";
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { resolve } from "node:path";
export interface DaytonaProviderConfig {
endpoint?: string;
@ -176,6 +179,51 @@ export class DaytonaProvider implements SandboxProvider {
}
}
private shellSingleQuote(value: string): string {
return `'${value.replace(/'/g, `'\"'\"'`)}'`;
}
private readLocalCodexAuth(): string | null {
const authPath = resolve(homedir(), ".codex", "auth.json");
try {
return readFileSync(authPath, "utf8");
} catch {
return null;
}
}
private buildCloneRepoScript(req: CreateSandboxRequest, repoDir: string): string {
const usesGithubHttpAuth = req.repoRemote.startsWith("https://github.com/");
const githubPath = usesGithubHttpAuth ? req.repoRemote.slice("https://github.com/".length) : "";
const lines = [
"set -eu",
"export GIT_TERMINAL_PROMPT=0",
"export GIT_ASKPASS=/bin/echo",
`TOKEN=${JSON.stringify(req.githubToken ?? "")}`,
'if [ -z "$TOKEN" ]; then',
' if [ -n "${GH_TOKEN:-}" ]; then TOKEN="$GH_TOKEN"; else TOKEN="${GITHUB_TOKEN:-}"; fi',
"fi",
'AUTH_REMOTE=""',
...(usesGithubHttpAuth ? ['if [ -n "$TOKEN" ]; then', ` AUTH_REMOTE="https://x-access-token:${"$"}TOKEN@github.com/${githubPath}"`, "fi"] : []),
`rm -rf "${repoDir}"`,
`mkdir -p "${repoDir}"`,
`rmdir "${repoDir}"`,
// Foundry test repos can be private, so clone/fetch must use the sandbox's GitHub token when available.
...(usesGithubHttpAuth
? ['if [ -n "$AUTH_REMOTE" ]; then', ` git clone "$AUTH_REMOTE" "${repoDir}"`, "else", ` git clone "${req.repoRemote}" "${repoDir}"`, "fi"]
: [`git clone "${req.repoRemote}" "${repoDir}"`]),
`cd "${repoDir}"`,
...(usesGithubHttpAuth ? ['if [ -n "$AUTH_REMOTE" ]; then', ` git remote set-url origin "${req.repoRemote}"`, "fi"] : []),
// The task branch may not exist remotely yet (agent push creates it). Base off current branch (default branch).
`if git show-ref --verify --quiet "refs/remotes/origin/${req.branchName}"; then git checkout -B "${req.branchName}" "origin/${req.branchName}"; else git checkout -B "${req.branchName}" "$(git branch --show-current 2>/dev/null || echo main)"; fi`,
`git config user.email "foundry@local" >/dev/null 2>&1 || true`,
`git config user.name "Foundry" >/dev/null 2>&1 || true`,
];
return lines.join("\n");
}
id() {
return "daytona" as const;
}
@ -242,37 +290,7 @@ export class DaytonaProvider implements SandboxProvider {
});
const cloneStartedAt = Date.now();
await this.runCheckedCommand(
sandbox.id,
[
"bash",
"-lc",
`${JSON.stringify(
[
"set -euo pipefail",
"export GIT_TERMINAL_PROMPT=0",
"export GIT_ASKPASS=/bin/echo",
`TOKEN=${JSON.stringify(req.githubToken ?? "")}`,
'if [ -z "$TOKEN" ]; then TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}"; fi',
"GIT_AUTH_ARGS=()",
`if [ -n "$TOKEN" ] && [[ "${req.repoRemote}" == https://github.com/* ]]; then AUTH_HEADER="$(printf 'x-access-token:%s' "$TOKEN" | base64 | tr -d '\\n')"; GIT_AUTH_ARGS=(-c "http.https://github.com/.extraheader=AUTHORIZATION: basic $AUTH_HEADER"); fi`,
`rm -rf "${repoDir}"`,
`mkdir -p "${repoDir}"`,
`rmdir "${repoDir}"`,
// Foundry test repos can be private, so clone/fetch must use the sandbox's GitHub token when available.
`git "\${GIT_AUTH_ARGS[@]}" clone "${req.repoRemote}" "${repoDir}"`,
`cd "${repoDir}"`,
`if [ -n "$TOKEN" ] && [[ "${req.repoRemote}" == https://github.com/* ]]; then git config --local credential.helper ""; git config --local http.https://github.com/.extraheader "AUTHORIZATION: basic $AUTH_HEADER"; fi`,
`git "\${GIT_AUTH_ARGS[@]}" fetch origin --prune`,
// The task branch may not exist remotely yet (agent push creates it). Base off current branch (default branch).
`if git show-ref --verify --quiet "refs/remotes/origin/${req.branchName}"; then git checkout -B "${req.branchName}" "origin/${req.branchName}"; else git checkout -B "${req.branchName}" "$(git branch --show-current 2>/dev/null || echo main)"; fi`,
`git config user.email "foundry@local" >/dev/null 2>&1 || true`,
`git config user.name "Foundry" >/dev/null 2>&1 || true`,
].join("; "),
)}`,
].join(" "),
"clone repo",
);
await this.runCheckedCommand(sandbox.id, ["bash", "-lc", this.shellSingleQuote(this.buildCloneRepoScript(req, repoDir))].join(" "), "clone repo");
emitDebug("daytona.createSandbox.clone_repo.done", {
sandboxId: sandbox.id,
durationMs: Date.now() - cloneStartedAt,
@ -357,6 +375,15 @@ export class DaytonaProvider implements SandboxProvider {
const sandboxAgentExports = this.buildShellExports({
SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS: acpRequestTimeoutMs.toString(),
});
const codexAuth = this.readLocalCodexAuth();
const codexAuthSetup = codexAuth
? [
'mkdir -p "$HOME/.codex" "$HOME/.config/codex"',
`printf %s ${JSON.stringify(Buffer.from(codexAuth, "utf8").toString("base64"))} | base64 -d > "$HOME/.codex/auth.json"`,
'cp "$HOME/.codex/auth.json" "$HOME/.config/codex/auth.json"',
"unset OPENAI_API_KEY CODEX_API_KEY",
]
: [];
await this.ensureStarted(req.sandboxId);
@ -407,16 +434,16 @@ export class DaytonaProvider implements SandboxProvider {
[
"bash",
"-lc",
JSON.stringify(
this.shellSingleQuote(
[
"set -euo pipefail",
'export PATH="$HOME/.local/bin:$PATH"',
...sandboxAgentExports,
...codexAuthSetup,
"command -v sandbox-agent >/dev/null 2>&1",
"if pgrep -x sandbox-agent >/dev/null; then exit 0; fi",
'rm -f "$HOME/.codex/auth.json" "$HOME/.config/codex/auth.json"',
`nohup sandbox-agent server --no-token --host 0.0.0.0 --port ${DaytonaProvider.SANDBOX_AGENT_PORT} >/tmp/sandbox-agent.log 2>&1 &`,
].join("; "),
].join("\n"),
),
].join(" "),
"start sandbox-agent",

View file

@ -1,3 +1,6 @@
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { describe, expect, it } from "vitest";
import type { DaytonaClientLike, DaytonaDriver } from "../src/driver.js";
import type { DaytonaCreateSandboxOptions } from "../src/integrations/daytona/client.js";
@ -91,6 +94,10 @@ describe("daytona provider snapshot image behavior", () => {
const commands = client.executedCommands.join("\n");
expect(commands).toContain("GIT_TERMINAL_PROMPT=0");
expect(commands).toContain("GIT_ASKPASS=/bin/echo");
expect(commands).not.toContain("[[");
expect(commands).not.toContain("GIT_AUTH_ARGS=()");
expect(commands).not.toContain("${GIT_AUTH_ARGS[@]}");
expect(commands).not.toContain(".extraheader");
expect(handle.metadata.snapshot).toBe("snapshot-foundry");
expect(handle.metadata.image).toBe("ubuntu:24.04");
@ -100,6 +107,11 @@ describe("daytona provider snapshot image behavior", () => {
it("starts sandbox-agent with ACP timeout env override", async () => {
const previous = process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS;
const previousHome = process.env.HOME;
const tempHome = resolve(tmpdir(), `daytona-provider-test-${Date.now()}`);
mkdirSync(resolve(tempHome, ".codex"), { recursive: true });
writeFileSync(resolve(tempHome, ".codex", "auth.json"), JSON.stringify({ access_token: "test-token" }));
process.env.HOME = tempHome;
process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS = "240000";
try {
@ -111,15 +123,18 @@ describe("daytona provider snapshot image behavior", () => {
sandboxId: "sandbox-1",
});
const startCommand = client.executedCommands.find((command) =>
command.includes("nohup env SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS=240000 sandbox-agent server"),
const startCommand = client.executedCommands.find(
(command) => command.includes("export SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS=") && command.includes("sandbox-agent server --no-token"),
);
const joined = client.executedCommands.join("\n");
expect(joined).toContain("sandbox-agent/0.3.0/install.sh");
expect(joined).toContain("SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS=240000");
expect(joined).toContain("SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS");
expect(joined).toContain("apt-get install -y nodejs npm");
expect(joined).toContain("sandbox-agent server --no-token --host 0.0.0.0 --port 2468");
expect(joined).toContain('mkdir -p "$HOME/.codex" "$HOME/.config/codex"');
expect(joined).toContain("unset OPENAI_API_KEY CODEX_API_KEY");
expect(joined).not.toContain('rm -f "$HOME/.codex/auth.json"');
expect(startCommand).toBeTruthy();
} finally {
if (previous === undefined) {
@ -127,6 +142,12 @@ describe("daytona provider snapshot image behavior", () => {
} else {
process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS = previous;
}
if (previousHome === undefined) {
delete process.env.HOME;
} else {
process.env.HOME = previousHome;
}
rmSync(tempHome, { force: true, recursive: true });
}
});