diff --git a/server/src/__tests__/environment-execution-target.test.ts b/server/src/__tests__/environment-execution-target.test.ts index 6255e846f4..6a6c2dab2b 100644 --- a/server/src/__tests__/environment-execution-target.test.ts +++ b/server/src/__tests__/environment-execution-target.test.ts @@ -176,6 +176,106 @@ describe("resolveEnvironmentExecutionTarget", () => { }); }); + it("resolves sandbox targets for every remote-managed adapter, including grok_local", async () => { + for (const adapterType of [ + "claude_local", + "codex_local", + "cursor", + "gemini_local", + "grok_local", + "opencode_local", + "pi_local", + ]) { + mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({ + driver: "sandbox", + config: { + provider: "fake-plugin", + reuseLease: false, + timeoutMs: 30_000, + }, + }); + + const target = await resolveEnvironmentExecutionTarget({ + db: {} as never, + companyId: "company-1", + adapterType, + environment: { + id: "env-1", + driver: "sandbox", + config: { provider: "fake-plugin" }, + }, + leaseId: "lease-1", + leaseMetadata: {}, + lease: null, + environmentRuntime: null, + }); + + expect(target, `adapter ${adapterType}`).toMatchObject({ + kind: "remote", + transport: "sandbox", + providerKey: "fake-plugin", + }); + } + }); + + it("returns null for adapters without remote-managed environment support", async () => { + for (const driver of ["sandbox", "ssh"] as const) { + const target = await resolveEnvironmentExecutionTarget({ + db: {} as never, + companyId: "company-1", + adapterType: "process", + environment: { + id: "env-1", + driver, + config: { provider: "fake-plugin" }, + }, + leaseId: "lease-1", + leaseMetadata: {}, + lease: null, + environmentRuntime: null, + }); + + expect(target, `driver ${driver}`).toBeNull(); + } + expect(mockResolveEnvironmentDriverConfigForRuntime).not.toHaveBeenCalled(); + }); + + it("resolves SSH execution targets for grok_local", async () => { + mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({ + driver: "ssh", + config: { + host: "ssh.example.test", + port: 22, + username: "paperclip", + remoteWorkspacePath: "/srv/paperclip", + privateKey: "PRIVATE KEY", + knownHosts: "[ssh.example.test]:22 ssh-ed25519 AAAA", + strictHostKeyChecking: true, + }, + }); + + const target = await resolveEnvironmentExecutionTarget({ + db: {} as never, + companyId: "company-1", + adapterType: "grok_local", + environment: { + id: "env-ssh-1", + driver: "ssh", + config: {}, + }, + leaseId: "lease-ssh-1", + leaseMetadata: {}, + lease: null, + environmentRuntime: null, + }); + + expect(target).toMatchObject({ + kind: "remote", + transport: "ssh", + remoteCwd: "/srv/paperclip", + }); + }); + it("resolves SSH execution targets in bridge mode", async () => { mockResolveEnvironmentDriverConfigForRuntime.mockResolvedValue({ driver: "ssh", diff --git a/server/src/services/environment-execution-target.ts b/server/src/services/environment-execution-target.ts index 207e654fb0..f0ca1b68ca 100644 --- a/server/src/services/environment-execution-target.ts +++ b/server/src/services/environment-execution-target.ts @@ -1,5 +1,6 @@ import type { Db } from "@paperclipai/db"; import type { Environment, EnvironmentLease } from "@paperclipai/shared"; +import { adapterSupportsRemoteManagedEnvironments } from "@paperclipai/shared"; import { adapterExecutionTargetToRemoteSpec, type AdapterExecutionTarget, @@ -33,14 +34,10 @@ export async function resolveEnvironmentExecutionTarget(input: { } if (input.environment.driver === "sandbox") { - if ( - input.adapterType !== "codex_local" && - input.adapterType !== "claude_local" && - input.adapterType !== "gemini_local" && - input.adapterType !== "opencode_local" && - input.adapterType !== "pi_local" && - input.adapterType !== "cursor" - ) { + // Keep this gate in lockstep with the shared capability metadata that the + // environment selector and capabilities API expose; a drift here lets the + // UI offer environments the runtime then refuses. + if (!adapterSupportsRemoteManagedEnvironments(input.adapterType)) { return null; } @@ -161,14 +158,7 @@ export async function resolveEnvironmentExecutionTarget(input: { } if ( - ( - input.adapterType !== "codex_local" && - input.adapterType !== "claude_local" && - input.adapterType !== "gemini_local" && - input.adapterType !== "opencode_local" && - input.adapterType !== "pi_local" && - input.adapterType !== "cursor" - ) || + !adapterSupportsRemoteManagedEnvironments(input.adapterType) || input.environment.driver !== "ssh" ) { return null;