67. Spolupráce v meshi jsou ověřené odkazy mezi runtime a jeden replikovaný deník skládaný všude stejně
Kontext
ADR 0050 dalo mesh objevování: podepsané inzeráty, jeden registr, důvěru jako štítek. Záměrně tam skončilo — „whoever later builds remote operations must bring their own authorization decision“ — a stejně tak kód. Změřeno 2026-09-15, mesh nemohl podporovat produkt, jako který byl prezentován:
- Nic kooperativního nepřekročilo stroj. Nároky žily na nástěnce peerů jednoho serveru (ADR 0044 shromažďuje nástěnky checkoutů jednoho stroje přes jejich soubory lease); handovery byly soubory pod
.ai/local/jednoho checkoutu; žádná událost, relace ani review nikam necestovaly. - Stejný repozitář na dvou strojích byly dva repozitáře. Inzeráty nesly
sha256(git common directory path)a cesta checkoutu na macOS se nikdy neshodovala s cestou checkoutu téhož klonu na Linuxu. - Dva worktrees jednoho stroje se navzájem neviděly. Oba servery drží stejný klíč uzlu; manažer přeskakoval datagramy toho druhého jako „self-heard“ a jeho multicast socket se nemohl navázat na port, který držel první.
- Přítomnost byla zahlédnutí, ne vztah. Uzel byl šedesát sekund po oznámení „přítomný“, ať už s ním bylo možné cokoli vyměnit, nebo ne.
Rozhodnutí
Spolupráce je vrstva nad objevováním uvnitř apps/majordomus-cli/src/mesh/ s těmito nosnými pravidly.
- Identity jsou typované a odlišné. Node je klíč stroje (ADR 0050). Runtime je server jednoho checkoutu na tom nodu:
runtime = digest(checkout id), takže dva worktrees jsou dva runtimes a restartovaný server je tentýž runtime. Instance je jeden běh procesu. Stream jenode-runtime-instance: jednotka, která zapisuje události. Relace je pojmenována uvnitř svého streamu. Discovery protocol přechází na verzi 2, aby nesl runtime; čtečka verze 2 čte verzi 1, čtečka verze 1 odmítne verzi 2 jakoversion. - Identita repozitáře je obsah, ne cesta.
digest(root commits), nebo deklarovanýcooperation.repository(mělký klon musí jeden deklarovat; fork, který nemá s originálem spolupracovat, smí). Runtimy různých identit repozitáře se nikdy nespojí. - Spojení se připouští handshake, nikdy objevením. Hello → welcome, každé zpráva podepsaná Ed25519 pod vlastní doménou, s kontrolou velikosti, tvaru, podpisu, čerstvosti, opakování nonce, rozsahu protokolu (link protocol 1..1), sebe sama, identity repozitáře a důvěry — v obou směrech — s typovaným, počítaným a vypsaným odmítnutím pro každou. Kandidáti pocházejí z důvěryhodných záznamů registru téhož repozitáře a z deklarovaných seedů.
- Transport je adaptér.
LinkTransport::post(endpoint, path, message); sdílený server odpovídá HTTP přes stávající minimální klient crate (ADR 0032: žádná závislost na HTTP klientu, žádné TLS, žádné async). Unit testy umísťují runtimes za transport v procesu, který běží stejným protokolem; integrační testy a lab používají sockety. - Všechno kooperativní je událostí jednoho deníku. Relace otevřena/uzavřena, nárok získán/uvolněn, předávka publikována/převzata, revize vyžádána/zodpovězena — každá podepsaná svým původem, identifikovaná
(stream, seq), opatřená Lamportovými hodinami a uložená do.ai/local/state/mesh/journal.jsonl. Relay předává bajty a nemůže padělat. Událost neznámého druhu se uloží a přepošle, ale neinterpretuje, takže novější peer nikdy nerozbije replikaci staršího. - Replikace jsou značky, ne zaplavení. Kolo synchronizace (každý heartbeat, podepsané, pod id spojení s rostoucím čítačem) nese značku nejvyššího dosaženého bodu odesílatele na proud a události, které příjemci chybí; odpověď nese totéž opačným směrem. Doručení je alespoň-jednou a aplikace idempotentní; v rámci proudu se události aplikují v pořadí sekvence (předčasný příchod čeká, omezeně, na svou mezeru); napříč proudy řadí skládání podle
(lamport, stream, seq). První synchronizace po handshaku je plná synchronizace stavu stejným mechanismem: nováček dostane, co jeho značky postrádají, což je po kompaktaci živý stav, ne nekonečná historie. - Živost je tep posuzovaný na lokálních hodinách. Každý runtime zvedá vlastní tep každý heartbeat; značky přenášejí tepy s údajem, jak dávno je odesílatel viděl stoupnout. Stream, jehož tep nestoupl do vypršení, je na tomto runtime expirovaný: jeho relace jsou expirované a jeho nároky přestávají vylučovat. Nesrovnávají se hodiny dvou strojů. Spoje mají vlastní stav (connecting, connected, degraded, unreachable, expired) s exponenciálním backoffem omezeným vypršením a restartovaný peer se znovu handshakuje do téhož řádku tabulky.
- Stav je jeden čistý fold.
mesh::state::fold(events, liveness)dává relace, tvrzení, překryvy, předávky, revize a digest. Tvrzení platí, dokud není uvolněno, jeho relace je otevřená a jeho stream živý. Dvě živá výlučná tvrzení různých relací, jejichž cesty se setkávají (peers::claims_meet, vlastní predikát nástěnky), jsou konflikt, který vyhrává nejnižší(lamport, stream, seq); příjem odmítne tvrzení, které by kolidovalo s tím, co tento runtime už ví. Při rozdělení sítě mohou nárokovat obě strany; po opětovném spojení každý runtime jmenuje stejného vítěze. Nikdy „vyhrává poslední paket“. - Nástěnka se promítá, nenahrazuje se. Při každém heartbeatu se připojené relace nástěnky stanou relacemi meshe a jejich oznámení poradními tvrzeními; lokální sběr podle ADR 0044 zůstává pohledem v rámci stroje.
- Každý povrch je schopnost.
mesh.cooperation,mesh.peers,mesh.peer,mesh.state,mesh.events,mesh.verifya příkazy (relace, claimy, předávky, review, dvě trasy odkazů) jsou deklarace modulu meshe; CLI, HTTP, OpenAPI, MCP a Cockpit jsou jejich projekce.scripts/ci/mesh-checkodmítá obslužnou funkci odkazu, transport nebo žurnál mimo subsystém a stránku Cockpitu, která čte runtime přímo.
Zamítnuté alternativy
- Roznášet nástěnku peerů. Nástěnka je poziční (
p1se znovu používá), v paměti a nepodepsaná; její replikace by replikovala její nejednoznačnosti. Žurnál je podepsaný a typovaný; nástěnka se do něj promítá. - Konsenzuální protokol (Raft) pro tvrzení. Přinesl by silnou konzistenci s požadavkem většiny, který flotila dvou notebooků nesplní, a asynchronní runtime, který ADR 0032 odmítá. Deterministické řešení konfliktů s pojmenovanými konflikty sedí relacím, které musí fungovat offline.
- Centrální koordinátor. Jediný bod selhání a kanonické úložiště jinde — odmítnuto ze stejných důvodů, z jakých ADR 0050 odmítlo povinný rendezvous.
- Vypršení lease podle hodin reálného času. Hodiny strojů se neshodují; předávané pulzy stárnoucí na lokálních monotónních hodinách nepotřebují, aby se shodovaly.
- Spoje mTLS. Zásobník TLS je závislost, kterou ADR 0032 drží venku; podepsané zprávy dávají autenticitu a důvěrnost je záležitostí soukromé sítě nebo overlaye.
Důsledky
- Mesh nyní nese metadata o práci mezi důvěryhodnými runtime jednoho repozitáře. Postoj
SECURITY.mdplatí ve výchozím stavu — nic se neotevře bez povolené deklarace a link potřebuje důvěryhodný klíč — ale povolený mesh s důvěryhodnými peery posílá záměry relací, rozsahy nároků, předměty review a těla publikovaných předávek těmto peerům, nešifrované, pokud síť nešifruje.docs/MESH.mdto říká. - „a trusted node gains nothing“ z ADR 0050 je upřesněno, ne zrušeno: události propojeného peera mohou odmítnout lokální exkluzivní nárok a mohou nabídnout předávku, kterou lokální příkaz smí zapsat do tohoto checkoutu. Nic, co peer pošle, nic nespouští.
- Discovery protocol 2 nemůže číst executable starší než tato změna: smíšená flotila se upgraduje společně a odmítnutí se započítá jako
version, ne tiše. - Soubor žurnálu pro každý checkout existuje pod
.ai/local/state/mesh/. Je omezen kompakcí dávno vypršelých streamů (předávky se zachovají) a nikdy se necommituje. - Přijetí těchto záruk je automatizováno na třech úrovních: unit a property testy, vícepřocesové integrační testy přes TCP (
tests/mesh_cooperation.rs) a kontejnerová laboratoř na skutečném mostu, která partitionuje a zabíjí uzly (test/mesh-lab/run, job CImesh-lab).
Dodatek, 2026-09-29: komprimace uchovává, co je živé
Důsledek výše říkal, že žurnál je omezen komprimací dávno vypršených streamů a že předávky se ponechávají. Tři vady zaznamenané na PR #588 ukázaly, co to vynechalo, a odpovědi jsou zaznamenány zde, ne v novém ADR, protože nesou totéž rozhodnutí dál: komprimace nesmí změnit, co fold říká o čemkoli živém.
- Předávka drží svůj stream omezenou dobu. Mrtvý stream držící předávku, kterou nikdo nevzal, zůstává nejvýše den po retenci (
HANDOVER_RETENTION); předávka, kterou někdo vzal, nedrží nic. Držena navždy, stroj, jehož běhy každý něco předaly, zaplnil svou kvótu 64 streamů mrtvými a každý další běh byl odmítnut. - Převzetí nebo odpověď zůstává s tím, k čemu se vztahuje. Mrtvý stream držící převzetí předávky nebo odpověď na revizi zůstává, dokud zůstává stream samotné předávky či revize, takže stále nabízená předávka se nikdy nejeví jako nepřevzatá a zodpovězená revize se nikdy znovu neotevře. Kdo převzal a kdo odpověděl, čte samotný fold.
- Zhuštěný stream, který znovu pulzuje, je slyšet znovu, celý. Nový puls, ověřený a vyšší než ten, který drží jeho náhrobek, náhrobek zruší a stream se načte znovu od první události, nikoli od značky náhrobku: to, co řekl před zhuštěním — stále platné tvrzení — je s ním opět živé, a pouhý zbytek by připustil tvrzení v rozporu.
docs/MESH.md („Compaction“) uvádí sémantiku, včetně toho, proč dva runtime mohou vypisovat různé digesty, zatímco každý kompaktuje podle svého rozvrhu; testy jednotek a vlastností žurnálu, tests/mesh_compaction.rs a test/cases/493_mesh_compaction_keeps_what_is_live.sh, ji drží.
Dodatek, 2026-10-15: klíč uzlu se rotuje následnictvím, které podepíše jeho předchůdce (I2705)
I2134 způsobilo, že klíč stažený z trust.allow ztratí své odkazy bez restartu (Cooperation::retrust, voláno z živého načtení). Zanechalo dvě věci nedořešené: klíč nelze nahradit, jen odříznout (hrozba T11), a události staženého klíče zůstávají v běžícím žurnálu, dokud načtení při příštím restartu neuplatní platnou důvěru (I2124). Obojí vyplývá z pravidel tohoto záznamu a rozhoduje se zde, ne v novém ADR. Odpověď formují dva fakty: klíč uzlu je celostrojový (node.json pod $XDG_STATE_HOME/majordomus nebo ~/.local/state/majordomus, sdílený každým worktree a repozitářem na stroji) a id uzlu je digest klíče, takže nový klíč je nový uzel.
Rotace je výrok o nástupnictví. {schema: majordomus-mesh-succession/v1, old key, new key, at}, podepsaný pod vlastní doménou starým klíčem a spolupodepsaný novým: starý podpis říká, že držitel důvěryhodného klíče předává, nový, že držitel nástupce existuje a souhlasil. Výrok nejmenuje žádný repozitář, protože klíč, jehož je nástupcem, patří stroji. Rotace zapíše nový node.json jako soukromý od prvního bajtu, ponechá výrok vedle něj (jen veřejné poloviny) a zničí staré tajemství.
Cestuje jako závěrečná událost každého proudu se starým klíčem. Každý runtime běžícího stroje drží starý klíč v paměti; zapíše key_succeeded nesoucí prohlášení jako poslední událost svého proudu, podepsanou starým klíčem, a pokračuje pod novým klíčem na novém proudu, přesně jako po restartu: relace a claimy se znovu převezmou pod novým proudem a ty starého proudu vyprší s jeho beatem. O claimech se nerozhoduje nic nového. Událost se replikuje jako každá jiná, takže každý runtime každého repozitáře, ve kterém stroj spolupracuje, se o nástupnictví dozví, včetně runtimů, s nimiž se nikdy přímo nepropojil.
Peer následuje posloupnost, a to jen skrze platnou důvěru. Každý runtime skládá posloupnosti, které drží, do své důvěry. Nástupce je důvěryhodný, dokud je jeho předchůdce důvěryhodný podle platné deklarace, a jen tehdy. Stažení ukradeného klíče z trust.allow trunku proto stahuje i každého nástupce, kterého zloděj jmenoval, a rotaci nelze použít k úniku před odvoláním. Hloubka je jedna: klíč důvěryhodný jen skrze posloupnost nemůže jmenovat nástupce, dokud ho trunk neuvede. Dva různí nástupci jednoho klíče jsou fork. Fork nedůvěřuje žádnému a starý klíč zůstává odmítnut, protože ho může vyrobit jen ukradený klíč, a mesh doctor ho jmenuje.
Nahrazený klíč je vyřazen, ne odvolán. Od okamžiku, kdy runtime složí nástupnictví, je hello starého klíče odmítnuto untrusted s detailem jmenujícím nástupce. Kód odmítnutí zůstává untrusted, protože starší peer by nový kód četl jako malformed. Stream starého klíče po své závěrečné události nic nepřijímá a stream starého klíče, který runtime dříve neviděl, je odmítnut. Streamy, které jsou stále otevřené bez závěrečné události, se přijímají po dobu jedné expirace a po ní se odmítají. Historie vyřazeného klíče je legitimní a uchovává se.
Události staženého klíče opustí běžící žurnál při retrustu, který ho stáhl. Klíč, kterému se už nedůvěřuje a který nemá důvěryhodného nástupce, se považuje za kompromitovaný. Při tomto retrustu běžící žurnál zahodí jeho události predikátem, který používá opětovné načtení (Journal::open_trusting), spočítá je podle klíče ve čtení spolupráce a označí jeho streamy náhrobkem, aby je peeři neposílali znovu. Takový náhrobek zvedne jen beat od důvěryhodného klíče. Prohlášení o posloupnosti klíče zůstávají, neúčinná, jako audit toho, co řekl. Soubor žurnálu se přepíše při příštím zhuštění jako dnes a příští opětovné načtení už ty řádky odmítá.
Kořenem důvěry zůstává trust.allow trunku. Posloupnost překlene čas, než člověk commitne náhradu. mesh rotate vypíše změnu k commitu na výchozí větev (nový klíč dovnitř, starý ven) a mesh doctor varuje, dokud je klíč důvěryhodný jen přes posloupnost. Jakmile commit přistane, nový klíč je důvěryhodný přímo a starý je vyřazen, ne vymazán, protože má důvěryhodného nástupce. Pracovní strom stále nemůže rozšířit důvěru trunku: důvěra nástupce pochází z prohlášení a z výpisu předchůdce v trunku a kopie deklarace v pracovním stromu nehraje žádnou roli.
Zamítnuto: důvěřovat nástupci jen na základě prohlášení (ukradený klíč uniká odvolání); rotovat jen commitem nového trust.allow (každý peer odmítá stroj, dokud commit nepřistane všude, což je právě výpadek, kterému má rotace předejít); nový kód odmítnutí (viz výše); a purge historie vyřazeného klíče (řádná rotace by zahodila nespotřebované předávky a auditní stopu). Poradci, které plán uvažování vybral, byli v době tohoto rozhodnutí oba rate-limited. Rozhodnutí stojí na lokálních důkazech zaznamenaných u úlohy.
Práce jsou pět slicí o nejvýše hodině, každý jeden pull request, v tomto pořadí:
- Výrok a důvěra, kterou uděluje (
mesh/identity.rs,mesh/trust.rs; žádný sdílený soubor, takže může začít hned, vedle otevřených #1179, #1188, #1197, #1207 a #1208).NodeIdentity::rotatevrací podepsanéSuccessiona zapisuje nový klíč pomocíwrite_private.trust::evaluatepřijímá nástupnictví a odpovídáTrusted("succession"),Retired(successor)neboRejected("succession fork"). Testy:mesh::identitya_rotation_is_signed_by_the_old_key_and_countersigned_by_the_newamesh::trusta_successor_is_trusted_only_while_its_predecessor_isaa_fork_trusts_neither. - Běžící žurnál zahodí stažený klíč (
mesh/journal.rsJournal::retain_trusted, náhrobek, který zvedne jen důvěryhodný puls;mesh/cooperation.rsretrustho volá a hlásípurged). Tento řez následuje po přistání #1179 a #1207, protože oba se dotýkajíjournal.rs. Test:mesh::cooperationa_withdrawn_keys_events_leave_the_running_journal_without_a_restart, plus jeho dvojčetests/mesh_cooperation.rspřes TCP. Uzavírá druhé akceptační kritérium I2705. - Posloupnost se sleduje (
mesh/journal.rsEventBody::KeySucceeded, uvedeno v.ai/repo/ci/mesh-event-fields.yaml;mesh/cooperation.rsskládá posloupnosti do důvěry, při ingestu důvěřuje znovu, odmítá hello a nové streamy vyřazeného klíče, starý stream uzavírá jeho závěrečnou událostí a vyřazený klíč vyjímá z purge ve slicu 2). Tento slice jde po #1188 (seznam polí událostí), #1197 (pořadí kontroly hello) a slicech 1 a 2. Test:mesh::cooperationa_peer_that_folds_a_succession_admits_the_new_key_and_refuses_the_old. - Stroj rotuje (
capability/builtin/mesh.rsdeklarujemesh.rotatea následujemajordomus generate;Cooperation::rotatezapíše uzavírací událost a přejde na nový stream;live.rspřepne každý další server stroje, když se změnínode.json). Tato část jde po části 3 a #1208 (manager.rs). Test:tests/mesh_cooperation.rsa_rotated_key_is_followed_and_the_old_key_is_refused, dva procesy přes TCP. Jeho výsledek je důkazkey_rotation_testz I2705. - Peer, který událost zmeškal, doctor a slova (
mesh/link.rsamesh/cooperation.rs: hello nového klíče může nést prohlášení;mesh/doctor.rsvaruje před důvěrou drženou jen přes posloupnost a před forkem;docs/MESH.mdzískává „Rotating a node key“). Tento kus jde poslední. Testy:mesh::cooperationa_hello_carrying_a_succession_is_admitted_by_a_peer_that_missed_the_eventa unit testmesh::doctorpro varování.