Chattbryggor
En chattbrygga gör en extern chattprodukt — i dag Slack — till en förstklassig väg in i en arbetsyta i Control Center. Nämn boten och en agent vaknar; svara i Slack-tråden och samtalet fortsätter; skriv ett slash-kommando och ett ärende skapas. Under ytan ligger samma ytor, avskrifter, ärenden och tillskrivning som skrivbordsappen använder. Chatten är en yta på arbetsytan, inte en kopia av den.
Slack är den enda leverantör som levereras. Det finns ingen implementering för Discord, Telegram, Teams eller Matrix någonstans i produkten. Funktionen är medvetet skriven leverantörsblind — att lägga till en andra produkt är ett enum-värde, en adapter och en plugin — men i dag betyder ”chatt” Slack.
Två ytor täcker det, och de tillhör olika personer. Att ansluta en arbetsyta till Slack och att anpassa boten är admin-åtgärder under Inställningar → Arbetsyta → Allmänt. Att länka sitt eget chattkonto till sin identitet i Control Center är en medlemsåtgärd under Inställningar → Arbetsyta → Profil och identitet.
Tre beslut formar hela funktionen: vilken riktning anslutningen går i, vad som auktoriserar ett meddelande som kommer utifrån och var den produktspecifika kunskapen får bo.
Medvetet utgående
Avsnitt med rubriken “Medvetet utgående”Bryggan lyssnar aldrig på leverantören. Slack nås över Socket Mode: servern frågar Slack efter en socket-URL och kopplar upp utåt och tar sedan emot omnämningar, direktmeddelanden och slash-kommandon över den anslutningen.
Konsekvensen är själva poängen. En bärbar dator bakom NAT, en hemmaserver utan domän, en maskin som flyttar mellan nätverk — alla kan köra bryggan utan inkommande port, utan tunnel, utan TLS-certifikat och utan en publik URL att rotera när den ändras. Det finns ingen webhook-slutpunkt att säkra, eftersom det inte finns någon webhook-slutpunkt. När Control Center sätter ihop appdefinitionen framtvingar det Socket Mode och tar bort varje request-URL, för en app som är konfigurerad för båda vore en app som bara fungerar till hälften.
Priset är en anslutning som måste hållas vid liv, och det är serverns uppgift: återanslutningen är exponentiell backoff med jitter, begränsad till 30 sekunder, en socketrotation som leverantören initierar återansluter utan att trappa upp den backoffen, och en avvisad inloggningsuppgift är slutgiltig — att försöka igen med en återkallad token i evighet skulle bara ge en hastighetsbegränsning, så bryggan stannar och väntar på nya tokens medan den rapporterar leverantörens eget fel ordagrant i inställningarna.
En arbetsyta utan chattapp kostar ingenting alls: ingen socket, inga timers, ingen trafik. Och hela saken startar efter serverns ready-banner, för en chattapp som är långsam att svara får aldrig fördröja uppstarten.
Medlemskapet auktoriserar, inte innehavet av en token
Avsnitt med rubriken “Medlemskapet auktoriserar, inte innehavet av en token”Personen som installerade appen är inte personen som skickar meddelandet. Därför löses varje inkommande händelse upp till en principal innan något händer, och svaret blir ett nej med en förklaring:
- Ett olänkat chattkonto får veta hur det länkar, i den Slack-tråd det skrev i.
- Ett länkat konto vars användare inte är medlem i den här arbetsytan uppmanas att be en admin om en inbjudan.
- En medlem vars roll är skrivskyddad (
viewer,guest) får veta att rollen inte kan starta arbete.
Inget av detta fortsätter. Ett chattmeddelande tillskrivs aldrig någon som inte skickade det — och det är vad som håller revisionsspåret, hastighetsbegränsningarna per principal och ärendenas avsändare ärliga. Se Multiplayer – identitet, medlemskap och närvaro för den medlemskapsmodell som detta lutar sig mot.
Länkning är identitetsuppslagning, inte en åtkomstbeviljning. Medlemskapet kontrolleras på nytt vid varje händelse, så en länk som tillhör någon som sedan dess tagits bort leder till ett nej i stället för till dennes gamla privilegier. Två vägar upprättar en länk: en kortlivad engångskod som medlemmen skriver i chatten, eller en automatisk matchning mot leverantörens verifierade e-post — och e-postvägen kräver fortfarande att den matchade användaren redan är medlem, så en främling vars adress råkar finnas på servern länkas till ingenting.
Koderna hålls medvetet i minnet, med en livstid på 15 minuter. Att spara en skulle skapa en varaktig rad formad som en inloggningsuppgift för något som är tänkt att försvinna, och att en serveromstart ogiltigförklarar varje utestående kod är rätt beteende, inte en regression — medlemmen trycker på ”länka” igen.
Ett nej besvaras i den Slack-tråd som meddelandet öppnade, som ett riktigt svar. Ett svar i tråden märker meddelandet det svarar på, når fram till just den person som behöver det och håller ändå ”you are not linked” borta från själva kanalen. Att skicka ett meddelande är dessutom den enda förmåga som varje leverantör har, så förklaringen hänger aldrig på en funktion som appen kanske inte bär.
Härkomst och ekoproblemet
Avsnitt med rubriken “Härkomst och ekoproblemet”En brygga som speglar åt båda hållen hamnar i en loop om inget skiljer de två
riktningarna åt. Varje meddelande som bryggan tar in stämplas med
metadata['chat'] — leverantör, team, samtal, tråd, meddelande och avsändare.
Den utgående speglingen hoppar över varje meddelande som bär den stämpeln.
Den enda regeln är vad som gör dubbelriktad spegling säker: ett mänskligt meddelande som skrivs i Control Center vidarebefordras ut till den länkade tråden (med författarens namn framför), medan ett meddelande som kom från chatten stannar där det kom in.
Det som går utåt är medvetet smalt. Agentens svar vidarebefordras som
vanlig chattext. En kort tanke kan synas på kortets Thinking…-rad.
Verktygsutdata är en vägg av diffar och stannar i Control Center, vilket också
är dit ett avkortat svar pekar.
Ett kort rapporterar arbetet; texten är fortfarande meddelandet
Avsnitt med rubriken “Ett kort rapporterar arbetet; texten är fortfarande meddelandet”Där leverantören kan visa dem bär en tur ett uppgiftskort, som redigeras på plats så länge turen pågår och ligger ovanför det svar det förklarar.
Kortet är en grupperad plan, inte ett ihopmosat stycke. Slack sammanfogar
details och output på samma uppgift, så en rads detaljer skickas en gång
(tanken på Thinking…) och output strömmas aldrig. Uppsättningen är en rad
vars titel byts ut (Working on it…, Cloning …). Resonemanget lägger till en
Thinking…-rad. Varje verktyg är en egen rad (Read lib/main.dart). Svaret
är vanlig text under kortet — Slacks planvy läser det från ett senare
markdown-tillägg, inte från en bit inblandad bland uppgifterna. Inget annat:
ingen verktygsutdata och bara ett verktygsargument från en kort
tillåtelselista, för ett okänt verktygs argument kan vara en hel fil och en
rad är en rad.
Kortet täcker också väntan före turen. En första omnämning skapar samtalet,
och att skapa det klonar varje repo som agenten ska arbeta i — vilket på ett
riktigt repo är minuter där agenten ännu inte har fått någon fråga och tråden
ser ut som om boten ignorerade begäran. Därför öppnas kortet i samma stund som
omnämningen accepteras (Working on it…), och sedan uppdateras
uppsättningsraden med samma ord som skrivbordsappen visar — Cloning acme/widgets…, Setting up Reviewer…, Starting the agent… — därefter en
Thinking…-rad så fort agenten börjar resonera, sedan en rad per verktyg. Ett
kort, ett meddelande, från ”jag hörde dig” hela vägen till svaret som vanlig
text under. Uppsättningen rapporteras bara där någon i chatten väntar på den:
ett samtal som förbereds i appen, eller ett strandat som återupptas vid
serverns uppstart, förbereds helt utan kort.
Det bär det enda som chatten inte kan ge läsaren: en länk
View in Control Center till samtalet bakom svaret. Chattprodukter
accepterar http(s) i en länk och inget annat, så knappen pekar på en liten
sida som den här servern levererar, vilken skickar vidare webbläsaren till
skrivbordsappens control-center://-länk. Den sidan läser ingenting och löser
upp ingenting — id:na i dess URL ekar in i en djuplänk och ingen annanstans —
och en server utan nåbar publik URL ger helt enkelt ett kort utan knapp i
stället för ett trasigt.
Kort är en förmåga som vilken annan som helst. En leverantör utan dem beter
sig precis som förut, bara text, och en leverantör som nekar ett kort vid
anropstillfället behåller sin ström: svaret går alltid före kortet som
beskriver det. När strömning är helt av åker det färdiga kortet med i det enda
postade meddelandet i stället. Ett skapat ärende får samma behandling —
bekräftelsen för /cc ticket är ett slutfört kort vars enda skäl att finnas är
länken till ärendet som just skapades.
Förmågor förhandlas fram och behålls
Avsnitt med rubriken “Förmågor förhandlas fram och behålls”Chattprodukter skiljer sig åt i vad de kan rendera, så bryggan är skriven mot den rikaste ytan — ett svar som växer live inne i en tråd, en tillfällig status ”is thinking…”, en tråd med titel, ett efemärt svar på ett slash-kommando — och trappar ned en förmåga i taget. En leverantör utan något av detta fungerar ändå; svaren kommer helt enkelt som ett meddelande.
Att annonsera en förmåga är inte att lova den. Slacks strömmande svar kräver en funktion i ett betalt abonnemang, och Slack nekar först vid anropstillfället. Ett nej som markerats som permanent kommer ihåg under hela anslutningens livstid: bryggan slutar fråga och postar hela svar från och med då, och inställningarna säger det rakt ut, för ”mina svar slutade strömmas” ska gå att få svar på utan att läsa en logg. Uppgiftskort släpps på samma sätt om Slack nekar ett.
Varje gräns tillhör leverantören och inte vidarebefordran: taket för meddelanden, taket för strömmade bitar och huruvida en ström behöver ett trådankare deklareras alla av adaptern, och ett långt svar delas vid en stycke-, rad- eller ordgräns i stället för mitt i ett ord.
En brygga, en adapter per produkt
Avsnitt med rubriken “En brygga, en adapter per produkt”Allt som är produktspecifikt ligger bakom en port. Den generiska kärnan känner
till markdown, chattsamtal, chatt-trådar och medlemmar; den vet ingenting om
Slack-kuvert, mrkdwn eller gateway-intents. Tre delar får det att fungera:
- Adaptern normaliserar en produkt: dess transport, dess API-anrop, dess
text-codec och dess egenheter i kuverten. Slacks är lärorika — en
kanalomnämning kommer in två gånger (som
app_mentionoch sommessage), och de flesta meddelandetyper är dekoration snarare än nya förfrågningar. Båda löses upp i adaptern, så kärnan får aldrig veta att Slack levererar dubbelt. - Deskriptorn är leverantörens publika form: vilka inloggningsuppgifter den behöver, vilka format de har, vilka URL:er som hjälper en användare att hitta dem, vad den kan och huruvida dess app kan skapas eller redigeras härifrån. Inställningskortet, anslutningsdialogen och valideringen av argument genereras alla från den — klienten skickar tillbaka en leverantör plus en map med fält-id:n som nycklar.
- Pluginen är kompositionen på serversidan: hur inloggningsuppgifter blir en verifierad anslutning, hur en adapter byggs och hur appen hos leverantören hanteras.
Att lägga till en produkt är alltså ett enum-värde, en adapter och en plugin. Ingen ny RPC-operation, ingen schemaändring, ingen klientändring — och klienten förblir ärlig, för ett fält den aldrig har hört talas om renderas ändå med den etikett som servern angav.
Två detaljer hindrar abstraktionen från att läcka uppåt. De apphanteringssteg som en leverantör inte har något API för (generera en token, installera appen, installera om efter en behörighetsändring) returneras som namngivna steg med egna länkar, så skärmen för att slutföra uppsättningen visar dem utan att veta vilken leverantör det gäller. Och varje instruktion som bryggan skriver namnger leverantören och det kommando som appen faktiskt använder, så ett omdöpt kommando eller en andra leverantör läses korrekt utan någon ny sträng.
Var inloggningsuppgifterna finns
Avsnitt med rubriken “Var inloggningsuppgifterna finns”En arbetsytas chatt-tokens ligger i arbetsytans egen katalog, bredvid dess
databas — som vanlig JSON på
<dataDir>/<workspaceId>/chat_credentials/<provider>.json, inte i databasen
och medvetet inte i operativsystemets nyckelring. Två konsekvenser är lätta
att tappa bort senare: att ta bort en arbetsyta avlänkar katalogen och dess
tokens följer med, och att exportera eller säkerhetskopiera en arbetsyta
kopierar workspace.db, så en arbetsyta som lämnas till någon annan bär inte
levande bottokens inuti sig. Filen ärver filsystemets förtroendegräns på
servervärden — allt med läsåtkomst till datakatalogen kan läsa en bottoken,
vilket är samma gräns som databaserna ligger bakom.
En fil per leverantör, skriven atomiskt och endast åtkomlig för ägaren: en återkallad app raderas genom att en fil avlänkas, och en trasig fil kostar en leverantör i stället för alla. Inloggningsuppgifter färdas in via anslutningsoperationen och kommer aldrig ut igen — statusläsningar svarar bara med anslutningsmetadata.
Det som medvetet inte erbjuds
Avsnitt med rubriken “Det som medvetet inte erbjuds”- Strukturella appinställningar. Transport, behörighetsomfång och händelseprenumerationer framtvingas, de exponeras inte. En app som kan konfigureras till tystnad från en inställningsskärm är en supportbiljett, inte en funktion.
- Samtal plus slash-kommandon. Slack-ytan är meddelanden, svar i trådar och slash-kommandon.
- En andra källa till sanning. Chatten är inte en plats där arbete lagras. Slack-tråden är en yta, svaret är en avskrift, ärendet är ett ärende — allt i arbetsytan, nåbart från varje klient.
- Multiplayer – identitet, medlemskap och närvaro: de principaler och roller som bryggan löser upp genom
- Arbetsytor och isolering: varför länkar och inloggningsuppgifter är per arbetsyta
- Agentmodellen: vad som vaknar när en omnämning kommer in
- Ärenden och delegering: vad
/cc ticketskapar - Arkitektur: var bryggan sitter mellan server och klienter
- Koppla Slack till din arbetsyta
- Konfigurera Slack-integration
- Länka ditt Slack-konto
- Anpassa chattboten
- PR-samtal: samma tre beslut, fattade för en forge