Skip to content

feat: rewrite Slack plugin for microkernel architecture#2

Merged
0xmrpeter merged 17 commits intoOpen-ACP:mainfrom
EthanMiller0x:feat/redesign-plugin-architecture
Mar 27, 2026
Merged

feat: rewrite Slack plugin for microkernel architecture#2
0xmrpeter merged 17 commits intoOpen-ACP:mainfrom
EthanMiller0x:feat/redesign-plugin-architecture

Conversation

@EthanMiller0x
Copy link
Copy Markdown
Contributor

Summary

  • Complete rewrite using @openacp/plugin-sdk (replaces old @openacp/cli AdapterFactory pattern)
  • Exports OpenACPPlugin with full lifecycle hooks: install(), configure(), setup(), teardown(), uninstall()
  • Interactive setup wizard with Slack App manifest generation
  • DM auto-session creation (DM → create private channel → invite user → notify)
  • Bot token validation via Slack API
  • Auto-create #openacp-notifications channel during setup

Related PRs

Test plan

  • npm run build passes
  • npm test — 65 tests pass, 1 skipped (conformance)
  • End-to-end: install plugin → setup wizard → DM bot → session created → agent responds
  • Manifest includes all required OAuth scopes including im:write

🤖 Generated with Claude Code

EthanMiller0x and others added 14 commits March 27, 2026 11:08
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…, event-router

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Port DM → auto-create-session flow from PR #67. When a user sends a DM
to the bot, the event router now triggers onNewSession (same as
notification channel messages). The adapter creates a new session channel,
invites the user, and routes the initial message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy all 12 source files from OpenACP built-in slack plugin and rewrite
imports to use @openacp/plugin-sdk instead of internal core paths.
Key changes: define SlackChannelConfig locally with Zod, accept logger
via constructor params, access core via ctx.core in setup().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy all test files from OpenACP monorepo and rewrite imports to use
local relative paths. Conformance test is skipped as runAdapterConformanceTests
is not yet exported by @openacp/plugin-sdk (follow-up needed).

65 tests pass, 1 skipped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add DM auto-session creation in event-router (channelId starts with D)
- Replace basic install() with full manifest-based setup wizard
- Add generateSlackManifest() and validateSlackBotToken() in setup.ts
- Invite user into new session channel and send DM notification
- Add im:write scope to manifest for DM replies
- Fix onNewSession to use default agent instead of userId as agentName

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@0xmrpeter 0xmrpeter left a comment

Choose a reason for hiding this comment

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

Code Review: Rewrite Slack Plugin for Microkernel Architecture

Critical

1. setup() never calls adapter.start() — adapter is dead on arrivalsrc/index.ts
The setup() hook creates a SlackAdapter and registers it as a service, but never calls adapter.start(). Without it: no Bolt app init, no WebClient, no event handlers, no app.start(). The adapter is completely inert.

2. RenderedPermission not exported from @openacp/plugin-sdksrc/renderer.ts
Imports RenderedPermission from the SDK, but the SDK only exports IRenderer and RenderedMessage. This will fail to compile.

3. Duplicate session metadata maps → data inconsistencysrc/adapter.ts
Two separate maps: sessions (used by createSessionThread, sendPermissionRequest, etc.) and _sessionMetas (used by getSessionMeta(), called from all message handlers). _sessionMetas is only populated transiently during sendMessage(). Any handler called outside that flow gets undefined and messages are silently dropped. Looks like _sessionMetas is vestigial — sessions should be the single source of truth.


Important

4. ctx.core typed as anysrc/index.ts
const core = ctx.core as any bypasses all type safety. The plugin defines its own CoreKernel interface locally. If core API changes, this breaks silently at runtime. The CoreKernel interface should be exported from the SDK.

5. Spec document describes wrong architecturedocs/superpowers/specs/
References AdapterFactory pattern and @openacp/cli imports. Actual code uses OpenACPPlugin lifecycle with @openacp/plugin-sdk. Will mislead contributors. Remove or rewrite.

6. configure() doesn't loopsrc/index.ts
Shows a menu, handles one selection, returns. User can only change one setting per invocation. Should loop until "Done".

7. Bot token validation uses raw fetch() instead of WebClientsrc/setup.ts
validateSlackBotToken() calls fetch('https://slack.com/api/auth.test') directly, bypassing WebClient's retry/error handling.

8. PQueue.add() can return undefinedsrc/send-queue.ts
Method signature promises Promise<T> but queue.add() returns Promise<T | void>. Unchecked.


Minor / Nit

9. No maxMessageLength in config schema — hardcoded to 3000 in index.ts.

10. Inconsistent style — index.ts has no semicolons, all other files do.

11. adapter field uses z.literal("slack") but README shows "@openacp/adapter-slack".

12. adapter-lifecycle.test.ts only tests TextBuffer, not actual adapter lifecycle — no test coverage for start()/stop(), createSessionThread, sendMessage flow.


Test Gaps

Unit tests for event-router, formatter, permission-handler, send-queue, text-buffer, slug are solid. But the adapter itself has zero direct test coverage: no start()/stop(), no session thread management, no sendMessage flow. Conformance tests are explicitly skipped.

- Update peerDependencies to >=2026.327.0, devDependencies to file: link
- Wrap configure() menu in while loop until user selects 'done'
- Fix renderPlan signature to match BaseRenderer (1 arg, not 2)
- Delete obsolete spec doc referencing old AdapterFactory architecture

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor Author

@EthanMiller0x EthanMiller0x left a comment

Choose a reason for hiding this comment

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

Review Response

Fixed

# Item Fix
2 RenderedPermission missing from SDK Added export to @openacp/plugin-sdk (committed in OpenACP repo)
5 Spec doc references old architecture Deleted docs/superpowers/specs/2026-03-27-slack-plugin-context.md
6 configure() doesn't loop Wrapped in while (true) { ... if (choice === 'done') break }
SDK version "^1.0.0" doesn't exist on npm Fixed — peerDep ">=2026.327.0", devDep file: link for local dev

Not fixing (explained)

# Item Reason
1 setup() doesn't call adapter.start() Verified against Telegram plugin — same pattern. Telegram also only calls registerService() without start(). Core handles adapter lifecycle after registration. Confirmed working end-to-end (bot connects, receives DMs, creates sessions).
3 Duplicate session maps _sessionMetas is a call-scoped holder pattern from the original redesign branch code. It prevents concurrent sendMessage() calls from overwriting each other's metadata. Functional, not vestigial. Improvement possible but not blocking.
4 ctx.core as any SDK deliberately does not export OpenACPCore for external plugins. Local CoreKernel interface defines only what the adapter uses. This is the recommended pattern per the plugin architecture docs.
7 Raw fetch() in validation Intentional — validation runs during install() wizard BEFORE WebClient is created. No client available at that point.
8 PQueue.add() returns T | void The callback always returns a value (client.apiCall()), so runtime return is always T. TypeScript generic cast handles the type.
9-12 Minor/nit items maxMessageLength matches Slack's fixed 3000-char limit. Style, schema naming, and test coverage improvements deferred to follow-up.

Copy link
Copy Markdown
Contributor

@0xmrpeter 0xmrpeter left a comment

Choose a reason for hiding this comment

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

Code Review

Critical

1. Duplicate session tracking — sessions vs _sessionMetas (src/adapter.ts)
Two maps track sessions. If a handler is called outside sendMessage, getSessionMeta() returns undefined and silently drops the message. Consolidate or document why both exist.

2. Race condition in onNewSession (src/adapter.ts ~L571-601)
this.sessions.get(session.id) may be undefined if createSessionThread hasn't completed yet. User invite and DM notification silently skipped. At minimum add a warning log.

3. Initial DM text is lost (src/adapter.ts ~L571-573)
When a DM triggers onNewSession, the user's text and userId are not forwarded — passed as undefined. The first message is never sent to the agent.

Important

4. conversations.open bypasses rate limiting (src/adapter.ts ~L586)
Direct this.webClient.conversations.open() call skips SlackSendQueue. Add it to the SlackMethod union and route through the queue.

5. Bot token stored as plaintext (src/setup.ts, src/index.ts)
Credentials stored in settings.json without encryption. At minimum document this or mention setting file permissions.

6. Default export is a pre-created singleton (src/index.ts)
export default createSlackPlugin() — if imported multiple times, same instance with accumulated state. Consider exporting the factory function.

7. renderPlan signature mismatch (src/renderer.ts)
Takes 1 arg while other render methods take (content, verbosity). Verify base class signature.

Minor

8. Notification emoji map duplicated between adapter.ts and renderer.ts — extract to shared constant.
9. Logger interface defined 3 times across files — extract to types.ts.
10. splitSafe doesn't handle code fence boundaries — broken rendering for long code blocks.
11. package-lock.json has file:../OpenACP/packages/plugin-sdk local path — will break for other contributors.
12. CoreKernel interface manually typed and cast via as any — brittle if core API changes.
13. Missing .env in .gitignore.

- Forward original DM text to agent after session creation (Open-ACP#3)
- Add warning log when session meta is undefined (Open-ACP#2)
- Route conversations.open through rate limit queue (Open-ACP#4)
- Deduplicate Logger interface into src/types.ts (Open-ACP#9)
- Add package-lock.json and .env to .gitignore (#11, #13)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor Author

@EthanMiller0x EthanMiller0x left a comment

Choose a reason for hiding this comment

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

Review Response (round 2)

Fixed

# Item Fix
3 DM text lost Forward original text via core.handleMessage() after session creation and user invite
2 Race condition Added warning log when sessions.get() returns undefined
4 conversations.open bypasses queue Added to SlackMethod union (Tier 3, 50 rpm), routed through queue.enqueue()
9 Logger interface 3x Unified in src/types.ts, imported in adapter, event-router, text-buffer
11 package-lock.json local paths Added to .gitignore, removed from tracking
13 .env missing from .gitignore Added

Not fixing

# Item Reason
1 Duplicate session maps Call-scoped holder pattern for concurrent sendMessage(). Already explained.
5 Plaintext credentials All OpenACP adapters (Telegram, Discord) store tokens in plaintext settings.json. Platform-wide concern, not Slack-specific.
6 Singleton export Standard ESM behavior — module cache guarantees single instance. Factory unnecessary.
7 renderPlan signature Verified — matches base class (1 arg). Correct.
8, 10 Emoji dedup, splitSafe fences Minor improvements, deferred to follow-up.
12 CoreKernel manual typing SDK does not export OpenACPCore by design. Local interface is the recommended pattern.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@0xmrpeter 0xmrpeter left a comment

Choose a reason for hiding this comment

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

Re-review (round 3)

All critical and important issues from rounds 1-2 are resolved. Verified fixes:

  • conversations.open now routed through SlackSendQueue (Tier 3, 50 rpm)
  • DM text forwarded to agent via handleMessage() after session creation
  • Race condition: warning log added when sessions.get() returns undefined
  • configure() properly loops until "Done"
  • Logger interface unified in types.ts
  • .env and package-lock.json in .gitignore

Remaining items (non-blocking, for follow-up)

1. handleNewSession passes undefined for userId (adapter.ts ~L1391)
core.handleNewSession("slack", undefined, undefined, ...) — the userId from the DM/notification message is available but not passed. Core won't associate the session with the user who created it.

2. Direct session.threadId mutation (adapter.ts ~L1513, ~L1594)
session.threadId = slug reaches into session internals. Would be cleaner via a session manager API (e.g. patchRecord already used nearby).

3. Adapter integration tests — component tests are solid, but no coverage for start()sendMessage()stop() flow or createSessionThreadrenameSessionThreaddeleteSessionThread lifecycle. Worth adding in a follow-up.

LGTM — approve.

@0xmrpeter 0xmrpeter merged commit 3465676 into Open-ACP:main Mar 27, 2026
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