Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
9 changes: 9 additions & 0 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
77 changes: 77 additions & 0 deletions test/e2e/test-messaging-providers.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# 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.
Expand Down
14 changes: 14 additions & 0 deletions test/local-slack-auth-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
4 changes: 3 additions & 1 deletion test/nemoclaw-start.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.

it("logs caught Slack errors as warnings instead of crashing", () => {
Expand Down
8 changes: 5 additions & 3 deletions test/policies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});

Expand Down
Loading