Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
fd876ff
fix(sandbox): disable sandboxing gracefully when no image is configured
awtprod Aug 17, 2026
c0d97dc
fix(sandbox): migrate bootstrap to podman-static v5.8.4
Aug 17, 2026
944088d
fix(sandbox): disable workspace/desktop volume quotas alongside conta…
Aug 18, 2026
a016655
fix(sandbox): stop swallowing the egress-proxy alias failure in boots…
Aug 18, 2026
ead4a3e
fix(server): stop forcing sandbox git resolution when no sandbox imag…
Aug 18, 2026
4590f17
fix(sandbox): pin a ref before bundling a local repo seed instead of …
Aug 18, 2026
5061084
fix(server): run git bundle verify inside the seeded repository
Aug 18, 2026
ffb3384
fix(server): honor T3_SANDBOX_RUNTIME when provisioning a sandbox
Aug 18, 2026
e09d096
fix(server): run git bundle verify where a repository exists
Aug 18, 2026
dd234e5
fix(server): fetch the seed bundle's ref instead of cloning it
Aug 18, 2026
2c397c8
fix(server): let sandboxed agents reach the credential proxy
Aug 18, 2026
cebf612
fix(server): log why a Claude turn died instead of only that it did
Aug 19, 2026
b913c88
fix(sandbox): stop one thread's broken container from killing the server
Aug 19, 2026
21b3c0c
fix(server): stop the sandbox HOME from breaking the runtime CLI itself
Aug 19, 2026
f70e3ca
fix(server): share one sandbox runtime manager across checkpoints and…
Aug 19, 2026
8c1c03f
fix(server): generate thread titles from the host workspace, not the …
Aug 19, 2026
22e9025
fix(server): surface the real error behind a sandbox lifecycle failure
Aug 19, 2026
59c6307
fix(server): let sandbox checkpoint probes read a non-zero git exit
Aug 19, 2026
8f2eaae
fix(server): stop reporting a desktop that headless sandboxes never s…
Aug 20, 2026
2128e7d
fix(server): stop stranding sandbox branches after a server restart
Aug 20, 2026
f669824
fix(server): let a stopped sandbox come back with its work
Aug 20, 2026
fe5b48d
fix(server): reclaim a thread's sandbox when the thread settles
Aug 20, 2026
c6d6f3e
fix(server): forget the resume cursor when a thread gets a fresh cont…
Aug 20, 2026
6b57d6c
fix(server): carry the provider conversation store across sandbox tea…
Aug 20, 2026
78b6887
fix(server): use namespace node imports in headless credential proxy …
Aug 21, 2026
2bf9819
fix(server): convert four provider command tests off manual Effect.ru…
Aug 21, 2026
be0eea5
fix(server): clear the typecheck errors CI's Check job reports after …
Aug 21, 2026
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
22 changes: 20 additions & 2 deletions apps/mobile/src/features/threads/SandboxDesktopPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export function SandboxDesktopPanel(props: {
const exportBranch = useAtomCommand(threadEnvironment.exportSandboxBranch, {
reportFailure: false,
});
const stop = useAtomCommand(threadEnvironment.stopSandbox, { reportFailure: false });
const viewerTicket = useAtomCommand(threadEnvironment.requestSandboxViewerTicket, {
reportFailure: false,
});
Expand All @@ -45,6 +46,11 @@ export function SandboxDesktopPanel(props: {
);
const target = { environmentId: props.environmentId } as const;
const human = sandbox?.controller.kind === "human" ? sandbox.controller : null;
// Headless deployments run the sandbox with no desktop, so there is nothing
// to view or take control of -- but stopping and exporting still apply.
const desktopUnavailable = sandbox != null && sandbox.desktop.status === "unavailable";
const stoppable =
sandbox != null && !["stopped", "expired", "deleted"].includes(sandbox.lifecycle);
const openViewer = async () => {
if (busy) return;
setBusy(true);
Expand All @@ -67,7 +73,9 @@ export function SandboxDesktopPanel(props: {
<View className="border-t border-border bg-card px-4 py-2">
<View className="flex-row items-center gap-2">
<View className="min-w-0 flex-1">
<Text className="text-xs font-t3-bold text-foreground">Isolated desktop</Text>
<Text className="text-xs font-t3-bold text-foreground">
{desktopUnavailable ? "Isolated sandbox" : "Isolated desktop"}
</Text>
<Text className="text-xs text-foreground-muted" numberOfLines={1}>
{sandbox
? `${sandbox.lifecycle.replaceAll("-", " ")} · ${sandbox.branch.branchName}`
Expand All @@ -90,7 +98,7 @@ export function SandboxDesktopPanel(props: {
<Text className="text-sm font-t3-bold text-accent">Open</Text>
</Pressable>
) : null}
{sandbox && human === null && sandbox.lifecycle === "ready" ? (
{sandbox && human === null && sandbox.lifecycle === "ready" && !desktopUnavailable ? (
<Pressable
disabled={busy}
onPress={() =>
Expand Down Expand Up @@ -135,6 +143,16 @@ export function SandboxDesktopPanel(props: {
<Text className="text-sm font-t3-bold text-accent">Export</Text>
</Pressable>
) : null}
{stoppable ? (
<Pressable
disabled={busy || human !== null}
onPress={() =>
void run(() => stop({ ...target, input: { threadId: props.thread.id } }))
}
>
<Text className="text-sm font-t3-bold text-danger-foreground">Stop</Text>
</Pressable>
) : null}
</View>
{sandbox ? (
<Text className="mt-1 text-[11px] text-foreground-muted">
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/checkpointing/CheckpointStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,10 @@ export const make = Effect.gen(function* () {
args,
cwd,
...(options?.env === undefined ? {} : { env: options.env }),
// Without this the backend throws on any non-zero exit and the
// `allowNonZero` branch below is unreachable -- so a `rev-parse` probe
// for an absent ref failed the turn instead of answering "no".
...(options?.allowNonZero === true ? { allowNonZeroExit: true } : {}),
timeoutMs: 30_000,
})
.pipe(
Expand Down
14 changes: 14 additions & 0 deletions apps/server/src/orchestration/CommandDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ import * as ServerConfig from "../config.ts";
import { resolveInteractiveEfficiency } from "../efficiency/EfficiencyRouting.ts";
import * as GitWorkflowService from "../git/GitWorkflowService.ts";
import { ProviderRegistry } from "../provider/Services/ProviderRegistry.ts";
import { T3ProjectFileLoader } from "../project/T3ProjectFileLoader.ts";
import {
resolveSandboxImage,
resolveSandboxPreviewProxyImage,
} from "../sandbox/SandboxRuntimeManager.ts";
import { ServerSettingsService } from "../serverSettings.ts";
import * as WorkspacePaths from "../workspace/WorkspacePaths.ts";
import { normalizeDispatchCommand } from "./Normalizer.ts";
Expand Down Expand Up @@ -100,6 +105,7 @@ export const make = Effect.gen(function* () {
const providerRegistry = yield* ProviderRegistry;
const serverSettings = yield* ServerSettingsService;
const gitWorkflow = yield* GitWorkflowService.GitWorkflowService;
const projectFileLoader = yield* T3ProjectFileLoader;
const setupScriptRunner = yield* ProjectSetupScriptRunner.ProjectSetupScriptRunner;
const vcsStatusBroadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
const fileSystem = yield* FileSystem.FileSystem;
Expand Down Expand Up @@ -410,6 +416,14 @@ export const make = Effect.gen(function* () {
return yield* new OrchestrationDispatchCommandError({
message: `Project '${create.projectId}' was not found.`,
});
const projectFile = Option.getOrUndefined(
yield* projectFileLoader.load(project.workspaceRoot),
);
if (
resolveSandboxImage(projectFile) === undefined ||
resolveSandboxPreviewProxyImage() === undefined
)
return resolvedCommand;
const local = yield* gitWorkflow
.localStatus({ cwd: project.workspaceRoot })
.pipe(
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/orchestration/Layers/OrchestrationReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ProviderCommandReactor } from "../Services/ProviderCommandReactor.ts";
import { ProviderRuntimeIngestionService } from "../Services/ProviderRuntimeIngestion.ts";
import { ThreadDeletionReactor } from "../Services/ThreadDeletionReactor.ts";
import { SandboxLifecycleReactor } from "../Services/SandboxLifecycleReactor.ts";
import { SandboxSettleCleanupReactor } from "../Services/SandboxSettleCleanupReactor.ts";
import * as AgentAwarenessRelay from "../../relay/AgentAwarenessRelay.ts";

export const makeOrchestrationReactor = Effect.gen(function* () {
Expand All @@ -18,6 +19,7 @@ export const makeOrchestrationReactor = Effect.gen(function* () {
const checkpointReactor = yield* CheckpointReactor;
const threadDeletionReactor = yield* ThreadDeletionReactor;
const sandboxLifecycleReactor = yield* SandboxLifecycleReactor;
const sandboxSettleCleanupReactor = yield* SandboxSettleCleanupReactor;
const agentAwarenessRelay = yield* AgentAwarenessRelay.AgentAwarenessRelay;

const start: OrchestrationReactorShape["start"] = Effect.fn("start")(function* () {
Expand All @@ -26,6 +28,7 @@ export const makeOrchestrationReactor = Effect.gen(function* () {
yield* checkpointReactor.start();
yield* threadDeletionReactor.start();
yield* sandboxLifecycleReactor.start();
yield* sandboxSettleCleanupReactor.start();
yield* agentAwarenessRelay.start();
});

Expand All @@ -35,6 +38,7 @@ export const makeOrchestrationReactor = Effect.gen(function* () {
yield* checkpointReactor.drain;
yield* threadDeletionReactor.drain;
yield* sandboxLifecycleReactor.drain;
yield* sandboxSettleCleanupReactor.drain;
});

return {
Expand Down
Loading
Loading