From f24af20b2a5949ae28a67c2166c430f65517ce59 Mon Sep 17 00:00:00 2001 From: Tiberiu Socaci Date: Fri, 6 Nov 2026 20:51:19 +0200 Subject: [PATCH] fix: clarify direct-host network policy in MCP description Signed-off-by: Tiberiu Socaci --- CHANGELOG.md | 3 +++ FEATURES.md | 3 +++ TEST-PLAN.md | 12 ++++++++++++ src/mcp/tools/channel-admin.js | 8 +++++--- test/channel-policy-audit.test.js | 14 ++++++++++++++ 5 files changed, 37 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b06986fa..6e029d65 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ release gates. Stable promotion and publication require approval of the exact te ### Beta development notes +- Clarify that the network tool's policy is advisory in `/sudo` direct-host threads, with no + OS-level egress cutoff. Ordinary proxy-mode container enforcement is unchanged. + - Ship the documentation favicon so handbook pages no longer request a missing asset. - Revoke pending identity-link proofs for a recovered administrator, preventing an older proof diff --git a/FEATURES.md b/FEATURES.md index cc6b9bbb..8786d830 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -3725,6 +3725,9 @@ A categorized catalog of what's shipped. Cross-linked to `TEST-PLAN.md` checks. `run_config` event records `networkEnforced` and `egress` beside `networkPolicy` so an operator reading it after an incident cannot mistake one for the other. `NETWORK_POLICY_ENFORCED` in `src/engines/network-policy.js` is now `true`; every surface asks `networkEnforcedFor(target)`. + The shared `set_channel_network` tool description also explicitly distinguishes ordinary + proxy-mode enforcement from legacy bridge/raw-socket policy and `/sudo` direct-host policy, + which has no OS-level egress cutoff. This description does not change runtime enforcement. → TEST-PLAN: Sandbox boundaries; Egress proxy, placeholders and `--network none`. - **Retired 2026-09-03 (Linux + containers only):** nothing replaces it — with no host sandbox there is no user namespace for AppArmor to restrict, and rootless Podman brings its own uid mapping (`--userns=keep-id`, `/etc/subuid`). **Linux hosts keep their Bash sandbox under Ubuntu's AppArmor userns restriction**: Ubuntu 23.10+ diff --git a/TEST-PLAN.md b/TEST-PLAN.md index dd358306..15a5b28d 100644 --- a/TEST-PLAN.md +++ b/TEST-PLAN.md @@ -1,5 +1,17 @@ # ChannelGate — Test Plan +## Network tool description and direct-host policy + +Run `node --test test/channel-policy-audit.test.js test/network-policy.test.js +test/runtime-access-facts.test.js`. The registered `set_channel_network` description must distinguish +ordinary proxy-mode enforcement from advisory legacy bridge/raw sockets and `/sudo` direct-host +execution with no OS-level egress cutoff. Preserve public internet without a domain allowlist, +OFF access to engine endpoints and selected connectors, proxy denial of private/loopback/metadata +addresses, and no grant of host credentials or mounts. The schema regression does not exercise +or change network enforcement. Live MCP-DESCRIPTIONS-01 acceptance remains separate for Claude +and Codex: inspect the actual delivered tool description and resolved runtime facts without +changing the fixture's network setting or entering `/sudo`. + ## Documentation favicon Run `cd documentation && npm run build && npm test`. The output validator requires the diff --git a/src/mcp/tools/channel-admin.js b/src/mcp/tools/channel-admin.js index d3cf825a..b0d88a2b 100644 --- a/src/mcp/tools/channel-admin.js +++ b/src/mcp/tools/channel-admin.js @@ -214,10 +214,12 @@ export function register(server, ctx) { "set_channel_network", { description: - "ADMIN ONLY. Set this channel's allowed network policy. On permits internet use (any public host, " + + "ADMIN ONLY. Set this channel's allowed network policy. For ordinary proxy-mode containers, " + + "on permits internet use (any public host, " + "no domain allowlist); off limits the channel to the engine endpoints and its selected connectors. " + - "Enforced by the gateway's egress proxy (the container has no network of its own) unless the gateway " + - "runs the legacy open-bridge egress mode or this channel was given raw sockets — then it is advisory. " + + "The gateway's egress proxy enforces this policy; the container has no network of its own. " + + "Legacy open-bridge egress or raw sockets make the policy advisory. In /sudo direct-host threads " + + "it is advisory, with no OS-level egress cutoff. " + "Private, loopback and cloud-metadata addresses are never reachable through the proxy. It grants no " + "host credential files or mounts. Takes effect on the next request.", inputSchema: { enabled: z.boolean() }, diff --git a/test/channel-policy-audit.test.js b/test/channel-policy-audit.test.js index efcdc587..59aeb017 100644 --- a/test/channel-policy-audit.test.js +++ b/test/channel-policy-audit.test.js @@ -147,6 +147,20 @@ test("an environment-secret change stays in its own name-only audit and never du // ── The MCP twins ───────────────────────────────────────────────────────────────────────────── +test("the network tool description distinguishes proxy enforcement from direct-host policy", () => { + const definitions = new Map(); + const ctx = ctxFromClaims({ channelId: CHANNEL_ID, slug: entry.slug, authorId: ADMIN_ID, threadKey: "1700000000.000100", principalTrusted: true, engine: "claude" }); + registerChannelAdmin({ registerTool: (name, definition) => definitions.set(name, definition) }, ctx); + const description = definitions.get("set_channel_network").description; + assert.match(description, /ordinary proxy-mode containers/i); + assert.match(description, /\/sudo direct-host.*advisory.*no OS-level egress cutoff/i); + assert.match(description, /legacy open-bridge.*raw sockets.*advisory/i); + assert.match(description, /any public host.*no domain allowlist/i); + assert.match(description, /off limits.*engine endpoints.*selected connectors/i); + assert.match(description, /Private, loopback and cloud-metadata.*never reachable through the proxy/); + assert.match(description, /no host credential files or mounts/); +}); + test("the MCP set_channel_network handler logs the change with the Slack author", async () => { const tools = new Map(); const ctx = ctxFromClaims({ channelId: CHANNEL_ID, slug: entry.slug, authorId: ADMIN_ID, threadKey: "1700000000.000100", principalTrusted: true, engine: "claude" });