Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 9 additions & 1 deletion extensions/loom/sandbox/sandbox-config.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down
22 changes: 22 additions & 0 deletions tests/sandbox-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});

Expand Down Expand Up @@ -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,
Expand Down
Loading