diff --git a/extensions/loom/sandbox/sandbox-config.ts b/extensions/loom/sandbox/sandbox-config.ts index 8f303e8f..bface652 100644 --- a/extensions/loom/sandbox/sandbox-config.ts +++ b/extensions/loom/sandbox/sandbox-config.ts @@ -1,5 +1,6 @@ import * as path from "path"; import type { SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime"; +import { normalizeGalaxyUrl } from "../profiles"; /** * Inputs for deriving the OS-sandbox profile. Pure: no I/O, fully testable. @@ -42,7 +43,14 @@ const DENY_WRITE = [".env", ".env.*", "*.pem", "*.key"]; export function hostFromUrl(url?: string): string | undefined { if (!url) return undefined; try { - return new URL(url).hostname || undefined; + // Normalize via the same helper /connect and the profile system use, so a + // scheme-less GALAXY_URL ("usegalaxy.org") still resolves to a host. + const parsed = new URL(normalizeGalaxyUrl(url)); + // Galaxy speaks http(s) only (validateGalaxyUrl enforces this on connect), + // so keep the host only for those schemes -- an ftp:// or file:// GALAXY_URL + // must not seed the bash network allowlist. + if (parsed.protocol !== "https:" && parsed.protocol !== "http:") return undefined; + return parsed.hostname || undefined; } catch { return undefined; } diff --git a/tests/sandbox-config.test.ts b/tests/sandbox-config.test.ts index bd74418e..cf79c313 100644 --- a/tests/sandbox-config.test.ts +++ b/tests/sandbox-config.test.ts @@ -8,6 +8,23 @@ describe("hostFromUrl", () => { expect(hostFromUrl("https://my.galaxy.example:8080/x")).toBe("my.galaxy.example"); expect(hostFromUrl(undefined)).toBeUndefined(); expect(hostFromUrl("not a url")).toBeUndefined(); + expect(hostFromUrl(" ")).toBeUndefined(); + }); + + it("extracts the host from a scheme-less URL (the form config/env often supply)", () => { + expect(hostFromUrl("usegalaxy.org")).toBe("usegalaxy.org"); + expect(hostFromUrl("test.galaxyproject.org/")).toBe("test.galaxyproject.org"); + expect(hostFromUrl("my.galaxy.example:8080/x")).toBe("my.galaxy.example"); + }); + + it("keeps a loopback http host (the one non-https scheme Galaxy permits)", () => { + expect(hostFromUrl("http://127.0.0.1:8080")).toBe("127.0.0.1"); + }); + + it("ignores non-http(s) schemes so they can't seed the network allowlist", () => { + // Galaxy speaks http(s); ftp:// / file:// (even with a host) must not allowlist one. + expect(hostFromUrl("ftp://x.org")).toBeUndefined(); + expect(hostFromUrl("file://evil.example/etc/passwd")).toBeUndefined(); }); }); @@ -41,6 +58,11 @@ describe("buildSandboxConfig", () => { expect(c.network!.allowedDomains).toContain("usegalaxy.org"); }); + it("allowlists the Galaxy host even when the configured URL is scheme-less", () => { + const c = buildSandboxConfig({ ...base, galaxyUrl: "usegalaxy.org" }); + expect(c.network!.allowedDomains).toContain("usegalaxy.org"); + }); + it("includes extra write roots and extra allowed domains", () => { const c = buildSandboxConfig({ ...base,