Skip to content

feat(slack): App Home tab — integrations list + per-user Connect/Disconnect#653

Merged
buremba merged 3 commits into
mainfrom
feat/slack-app-home
May 13, 2026
Merged

feat(slack): App Home tab — integrations list + per-user Connect/Disconnect#653
buremba merged 3 commits into
mainfrom
feat/slack-app-home

Conversation

@buremba
Copy link
Copy Markdown
Member

@buremba buremba commented May 13, 2026

What

Rebuilds the Slack App Home tab (the old views.publish content — "AI coding assistant", "Open Settings" — was deleted in the Chat-SDK migration and never re-published; Slack was serving a stale cached view). The new tab:

  • Header — bot name + "Mention me in any channel, or send me a DM, to start a thread." Generic, on-brand, no positional copy.
  • Integrations — one row per MCP server the owning agent has, with live per-user OAuth status:
    • ✅ Name · connected + a Disconnect button (revokes the stored credential via deleteCredential)
    • ⚪ Name · not connected + a Connect button → runs the MCP auth-code OAuth flow (startAuthCodeFlow, well-known discovery, PKCE, per-user scope key) and re-publishes the tab with an Open sign-in ↗ link for that row
    • ⚪ Name (no button) for integrations that don't require sign-in
    • internal plumbing MCPs (lobu-memory) hidden
  • Preview workspaces get the lobu run / /lobu link <code> instructions instead; the integrations section is skipped (no MCP-config access for placeholder agents).
  • Degrades gracefully when the MCP config service / secret store / public URL aren't available (header + tips only).

How it's wired

  • registerSlackAppHome(chat, connection, { mcpConfigService, secretStore, publicGatewayUrl }) in slack-platform-bridge.ts — subscribes to chat.onAppHomeOpened and chat.onAction([connect, disconnect]) (first-class Chat-SDK hooks; the Slack adapter already routes block_actions with container.type === "view" to processAction).
  • Status read via getStoredCredential against the same credential store the MCP proxy writes (mcp-auth/{agentId}/{userId}/{mcpId}/credential).
  • getMcpConfigService() added to the CoreServices interface (the concrete impl already had it).
  • deleteCredential exported from routes/internal/device-auth.ts (previously only reachable via the worker-authenticated DELETE /internal/device-auth/credential route).

Security

  • No cross-user / cross-agent reach — both buttons act on event.user.userId (from Slack's signed block_actions payload) as the credential scope key; the agent id is fixed by the connection, not the payload.
  • Slack-signature-gated — actions arrive only through POST /api/v1/webhooks/:connectionId, which verifies the connection's signing secret before dispatch.
  • mcpId validated — the only attacker-influenced field (value) is checked against getMcpStatus(agentId) before any secretStore.delete / OAuth start; unknown ids are a logged no-op.
  • OAuth — PKCE + OAuthStateStore (5-min TTL, atomic single-use), server-controlled redirect URI, userId baked into the stored state; the authorization link is published only to the user who clicked (views.publish is per-user_id).
  • No tokens / config secrets / other users' state are shown; errors are logged, never echoed.

Known limitations (not gaps, but worth noting)

  • Block Kit url buttons are static at publish time, so Connect is necessarily two clicks (Connect → row updates to "Open sign-in ↗" → that opens the consent page). A one-click version would need a new signed-token gateway route; deferred.
  • Connect uses .well-known discovery (wwwAuthenticate: null); MCP servers that advertise OAuth metadata only via the WWW-Authenticate header on a 401 won't be discoverable from the Home tab (the in-thread flow gets that header for free by reacting to a real 401). Such a Connect click fails closed (logged, tab unchanged). Pre-existing limitation of startAuthCodeFlow.
  • "Connected" = "a credential row exists", not "the token is currently valid" — same staleness as the rest of the system; refreshed/invalidated on the next tool call.
  • Channel-scoped MCPs (auth_scope = "channel") are shown with a per-user scope key, which won't reflect a shared channel credential. Rare opt-in; not special-cased.

Tests

packages/server/src/gateway/__tests__/slack-platform-bridge.test.ts — 9 tests: status badges (connect vs disconnect button), disconnect → revoke + re-publish, disconnect rejects a crafted unknown mcpId, connect → mints URL + re-publish (mocked flow), no-deps fallback, preview workspace.

make build-packages, bun run typecheck, full src/gateway suite green (the one pre-existing platform-file-handler.test.ts failure also fails on clean main — unrelated).

🤖 Generated with Claude Code

buremba added 3 commits May 13, 2026 03:35
The App Home tab now shows each integration's per-user OAuth status
(Connected/Disconnect vs. Connect). Connect runs the MCP auth-code flow
and re-publishes the tab with an 'Open sign-in' link; Disconnect revokes
the stored credential. Adds getMcpConfigService to CoreServices and
exports deleteCredential from the device-auth route module.
@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d891940dd8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bun.lock
"vite": "^6.0.0",
"vitest": "^2.1.8",
},
},
},
"packages": {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Restore the packages/web workspace lock entries

This lockfile was generated without the packages/web submodule initialized: the commit removes the packages/web importer and hundreds of its resolved packages even though the root package.json still declares packages/* workspaces and packages/web is a tracked submodule. In any checkout/CI job that initializes the web submodule, a frozen root bun install will see a workspace that is missing from the lockfile (or rewrite the lock), and web builds/dev installs lose the pinned dependency graph. Please regenerate the lockfile with packages/web present or revert this unrelated lockfile pruning.

Useful? React with 👍 / 👎.

@buremba buremba merged commit 562d071 into main May 13, 2026
15 of 18 checks passed
@buremba buremba deleted the feat/slack-app-home branch May 13, 2026 02:52
buremba added a commit that referenced this pull request May 13, 2026
PR #653's lockfile was regenerated in a worktree without the packages/web
submodule initialized, which pruned the web importer and its resolved
dependency graph. Restore bun.lock from the pre-#653 state (df #653 added
no new dependencies) so frozen installs / web builds keep the pinned graph.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants