Channel Slack¶
L'adapter bidirectionnel channel/slack reçoit les événements Slack Events API
par HTTP, vérifie leur signature sur le corps brut, normalise les messages dans
le contrat channel, puis publie les réponses texte avec
chat.postMessage. Il n'ajoute ni SDK Slack ni logique conversationnelle dans
le domaine.
Intention¶
Utiliser cet adapter pour connecter une Slack App à un handler ou un agent
Arclith. La v1 couvre les Request URLs HTTP, url_verification, message,
app_mention et les réponses texte dans le thread d'origine.
Socket Mode, les slash commands, les block actions et l'installation OAuth
multi-workspace automatisée restent hors scope. channel/slack transporte les
messages ; le transcript et l'état d'agent restent des responsabilités
applicatives, conformément à Channel n'est pas Chat.
Position Hexagonale¶
FastAPI, les headers Slack et httpx restent dans l'adapter. Les modèles et
ports communs dans domain/ ne dépendent pas du fournisseur.
Quickstart¶
Ajouter L'adapter¶
| Bash | |
|---|---|
La commande :
- crée
config/adapters/bidirectional/slack.yaml; - ajoute l'extra
arclith[channel], qui fournithttpx; - crée les mappings
ARCLITH_SLACK_SIGNING_SECRETetARCLITH_SLACK_BOT_TOKENdansconfig/secrets.yaml; - ne place aucun token dans un fichier versionné.
Le fichier scoped généré est chargeable avant que les secrets soient fournis :
| YAML | |
|---|---|
Par défaut, le montage du router construit aussi le sender Slack : il exige donc
signing_secret et bot_token, puis vérifie immédiatement la présence de
l'extra channel. Si le service injecte explicitement un sender personnalisé,
seul signing_secret est requis par l'adapter entrant. Dans les deux cas, une
configuration requise manquante échoue au démarrage, pas lors du premier
événement.
Brancher Le Router¶
Le handler doit recopier conversation_id et thread_id dans sa réponse pour
répondre dans le canal et le thread Slack d'origine :
MemoryChannel ne convient qu'au test local. En production, injecter un
ChannelEventStore atomique partagé entre les replicas et un resolver
d'identité tenant-aware.
Avec le bootstrap Arclith, utiliser normalement les settings déjà résolus :
| Python | |
|---|---|
Pour un worker sortant, arclith.channel_sender("slack") construit le même
SlackChannelSender à partir de la configuration.
Tester Le Challenge Localement¶
Slack signe la chaîne v0:<timestamp>:<corps-brut> avec HMAC-SHA256. Le corps
calculé doit être envoyé sans nouvelle sérialisation :
La réponse attendue est {"challenge":"local-challenge"}. Le challenge est
authentifié mais ne déclenche ni resolver, ni claim, ni handler.
Formation¶
Commencer par le quickstart Channel pour comprendre les quatre ports et l'idempotence sans fournisseur. Cette page ajoute le protocole Slack, la signature et les contraintes d'acquittement.
Événements Entrants¶
L'enveloppe externe tolère les champs additionnels de Slack pour rester compatible avec les évolutions du fournisseur. Les champs utilisés sont typés et seuls les éléments documentés ci-dessous traversent la frontière.
| Événement | Traitement |
|---|---|
url_verification |
retourne le challenge signé, sans dispatch |
event_callback + message |
normalise un message utilisateur |
event_callback + app_mention |
normalise une mention de l'application |
| événement non supporté | retourne 200 avec status: ignored |
événement avec bot_id, app_id ou subtype non supporté |
retourne 200 avec status: ignored |
Les messages file_share sont acceptés. Chaque fichier devient un
ChannelAttachment(kind="slack_file") avec nom, MIME type, taille, URL privée
HTTPS et slack_file_id. L'adapter ne télécharge jamais le fichier et ne met
jamais le bot token dans son URL. Les autres subtypes, notamment édition,
suppression et bot_message, sont ignorés pour éviter les boucles et les
événements non conversationnels.
La normalisation produit :
| Champ commun | Source Slack |
|---|---|
provider_event_id |
event_id de l'enveloppe |
conversation_id |
event.channel |
thread_id |
event.thread_ts, sinon event.ts pour créer un thread racine |
sender.external_user_id |
event.user |
sender.external_workspace_id |
team_id |
sender.external_tenant_id |
enterprise_id, sinon team_id |
text |
event.text non vide |
attachments |
fichiers Slack supportés, sans contenu binaire |
metadata |
event_type, event_ts, retry_num, retry_reason uniquement |
Les identifiants Slack restent des affirmations externes. Ils ne deviennent
jamais directement un user ou tenant applicatif : le
ChannelIdentityResolver doit retourner un ResolvedChannelIdentity explicite.
Réponses Sortantes¶
SlackChannelSender appelle uniquement l'URL fixe
https://slack.com/api/chat.postMessage, sans suivre de redirection. Il envoie
le token dans Authorization: Bearer, jamais dans l'URL ou le corps.
La v1 prend en charge les réponses texte. Elle transmet :
conversation_idcommechannel;thread_idcommethread_ts;message_idcommeclient_msg_id;textcomme contenu du message.
Les pièces jointes sortantes sont refusées explicitement. Un envoi réussi
retourne un ChannelDeliveryReceipt dont provider_message_id est le ts
Slack. Les textes de plus de 40 000 caractères sont refusés avant l'appel. Si
allowed_channel_ids est configuré, le sender applique la même allowlist aux
messages sortants afin qu'un handler ne puisse pas contourner la restriction
inbound.
Acquittement Et Idempotence¶
Slack attend une réponse HTTP 2xx en moins de trois secondes et retente les
événements non acquittés. L'endpoint retourne toujours 200 pour un challenge,
un événement ignoré, un doublon, un traitement terminé ou une prise en charge
durable.
Le statut accepted n'est émis que si le handler a déjà persisté ou publié le
travail dans une infrastructure durable. Pour les traitements longs, le
handler doit donc mettre le message en file puis retourner
ChannelHandlerResult(status="accepted"). Une simple tâche locale FastAPI ne
constitue pas cette garantie.
Le dispatcher claim atomiquement ("slack", event_id) avant la résolution
d'identité. Les retries portant le même event_id obtiennent
status: duplicate sans rejouer le handler.
Après une réussite du handler, une erreur chat.postMessage ne libère pas le
claim : cela évite de répéter des effets métier. Une réponse critique doit être
écrite dans un outbox ou une file outbound et retentée séparément.
Configuration Slack¶
Dans la Slack App :
- activer Event Subscriptions et configurer la Request URL HTTPS vers le path du router ;
- souscrire
app_mentionet/ou les variantesmessage.*nécessaires ; - installer l'application dans le workspace ;
- exposer le Signing Secret et le Bot User OAuth Token via la capability
secrets; - réinstaller l'application après tout changement de scopes.
Scopes minimaux selon les subscriptions choisies :
| Besoin | Scope bot |
|---|---|
| recevoir les mentions | app_mentions:read |
| lire les messages de canaux publics | channels:history |
| lire les messages de canaux privés | groups:history |
| lire les messages directs | im:history |
| lire les messages directs de groupe | mpim:history |
| envoyer une réponse | chat:write |
| publier dans un canal public sans invitation | chat:write.public, optionnel |
| télécharger ultérieurement un fichier privé | files:read, hors de cet adapter |
Limiter workspace_id en mono-workspace et renseigner
allowed_channel_ids dès que le service n'a pas vocation à répondre partout.
Une liste vide autorise tous les canaux du workspace accepté.
Références officielles : Events API,
vérification des requêtes,
app_mention et
chat.postMessage.
Statuts HTTP¶
Le router déclare explicitement toutes ses réponses OpenAPI :
| HTTP | Code JSON | Cause |
|---|---|---|
200 |
— | challenge, terminé, accepted, duplicate ou ignored |
400 |
invalid_payload |
JSON ou enveloppe Slack invalide |
401 |
invalid_signature |
signature absente, invalide ou vieille de plus de la tolérance |
403 |
identity_not_resolved, channel_unauthorized |
identité, workspace, canal ou credentials refusés |
413 |
payload_too_large |
stream supérieur à max_payload_bytes |
415 |
unsupported_media_type |
media type différent de application/json |
422 |
invalid_event |
événement supporté mais champs invalides |
429 |
slack_rate_limited |
Web API limitée ; Retry-After numérique est propagé |
502 |
delivery_failed |
chat.postMessage refuse le message ou répond de façon invalide |
503 |
slack_unavailable |
timeout, transport, erreur 5xx ou erreur Slack transitoire |
Les détails sont stables et ne reprennent jamais le corps Slack, une signature, un token ou un message complet.
Sécurité¶
- Le stream HTTP est borné avant le parsing.
- La signature
v0est calculée sur les octets bruts et comparée avechmac.compare_digest. - Les requêtes hors de
signature_tolerance_secondssont rejetées. signing_secretetbot_tokenutilisentSecretStret ne sont pas rendus en clair par les settings.- L'endpoint Web API est constant ; ni le payload entrant ni la réponse du handler ne peuvent choisir une URL.
- Les redirects sortantes sont désactivées et le timeout est borné.
- Les payloads complets, textes, signatures et tokens ne sont jamais loggés par l'adapter.
- Les fichiers privés ne sont pas téléchargés ; un téléchargement ultérieur doit appliquer auth, taille, MIME type, timeout et stockage sûr.
Production¶
Avant d'exposer la Request URL :
- injecter les deux secrets depuis Vault ou des variables d'environnement ;
- restreindre workspace et canaux ;
- utiliser un event store atomique partagé ;
- garantir un acquittement sous trois secondes avec un handler durable court ;
- isoler la reprise outbound dans un outbox si la réponse est critique ;
- appliquer une politique egress limitée à Slack ;
- journaliser uniquement IDs techniques, statut, latence et corrélation à cardinalité bornée.
Validation¶
| Bash | |
|---|---|
Vérifier ensuite make quality, make precommit, les deux lockfiles et
make docs.
Troubleshooting¶
| Symptôme | Diagnostic | Correction |
|---|---|---|
401 invalid_signature |
timestamp périmé ou corps modifié avant vérification | vérifier l'horloge et lire le corps brut avant tout parsing |
échec au montage pour signing_secret |
secret non résolu | vérifier le mapping ARCLITH_SLACK_SIGNING_SECRET |
échec du sender pour bot_token |
token absent | vérifier ARCLITH_SLACK_BOT_TOKEN et l'installation de l'app |
403 identity_not_resolved |
tuple user/enterprise/workspace non mappé | créer le mapping explicite dans le resolver |
403 channel_unauthorized |
workspace ou canal hors allowlist, ou token refusé | vérifier la configuration et les scopes sans afficher le token |
duplicate |
event_id déjà claim après un retry Slack |
ne pas régénérer l'ID ; inspecter le store partagé |
429 |
limite chat.postMessage |
respecter Retry-After dans la reprise outbound |
502 ou 503 |
refus ou indisponibilité Slack | corréler l'event ID et utiliser un outbox pour les réponses critiques |
Projet¶
Garder ChannelMessageHandler dans application/, le mapping d'identité et le
store partagé derrière leurs ports, et le router Slack dans le bootstrap
FastAPI. Ne placer aucun appel Slack, secret, parsing de payload ou règle de
thread dans domain/.