Gå til innholdet

Chatbroer

En chatbro gjør et eksternt chatprodukt — Slack i dag — til en førsteklasses vei inn i et Control Center-arbeidsområde. @-nevn boten, og en agent våkner; svar i Slack-tråden, og samtalen fortsetter; skriv en skråstrekkommando, og en sak opprettes. Under ligger de samme områdene, transkriptene, sakene og tilskrivningen som skrivebordsappen bruker. Chat er en flate i arbeidsområdet, ikke en kopi av det.

Slack er den eneste leverandøren som finnes. Det finnes ingen Discord-, Telegram-, Teams- eller Matrix-implementasjon noe sted i produktet. Funksjonen er bevisst skrevet leverandørblind — å legge til et nytt produkt er én enumverdi, én adapter og én plugin — men i dag betyr «chat» Slack.

To flater dekker det, og de tilhører ulike personer. Å koble et arbeidsområde til Slack og tilpasse boten er admin-handlinger under Innstillinger → Arbeidsområde → Generelt. Å knytte din egen chatkonto til Control Center-identiteten din er en medlems-handling under Innstillinger → Arbeidsområde → Profil og identitet.

Tre beslutninger former hele funksjonen: hvilken retning tilkoblingen går i, hva som autoriserer en melding som kommer utenfra og hvor den produktspesifikke kunnskapen får bo.

Broen lytter aldri etter leverandøren. Slack nås over Socket Mode: serveren ber Slack om en socket-URL og ringer ut, og mottar deretter omtaler, direktemeldinger og skråstrekkommandoer over den tilkoblingen.

Konsekvensen er selve poenget. En bærbar maskin bak NAT, en hjemmeserver uten domene, en maskin som flytter seg mellom nettverk — alle kan kjøre broen med ingen innkommende port, ingen tunnel, ingen TLS-sertifikat og ingen offentlig URL å rotere når den endres. Det finnes ikke noe webhook-endepunkt å sikre, fordi det ikke finnes noe webhook-endepunkt. Når Control Center setter sammen appdefinisjonen, tvinger den gjennom Socket Mode og fjerner enhver request-URL, fordi en app som er konfigurert for begge, ville være en app som bare halvveis virker.

Prisen er en tilkobling som må holdes i live, og det er serverens jobb: gjenoppkobling skjer med eksponentiell backoff og jitter begrenset til 30 sekunder, en leverandørinitiert socketrotasjon kobler til på nytt uten å trappe opp den backoffen, og en avvist legitimasjon er terminal — å prøve et tilbakekalt token på nytt i det uendelige ville bare gi en hastighetsbegrensning, så broen stopper og venter på nye tokens mens den rapporterer leverandørens egen feil ordrett i innstillingene.

Et arbeidsområde uten chat-app koster ingenting i det hele tatt: ingen socket, ingen timere, ingen trafikk. Og det hele starter etter serverens klarmelding, fordi en chat-app som er treg til å svare, aldri må forsinke oppstarten.

Den som installerte appen, er ikke den som sender meldingen. Derfor løses hver innkommende hendelse opp til en Principal før noe skjer, og den feiler lukket med en forklaring:

  • En chatkonto som ikke er knyttet, får beskjed om hvordan den knyttes, i Slack-tråden den skrev i.
  • En knyttet konto der brukeren ikke er medlem av dette arbeidsområdet, får beskjed om å be en admin om en invitasjon.
  • Et medlem med en skrivebeskyttet rolle (viewer, guest) får beskjed om at rollen ikke kan starte arbeid.

Ingen av disse går videre. En chatmelding tilskrives aldri noen som ikke sendte den — og det er dette som holder revisjonssporet, hastighetsbegrensningene per principal og saksrapportørene ærlige. Se Flerbruker — identitet, medlemskap og tilstedeværelse for medlemskapsmodellen dette lener seg på.

Knytning er identitetsoppslag, ikke en tilgangstildeling. Medlemskapet sjekkes på nytt for hver hendelse, så en knytning som tilhører noen som siden er fjernet, ender i en avvisning i stedet for i de gamle privilegiene deres. To veier etablerer en knytning: en kortlivet engangskode medlemmet skriver inn i chatten, eller en automatisk match på leverandørens verifiserte e-post — og e-postveien krever fortsatt at den matchede brukeren allerede er medlem, så en fremmed hvis adresse tilfeldigvis finnes på serveren, knyttes til ingenting.

Kodene holdes bevisst i minnet, med en levetid på 15 minutter. Å lagre én ville skape en varig rad formet som en legitimasjon for noe som er ment å fordampe, og at en serveromstart ugyldiggjør hver utestående kode, er riktig oppførsel, ikke en regresjon — medlemmet knytter kontoen på nytt.

En avvisning besvares i Slack-tråden meldingen åpnet, som et ekte svar. Et svar i tråden merker meldingen det svarer på, når den ene personen som trenger det, og holder samtidig «du er ikke knyttet til» utenfor selve kanalen. Å legge ut en melding er også den ene kapabiliteten alle leverandører har, så forklaringen avhenger aldri av en funksjon appen kanskje ikke har.

En bro som speiler begge veier, vil gå i løkke med mindre noe skiller de to retningene. Hver melding broen tar inn, stemples med metadata['chat'] — leverandør, team, samtale, tråd, melding og avsender. Den utgående speilingen hopper over enhver melding som bærer det stempelet.

Den ene regelen er det som gjør toveis speiling trygt: en melding et menneske skriver i Control Center, videresendes ut til den tilkoblede tråden (med forfatterens navn som prefiks), mens en melding som kom fra chatten, stopper der den kom inn.

Det som går ut, er bevisst smalt. Agentens svar videresendes som vanlig chattekst. En kort tanke kan dukke opp på Thinking…-raden i kortet. Verktøyutdata er en vegg av differ og blir værende i Control Center, som også er dit et avkortet svar peker.

Et kort rapporterer arbeidet; teksten er fortsatt meldingen

Seksjon med tittelen «Et kort rapporterer arbeidet; teksten er fortsatt meldingen»

Der leverandøren rendrer dem, bærer en tur ett oppgavekort, redigert på stedet så lenge turen varer og plassert over svaret det forklarer.

Kortet er én gruppert plan, ikke et most sammen avsnitt. Slack setter sammen details og output på samme oppgave, så en rads detaljer sendes én gang (tanken på Thinking…), og output strømmes aldri. Oppsett er én rad der tittelen byttes ut (Working on it…, Cloning …). Resonnering legger til en Thinking…-rad. Hvert verktøy er sin egen rad (Read lib/main.dart). Svaret er vanlig tekst under kortet — Slacks planvisning leser det fra en senere markdown-innføyelse, ikke fra en blokk blandet inn i oppgavene. Ingenting annet: ingen verktøyutdata, og bare et verktøyargument fra en kort tillatelsesliste, fordi argumentene til et ukjent verktøy kan være en hel filkropp, og en rad er én linje.

Kortet dekker også ventingen før turen. En første omtale oppretter samtalen, og å opprette den kloner hvert repo agenten skal jobbe i — noe som på et virkelig repo er minutter der agenten ennå ikke er bedt om noe, og tråden ser ut som om boten ignorerte forespørselen. Derfor åpnes kortet i det øyeblikket omtalen godtas (Working on it…), og deretter oppdateres den oppsettraden med de samme ordene skrivebordet viser — Cloning acme/widgets…, Setting up Reviewer…, Starting the agent… — så en Thinking…-rad så snart agenten begynner å resonnere, og deretter én rad per verktøy. Ett kort, én melding, fra «jeg hørte deg» og helt fram til svaret som vanlig tekst under. Oppsett rapporteres bare der noen i chatten venter på det: en samtale som klargjøres i appen, eller en strandet en som gjenopptas ved serveroppstart, provisjoneres helt uten noe kort.

Det bærer det ene chatten ikke kan gi leseren: en View in Control Center-lenke til samtalen bak svaret. Chatprodukter godtar http(s) i en lenke og ingenting annet, så knappen peker på en liten side denne serveren betjener, som sender nettleseren videre til skrivebordsappens control-center://-lenke. Den siden leser ingenting og løser ingenting opp — id-ene i URL-en dens ekkoes inn i en dyp lenke og ingen andre steder — og en server uten en tilgjengelig offentlig URL produserer rett og slett et kort uten knapp i stedet for et ødelagt et.

Kort er en kapabilitet som alle andre. En leverandør uten dem oppfører seg nøyaktig som før, bare tekst, og en leverandør som avviser ett på kalltidspunktet, beholder strømmen sin: svaret går alltid foran kortet som beskriver det. Når strømming er helt av, følger det ferdige kortet med den ene meldingen som legges ut i stedet. En sak som opprettes, får samme behandling — bekreftelsen på /cc ticket er et fullført kort, hvis eneste grunn til å finnes er lenken til saken som nettopp ble opprettet.

Chatprodukter er ulike i hva de kan rendre, så broen er skrevet mot den rikeste flaten — et svar som vokser live inne i en tråd, en forbigående «is thinking…»-status, en tråd med tittel, et flyktig svar på en skråstrekkommando — og nedgraderer én kapabilitet om gangen. En leverandør uten noe av dette virker fortsatt; svarene kommer bare som én melding.

Å annonsere en kapabilitet er ikke å love den. Slacks strømmende svar krever en funksjon i en betalt plan, og Slack avviser først på kalltidspunktet. En avvisning som er merket permanent, huskes for hele tilkoblingens levetid: broen slutter å spørre og legger ut hele svar fra da av, og innstillingene sier det rett ut, fordi «svarene mine sluttet å strømme» skal kunne besvares uten å lese en logg. Oppgavekort droppes på samme måte hvis Slack avviser ett.

Hver grense tilhører leverandøren og ikke reléet: taket for meldinger, taket for strømmeblokker og hvorvidt en strøm trenger et trådanpunkt, erklæres alle av adapteren, og et langt svar deles ved et avsnitt, en linje eller en ordgrense i stedet for midt i et ord.

Alt produktformet bor bak en port. Den generiske kjernen vet om markdown, chatsamtaler, chattråder og medlemmer; den vet ingenting om Slack-konvolutter, mrkdwn eller gateway-intents. Tre deler får det til å virke:

  • Adapteren normaliserer ett produkt: transporten, API-kallene, tekstkodeken og konvolutt-særegenhetene. Slacks er lærerike — en kanal-omtale kommer inn to ganger (som app_mention og som message), og de fleste meldingsundertyper er staffasje snarere enn nye forespørsler. Begge løses i adapteren, så kjernen aldri får vite at Slack leverer dobbelt.
  • Deskriptoren er leverandørens offentlige form: hvilken legitimasjon den trenger, hvilke formater den har, hvilke URL-er som hjelper en bruker å finne den, hva den kan gjøre, og hvorvidt appen kan opprettes eller redigeres herfra. Innstillingskortet, kobledialogen og argumentvalideringen genereres alle fra den — klienten sender tilbake en leverandør pluss et kart nøklet på felt-id.
  • Pluginen er sammensetningen på serversiden: hvordan legitimasjon blir en verifisert tilkobling, hvordan en adapter bygges, og hvordan appen på leverandørsiden administreres.

Så å legge til et produkt er én enumverdi, én adapter og én plugin. Ingen ny RPC-operasjon, ingen skjemaendring, ingen klientendring — og klienten holder seg ærlig, fordi et felt den aldri har hørt om, fortsatt rendres med etiketten serveren oppgav.

To detaljer hindrer abstraksjonen i å lekke oppover. App-administrasjonsstegene en leverandør ikke har API for (generer et token, installer appen, installer på nytt etter en tillatelsesendring), returneres som navngitte steg med sine egne lenker, så skjermen for å fullføre oppsettet viser dem uten å vite hvilken leverandør det er. Og hver instruksjon broen skriver, navngir leverandøren og kommandoen appen faktisk bruker, så en omdøpt kommando eller en ny leverandør leses riktig uten noen ny streng.

Et arbeidsområdes chattokens ligger i arbeidsområdets egen katalog, ved siden av databasen — som ren JSON på <dataDir>/<workspaceId>/chat_credentials/<provider>.json, ikke i databasen og bevisst ikke i operativsystemets nøkkelring. To konsekvenser er lette å miste av syne senere: å slette et arbeidsområde fjerner katalogen, og tokenene blir med den, og å eksportere eller sikkerhetskopiere et arbeidsområde kopierer workspace.db, så et arbeidsområde som gis videre til noen andre, ikke bærer levende bottokens inni seg. Filen arver serververtens tillitsgrense i filsystemet — alt med lesetilgang til datakatalogen kan lese et bottoken, som er den samme grensen databasene ligger bak.

Én fil per leverandør, skrevet atomisk og kun for eieren: en tilbakekalt app slettes ved å fjerne én fil, og en ødelagt fil koster én leverandør i stedet for alle. Legitimasjon går inn via koblingsoperasjonen og kommer aldri ut igjen — statuslesinger svarer bare med tilkoblingsmetadata.

  • Strukturelle appinnstillinger. Transporten, scope-ene og hendelsesabonnementene tvinges gjennom, ikke eksponeres. En app som kan konfigureres til stillhet fra en innstillingsskjerm, er en supporthenvendelse, ikke en funksjon.
  • Samtale pluss skråstrekkommandoer. Slack-flaten er meldinger, trådsvar og skråstrekkommandoer.
  • En andre sannhetskilde. Chat er ikke et sted der arbeid lagres. Slack-tråden er et område, svaret er et transkript, saken er en sak — alle sammen i arbeidsområdet, tilgjengelige fra hver klient.