chore(foundry): improve sandbox impl + status pill (#252)

* 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>

* Add header status pill showing task/session/sandbox state

Surface aggregate status (error, provisioning, running, ready, no sandbox)
as a colored pill in the transcript panel header. Integrates task runtime
status, session status, and sandbox availability via the sandboxProcesses
interest topic so the pill accurately reflects unreachable sandboxes.

Includes mock tasks demonstrating error, provisioning, and running states,
unit tests for deriveHeaderStatus, and workspace-dashboard integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Nathan Flurry 2026-03-14 12:14:06 -07:00 committed by GitHub
parent 5a1b32a271
commit 70d31f819c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
82 changed files with 2625 additions and 4166 deletions

View file

@ -1,184 +0,0 @@
import { describe, expect, it } from "vitest";
import type { DaytonaClientLike, DaytonaDriver } from "../src/driver.js";
import type { DaytonaCreateSandboxOptions } from "../src/integrations/daytona/client.js";
import { DaytonaProvider } from "../src/providers/daytona/index.js";
class RecordingDaytonaClient implements DaytonaClientLike {
createSandboxCalls: DaytonaCreateSandboxOptions[] = [];
executedCommands: string[] = [];
async createSandbox(options: DaytonaCreateSandboxOptions) {
this.createSandboxCalls.push(options);
return {
id: "sandbox-1",
state: "started",
snapshot: "snapshot-foundry",
labels: {},
};
}
async getSandbox(sandboxId: string) {
return {
id: sandboxId,
state: "started",
snapshot: "snapshot-foundry",
labels: {},
};
}
async startSandbox(_sandboxId: string, _timeoutSeconds?: number) {}
async stopSandbox(_sandboxId: string, _timeoutSeconds?: number) {}
async deleteSandbox(_sandboxId: string) {}
async executeCommand(_sandboxId: string, command: string) {
this.executedCommands.push(command);
return { exitCode: 0, result: "" };
}
async getPreviewEndpoint(sandboxId: string, port: number) {
return {
url: `https://preview.example/sandbox/${sandboxId}/port/${port}`,
token: "preview-token",
};
}
}
function createProviderWithClient(client: DaytonaClientLike): DaytonaProvider {
const daytonaDriver: DaytonaDriver = {
createClient: () => client,
};
return new DaytonaProvider(
{
apiKey: "test-key",
image: "ubuntu:24.04",
},
daytonaDriver,
);
}
describe("daytona provider snapshot image behavior", () => {
it("creates sandboxes using a snapshot-capable image recipe", async () => {
const client = new RecordingDaytonaClient();
const provider = createProviderWithClient(client);
const handle = await provider.createSandbox({
workspaceId: "default",
repoId: "repo-1",
repoRemote: "https://github.com/acme/repo.git",
branchName: "feature/test",
taskId: "task-1",
});
expect(client.createSandboxCalls).toHaveLength(1);
const createCall = client.createSandboxCalls[0];
if (!createCall) {
throw new Error("expected create sandbox call");
}
expect(typeof createCall.image).not.toBe("string");
if (typeof createCall.image === "string") {
throw new Error("expected daytona image recipe object");
}
const dockerfile = createCall.image.dockerfile;
expect(dockerfile).toContain("apt-get install -y curl ca-certificates git openssh-client nodejs npm");
expect(dockerfile).toContain("sandbox-agent/0.3.0/install.sh");
const installAgentLines = dockerfile.match(/sandbox-agent install-agent [a-z0-9-]+/gi) ?? [];
expect(installAgentLines.length).toBeGreaterThanOrEqual(2);
const commands = client.executedCommands.join("\n");
expect(commands).toContain("GIT_TERMINAL_PROMPT=0");
expect(commands).toContain("GIT_ASKPASS=/bin/echo");
expect(handle.metadata.snapshot).toBe("snapshot-foundry");
expect(handle.metadata.image).toBe("ubuntu:24.04");
expect(handle.metadata.cwd).toBe("/home/daytona/foundry/default/repo-1/task-1/repo");
expect(client.executedCommands.length).toBeGreaterThan(0);
});
it("starts sandbox-agent with ACP timeout env override", async () => {
const previous = process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS;
process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS = "240000";
try {
const client = new RecordingDaytonaClient();
const provider = createProviderWithClient(client);
await provider.ensureSandboxAgent({
workspaceId: "default",
sandboxId: "sandbox-1",
});
const startCommand = client.executedCommands.find((command) =>
command.includes("nohup env SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS=240000 sandbox-agent server"),
);
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("apt-get install -y nodejs npm");
expect(joined).toContain("sandbox-agent server --no-token --host 0.0.0.0 --port 2468");
expect(startCommand).toBeTruthy();
} finally {
if (previous === undefined) {
delete process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS;
} else {
process.env.HF_SANDBOX_AGENT_ACP_REQUEST_TIMEOUT_MS = previous;
}
}
});
it("fails with explicit timeout when daytona createSandbox hangs", async () => {
const previous = process.env.HF_DAYTONA_REQUEST_TIMEOUT_MS;
process.env.HF_DAYTONA_REQUEST_TIMEOUT_MS = "120";
const hangingClient: DaytonaClientLike = {
createSandbox: async () => await new Promise(() => {}),
getSandbox: async (sandboxId) => ({ id: sandboxId, state: "started" }),
startSandbox: async () => {},
stopSandbox: async () => {},
deleteSandbox: async () => {},
executeCommand: async () => ({ exitCode: 0, result: "" }),
getPreviewEndpoint: async (sandboxId, port) => ({
url: `https://preview.example/sandbox/${sandboxId}/port/${port}`,
token: "preview-token",
}),
};
try {
const provider = createProviderWithClient(hangingClient);
await expect(
provider.createSandbox({
workspaceId: "default",
repoId: "repo-1",
repoRemote: "https://github.com/acme/repo.git",
branchName: "feature/test",
taskId: "task-timeout",
}),
).rejects.toThrow("daytona create sandbox timed out after 120ms");
} finally {
if (previous === undefined) {
delete process.env.HF_DAYTONA_REQUEST_TIMEOUT_MS;
} else {
process.env.HF_DAYTONA_REQUEST_TIMEOUT_MS = previous;
}
}
});
it("executes backend-managed sandbox commands through provider API", async () => {
const client = new RecordingDaytonaClient();
const provider = createProviderWithClient(client);
const result = await provider.executeCommand({
workspaceId: "default",
sandboxId: "sandbox-1",
command: "echo backend-push",
label: "manual push",
});
expect(result.exitCode).toBe(0);
expect(client.executedCommands).toContain("echo backend-push");
});
});

View file

@ -3,7 +3,6 @@ import { join } from "node:path";
import { ConfigSchema, type AppConfig } from "@sandbox-agent/foundry-shared";
import type { BackendDriver } from "../../src/driver.js";
import { initActorRuntimeContext } from "../../src/actors/context.js";
import { createProviderRegistry } from "../../src/providers/index.js";
import { createDefaultAppShellServices } from "../../src/services/app-shell-runtime.js";
export function createTestConfig(overrides?: Partial<AppConfig>): AppConfig {
@ -21,7 +20,8 @@ export function createTestConfig(overrides?: Partial<AppConfig>): AppConfig {
backup_retention_days: 7,
},
providers: {
daytona: { image: "ubuntu:24.04" },
local: {},
e2b: {},
},
...overrides,
});
@ -29,7 +29,6 @@ export function createTestConfig(overrides?: Partial<AppConfig>): AppConfig {
export function createTestRuntimeContext(driver: BackendDriver, configOverrides?: Partial<AppConfig>): { config: AppConfig } {
const config = createTestConfig(configOverrides);
const providers = createProviderRegistry(config, driver);
initActorRuntimeContext(config, providers, undefined, driver, createDefaultAppShellServices());
initActorRuntimeContext(config, undefined, driver, createDefaultAppShellServices());
return { config };
}

View file

@ -1,23 +1,10 @@
import type {
BackendDriver,
DaytonaClientLike,
DaytonaDriver,
GitDriver,
GithubDriver,
StackDriver,
SandboxAgentDriver,
SandboxAgentClientLike,
TmuxDriver,
} from "../../src/driver.js";
import type { ListEventsRequest, ListPage, ListPageRequest, ProcessInfo, ProcessLogsResponse, SessionEvent, SessionRecord } from "sandbox-agent";
import type { BackendDriver, GitDriver, GithubDriver, StackDriver, TmuxDriver } from "../../src/driver.js";
export function createTestDriver(overrides?: Partial<BackendDriver>): BackendDriver {
return {
git: overrides?.git ?? createTestGitDriver(),
stack: overrides?.stack ?? createTestStackDriver(),
github: overrides?.github ?? createTestGithubDriver(),
sandboxAgent: overrides?.sandboxAgent ?? createTestSandboxAgentDriver(),
daytona: overrides?.daytona ?? createTestDaytonaDriver(),
tmux: overrides?.tmux ?? createTestTmuxDriver(),
};
}
@ -63,79 +50,6 @@ export function createTestGithubDriver(overrides?: Partial<GithubDriver>): Githu
};
}
export function createTestSandboxAgentDriver(overrides?: Partial<SandboxAgentDriver>): SandboxAgentDriver {
return {
createClient: (_opts) => createTestSandboxAgentClient(),
...overrides,
};
}
export function createTestSandboxAgentClient(overrides?: Partial<SandboxAgentClientLike>): SandboxAgentClientLike {
const defaultProcess: ProcessInfo = {
id: "process-1",
command: "bash",
args: ["-lc", "echo test"],
createdAtMs: Date.now(),
cwd: "/workspace",
exitCode: null,
exitedAtMs: null,
interactive: true,
pid: 123,
status: "running",
tty: true,
};
const defaultLogs: ProcessLogsResponse = {
processId: defaultProcess.id,
stream: "combined",
entries: [],
};
return {
createSession: async (_prompt) => ({ id: "test-session-1", status: "running" }),
sessionStatus: async (sessionId) => ({ id: sessionId, status: "running" }),
listSessions: async (_request?: ListPageRequest): Promise<ListPage<SessionRecord>> => ({
items: [],
nextCursor: undefined,
}),
listEvents: async (_request: ListEventsRequest): Promise<ListPage<SessionEvent>> => ({
items: [],
nextCursor: undefined,
}),
createProcess: async () => defaultProcess,
listProcesses: async () => ({ processes: [defaultProcess] }),
getProcessLogs: async () => defaultLogs,
stopProcess: async () => ({ ...defaultProcess, status: "exited", exitCode: 0, exitedAtMs: Date.now() }),
killProcess: async () => ({ ...defaultProcess, status: "exited", exitCode: 137, exitedAtMs: Date.now() }),
deleteProcess: async () => {},
sendPrompt: async (_request) => {},
cancelSession: async (_sessionId) => {},
destroySession: async (_sessionId) => {},
...overrides,
};
}
export function createTestDaytonaDriver(overrides?: Partial<DaytonaDriver>): DaytonaDriver {
return {
createClient: (_opts) => createTestDaytonaClient(),
...overrides,
};
}
export function createTestDaytonaClient(overrides?: Partial<DaytonaClientLike>): DaytonaClientLike {
return {
createSandbox: async () => ({ id: "sandbox-test-1", state: "started" }),
getSandbox: async (sandboxId) => ({ id: sandboxId, state: "started" }),
startSandbox: async () => {},
stopSandbox: async () => {},
deleteSandbox: async () => {},
executeCommand: async () => ({ exitCode: 0, result: "" }),
getPreviewEndpoint: async (sandboxId, port) => ({
url: `https://preview.example/sandbox/${sandboxId}/port/${port}`,
token: "preview-token",
}),
...overrides,
};
}
export function createTestTmuxDriver(overrides?: Partial<TmuxDriver>): TmuxDriver {
return {
setWindowStatus: () => 0,

View file

@ -1,14 +1,5 @@
import { describe, expect, it } from "vitest";
import {
taskKey,
taskStatusSyncKey,
historyKey,
projectBranchSyncKey,
projectKey,
projectPrSyncKey,
sandboxInstanceKey,
workspaceKey,
} from "../src/actors/keys.js";
import { taskKey, historyKey, projectBranchSyncKey, projectKey, projectPrSyncKey, taskSandboxKey, workspaceKey } from "../src/actors/keys.js";
describe("actor keys", () => {
it("prefixes every key with workspace namespace", () => {
@ -16,11 +7,10 @@ describe("actor keys", () => {
workspaceKey("default"),
projectKey("default", "repo"),
taskKey("default", "repo", "task"),
sandboxInstanceKey("default", "daytona", "sbx"),
taskSandboxKey("default", "sbx"),
historyKey("default", "repo"),
projectPrSyncKey("default", "repo"),
projectBranchSyncKey("default", "repo"),
taskStatusSyncKey("default", "repo", "task", "sandbox-1", "session-1"),
];
for (const key of keys) {

View file

@ -1,52 +0,0 @@
import { describe, expect, it } from "vitest";
import { ConfigSchema, type AppConfig } from "@sandbox-agent/foundry-shared";
import { createProviderRegistry } from "../src/providers/index.js";
function makeConfig(): AppConfig {
return ConfigSchema.parse({
auto_submit: true,
notify: ["terminal"],
workspace: { default: "default" },
backend: {
host: "127.0.0.1",
port: 7741,
dbPath: "~/.local/share/foundry/task.db",
opencode_poll_interval: 2,
github_poll_interval: 30,
backup_interval_secs: 3600,
backup_retention_days: 7,
},
providers: {
local: {},
daytona: { image: "ubuntu:24.04" },
},
});
}
describe("provider registry", () => {
it("defaults to local when daytona is not configured", () => {
const registry = createProviderRegistry(makeConfig());
expect(registry.defaultProviderId()).toBe("local");
});
it("prefers daytona when an api key is configured", () => {
const registry = createProviderRegistry(
ConfigSchema.parse({
...makeConfig(),
providers: {
...makeConfig().providers,
daytona: {
...makeConfig().providers.daytona,
apiKey: "test-token",
},
},
}),
);
expect(registry.defaultProviderId()).toBe("daytona");
});
it("returns the built-in provider", () => {
const registry = createProviderRegistry(makeConfig());
expect(registry.get("daytona").id()).toBe("daytona");
});
});

View file

@ -0,0 +1,50 @@
import { describe, expect, it } from "vitest";
import { ConfigSchema, type AppConfig } from "@sandbox-agent/foundry-shared";
import { availableSandboxProviderIds, defaultSandboxProviderId, resolveSandboxProviderId } from "../src/sandbox-config.js";
function makeConfig(overrides?: Partial<AppConfig>): AppConfig {
return ConfigSchema.parse({
auto_submit: true,
notify: ["terminal"],
workspace: { default: "default" },
backend: {
host: "127.0.0.1",
port: 7741,
dbPath: "~/.local/share/foundry/task.db",
opencode_poll_interval: 2,
github_poll_interval: 30,
backup_interval_secs: 3600,
backup_retention_days: 7,
},
providers: {
local: {},
e2b: {},
},
...overrides,
});
}
describe("sandbox config", () => {
it("defaults to local when e2b is not configured", () => {
const config = makeConfig();
expect(defaultSandboxProviderId(config)).toBe("local");
expect(availableSandboxProviderIds(config)).toEqual(["local"]);
});
it("prefers e2b when an api key is configured", () => {
const config = makeConfig({
providers: {
local: {},
e2b: { apiKey: "test-token" },
},
});
expect(defaultSandboxProviderId(config)).toBe("e2b");
expect(availableSandboxProviderIds(config)).toEqual(["e2b", "local"]);
expect(resolveSandboxProviderId(config, "e2b")).toBe("e2b");
});
it("rejects selecting e2b without an api key", () => {
const config = makeConfig();
expect(() => resolveSandboxProviderId(config, "e2b")).toThrow("E2B provider is not configured");
});
});

View file

@ -1,21 +0,0 @@
import { describe, expect, it } from "vitest";
import { resolveEventListOffset } from "../src/actors/sandbox-instance/persist.js";
describe("sandbox-instance persist event offset", () => {
it("returns newest tail when cursor is omitted", () => {
expect(resolveEventListOffset({ total: 180, limit: 50 })).toBe(130);
});
it("returns zero when total rows are below page size", () => {
expect(resolveEventListOffset({ total: 20, limit: 50 })).toBe(0);
});
it("uses explicit cursor when provided", () => {
expect(resolveEventListOffset({ cursor: "7", total: 180, limit: 50 })).toBe(7);
});
it("normalizes invalid cursors to zero", () => {
expect(resolveEventListOffset({ cursor: "-3", total: 180, limit: 50 })).toBe(0);
expect(resolveEventListOffset({ cursor: "not-a-number", total: 180, limit: 50 })).toBe(0);
});
});

View file

@ -56,7 +56,7 @@ describe("workspace isolation", () => {
workspaceId: "alpha",
repoId: repoA.repoId,
task: "task A",
providerId: "daytona",
providerId: "local",
explicitBranchName: "feature/a",
explicitTitle: "A",
});
@ -65,7 +65,7 @@ describe("workspace isolation", () => {
workspaceId: "beta",
repoId: repoB.repoId,
task: "task B",
providerId: "daytona",
providerId: "local",
explicitBranchName: "feature/b",
explicitTitle: "B",
});