Skip to content

refactor(channels): make ChannelDef.envKey optional and add static-token guards#3443

Merged
cv merged 6 commits into
mainfrom
refactor/channel-optional-envkey
May 15, 2026
Merged

refactor(channels): make ChannelDef.envKey optional and add static-token guards#3443
cv merged 6 commits into
mainfrom
refactor/channel-optional-envkey

Conversation

@laitingsheng

@laitingsheng laitingsheng commented May 13, 2026

Copy link
Copy Markdown
Contributor

Summary

ChannelDef.envKey is currently mandatory, but a channel whose credential is established inside the sandbox has no host-side env var to assign. Make envKey optional, add type guards so consumers can distinguish static-token channels from tokenless ones, and route inline ternary patterns through getChannelTokenKeys. Behaviour is unchanged for telegram, discord, and slack — all three still declare envKey.

Related Issue

Prerequisite for #3392 (WhatsApp Web QR-paired channel), which uses these helpers to enable a tokenless channel and addresses #361 and #513.

Changes

  • src/lib/sandbox/channels.ts:
    • ChannelDef.envKey is now envKey?: string.
    • getChannelTokenKeys(channel) returns [] when envKey is absent.
    • New channelUsesQrPairing(channel) boolean helper.
    • New channelHasStaticToken(channel) type guard that narrows to ChannelDef & { envKey: string }.
  • src/lib/onboard.ts:
    • Replace three inline channel.appTokenEnvKey ? [...] : [channel.envKey] patterns with getChannelTokenKeys.
    • Add def.envKey / ch.envKey narrowing guards before the picker loop accesses channel.envKey.
    • Skip the per-channel token prompt for tokenless channels via channelHasStaticToken.
    • Drop the two duplicated getMessagingToken closures in favour of a shared module so the file stays net-negative.
  • src/lib/onboard/messaging-token.ts (new): single source of truth for the messaging-token resolver. Accepts string | undefined and returns null for absent keys.
  • src/lib/sandbox/channels.test.ts: cover the new helpers and the tokenless branch of getChannelTokenKeys.

src/lib/onboard.ts budget: net −1.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Verification

  • `npx prek run --all-files` passes
  • `npm test` passes
  • Tests added or updated for new or changed behavior
  • No secrets, API keys, or credentials committed
  • Docs updated for user-facing behavior changes
  • `make docs` builds without warnings (doc changes only)
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Signed-off-by: Tinson Lai tinsonl@nvidia.com

Summary by CodeRabbit

  • New Features

    • Added explicit QR-code pairing support and helpers; channel definitions can now omit static config keys.
  • Bug Fixes

    • Messaging setup now skips channels without static tokens, avoids reading allowlist entries for channels lacking config keys, and consistently expands token keys when creating sandboxes.
  • Tests

    • Expanded tests for channel token/key helpers and token-shape behaviors.

Review Change Stack

…ken guards

ChannelDef previously required envKey, but a QR-paired channel cannot
satisfy that — its credential lives inside the sandbox, not in a host
env var. Mark envKey as optional, add channelUsesQrPairing and
channelHasStaticToken type guards, and have getChannelTokenKeys return
an empty list when envKey is absent. No current channel becomes
tokenless, so behaviour is unchanged for telegram, discord, and slack.

Replace inline 'channel.appTokenEnvKey ? [...] : [...]' patterns in
onboard.ts with getChannelTokenKeys, guard direct ch.envKey accesses,
and skip the token prompt loop for tokenless channels.

Extract the local getMessagingToken closure into a shared
src/lib/onboard/messaging-token.ts module — both copies in onboard.ts
were identical, and the shared helper now accepts an optional envKey
so callers can pass channel.envKey directly without an extra guard.

Signed-off-by: Tinson Lai <tinsonl@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented May 13, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR centralizes messaging token lookup into a new shared module, makes ChannelDef.envKey optional, adds token-shape helpers/type guards, and updates src/lib/onboard.ts to use those helpers with added guards in interactive and allowlist flows.

Changes

Messaging Token Centralization

Layer / File(s) Summary
Replace local token helpers with shared imports
src/lib/onboard.ts, src/lib/onboard/messaging-token.ts
Imports getMessagingToken, channelHasStaticToken, and getChannelTokenKeys; removes the file-local getMessagingToken helper and switches setup flows to the shared lookup.
Provider conflict-check update
src/lib/onboard.ts
Uses getChannelTokenKeys(def) to expand credential env keys and getMessagingToken(def.envKey) to detect existing tokens when preparing credential hashing.
Sandbox enabled/disabled env-key expansion
src/lib/onboard.ts
Replaces conditional app-token branching with getChannelTokenKeys(c) to expand enabled/disabled messaging env-key lists during sandbox creation.
Interactive and allowlist guards for tokenless channels
src/lib/onboard.ts
Requires ch.envKey before reading user-id allowlist values and skips interactive token/app-token prompts for channels where channelHasStaticToken(ch) is false.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

enhancement: messaging

Suggested reviewers

  • prekshivyas
  • ericksoa
  • cv

🐰
Tokens now dance in harmony,
Channels lean and light as air,
QR whispers where keys are gone,
Shared lookups hum without a scare,
A rabbit hops — refactor flair.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main changes: making ChannelDef.envKey optional and adding static-token guards, which are the core objectives of this refactoring.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/channel-optional-envkey

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@laitingsheng laitingsheng added the refactor PR restructures code without intended behavior change label May 13, 2026
@github-actions

github-actions Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

E2E Advisor Recommendation

Required E2E: messaging-providers-e2e, token-rotation-e2e, cloud-onboard-e2e
Optional E2E: credential-sanitization-e2e, telegram-injection-e2e, hermes-slack-e2e

Dispatch hint: messaging-providers-e2e,token-rotation-e2e,cloud-onboard-e2e

Auto-dispatched E2E: messaging-providers-e2e, token-rotation-e2e, cloud-onboard-e2e via nightly-e2e.yaml at f2d3945006433be03d6fbdc4dba527c4418a113anightly run

Workflow run

Full advisor summary

E2E Recommendation Advisor

Base: origin/main
Head: HEAD
Confidence: high

Required E2E

  • messaging-providers-e2e (~45 min): Directly validates Telegram, Discord, and Slack credential provider creation, placeholder isolation, sandbox attachment, network policy/proxy rewrite, and absence of raw token leakage through the code paths changed here.
  • token-rotation-e2e (~45 min): Covers token-key enumeration, credential hash drift, provider reuse, and sandbox rebuild/reuse behavior after rotating Telegram, Discord, and Slack credentials; these are directly impacted by getMessagingToken/getChannelTokenKeys changes.
  • cloud-onboard-e2e (~45 min): Provides a full non-interactive onboarding and sandbox creation smoke over the modified onboard.ts createSandbox/setup flow, catching regressions outside messaging-specific assertions.

Optional E2E

  • credential-sanitization-e2e (~60 min): Useful adjacent confidence for credential boundary regressions because this PR changes credential/token handling, but messaging-providers-e2e already checks the primary token isolation path.
  • telegram-injection-e2e (~60 min): Optional security regression for Telegram bridge behavior after messaging onboarding changes; not directly modified by this diff.
  • hermes-slack-e2e (~45 min): Optional agent-specific Slack provider/policy confidence because the shared channel definition and Slack token-key helpers changed, though the primary OpenClaw messaging path is covered by messaging-providers-e2e.

New E2E recommendations

  • tokenless/QR-paired messaging channels (medium): The PR introduces channelUsesQrPairing/channelHasStaticToken behavior for channels without static envKey, but existing E2E coverage appears focused on Telegram/Discord/Slack static-token providers and may not exercise a tokenless/QR-pairing onboarding path.
    • Suggested test: Add an E2E scenario for a tokenless messaging channel that verifies onboarding skips static token prompts, creates no credential provider, persists channel config correctly, and does not regress sandbox creation.

Dispatch hint

  • Workflow: nightly-e2e.yaml
  • jobs input: messaging-providers-e2e,token-rotation-e2e,cloud-onboard-e2e

@laitingsheng laitingsheng marked this pull request as ready for review May 13, 2026 07:26
@cv cv added v0.0.42 and removed v0.0.41 labels May 14, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/lib/onboard.ts (1)

344-345: Please run the messaging-focused E2Es on this onboarding path.

This touches core onboarding behavior for channel selection/token handling, so messaging-compatible-endpoint-e2e, hermes-discord-e2e, and hermes-slack-e2e are the highest-signal regressions to exercise before merge.

As per coding guidelines, "src/lib/onboard.ts: This file contains core onboarding logic. Changes here affect the full sandbox creation and configuration flow." and the listed E2E recommendations.

Also applies to: 5089-5090, 5138-5138, 5150-5150, 5687-5687, 8263-8263

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard.ts` around lines 344 - 345, Run the messaging-focused
end-to-end suites to validate changes to channel selection and token handling in
src/lib/onboard.ts: execute messaging-compatible-endpoint-e2e,
hermes-discord-e2e, and hermes-slack-e2e (and re-run any failing runs listed
near the referenced changes) to exercise logic in channelHasStaticToken,
getChannelTokenKeys, listChannels and getMessagingToken; if tests fail, trace
the onboarding flow in onboard.ts, reproduce the failing scenario locally, and
fix the code paths that mishandle channel selection or token retrieval so the
E2Es pass before merging.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/onboard.ts`:
- Line 8263: The loop currently uses "if (!channelHasStaticToken(ch)) continue;"
which skips all subsequent per-channel prompts; remove that continue and instead
only gate the static-token/app-token prompt with "if (channelHasStaticToken(ch))
{ ... }" so that other per-channel config prompts (e.g., serverIdEnvKey,
requireMentionEnvKey, userIdEnvKey) still run for tokenless channels; update the
code around the token collection logic (where channelHasStaticToken(ch) is
checked) to conditionally prompt for and persist tokens but leave the rest of
the per-channel flow (the prompts that set serverIdEnvKey, requireMentionEnvKey,
userIdEnvKey) outside that conditional so they always execute for each ch in the
loop.

---

Nitpick comments:
In `@src/lib/onboard.ts`:
- Around line 344-345: Run the messaging-focused end-to-end suites to validate
changes to channel selection and token handling in src/lib/onboard.ts: execute
messaging-compatible-endpoint-e2e, hermes-discord-e2e, and hermes-slack-e2e (and
re-run any failing runs listed near the referenced changes) to exercise logic in
channelHasStaticToken, getChannelTokenKeys, listChannels and getMessagingToken;
if tests fail, trace the onboarding flow in onboard.ts, reproduce the failing
scenario locally, and fix the code paths that mishandle channel selection or
token retrieval so the E2Es pass before merging.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: a771d6aa-326a-41b2-876c-0810cc363faa

📥 Commits

Reviewing files that changed from the base of the PR and between 8565be4 and de1cfa2.

📒 Files selected for processing (1)
  • src/lib/onboard.ts

Comment thread src/lib/onboard.ts
@github-actions

Copy link
Copy Markdown
Contributor

Selective E2E Results — ✅ All requested jobs passed

Run: 25876096387
Target ref: de1cfa206ff74cc3092ac70f34e0920b375ad206
Workflow ref: main
Requested jobs: messaging-providers-e2e,token-rotation-e2e
Summary: 1 passed, 0 failed, 0 skipped

Job Result
messaging-providers-e2e ✅ success
token-rotation-e2e ⚠️ cancelled

@github-actions

Copy link
Copy Markdown
Contributor

Selective E2E Results — ✅ All requested jobs passed

Run: 25877273658
Target ref: 9b9901c5f13d0e98febf4d56b20aab2b74c8a225
Workflow ref: main
Requested jobs: messaging-providers-e2e,token-rotation-e2e
Summary: 2 passed, 0 failed, 0 skipped

Job Result
messaging-providers-e2e ✅ success
token-rotation-e2e ✅ success

@github-actions

Copy link
Copy Markdown
Contributor

Selective E2E Results — ✅ All requested jobs passed

Run: 25880983733
Target ref: 9798f39494e7f8e5a3c6a6a5b3b575eae0c89260
Workflow ref: main
Requested jobs: messaging-providers-e2e,token-rotation-e2e
Summary: 2 passed, 0 failed, 0 skipped

Job Result
messaging-providers-e2e ✅ success
token-rotation-e2e ✅ success

@cv cv added v0.0.43 and removed v0.0.42 labels May 14, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/lib/onboard.ts (1)

8260-8319: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't short-circuit tokenless channels out of the rest of setup.

This continue still skips every later prompt in the loop, so tokenless channels never collect server/mention/allowlist config. Only gate the static-token/app-token prompts behind channelHasStaticToken(ch) and let the rest of the per-channel setup keep running.

Suggested fix
-    if (!channelHasStaticToken(ch)) continue;
-    if (getMessagingToken(ch.envKey)) {
-      console.log(`  ✓ ${ch.name} — already configured`);
-    } else {
-      console.log("");
-      console.log(`  ${ch.help}`);
-      const token = normalizeCredentialValue(await prompt(`  ${ch.label}: `, { secret: true }));
-      if (token && ch.tokenFormat && !ch.tokenFormat.test(token)) {
-        console.log(
-          `  ✗ Invalid format. ${ch.tokenFormatHint || "Check the token and try again."}`,
-        );
-        console.log(`  Skipped ${ch.name} (invalid token format)`);
-        enabled.delete(ch.name);
-        continue;
-      }
-      if (token) {
-        saveCredential(ch.envKey, token);
-        process.env[ch.envKey] = token;
-        console.log(`  ✓ ${ch.name} token saved`);
-      } else {
-        console.log(`  Skipped ${ch.name} (no token entered)`);
-        enabled.delete(ch.name);
-        continue;
-      }
-    }
-    if (ch.appTokenEnvKey) {
-      const existingAppToken = getMessagingToken(ch.appTokenEnvKey);
-      if (existingAppToken) {
-        console.log(`  ✓ ${ch.name} app token — already configured`);
-      } else {
-        console.log("");
-        console.log(`  ${ch.appTokenHelp}`);
-        const appToken = normalizeCredentialValue(
-          await prompt(`  ${ch.appTokenLabel}: `, { secret: true }),
-        );
-        if (appToken && ch.appTokenFormat && !ch.appTokenFormat.test(appToken)) {
-          console.log(
-            `  ✗ Invalid format. ${ch.appTokenFormatHint || "Check the token and try again."}`,
-          );
-          console.log(`  Skipped ${ch.name} app token (invalid token format)`);
-          enabled.delete(ch.name);
-          continue;
-        }
-        if (appToken) {
-          saveCredential(ch.appTokenEnvKey, appToken);
-          process.env[ch.appTokenEnvKey] = appToken;
-          console.log(`  ✓ ${ch.name} app token saved`);
-        } else {
-          console.log(`  Skipped ${ch.name} app token (Socket Mode requires both tokens)`);
-          enabled.delete(ch.name);
-          continue;
-        }
-      }
-    }
+    if (channelHasStaticToken(ch)) {
+      if (getMessagingToken(ch.envKey)) {
+        console.log(`  ✓ ${ch.name} — already configured`);
+      } else {
+        console.log("");
+        console.log(`  ${ch.help}`);
+        const token = normalizeCredentialValue(await prompt(`  ${ch.label}: `, { secret: true }));
+        if (token && ch.tokenFormat && !ch.tokenFormat.test(token)) {
+          console.log(
+            `  ✗ Invalid format. ${ch.tokenFormatHint || "Check the token and try again."}`,
+          );
+          console.log(`  Skipped ${ch.name} (invalid token format)`);
+          enabled.delete(ch.name);
+          continue;
+        }
+        if (token) {
+          saveCredential(ch.envKey, token);
+          process.env[ch.envKey] = token;
+          console.log(`  ✓ ${ch.name} token saved`);
+        } else {
+          console.log(`  Skipped ${ch.name} (no token entered)`);
+          enabled.delete(ch.name);
+          continue;
+        }
+      }
+      if (ch.appTokenEnvKey) {
+        const existingAppToken = getMessagingToken(ch.appTokenEnvKey);
+        if (existingAppToken) {
+          console.log(`  ✓ ${ch.name} app token — already configured`);
+        } else {
+          console.log("");
+          console.log(`  ${ch.appTokenHelp}`);
+          const appToken = normalizeCredentialValue(
+            await prompt(`  ${ch.appTokenLabel}: `, { secret: true }),
+          );
+          if (appToken && ch.appTokenFormat && !ch.appTokenFormat.test(appToken)) {
+            console.log(
+              `  ✗ Invalid format. ${ch.appTokenFormatHint || "Check the token and try again."}`,
+            );
+            console.log(`  Skipped ${ch.name} app token (invalid token format)`);
+            enabled.delete(ch.name);
+            continue;
+          }
+          if (appToken) {
+            saveCredential(ch.appTokenEnvKey, appToken);
+            process.env[ch.appTokenEnvKey] = appToken;
+            console.log(`  ✓ ${ch.name} app token saved`);
+          } else {
+            console.log(`  Skipped ${ch.name} app token (Socket Mode requires both tokens)`);
+            enabled.delete(ch.name);
+            continue;
+          }
+        }
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard.ts` around lines 8260 - 8319, The loop over selected channels
currently uses "if (!channelHasStaticToken(ch)) continue;" which short-circuits
the rest of per-channel setup (server/mention/allowlist config) for tokenless
channels; instead, only skip the static-token/app-token prompt blocks when
channelHasStaticToken(ch) is false. Move the static token prompt logic (the
branch that checks getMessagingToken(ch.envKey), reads prompt, validates format,
calls saveCredential and sets process.env) and the app token block (the
ch.appTokenEnvKey branch) behind a conditional like "if
(channelHasStaticToken(ch)) { ... }" so that channels without static tokens
still execute the subsequent per-channel configuration and you remove the early
"continue" that currently skips the rest of the loop.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/lib/onboard.ts`:
- Around line 8260-8319: The loop over selected channels currently uses "if
(!channelHasStaticToken(ch)) continue;" which short-circuits the rest of
per-channel setup (server/mention/allowlist config) for tokenless channels;
instead, only skip the static-token/app-token prompt blocks when
channelHasStaticToken(ch) is false. Move the static token prompt logic (the
branch that checks getMessagingToken(ch.envKey), reads prompt, validates format,
calls saveCredential and sets process.env) and the app token block (the
ch.appTokenEnvKey branch) behind a conditional like "if
(channelHasStaticToken(ch)) { ... }" so that channels without static tokens
still execute the subsequent per-channel configuration and you remove the early
"continue" that currently skips the rest of the loop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 0cb5b571-b1a6-40b0-8386-a3e1f7fd4a01

📥 Commits

Reviewing files that changed from the base of the PR and between 9798f39 and f2d3945.

📒 Files selected for processing (1)
  • src/lib/onboard.ts

@github-actions

Copy link
Copy Markdown
Contributor

Selective E2E Results — ✅ All requested jobs passed

Run: 25895812383
Target ref: f2d3945006433be03d6fbdc4dba527c4418a113a
Workflow ref: main
Requested jobs: messaging-providers-e2e,token-rotation-e2e,cloud-onboard-e2e
Summary: 3 passed, 0 failed, 0 skipped

Job Result
cloud-onboard-e2e ✅ success
messaging-providers-e2e ✅ success
token-rotation-e2e ✅ success

@cv cv added v0.0.44 and removed v0.0.43 labels May 15, 2026
@cv cv merged commit 6a7ed20 into main May 15, 2026
27 checks passed
@cv cv added the integration: whatsapp WhatsApp integration or channel behavior label May 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

integration: whatsapp WhatsApp integration or channel behavior refactor PR restructures code without intended behavior change

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants