-
Notifications
You must be signed in to change notification settings - Fork 3k
fix(policy): disclose full messaging-preset egress before channels apply #7187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
a2b6296
fix(policy): disclose full messaging-preset egress before channels apply
laitingsheng a7b18a7
fix(policy): read real preset fixtures in --from-dir egress test and …
laitingsheng 4cef61d
fix(policy): disclose preset scope before lifecycle effects
apurvvkumaria 5bd1f73
refactor(onboard): keep policy disclosure out of entrypoint
apurvvkumaria 2d73e77
fix(policy): sanitize disclosure output and cover every preset-applic…
laitingsheng fc8ed4c
fix(policy): preserve owned preset refresh transactions
apurvvkumaria d868b11
style(policy): format owned preset refresh
apurvvkumaria feed1fa
refactor(policy): consolidate channel scope disclosure
apurvvkumaria 15578d3
Merge remote-tracking branch 'origin/fix/7179-disclose-preset-egress-…
laitingsheng 888cb91
Merge remote-tracking branch 'origin/main' into fix/7179-disclose-pre…
laitingsheng 990d0c4
Merge remote-tracking branch 'origin/fix/7179-disclose-preset-egress-…
laitingsheng ab1e3c9
test(policy): mock live policy read in start dry-run disclosure test
laitingsheng a835d7a
fix(policy): avoid duplicate egress disclosure
apurvvkumaria 4b12426
docs(policy): clarify no-op egress disclosure
apurvvkumaria 13fb5bf
fix(policy): revalidate disclosure state before mutation
cv f6518c3
Merge branch 'main' into fix/7179-disclose-preset-egress-before-channels
cv 25aa91b
Merge branch 'main' into fix/7179-disclose-preset-egress-before-channels
cv 40144dd
fix(policy): redact preset scope disclosure
cv 297c9a1
chore(ci): refresh policy disclosure checks
cv 5b894f5
Merge branch 'main' into fix/7179-disclose-preset-egress-before-channels
cv File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
src/lib/actions/sandbox/policy-channel-scope-disclosure.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { afterEach, beforeEach, describe, expect, it, type MockInstance, vi } from "vitest"; | ||
|
|
||
| import * as defs from "../../agent/defs"; | ||
| import * as policy from "../../policy"; | ||
| import * as registry from "../../state/registry"; | ||
| import { addSandboxChannel } from "./policy-channel"; | ||
|
|
||
| const WHATSAPP_PRESET = `preset: | ||
| name: whatsapp | ||
| description: "WhatsApp Web WebSocket and media" | ||
| network_policies: | ||
| whatsapp: | ||
| name: whatsapp | ||
| endpoints: | ||
| - host: web.whatsapp.com | ||
| port: 443 | ||
| access: full | ||
| tls: skip | ||
| - host: raw.githubusercontent.com | ||
| port: 443 | ||
| protocol: rest | ||
| enforcement: enforce | ||
| rules: | ||
| - allow: | ||
| method: GET | ||
| path: "/WhiskeySockets/Baileys/master/src/Defaults/index.ts" | ||
| binaries: | ||
| - { path: /usr/local/bin/node } | ||
| `; | ||
|
|
||
| let exitMock: MockInstance; | ||
| let logSpy: MockInstance; | ||
|
|
||
| function agentFixture(name: string): defs.AgentDefinition { | ||
| return { name } as defs.AgentDefinition; | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| delete process.env.NEMOCLAW_NON_INTERACTIVE; | ||
| logSpy = vi.spyOn(console, "log").mockImplementation(() => undefined); | ||
| vi.spyOn(console, "error").mockImplementation(() => undefined); | ||
| exitMock = vi.spyOn(process, "exit").mockImplementation(((code?: number) => { | ||
| throw new Error(`process.exit(${code})`); | ||
| }) as never); | ||
|
|
||
| vi.spyOn(registry, "getSandbox").mockReturnValue({ name: "sb-scope" }); | ||
| vi.spyOn(defs, "loadAgent").mockReturnValue(agentFixture("openclaw")); | ||
| vi.spyOn(policy, "loadPresetForSandbox").mockReturnValue(WHATSAPP_PRESET); | ||
| vi.spyOn(policy, "parsePresetPolicyKeys").mockReturnValue(["whatsapp"]); | ||
| }); | ||
|
|
||
| afterEach(() => { | ||
| vi.restoreAllMocks(); | ||
| }); | ||
|
|
||
| function collectLogOutput(): string { | ||
| return (logSpy.mock.calls as unknown[][]).map((call) => call.map(String).join(" ")).join("\n"); | ||
| } | ||
|
|
||
| describe("channels add --dry-run discloses effective preset egress before mutation (#7179)", () => { | ||
| it("prints every declared endpoint host with its port and mode", async () => { | ||
| await addSandboxChannel("sb-scope", { channel: "whatsapp", dryRun: true }); | ||
|
|
||
| const output = collectLogOutput(); | ||
| expect(output).toContain("Effective egress that would be opened:"); | ||
| expect(output).toContain("- web.whatsapp.com:443 (access: full, tls: skip)"); | ||
| expect(output).toContain( | ||
| "- raw.githubusercontent.com:443 (protocol: rest, enforcement: enforce)", | ||
| ); | ||
| }); | ||
|
|
||
| it("names the narrowly scoped Baileys version-fetch method and path, not just the host", async () => { | ||
| await addSandboxChannel("sb-scope", { channel: "whatsapp", dryRun: true }); | ||
|
|
||
| const output = collectLogOutput(); | ||
| expect(output).toMatch( | ||
| /allow:\s+GET\s+\/WhiskeySockets\/Baileys\/master\/src\/Defaults\/index\.ts/, | ||
| ); | ||
| }); | ||
|
|
||
| it("lists declared binaries alongside the endpoints", async () => { | ||
| await addSandboxChannel("sb-scope", { channel: "whatsapp", dryRun: true }); | ||
|
|
||
| const output = collectLogOutput(); | ||
| expect(output).toContain("binaries:"); | ||
| expect(output).toContain("- /usr/local/bin/node"); | ||
| }); | ||
|
|
||
| it("emits the scope block before the 'would enable channel' summary", async () => { | ||
| await addSandboxChannel("sb-scope", { channel: "whatsapp", dryRun: true }); | ||
|
|
||
| const lines = (logSpy.mock.calls as unknown[][]).map((call) => call.map(String).join(" ")); | ||
| const scopeHeader = lines.findIndex((line) => | ||
| line.includes("Effective egress that would be opened:"), | ||
| ); | ||
| const wouldEnable = lines.findIndex((line) => line.includes("--dry-run: would enable channel")); | ||
| expect(scopeHeader).toBeGreaterThan(-1); | ||
| expect(wouldEnable).toBeGreaterThan(scopeHeader); | ||
| void exitMock; | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,131 @@ | ||
| // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| import { describe, expect, it } from "vitest"; | ||
|
|
||
| import { renderPresetScope } from "./preset-scope-render"; | ||
|
|
||
| const WHATSAPP_LIKE_PRESET = `preset: | ||
| name: whatsapp | ||
| description: "WhatsApp Web WebSocket and media" | ||
| network_policies: | ||
| whatsapp: | ||
| name: whatsapp | ||
| endpoints: | ||
| - host: web.whatsapp.com | ||
| port: 443 | ||
| access: full | ||
| tls: skip | ||
| - host: "*.whatsapp.net" | ||
| port: 443 | ||
| protocol: rest | ||
| enforcement: enforce | ||
| rules: | ||
| - allow: { method: GET, path: "/**" } | ||
| - allow: { method: POST, path: "/**" } | ||
| - host: raw.githubusercontent.com | ||
| port: 443 | ||
| protocol: rest | ||
| enforcement: enforce | ||
| rules: | ||
| - allow: | ||
| method: GET | ||
| path: "/WhiskeySockets/Baileys/master/src/Defaults/index.ts" | ||
| binaries: | ||
| - { path: /usr/local/bin/node } | ||
| - { path: /usr/bin/node } | ||
| `; | ||
|
|
||
| describe("renderPresetScope", () => { | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| it("returns an empty list for content with no network_policies", () => { | ||
| expect(renderPresetScope("preset:\n name: x\n description: 'y'\n")).toEqual([]); | ||
| expect(renderPresetScope("")).toEqual([]); | ||
| }); | ||
|
|
||
| it("returns an empty list for malformed YAML instead of throwing", () => { | ||
| expect(renderPresetScope("::: not yaml :::")).toEqual([]); | ||
| }); | ||
|
|
||
| it("renders full L4 tunnel endpoints with access + tls but no rule lines", () => { | ||
| const lines = renderPresetScope(WHATSAPP_LIKE_PRESET); | ||
| const joined = lines.join("\n"); | ||
| expect(joined).toContain("- web.whatsapp.com:443 (access: full, tls: skip)"); | ||
| const idx = lines.findIndex((line) => line.includes("web.whatsapp.com:443")); | ||
|
github-advanced-security[bot] marked this conversation as resolved.
Fixed
|
||
| expect(idx).toBeGreaterThan(-1); | ||
| expect(lines[idx + 1] ?? "").not.toMatch(/^\s+allow:/); | ||
| }); | ||
|
|
||
| it("renders REST endpoints with per-rule methods and paths", () => { | ||
| const lines = renderPresetScope(WHATSAPP_LIKE_PRESET); | ||
| const joined = lines.join("\n"); | ||
| expect(joined).toContain("- *.whatsapp.net:443 (protocol: rest, enforcement: enforce)"); | ||
| expect(joined).toMatch(/allow:\s+GET\s+\/\*\*/); | ||
| expect(joined).toMatch(/allow:\s+POST\s+\/\*\*/); | ||
| }); | ||
|
|
||
| it("surfaces the narrowly scoped Baileys version-fetch path, not just the host", () => { | ||
| const joined = renderPresetScope(WHATSAPP_LIKE_PRESET).join("\n"); | ||
| expect(joined).toContain("raw.githubusercontent.com:443"); | ||
| expect(joined).toContain("/WhiskeySockets/Baileys/master/src/Defaults/index.ts"); | ||
| }); | ||
|
|
||
| it("lists declared binaries", () => { | ||
| const joined = renderPresetScope(WHATSAPP_LIKE_PRESET).join("\n"); | ||
| expect(joined).toContain("binaries:"); | ||
| expect(joined).toContain("- /usr/local/bin/node"); | ||
| expect(joined).toContain("- /usr/bin/node"); | ||
| }); | ||
|
|
||
| it("prints one policy block per preset network policy", () => { | ||
| const multi = `network_policies: | ||
| policy_a: | ||
| name: policy_a | ||
| endpoints: | ||
| - host: a.example | ||
| port: 443 | ||
| protocol: rest | ||
| rules: | ||
| - allow: { method: GET, path: "/a" } | ||
| policy_b: | ||
| name: policy_b | ||
| endpoints: | ||
| - host: b.example | ||
| port: 443 | ||
| access: full | ||
| `; | ||
| const joined = renderPresetScope(multi).join("\n"); | ||
| expect(joined).toContain("policy 'policy_a':"); | ||
| expect(joined).toContain("policy 'policy_b':"); | ||
| expect(joined).toContain("- a.example:443"); | ||
| expect(joined).toContain("- b.example:443"); | ||
| }); | ||
|
|
||
| it("skips malformed endpoint entries without dropping the surrounding scope", () => { | ||
| const partial = `network_policies: | ||
| mixed: | ||
| name: mixed | ||
| endpoints: | ||
| - host: 42 | ||
| - foo: bar | ||
| - host: good.example | ||
| port: 443 | ||
| protocol: rest | ||
| rules: | ||
| - allow: { method: GET, path: "/**" } | ||
| `; | ||
| const joined = renderPresetScope(partial).join("\n"); | ||
| expect(joined).toContain("- good.example:443"); | ||
| expect(joined).not.toMatch(/^\s+- 42/m); | ||
| }); | ||
|
|
||
| it("emits (no endpoints declared) rather than skipping an empty policy", () => { | ||
| const empty = `network_policies: | ||
| bare: | ||
| name: bare | ||
| endpoints: [] | ||
| `; | ||
| const joined = renderPresetScope(empty).join("\n"); | ||
| expect(joined).toContain("policy 'bare':"); | ||
| expect(joined).toContain("(no endpoints declared)"); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.