Files
kandev/apps/backend/internal/slack/client.go
T
João Salavisa 7e0b46c227 feat(slack): cookie-mode integration that triages threads via a utility agent (#775)
* feat(slack): add cookie-mode integration that triages messages via utility agent

Adds an install-wide Slack integration that polls the connected user's
own messages for `!kandev <instruction>`, hands the thread context to a
configured utility agent with the Kandev MCP server attached, and
creates the resulting task. Auth uses the user's xoxc- token + d cookie
so it works on locked-down Enterprise Slack tenants where bot installs
aren't possible.

Shared infra:
- ACP host-utility executor accepts McpServers on session/new
- New MCP tool list_repositories_kandev so the triage agent can pick the
  right repo
- Template engine exposes Slack-specific variables for prompt injection
- secretadapter + healthpoll reused from jira/linear

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(slack): address PR review feedback round 9

- Drop dead `var _ = time.Second` no-op and its misleading comment in
  trigger_test.go.
- `RecordAuthHealth` now invalidates the cached client when the probe
  fails, so the poller self-recovers from credential rotation without
  requiring a re-save.
- Stable secondary sort key (`workspace_id DESC`) on the legacy →
  singleton migration so promoted-row selection is deterministic on
  `updated_at` ties.
- Extract `categorySlack` constant in `engine.go` and update the
  `VariableInfo.Category` doc-comment to list the full set of values.
- Realign `slack-integration/spec.md` with what shipped: cookie-mode +
  `!kandev` triage as v1, with bot install / User OAuth / reaction &
  slash & shortcut triggers moved under Future scope.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-05 09:03:42 +01:00

64 lines
2.8 KiB
Go

package slack
import (
"context"
"errors"
"fmt"
)
// ErrNotConfigured is returned when a Slack operation is attempted without a
// stored configuration or credentials. Wording is install-wide ("not
// configured", not "workspace not configured") because the WS path surfaces
// this raw string to clients via err.Error() without rewriting; the HTTP
// path overrides with its own friendlier message in writeClientError.
var ErrNotConfigured = errors.New("slack: not configured")
// Client is the minimal Slack Web API surface this integration needs. The real
// implementation in cookie_client.go authenticates as the user via the xoxc-
// token + d cookie pair; tests substitute a fake.
type Client interface {
// AuthTest hits the `auth.test` endpoint, which is the cheapest call
// that confirms credentials are still valid and returns the
// authenticated user/team identifiers. Used by the health poller and the
// connection-test handler.
AuthTest(ctx context.Context) (*TestConnectionResult, error)
// SearchMessages runs Slack's `search.messages` with a query string —
// the trigger uses this to find the user's own messages with the
// configured command prefix. Returns a flat list of matches.
SearchMessages(ctx context.Context, query string) ([]SlackMessage, error)
// ConversationsReplies fetches the full thread for a parent message.
// When threadTS is empty (the message isn't part of a thread), returns
// just the single message anchored at triggerTS — implementations must
// pin to triggerTS, not "the channel's latest message", or concurrent
// activity in the channel between trigger and processing will give the
// agent unrelated context.
ConversationsReplies(ctx context.Context, channelID, threadTS, triggerTS string) ([]SlackMessage, error)
// ChatGetPermalink resolves a message's canonical Slack URL.
ChatGetPermalink(ctx context.Context, channelID, ts string) (string, error)
// ChatPostMessage posts a reply (in cookie mode, as the authenticated
// user). When threadTS is non-empty, the message is posted in-thread.
ChatPostMessage(ctx context.Context, channelID, threadTS, text string) error
// ReactionsAdd adds an emoji reaction to a message. `name` is the bare
// emoji name (no surrounding colons), e.g. "eyes" or "white_check_mark".
// Implementations swallow Slack's `already_reacted` error so the trigger
// can call this idempotently across watermark retries.
ReactionsAdd(ctx context.Context, channelID, ts, name string) error
}
// APIError captures a Slack API failure. Slack returns 200 with `{"ok": false,
// "error": "..."}` for almost every failure mode, so StatusCode is the HTTP
// code (usually 200) and Message is Slack's `error` string.
type APIError struct {
StatusCode int
Message string
}
func (e *APIError) Error() string {
return fmt.Sprintf("slack api: status %d: %s", e.StatusCode, e.Message)
}