Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
14 changes: 14 additions & 0 deletions docs/network-policy/integration-policy-examples.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,20 @@ $ nemoclaw my-assistant policy-add github --yes
$ nemoclaw my-assistant policy-add jira --yes
```

The `jira` preset intentionally allows Node.js access to Atlassian Cloud and
does not allow `curl`. When validating it manually, avoid plain `curl -s`
against `auth.atlassian.com`: Atlassian can return an empty redirect body even
when the request succeeds. Use an explicit status probe instead:

```console
$ node -e "require('https').get('https://api.atlassian.com', r => console.log(r.statusCode))"
$ curl -sS -o /dev/null -w '%{http_code}' --max-time 10 https://auth.atlassian.com
```

Before approval, the curl probe should report `000` or a local policy denial.
After approving the blocked request in OpenShell, it should report an HTTP
status such as `301` or `200`.

Remove access when the task is done:

```console
Expand Down
1 change: 1 addition & 0 deletions nemoclaw-blueprint/policies/presets/jira.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ network_policies:
- allow: { method: POST, path: "/**" }
binaries:
- { path: /usr/local/bin/node }
- { path: /usr/bin/node }
15 changes: 15 additions & 0 deletions src/lib/policy/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,21 @@ const MESSAGING_PRESET_LABELS: Record<string, string> = {
};

function getMessagingPresetWarning(presetName: string): string | null {
if (presetName === "jira") {
return [
"Jira preset validation uses per-binary policy signals.",
"Node HTTPS is allowed for Atlassian API traffic:",
'node -e "require(\'https\').get(\'https://api.atlassian.com\', r => console.log(r.statusCode))"',
"curl is intentionally not in the preset binary allowlist. Avoid plain",
"curl -s probes for auth.atlassian.com: Atlassian can return an empty",
"redirect body, which looks the same as a blocked request. Use an",
"observable status probe instead:",
"curl -sS -o /dev/null -w '%{http_code}' --max-time 10 https://auth.atlassian.com",
"Before approval, expect 000 or a local policy denial; after approval,",
"expect an HTTP status such as 301 or 200.",
].join("\n ");
}

const label = MESSAGING_PRESET_LABELS[presetName];
if (!label) return null;
const lines = [
Expand Down
10 changes: 10 additions & 0 deletions test/policies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,16 @@ describe("policies", () => {
expect(warning).toContain('dns.resolve("gateway.discord.gg")');
});

it("adds Jira validation guidance that makes blocked versus redirected curl observable", () => {
const warning = policies.getMessagingPresetWarning("jira");

expect(warning).toContain("curl -s");
expect(warning).toContain("curl -sS -o /dev/null -w '%{http_code}'");
expect(warning).toContain("000");
expect(warning).toContain("Node HTTPS");
expect(warning).toContain("https://api.atlassian.com");
});

it("returns null for non-messaging presets", () => {
expect(policies.getMessagingPresetWarning("npm")).toBeNull();
expect(policies.getMessagingPresetWarning("pypi")).toBeNull();
Expand Down
18 changes: 18 additions & 0 deletions test/validate-blueprint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -545,6 +545,24 @@ describe("huggingface preset", () => {
});
});

describe("jira preset", () => {
const JIRA_PRESET_PATH = new URL(
"../nemoclaw-blueprint/policies/presets/jira.yaml",
import.meta.url,
);
const jiraPreset = loadYaml<PolicyPreset>(JIRA_PRESET_PATH);

it("regression #3758: Jira allows Node but not curl", () => {
const binaries = (jiraPreset.network_policies?.atlassian?.binaries ?? [])
.map((binary) => binary.path)
.sort();

expect(binaries).toEqual(["/usr/bin/node", "/usr/local/bin/node"]);
expect(binaries).not.toContain("/usr/bin/curl");
expect(binaries).not.toContain("/usr/local/bin/curl");
});
});

describe("messaging WebSocket presets", () => {
const DISCORD_PRESET_PATH = new URL(
"../nemoclaw-blueprint/policies/presets/discord.yaml",
Expand Down
Loading