Company messaging connections
Settings → Connections belongs to the currently selected company. Owners and admins can install Slack, reconnect it, assign a published company agent to a channel, remove assignments and disconnect. Members can see connection metadata without credentials or management controls. Destination metadata is manager-only until verified channel membership is available, so private channel names are not exposed to other company members. A company can have multiple Slack workspaces; each workspace belongs to one company, including after disconnection. This release has no automatic workspace transfer.
Automatic channel replies #
Invite the bot, assign a company agent, then mention it in that channel. The first mention privately provides a ten-minute link to confirm the sender's Stech member account. Confirmation shows the company, Slack workspace and Slack account ID. It never creates company membership or uses the installer as the sender. After linking, mention the agent again; pre-link mentions are not replayed.
Signed mentions become durable, deduplicated events and then ordinary company-agent requests. The existing answer worker uses that company's model, spending controls and policies. Replies appear in the original Slack thread. The first version answers the supplied mention only; it does not load Slack history, private web conversations or owner sessions. Channel answers and authorized diagnostics are visible to everyone in the channel, including Slack guests. Managers must only assign approved company channels.
Replies use the sender name stech-agents [Agent name] when the installation has granted chat:write.customize. The name and capability come from the assigned agent and connection checked immediately before delivery, never from model output. Older installations keep the default bot sender and a plain-text agent label above the answer until an owner/admin reconnects and grants the additional scope.
Ordinary answers use Slack's native Markdown for headings, bold, lists, code and links. Content containing <, @, or HTML-entity-shaped text stays entirely plain text so potential mentions remain inert and the original text is preserved. Both paths retain the full 9,000-character answer and proposal-footer budget.
Change proposals include the existing owner/requester page URL for the current proposal decision. This release does not accept Slack approval commands.
The data model separates provider account, destination and member identity from agent requests. Only Slack has an adapter. Other chat systems can use the same boundaries when requested.
Platform setup #
Use one dedicated platform-owned Slack app. For production, import the app manifest, with STECH_APP_URL=https://app.stech.com and STECH_API_PUBLIC_URL=https://api.stech.com. After deployment, verify the event URL in Slack and enable distribution separately. Do not import this manifest over an existing app with commands or interactive features. Store credentials only in the API environment:
STECH_SLACK_CLIENT_IDSTECH_SLACK_CLIENT_SECRETSTECH_SLACK_APP_IDandSTECH_SLACK_SIGNING_SECRET: set both to enable signed events and replies. Without them, installation remains available but chat delivery is inactive.STECH_API_PUBLIC_URL: public API origin used to construct/oauth/messaging/slack/callback.STECH_APP_URL: signed-in dashboard origin used for the fixed return URL and mutation origin check.- The existing API secrets-encryption key must be configured; installations use the existing
SecretsCryptoservice.
Leave all four Slack variables unset to disable the integration. Partial or malformed Slack configuration fails API startup.
Register the exact callback URL in the Slack app. Enable Event Subscriptions at STECH_API_PUBLIC_URL + /webhooks/messaging/slack/events with the app_mention bot event. No interactivity or slash-command endpoint is required. Enable distribution for installations into other workspaces. Request bot scopes app_mentions:read, chat:write, channels:read, groups:read and chat:write.customize. The first four remain mandatory; the optional customization grant enables the agent sender name. Adding it to the manifest does not upgrade existing grants: an owner/admin must reconnect through Slack consent. Do not enable token rotation for this initial adapter: rotating and enterprise-wide grants are rejected, rather than saved with an incomplete refresh lifecycle. Workspace administrator restrictions still apply.
The install link deliberately omits Slack's team parameter. The user chooses the workspace at Slack, then returns in the same authenticated Stech browser session. The installation remains bound to the company selected when starting, even if the user switches companies during consent. Successful callback status names that binding; switch back to manage it.
When the Slack integration is disabled, metadata remains readable but installation/channel discovery is disabled. This change does not configure credentials, alter production Compose overlays or restart services automatically.
Destination assignment #
Invite the bot into the desired Slack channel, then choose Assign channel. Only joined, active, non-shared public/private channels are selectable. The server rechecks the actual Slack channel on save. Slack Connect and other shared channels are excluded until external participant identity and audience rules are implemented. Display names are labels; durable routing uses channel IDs.
Assignments require an enabled agent in this company with an active owner. Installing a workspace grants no infrastructure access. The Network Doctor pilot's existing company/workspace gate and scoped read-only tools remain unchanged.
Disconnect clears the local encrypted credential and all assignments. It does not uninstall or revoke the Slack app remotely; an administrator can remove the app in Slack separately. A later reinstall into the same company replaces the encrypted grant and advances the installation revision. Existing member identity links remain within that same company, while pending work and link challenges from the previous revision are refused. Claiming the same workspace for another company is refused, even after disconnection.
Security and verification #
OAuth state uses a random 32-byte value; only its SHA-256 hash is stored, with a SHA-256 hash of the initiating authenticated session ID, initiating user, company, provider and a ten-minute expiry. Callback consumption requires that same user and session and is atomic and one-use. Current management membership is checked at callback and again under the connection transaction. Revocation, expiry, cancellation and replay refuse installation. Mutations and provider channel discovery require interactive sessions; session mutations also require an allowed Origin. API keys can only read their own company's sanitized metadata.
Credentials use existing authenticated encryption and never appear in list DTOs, browser responses or callback URLs. Provider HTTP/JSON failures use fixed sanitized errors. Provider requests have a ten-second timeout and one-MiB response limit, with no automatic retries. Installation conflicts cannot change tenant ownership.
Incoming events use HMAC over bounded original request bytes and a five-minute timestamp window. The configured app ID and saved Slack account resolve the tenant. The request thread, original agent owner, destination identity, installation revision and linked requester are fixed at acceptance. Disconnect/reinstall, reassignment, owner transfer and membership loss invalidate pending work; these checks also run before model/tool calls, answer persistence and reply admission.
Accepted mentions are acknowledged after their event receipt is saved; duplicate events reuse that receipt. Ignored events are acknowledged without creating work. The webhook makes no provider or model calls. Private link challenges store only hashes and require an explicit authenticated same-origin confirmation. No link is created on GET. A browser-supplied Slack user ID is never accepted.
Outgoing delivery has one persisted attempt. A failed or stale attempt is not retried; an ambiguous send is shown as unknown on the request page so a human can inspect the thread. A send already admitted can finish after local disconnection; Slack channel changes and a network send cannot be made atomic. The channel is checked again before posting. Answer rendering and sender capability are selected before that single send, with no retry to change format or sender. Potential mention controls use plain-text blocks; automatic mention parsing, broadcasts and unfurls stay disabled. The linking instruction has one fixed server-generated account-link URL.
Linked mentions and their answers use the existing request store with an explicit channel audience. Unlinked mentions remain event records and do not create agent requests. Request-page access and status updates follow the company-agent rules.
The worker processes a single event or delivery per tick. No broad history sync, file ingestion, token refresh, external/shared channels or automatic infrastructure execution is included. Unconsumed expired challenges and event deduplication receipts have no background pruning; retention pruning can be added when real usage establishes its requirements.
Focused checks:
# TEST_DATABASE_URL must name a disposable local PostgreSQL server's /postgres DB.
# Tests create uniquely named databases, apply every committed migration, then drop them.
TEST_DATABASE_URL=postgresql://[email protected]:PORT/postgres bun test \
api/tests/messaging-connections.integration.test.ts \
api/tests/messaging-connections-http.integration.test.ts \
api/tests/messaging-slack.test.ts \
api/tests/messaging-events.integration.test.ts \
api/tests/messaging-events-http.integration.test.ts \
api/tests/messaging-worker.integration.test.ts
(cd app && bun test app/settings/connections/Connections.test.tsx)The HTTP and worker fixtures use real Hono routes, PostgreSQL transactions, encryption and queue execution with injected browser sessions, model answers and Slack responses. They do not prove a live Slack installation or delivery. Activation requires a real workspace consent, member link, mention-to-thread reply, tenant denial and owner proposal check.
References: Slack OAuth, distribution, request signatures, app mentions, thread replies.