diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index dde957e35ea..e527060c310 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -588,6 +588,15 @@ install_slack_channel_guard() { return true; } + // Check for proxy/network errors targeting Slack domains. + // When the network policy blocks or rejects connections to Slack + // servers, the error comes from the HTTP client (CONNECT tunnel + // failure), not from @slack/ code. The stack won't contain @slack/ + // but the error message or URL may reference the Slack hostname. + if (msg.indexOf('slack.com') !== -1) { + return true; + } + return false; } diff --git a/test/e2e/test-messaging-providers.sh b/test/e2e/test-messaging-providers.sh index 098d52ff5aa..6ef798cfe35 100755 --- a/test/e2e/test-messaging-providers.sh +++ b/test/e2e/test-messaging-providers.sh @@ -195,6 +195,83 @@ if command -v openshell >/dev/null 2>&1; then fi pass "Pre-cleanup complete" +# Pre-merge Slack policy into the base sandbox policy. +# +# The base policy (openclaw-sandbox.yaml) includes Telegram and Discord +# network rules but NOT Slack — Slack access normally comes from the +# slack.yaml preset, applied in onboard Step 8. However, the sandbox +# container starts in Step 6, so the gateway boots without Slack access. +# The Slack SDK's connection attempt hangs or gets a CONNECT 403 before +# the preset is applied, preventing the gateway from serving on 18789. +# +# By appending the Slack rules to the base policy BEFORE install.sh, the +# sandbox is created with Slack access from the start. The Slack SDK gets +# a fast "invalid_auth" response, the channel guard catches it, and the +# gateway continues serving. +# Ref: #2340 +BASE_POLICY="$REPO/nemoclaw-blueprint/policies/openclaw-sandbox.yaml" +SLACK_PRESET="$REPO/nemoclaw-blueprint/policies/presets/slack.yaml" +if [ -f "$BASE_POLICY" ] && [ -f "$SLACK_PRESET" ] && ! grep -q "api.slack.com" "$BASE_POLICY"; then + BASE_POLICY_BAK="$(mktemp)" + cp "$BASE_POLICY" "$BASE_POLICY_BAK" + trap 'cp "$BASE_POLICY_BAK" "$BASE_POLICY" 2>/dev/null || true; rm -f "$BASE_POLICY_BAK"' EXIT + info "Pre-merging Slack network policy into base sandbox policy..." + cat >>"$BASE_POLICY" <<'SLACK_POLICY_EOF' + + # ── Slack — pre-merged for messaging E2E (#2340) ────────────── + # Normally applied as a preset in onboard Step 8, but the sandbox + # container starts before presets are applied. Inline here so the + # gateway has Slack access from first boot. + slack: + name: slack + endpoints: + - host: slack.com + port: 443 + protocol: rest + enforcement: enforce + rules: + - allow: { method: GET, path: "/**" } + - allow: { method: POST, path: "/**" } + - host: api.slack.com + port: 443 + protocol: rest + enforcement: enforce + rules: + - allow: { method: GET, path: "/**" } + - allow: { method: POST, path: "/**" } + - host: hooks.slack.com + port: 443 + protocol: rest + enforcement: enforce + rules: + - allow: { method: GET, path: "/**" } + - allow: { method: POST, path: "/**" } + - host: wss-primary.slack.com + port: 443 + access: full + tls: skip + - host: wss-backup.slack.com + port: 443 + access: full + tls: skip + binaries: + - { path: /usr/local/bin/node } + - { path: /usr/bin/node } +SLACK_POLICY_EOF + if ! grep -q "api.slack.com" "$BASE_POLICY"; then + fail "Failed to append Slack policy to base sandbox policy" + exit 1 + fi + pass "Slack network policy pre-merged into base policy" +else + if grep -q "api.slack.com" "$BASE_POLICY" 2>/dev/null; then + info "Slack policy already present in base policy — skipping pre-merge" + else + fail "Cannot pre-merge Slack policy: missing base policy or preset file" + exit 1 + fi +fi + # Run install.sh --non-interactive which installs Node.js, openshell, # NemoClaw, and runs onboard. Messaging tokens are already exported so # the onboard step creates providers and attaches them to the sandbox. diff --git a/test/local-slack-auth-test.sh b/test/local-slack-auth-test.sh index 73717163abb..5d5759bf9c1 100755 --- a/test/local-slack-auth-test.sh +++ b/test/local-slack-auth-test.sh @@ -122,6 +122,20 @@ fi # ────────────────────────────────────────────────────────────────── +header "T3b: Proxy CONNECT tunnel failure to slack.com — should be caught" +run_node " + Promise.reject(new Error('CONNECT tunnel to api.slack.com:443 failed with status 403')); + setTimeout(function() { console.log('ALIVE'); }, 200); +" + +if [ "$LAST_EXIT" -eq 0 ] && echo "$LAST_STDERR" | grep -q "caught by safety net"; then + pass "proxy CONNECT failure to slack.com caught (exit=$LAST_EXIT)" +else + fail "expected guard to catch CONNECT tunnel error, got exit=$LAST_EXIT stderr='$LAST_STDERR'" +fi + +# ────────────────────────────────────────────────────────────────── + header "T4: Non-Slack rejection — should NOT be caught (re-thrown)" run_node " Promise.reject(new Error('database connection failed')); diff --git a/test/nemoclaw-start.test.ts b/test/nemoclaw-start.test.ts index eccce386586..b6e4a0e93fc 100644 --- a/test/nemoclaw-start.test.ts +++ b/test/nemoclaw-start.test.ts @@ -645,13 +645,15 @@ describe("Slack channel guard — unhandled-rejection safety net (#2340)", () => expect(fn[1]).toContain("process.exit(1)"); }); - it("detects Slack errors by error code, message, and stack trace", () => { + it("detects Slack errors by error code, message, stack trace, and domain", () => { const fn = src.match(/install_slack_channel_guard\(\) \{([\s\S]*?)^}/m); expect(fn).toBeTruthy(); expect(fn[1]).toContain("slack_webapi_platform_error"); expect(fn[1]).toContain("invalid_auth"); expect(fn[1]).toContain("token_revoked"); expect(fn[1]).toContain("@slack/"); + // Proxy/network errors targeting Slack domains (CONNECT tunnel failures) + expect(fn[1]).toMatch(/msg\.indexOf\('slack\.com'\)\s*!==\s*-1/); }); it("logs caught Slack errors as warnings instead of crashing", () => { diff --git a/test/policies.test.ts b/test/policies.test.ts index 787108aaf50..2069e7424ef 100644 --- a/test/policies.test.ts +++ b/test/policies.test.ts @@ -164,12 +164,14 @@ describe("policies", () => { expect(content).toContain("port: 8000"); }); - it("local-inference preset restricts binaries to openclaw and claude", () => { + it("local-inference preset includes openclaw, claude, and common tool binaries", () => { const content = policies.loadPreset("local-inference"); expect(content).toContain("/usr/local/bin/openclaw"); expect(content).toContain("/usr/local/bin/claude"); - // Should NOT include node — only agent binaries need inference access - expect(content).not.toContain("/usr/local/bin/node"); + // node, curl, and python3 are needed for direct inference access (#2199) + expect(content).toContain("/usr/local/bin/node"); + expect(content).toContain("/usr/bin/curl"); + expect(content).toContain("/usr/bin/python3"); }); });