diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index e70c567432f..ec91e108375 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -3743,18 +3743,17 @@ openclaw() { esac ;; *) - echo "Error: 'openclaw channels $2' cannot modify channels inside the sandbox." >&2 + _nemoclaw_channel_operation_hint="" + case "${2:-}" in add | remove) _nemoclaw_channel_operation_hint="$2" ;; esac + _nemoclaw_channel_name_hint="" + case "${3:-}" in + discord | slack | teams | telegram | wechat | whatsapp) + _nemoclaw_channel_name_hint="$3" + ;; + esac + echo "Error: 'openclaw channels $_nemoclaw_channel_operation_hint' cannot modify channels inside the sandbox." >&2 echo "Changes inside the sandbox do not persist across rebuilds." >&2 - echo "" >&2 - echo "To add or remove messaging channels, exit the sandbox and run:" >&2 - echo " nemoclaw channels add " >&2 - echo " nemoclaw channels remove " >&2 - echo "" >&2 - echo "These stage the change and rebuild the sandbox to apply it." >&2 - echo "WhatsApp pairs entirely inside the sandbox; complete pairing via:" >&2 - echo " openclaw channels login --channel whatsapp" >&2 - echo "WeChat captures its token via a host-side QR during the host-side" >&2 - echo "'channels add wechat' flow — no in-sandbox login step." >&2 + echo "Run 'nemoclaw $(_nemoclaw_policy_denial_hint_label) channels $_nemoclaw_channel_operation_hint $_nemoclaw_channel_name_hint' on the host." >&2 return 1 ;; esac diff --git a/test/nemoclaw-start.test.ts b/test/nemoclaw-start.test.ts index f8c82d7e8c6..b2fd8fb9a70 100644 --- a/test/nemoclaw-start.test.ts +++ b/test/nemoclaw-start.test.ts @@ -954,11 +954,6 @@ describe("nemoclaw-start configure guard behavior", () => { expect(configSet.stderr).toContain("openclaw config set"); expect(configSet.stderr).toContain("nemoclaw onboard --resume"); - const channelsAdd = runGuardedOpenclaw(setup, ["channels", "add", "slack"]); - expect(channelsAdd.status).toBe(1); - expect(channelsAdd.stderr).toContain("openclaw channels add"); - expect(channelsAdd.stderr).toContain("nemoclaw channels add"); - const localAgent = runGuardedOpenclaw(setup, ["agent", "--local"]); expect(localAgent.status).toBe(1); expect(localAgent.stderr).toContain("--local"); @@ -1033,34 +1028,39 @@ exit 1 fs.rmSync(setup.tmpDir, { recursive: true, force: true }); } }); - // #2592 reported the guard did not fire for `openclaw channels add telegram` - // and `openclaw channels remove telegram` from inside the sandbox. The - // existing test above only exercises `add slack`. Lock in coverage for every - // (channel × op) combo so the guard cannot regress for any one of them - // while passing for another. - it("blocks every mutating channel-operation combination and surfaces the host-side hint (#2592)", () => { + it("blocks channel mutations and renders only validated host-side hints (#2592, #7292)", () => { const setup = writeProxyEnvWithGuard(); try { - const channels = ["slack", "telegram", "discord", "wechat", "whatsapp"]; - const ops = ["add", "remove"]; - for (const op of ops) { + const channels = ["discord", "slack", "teams", "telegram", "wechat", "whatsapp"]; + for (const op of ["add", "remove"]) { for (const channel of channels) { - const result = runGuardedOpenclaw(setup, ["channels", op, channel]); + const result = runGuardedShell(setup, [ + "export OPENSHELL_SANDBOX=my-assistant", + shellOpenclawCommand(["channels", op, channel]), + ]); expect(result.status, `channels ${op} ${channel} should be blocked`).toBe(1); - expect(result.stderr).toContain(`openclaw channels ${op}`); - expect(result.stderr).toContain(`nemoclaw channels ${op}`); + expect(result.stderr).toContain( + `Run 'nemoclaw my-assistant channels ${op} ${channel}' on the host.`, + ); } } + const marker = path.join(setup.tmpDir, "host-command-injection"); + for (const args of [ + ["channels", `add'; touch ${marker}; #`, "telegram"], + ["channels", "add", `telegram\n; touch ${marker}`], + ]) { + const result = runGuardedOpenclaw(setup, args); + expect(result.status).toBe(1); + expect(result.stderr).not.toContain(marker); + expect(result.stderr).toMatch(/channels (?: telegram|add )/); + } + expect(fs.existsSync(marker)).toBe(false); } finally { fs.rmSync(setup.tmpDir, { recursive: true, force: true }); } }); - // WhatsApp pairs entirely inside the sandbox via `openclaw channels login - // --channel whatsapp`, so the guard must allow that exact in-sandbox login - // path. WeChat completes pairing host-side and must stay blocked here so it - // cannot bypass NemoClaw's host-side registry/provider/rebuild path. - // `status` is read-only diagnostics and is similarly safe to allow. + // WhatsApp pairing is in-sandbox; status is read-only and does not persist changes. it("allows only WhatsApp `channels login` and read-only `channels status` inside the sandbox", () => { const setup = writeProxyEnvWithGuard(); try {