Tovább a tartalomhoz

Csevegőhidak

A csevegőhíd egy külső csevegőterméket — ma a Slacket — első osztályú belépési úttá tesz egy Control Center-munkaterületre. Ha megemlíti a botot, felébred egy ügynök; ha a Slack szálában válaszol, folytatódik a beszélgetés; ha beír egy slash-parancsot, létrejön egy jegy. Alatta ugyanazok a terek, átiratok, jegyek és ugyanaz a hozzárendelés, amelyeket az asztali alkalmazás használ. A csevegés felület a munkaterületen, nem annak másolata.

A Slack az egyetlen szállított szolgáltató. A termékben sehol sincs Discord-, Telegram-, Teams- vagy Matrix-implementáció. A funkció szándékosan szolgáltatófüggetlenül íródott — egy második termék hozzáadása egyetlen enumérték, egy adapter és egy plugin —, ma azonban a „csevegés” a Slacket jelenti.

Két felület fedi le, és ezek különböző emberekhez tartoznak. A munkaterület Slackhez csatlakoztatása és a bot testreszabása admin művelet a Beállítások → Munkaterület → Általános útvonalon. A saját csevegőfiók összekapcsolása a Control Center-identitással tagi művelet a Beállítások → Munkaterület → Profil és identitás útvonalon.

Három döntés alakítja az egész funkciót: melyik irányban fut a kapcsolat, mi jogosít fel egy kívülről érkező üzenetet, és hol élhet a termékspecifikus tudás.

A híd soha nem figyeli a szolgáltatót. A Slack Socket Mode-on keresztül érhető el: a szerver socket URL-t kér a Slacktől, és kifelé tárcsáz, majd ezen a kapcsolaton keresztül fogadja az említéseket, a közvetlen üzeneteket és a slash-parancsokat.

A következmény a lényeg. Egy NAT mögötti laptop, egy domain nélküli otthoni szerver, egy hálózatok között vándorló gép — mindegyik futtathatja a hidat bejövő port, alagút és TLS-tanúsítvány nélkül, és anélkül, hogy nyilvános URL-t kellene cserélni, amikor az megváltozik. Nincs biztosítandó webhookvégpont, mert nincs webhookvégpont. Amikor a Control Center összeállítja az alkalmazásdefiníciót, kikényszeríti a Socket Mode-ot, és eltávolít minden kérési URL-t, mert egy mindkettőre beállított alkalmazás félig működő alkalmazás lenne.

Az ára egy életben tartandó kapcsolat, ami a szerver dolga: az újracsatlakozás exponenciális visszakapcsolás (exponential backoff) jitterrel, legfeljebb 30 másodpercre korlátozva; a szolgáltató által kezdeményezett socket-rotáció újracsatlakozik, anélkül hogy fokozná ezt a visszakapcsolást; egy elutasított hitelesítő adat pedig végleges — egy visszavont token végtelen újrapróbálása csak sebességkorlátot hozna, ezért a híd leáll, és új tokenekre vár, miközben a szolgáltató saját hibáját szó szerint jelenti a beállításokban.

Egy csevegőalkalmazás nélküli munkaterület semmibe sem kerül: nincs socket, nincsenek időzítők, nincs forgalom. És az egész csak azután indul el, hogy a szerver jelzi a kész állapotot, mert egy lassan válaszoló csevegőalkalmazás soha nem késleltetheti az indítást.

A tagság jogosít fel, nem a token birtoklása

Szekció neve “A tagság jogosít fel, nem a token birtoklása”

Az alkalmazást telepítő személy nem azonos az üzenetet küldő személlyel. Ezért minden bejövő esemény Principalra oldódik fel, mielőtt bármi megtörténne, és minden esetben elutasítással zárul, magyarázattal együtt:

  • Egy nem összekapcsolt csevegőfiók abban a Slack szálban, amelybe írt, útmutatást kap az összekapcsolásra.
  • Egy összekapcsolt fiók, amelynek felhasználója nem tagja ennek a munkaterületnek, arra kap útmutatást, hogy kérjen meghívást egy admintól.
  • Egy tag, akinek a szerepköre csak olvasható (viewer, guest), megtudja, hogy a szerepköre nem indíthat munkát.

Ezek egyike sem folytatódik. Egy csevegőüzenetet soha nem rendelünk olyasvalakihez, aki nem küldte el — ez tartja korrekt módon az auditnaplót, a Principalonkénti sebességkorlátokat és a jegyek bejelentőit. A tagsági modellt, amelyre ez támaszkodik, lásd a Többszereplős mód és jelenlét oldalon.

Az összekapcsolás identitásfeloldás, nem hozzáférési jogosultság megadása. A tagság minden eseménynél újraellenőrzésre kerül, így egy olyan valakihez tartozó kapcsolat, akit azóta eltávolítottak, elutasítást eredményez a régi kiváltságai helyett. Két útvonal hoz létre kapcsolatot: egy rövid életű, egyszer használatos kód, amelyet a tag a csevegésben beír, vagy automatikus egyezés a szolgáltató ellenőrzött e-mail-címére — és az e-mailes útvonal továbbra is megköveteli, hogy az egyező felhasználó már tag legyen, így egy idegen, akinek a címe véletlenül létezik a szerveren, semmihez sem kapcsolódik.

A kódok szándékosan a memóriában maradnak, 15 perces élettartammal. Egy kód tartósítása egy tartós, hitelesítő adatra hasonlító sort hozna létre valami olyasmihez, aminek el kell párolognia, és az, hogy a szerver újraindítása minden függőben lévő kódot érvénytelenít, helyes viselkedés, nem visszaesés — a tag ismét az „A Slack fiókom összekapcsolása” gombra kattint.

Az elutasításra abban a Slack szálban érkezik válasz, amelyet az üzenet megnyitott, valódi válaszként. A szálban küldött válasz megjelöli azt az üzenetet, amelyre válaszol, eléri az egyetlen személyt, akinek szüksége van rá, és közben a „nincs összekapcsolva” üzenetet is távol tartja magától a csatornától. Az üzenetküldés ráadásul az az egyetlen képesség, amellyel minden szolgáltató rendelkezik, így a magyarázat soha nem függ olyan funkciótól, amelyet az alkalmazás esetleg nem hordoz.

Egy híd, amely mindkét irányba tükröz, hurkolódni fog, hacsak valami meg nem különbözteti a két irányt. Minden üzenet, amelyet a híd behoz, metadata['chat'] bélyegzőt kap — szolgáltató, csapat, beszélgetés, szál, üzenet és küldő. A kifelé irányuló tükrözés kihagy minden ilyen bélyegzőt viselő üzenetet.

Ez az egyetlen szabály teszi biztonságossá a kétirányú tükrözést: egy Control Centerben beírt emberi üzenet továbbítódik a csatolt szálba (az író nevével az élen), míg egy csevegésből érkező üzenet ott áll meg, ahol belépett.

Ami kifelé átmegy, szándékosan szűk. Az ügynök válasza közönséges csevegőszövegként továbbítódik. Egy rövid gondolat megjelenhet a kártya Thinking… sorában. Az eszközkimenet diffek falát alkotja, és a Control Centerben marad, ahová egy csonkolt válasz is mutat.

A kártya a munkáról számol be; a szöveg továbbra is az üzenet

Szekció neve “A kártya a munkáról számol be; a szöveg továbbra is az üzenet”

Ahol a szolgáltató megjeleníti őket, egy kör egyetlen feladatkártyát hordoz, amelyet a kör futásának teljes ideje alatt a helyén szerkesztenek, és amely az általa magyarázott válasz felett helyezkedik el.

A kártya egy csoportosított terv, nem összegyúrt bekezdés. A Slack ugyanahhoz a feladathoz fűzi a details és az output mezőt, ezért egy sor részletei egyszer mennek ki (a gondolat a Thinking… sorban), az output pedig soha nem streamelődik. A beállítás egyetlen sor, amelynek címe cserélődik (Working on it…, Cloning …). Az érvelés hozzáad egy Thinking… sort. Minden eszköz a saját sorát kapja (Read lib/main.dart). A válasz közönséges szöveg a kártya alatt — a Slack tervnézete egy későbbi markdown-hozzáfűzésből olvassa ki, nem a feladatok közé keveredett darabból. Semmi más: eszközkimenet nem, és az eszköz argumentumai közül is csak egy rövid engedélylistáról, mert egy ismeretlen eszköz argumentumai egy egész fájl törzsét is jelenthetik, egy sor pedig egyetlen sor.

A kártya a kör előtti várakozást is lefedi. Az első említés létrehozza a beszélgetést, annak létrehozása pedig minden repót klónoz, amelyben az ügynök dolgozni fog — ami egy valódi tárolónál perceket jelent, amikor az ügynöktől még senki sem kért semmit, a szál pedig úgy néz ki, mintha a bot figyelmen kívül hagyta volna a kérést. A kártya ezért abban a pillanatban nyílik meg, amikor az említést elfogadják (Working on it…), majd ez a beállítási sor ugyanazokkal a szavakkal frissül, amelyeket az asztali alkalmazás mutat — Cloning acme/widgets…, Setting up Reviewer…, Starting the agent… —, aztán egy Thinking… sor, amint az ügynök elkezd gondolkodni, majd soronként egy-egy eszköz. Egy kártya, egy üzenet, a „hallottalak” pillanattól a válaszig, amely alatta közönséges szövegként jelenik meg. A beállítás csak ott kerül jelentésre, ahol valaki a csevegésben vár rá: egy alkalmazásban éppen előkészített beszélgetés vagy egy szerverindításkor folytatott, elakadt beszélgetés kártya nélkül provisionálódik.

Azt az egyetlen dolgot hordozza, amelyet a csevegés nem adhat meg az olvasónak: egy View in Control Center hivatkozást a válasz mögötti beszélgetésre. A csevegőtermékek egy hivatkozásban http(s)-t fogadnak el, mást nem, ezért a gomb egy kis oldalra mutat, amelyet ez a szerver szolgál ki, és amely a böngészőt az asztali alkalmazás control-center:// hivatkozására irányítja. Az az oldal semmit nem olvas és semmit nem old fel — az URL-jében lévő azonosítók egy deep linkbe csengenek vissza, máshová nem —, és egy elérhető nyilvános URL nélküli szerver egyszerűen gomb nélküli kártyát állít elő, nem pedig egy hibásat.

A kártyák olyan képességek, mint bármelyik másik. Egy szolgáltató nélkülük pontosan úgy viselkedik, mint korábban, csak szöveggel, és egy szolgáltató, amely egy kártyát a hívás pillanatában elutasít, megtartja a streamjét: a válasz mindig megelőzi az őt leíró kártyát. Amikor a streamelés teljesen ki van kapcsolva, a kész kártya helyette az egyetlen elküldött üzenetre kerül rá. Egy létrehozott jegy ugyanezt a bánásmódot kapja — a /cc ticket visszaigazolása egy befejezett kártya, amelynek létezésének oka a nemrég létrehozott jegyre mutató hivatkozás.

A képességekről tárgyalunk, majd megjegyezzük őket

Szekció neve “A képességekről tárgyalunk, majd megjegyezzük őket”

A csevegőtermékek abban különböznek, hogy mit tudnak megjeleníteni, ezért a híd a leggazdagabb felületre íródott — egy válasz, amely élőben növekszik egy szálban, egy átmeneti „gondolkodik…” állapot, egy címmel ellátott szál, egy efemer válasz egy slash-parancsra —, és képességenként egyesével degradálódik. Egy szolgáltató, amelyből mindez hiányzik, akkor is működik; a válaszok egyszerűen egyetlen üzenetként érkeznek meg.

Egy képesség hirdetése nem ígéret annak meglétére. A Slack streamelő válaszaihoz egy fizetős csomag funkciója kell, és a Slack csak a hívás pillanatában utasít el. A véglegesként megjelölt elutasítást a kapcsolat teljes élettartamára megjegyezzük: a híd felhagy a kéréssel, és onnantól teljes válaszokat tesz közzé, a beállítások pedig ezt egyértelműen kiírják, mert a „leállt a válaszaim streamelése” kérdésre napló olvasása nélkül is válaszolni kell. A feladatkártyákat ugyanígy elhagyjuk, ha a Slack elutasít egyet.

Minden korlát a szolgáltatóhoz tartozik, nem a továbbítóhoz: az üzenetplafont, a streamdarabok plafonját és azt, hogy egy streamhez kell-e szálhorgony, mind az adapter deklarálja, és egy hosszú válasz bekezdés-, sor- vagy szóhatáron tördelődik, nem szó közben.

Minden termék alakú dolog egy port mögött él. Az általános mag ismeri a markdownt, a csevegőbeszélgetéseket, a csevegőszálakat és a tagokat; semmit nem tud a Slack borítékjairól, a mrkdwn-ről vagy a gateway-intentekről. Három darab működteti ezt:

  • Az adapter normalizál egy terméket: annak szállítási módját, API-hívásait, szövegkodekjét és borítékjainak sajátosságait. A Slackéi tanulságosak — egy csatornamegjelölés kétszer érkezik meg (mint app_mention és mint message), és a legtöbb üzenet-altípus díszítés, nem új kérés. Mindkettőt az adapter oldja fel, így a mag soha nem tudja meg, hogy a Slack kétszer kézbesít.
  • A deskriptor a szolgáltató nyilvános alakja: milyen hitelesítő adatokra van szüksége, milyen formátumúak azok, mely URL-ek segítenek a felhasználónak megtalálni őket, mire képes, és hogy az alkalmazása létrehozható vagy szerkeszthető-e innen. A beállítási kártya, a csatlakozási párbeszédablak és az argumentumok érvényesítése mind ebből generálódik — a kliens egy szolgáltatót küld vissza, valamint egy mezőazonosítóval kulcsolt leképezést.
  • A plugin a szerveroldali összeállítás: hogyan lesz a hitelesítő adatokból ellenőrzött kapcsolat, hogyan épül fel egy adapter, és hogyan kezeljük a szolgáltató oldali alkalmazást.

Így egy termék hozzáadása egyetlen enumérték, egy adapter és egy plugin. Nincs új RPC-művelet, nincs sémaváltozás, nincs kliensváltozás — és a kliens korrekt marad, mert egy általa soha nem hallott mező is a szerver által szolgáltatott címkével jelenik meg.

Két részlet akadályozza meg, hogy az absztrakció felfelé szivárogjon. Azokat az alkalmazáskezelési lépéseket, amelyekhez a szolgáltatónak nincs API-ja (token generálása, az alkalmazás telepítése, újratelepítés jogosultságváltozás után), saját hivatkozásokkal rendelkező, elnevezett lépésekként adjuk vissza, így a beállítás befejezése képernyő anélkül jeleníti meg őket, hogy tudná, melyik szolgáltatóról van szó. A híd által írt minden utasítás megnevezi a szolgáltatót és az alkalmazás által ténylegesen használt parancsot, így egy átnevezett parancs vagy egy második szolgáltató is helyesen olvasható új szöveg hozzáadása nélkül.

A munkaterület csevegési tokenjei a munkaterület saját könyvtárában, az adatbázisa mellett találhatók — egyszerű JSON-ként itt: <dataDir>/<workspaceId>/chat_credentials/<provider>.json, nem az adatbázisban, és szándékosan nem az operációs rendszer kulcstárában. Két következmény könnyen elvész később: egy munkaterület törlése leválasztja a könyvtárat, és a tokenek vele mennek; egy munkaterület exportálása vagy mentése pedig átmásolja a workspace.db-t, így egy másnak átadott munkaterület nem hordoz magában élő bottokeneket. A fájl a szervert futtató hoszt fájlrendszerének bizalmi határát örökli — bármi, aminek olvasási hozzáférése van az adatkönyvtárhoz, olvashat egy bottokent, és ugyanezen határ mögött vannak az adatbázisok is.

Szolgáltatónként egy fájl, atomikusan és csak a tulajdonos számára olvashatóan írva: egy visszavont alkalmazás egyetlen fájl leválasztásával törlődik, egy sérült fájl pedig egyetlen szolgáltatóba kerül, nem az összesbe. A hitelesítő adatok a csatlakozási műveleten keresztül bemennek, és soha nem jönnek ki — az állapotlekérdezések csak kapcsolati metaadatokat adnak vissza.

  • Szerkezeti alkalmazásbeállítások. A szállítást, a hatóköröket és az eseményfeliratkozásokat nem tesszük közzé, hanem kikényszerítjük. Egy alkalmazás, amelyet egy beállítási képernyőről el lehet hallgattatni, támogatási jegy, nem funkció.
  • Beszélgetés és slash-parancsok. A Slack-felület üzenetekből, szálválaszokból és slash-parancsokból áll.
  • Egy második igazságforrás. A csevegés nem hely, ahol a munkát tárolják. A Slack szál egy tér, a válasz egy átirat, a jegy egy jegy — mindegyik a munkaterületen, minden klienstől elérhetően.