Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions agents/hermes/policy-additions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,9 @@ network_policies:
rules:
- allow: { method: GET, path: "/**" }
binaries:
- { path: /usr/bin/curl }
- { path: /usr/local/bin/pip3 }
- { path: /usr/local/bin/curl }
- { path: /usr/bin/python3* }
- { path: /opt/hermes/.venv/bin/python }

Expand Down
2 changes: 2 additions & 0 deletions nemoclaw-blueprint/policies/presets/pypi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@ network_policies:
- allow: { method: GET, path: "/**" }
- allow: { method: HEAD, path: "/**" }
binaries:
- { path: /usr/bin/curl }
- { path: /usr/bin/python3* }
- { path: /usr/bin/pip* }
- { path: /usr/local/bin/curl }
- { path: /usr/local/bin/python3* }
- { path: /usr/local/bin/pip* }
- { path: /sandbox/.venv/bin/python* }
Expand Down
34 changes: 33 additions & 1 deletion test/e2e/test-network-policy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
#
# Covers:
# TC-NET-01: Deny-by-default egress (blocked URL returns 403)
# TC-NET-02: Whitelisted endpoint access (PyPI reachable via pip)
# TC-NET-02: Whitelisted endpoint access (PyPI reachable via pip and curl GET)
# TC-NET-03: Live policy-add without restart (slack preset)
# TC-NET-04: policy-add --dry-run (no changes applied)
# TC-NET-05: Hot-reload (policy change without sandbox restart)
Expand Down Expand Up @@ -272,6 +272,38 @@ test_net_02_whitelist_access() {
else
fail "TC-NET-02: Whitelist" "pip could not reach PyPI: ${response:0:200}"
fi

log " Probing PyPI read-only access from inside sandbox using curl..."

local pypi_code
pypi_code=$(sandbox_exec "curl -sS -o /dev/null -w '%{http_code}' --max-time 20 https://pypi.org/simple/requests/ 2>&1" 2>&1) || true
log " pypi.org GET status: $pypi_code"

if [ "$pypi_code" = "200" ]; then
pass "TC-NET-02: pypi.org reachable via curl GET after preset applied"
else
fail "TC-NET-02: Whitelist" "curl GET to pypi.org did not return 200: ${pypi_code:0:200}"
fi

local files_code
files_code=$(sandbox_exec "curl -sS -o /dev/null -w '%{http_code}' --max-time 20 https://files.pythonhosted.org/rg/ 2>&1" 2>&1) || true
log " files.pythonhosted.org GET status: $files_code"

if echo "$files_code" | grep -qE "^([23][0-9][0-9]|404)$"; then
pass "TC-NET-02: files.pythonhosted.org returns a real HTTP status via curl GET"
else
fail "TC-NET-02: Whitelist" "curl GET to files.pythonhosted.org did not return a real HTTP status: ${files_code:0:200}"
fi

local post_code
post_code=$(sandbox_exec "curl -sS -o /dev/null -w '%{http_code}' -X POST --max-time 20 https://pypi.org/simple/le/ 2>&1" 2>&1) || true
log " pypi.org POST status: $post_code"

if [ "$post_code" = "403" ]; then
pass "TC-NET-02: PyPI POST remains blocked under read-only preset"
else
fail "TC-NET-02: Whitelist" "curl POST to pypi.org should remain blocked with 403: ${post_code:0:200}"
fi
}

# =============================================================================
Expand Down
26 changes: 26 additions & 0 deletions test/policies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1511,6 +1511,32 @@ exit 1
expect(content.includes("method: HEAD")).toBe(true);
});

it("pypi preset lets curl verify read-only package index access (#4014)", () => {
const content = requirePresetContent(policies.loadPreset("pypi"));
const parsed = YAML.parse(content);
const pypiPolicy = parsed.network_policies?.pypi as
| {
binaries?: Array<{ path?: string }>;
endpoints?: Array<{
host?: string;
access?: string;
rules?: Array<{ allow?: { method?: string } }>;
}>;
}
| undefined;

const binaries = (pypiPolicy?.binaries ?? []).map((binary) => binary.path).sort();
expect(binaries).toEqual(
expect.arrayContaining(["/usr/bin/curl", "/usr/local/bin/curl"]),
);

for (const endpoint of pypiPolicy?.endpoints ?? []) {
expect(endpoint.access).toBeUndefined();
const methods = (endpoint.rules ?? []).map((rule) => rule.allow?.method).sort();
expect(methods).toEqual(["GET", "HEAD"]);
}
});

it("package-manager presets include binaries section", () => {
// Without binaries, the proxy can't match pip/npm traffic to the policy
// and returns 403.
Expand Down
Loading