feat(operator): slack-personal channel — your own Slack account via an MCP backend - #1264
Merged
Conversation
Records two accepted design decisions ahead of implementation: - 0201: a second Slack channel kind, slack-personal, that acts through the user's own account via an MCP backend chosen by tool contract (no Slack app, no bot token, no required vendor account), polling as simulated event subscription over the shared operator router. - 0202: per-scope autonomy for the shared identity — trigger policy plus a response-mode ladder (observe / acknowledge / draft / ask-to-send / auto-send), disclosure on by default, grace-period yielding.
… via an MCP backend Implements specs 0201/0202: - operator/mcp.rs: a minimal stdio MCP client (initialize handshake, tools/call with JSON-in-text results, close-aware pending map). No agent or model turn is involved; the daemon drives the backend with classic logic. - operator/slack_personal.rs: the channel adapter. Polls the backend's slack_sweep_messages as a simulated event subscription, keeps its own timestamp cursor, dedups echoes of its own posts by recorded ts (never by author — the user's self-DM is a private command line), and routes deliveries into the shared operator router under the same workspace:channel:thread key as the Socket Mode kind. - Response modes per spec 0202: draft (default — the reply lands in the user's Slack drafts) and auto (posts directly, with an agent-disclosure marker on by default). Failed turns are logged and cleared, never posted: this channel's words appear as the user's own. - Scope policy: DMs are in scope by default (dm trigger); channel messages need trigger=all plus an explicit allowlist entry — an unconfigured scope forwards nothing. - Protocol, TUI channel editor, and web UI grow the new kind and its options (mcp_command, trigger, response_mode, disclosure, poll_interval_secs), refused on kinds that do not read them (0180). - examples/fake_slack_mcp.rs: a contract-conforming fake backend for tests and hand-driven end-to-end runs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the
slack-personaloperator channel end-to-end, together with its design specs (0201: the channel kind; 0202: shared-identity autonomy). The channel acts through the user's own Slack account via a user-configured MCP backend — no Slack app, noxapp-/xoxb-tokens, no workspace-admin install — and everything it posts appears as the user, which drives every safety default below.How it works
operator/mcp.rs— a minimal stdio MCP client (spawn viash -c,initializehandshake,tools/call). Classic daemon logic; no agent or model turn anywhere in the channel.operator/slack_personal.rs— the adapter. Polls the backend'sslack_sweep_messagesas a simulated event subscription with its own timestamp cursor (started at "now": history while no daemon ran is never replayed through an operator that speaks as you). Deliveries route into the shared operator router under the sameworkspace:channel:thread_tskey as the Socket Mode kind; thread context is fetched and fenced with the same untrusted-text boundary; outstanding deliveries survive daemon restart via the same reconciliation path — except failures are logged and cleared, never posted, because failure prose would appear as the user's own words.trigger=dm); channel messages needtrigger=allplus an explicit channel-allowlist entry (empty allowlist = no channels, inverted from the bot kind on purpose). The user's own messages trigger the operator only in their DM with themself — the private command line.draft(default: the reply lands in the user's Slack drafts; their deliberate send makes the words theirs) andauto(posts directly, with an🤖 sent by an agentdisclosure marker on by default; opt-out is explicit). Echo suppression is by recorded sent-ts, never by author.slack_sweep_messages,slack_read_thread,slack_send_message,slack_create_draft); any conforming MCP server is a backend.examples/fake_slack_mcp.rsis a contract-conforming fake for tests and hand-driven runs.Testing
shsubprocess spawn), sweep policy (scope/trigger/self rules, cursor advance over rejections, echo dedup, ts instant-compare at f64's edge), config layer (save/report/preserve-on-omit, kind-mismatch refusals, poll floor, safe-default snapshot), supervisor desired-set, TUI editor cycle + defaults.cargo test --workspace, exit 0).claudeharness and the fake backend: DM swept → routed session created → reply delivered as a Slack draft with correct channel/thread; config hand-edit picked up by the file watcher (task rebound) → second DM auto-sent with the disclosure suffix. (Reply text happened to be a 529-overload notice from the API during the run — the harness's genuine output; the channel mechanics were exact.)Not in this slice (per spec, follow-ups)
🤖 Generated with Claude Code