From d78bd3b9a5f37802e99eace69eacf7ccce7cd54e Mon Sep 17 00:00:00 2001 From: Dotta Date: Wed, 3 Jun 2026 17:16:16 +0000 Subject: [PATCH] Preflight low-trust before workspace resolution --- .../heartbeat-workspace-session.test.ts | 52 +++++++++++++++++++ server/src/services/heartbeat.ts | 40 +++++++++++--- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/server/src/__tests__/heartbeat-workspace-session.test.ts b/server/src/__tests__/heartbeat-workspace-session.test.ts index df68a4940a2..967f071663c 100644 --- a/server/src/__tests__/heartbeat-workspace-session.test.ts +++ b/server/src/__tests__/heartbeat-workspace-session.test.ts @@ -15,6 +15,7 @@ import { prioritizeProjectWorkspaceCandidatesForRun, parseSessionCompactionPolicy, resolveNextSessionState, + resolveWorkspaceAfterLowTrustPreflight, resolveRuntimeSessionParamsForWorkspace, stripHostWorkspaceProvisionForLowTrustSandbox, stripWorkspaceRuntimeFromExecutionRunConfig, @@ -213,6 +214,57 @@ describe("preflightLowTrustWorkspaceIsolation", () => { }); }); +describe("resolveWorkspaceAfterLowTrustPreflight", () => { + it("fails non-sandbox low-trust runs before resolving workspaces", async () => { + let workspaceResolverReached = false; + + await expect(resolveWorkspaceAfterLowTrustPreflight({ + trustPreset: lowTrustResolution(), + isolatedWorkspacesEnabled: true, + effectiveExecutionWorkspaceMode: "isolated_workspace", + issue: { + companyId: "company-1", + id: "issue-1", + projectId: "project-1", + }, + resolveSelectedEnvironmentDriver: async () => "local", + resolveWorkspace: async () => { + workspaceResolverReached = true; + return buildResolvedWorkspace(); + }, + })).rejects.toMatchObject({ + status: 422, + details: expect.objectContaining({ + code: "low_trust_requires_sandbox_environment", + }), + }); + + expect(workspaceResolverReached).toBe(false); + }); + + it("preserves standard-trust workspace resolution", async () => { + const workspace = buildResolvedWorkspace({ cwd: "/tmp/standard-workspace" }); + + await expect(resolveWorkspaceAfterLowTrustPreflight({ + trustPreset: standardTrustResolution(), + isolatedWorkspacesEnabled: false, + effectiveExecutionWorkspaceMode: "shared_workspace", + issue: { + companyId: "company-1", + id: "issue-1", + projectId: "project-1", + }, + resolveSelectedEnvironmentDriver: async () => { + throw new Error("standard trust should not inspect the environment driver"); + }, + resolveWorkspace: async () => workspace, + })).resolves.toEqual({ + selectedEnvironmentDriver: null, + workspace, + }); + }); +}); + describe("resolveRuntimeSessionParamsForWorkspace", () => { it("migrates fallback workspace sessions to project workspace when project cwd becomes available", () => { const agentId = "agent-123"; diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index ecb4c2a3748..3f2f9d2ede5 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -801,6 +801,28 @@ export async function preflightLowTrustWorkspaceIsolation(input: { return selectedEnvironmentDriver ?? null; } +export async function resolveWorkspaceAfterLowTrustPreflight(input: { + trustPreset: TrustPresetResolution; + isolatedWorkspacesEnabled: boolean; + effectiveExecutionWorkspaceMode: string | null | undefined; + issue: { companyId: string; id?: string | null; projectId?: string | null } | null; + resolveSelectedEnvironmentDriver: () => Promise; + resolveWorkspace: () => Promise; +}): Promise<{ selectedEnvironmentDriver: string | null; workspace: TWorkspace }> { + const selectedEnvironmentDriver = await preflightLowTrustWorkspaceIsolation({ + trustPreset: input.trustPreset, + isolatedWorkspacesEnabled: input.isolatedWorkspacesEnabled, + effectiveExecutionWorkspaceMode: input.effectiveExecutionWorkspaceMode, + issue: input.issue, + resolveSelectedEnvironmentDriver: input.resolveSelectedEnvironmentDriver, + }); + + return { + selectedEnvironmentDriver, + workspace: await input.resolveWorkspace(), + }; +} + function deriveRepoNameFromRepoUrl(repoUrl: string | null): string | null { const trimmed = repoUrl?.trim() ?? ""; if (!trimmed) return null; @@ -7485,12 +7507,6 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) trustPreset.kind === "low_trust_review" && resolvedExecutionWorkspaceMode === "shared_workspace" ? "isolated_workspace" : resolvedExecutionWorkspaceMode; - const resolvedWorkspace = await resolveWorkspaceForRun( - agent, - context, - previousSessionParams, - { useProjectWorkspace: requestedExecutionWorkspaceMode !== "agent_default" }, - ); const issueRef = issueContext ? { id: issueContext.id, @@ -7646,7 +7662,10 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) ? persistedExecutionWorkspaceMode : requestedExecutionWorkspaceMode; const selectedEnvironmentId = environmentResolution.environmentId; - const lowTrustPreflightEnvironmentDriver = await preflightLowTrustWorkspaceIsolation({ + const { + selectedEnvironmentDriver: lowTrustPreflightEnvironmentDriver, + workspace: resolvedWorkspace, + } = await resolveWorkspaceAfterLowTrustPreflight({ trustPreset, isolatedWorkspacesEnabled, effectiveExecutionWorkspaceMode, @@ -7665,6 +7684,13 @@ export function heartbeatService(db: Db, options: HeartbeatServiceOptions = {}) }); return preflightEnvironment.driver; }, + resolveWorkspace: () => + resolveWorkspaceForRun( + agent, + context, + previousSessionParams, + { useProjectWorkspace: requestedExecutionWorkspaceMode !== "agent_default" }, + ), }); const workspaceManagedConfig = shouldReuseExisting ? { ...config }