| 1 | # Uitgaande follows van een ward — ontwerp
|
|---|
| 2 |
|
|---|
| 3 | Bijbehorende beads: **shaer-p729** (bouwen) en **shaer-yeo5** (de spec-vraag:
|
|---|
| 4 | informeren of gaten?). Dit document beantwoordt yeo5 en beschrijft wat p729
|
|---|
| 5 | inhoudt.
|
|---|
| 6 |
|
|---|
| 7 | FEP-633c §5.3 houdt follows *naar* een ward tegen tot de guardians beslissen.
|
|---|
| 8 | Follows *van* een ward gaan ongehinderd de deur uit: `ingestOutboxActivity`
|
|---|
| 9 | roept bij `case 'Follow'` meteen `followActor()` aan, zonder ward-check en
|
|---|
| 10 | zonder wachtrij.
|
|---|
| 11 |
|
|---|
| 12 | De guardians blijven niet in het duister. Sinds 1a2f206 krijgt elke guardian een
|
|---|
| 13 | directe note zodra zijn ward iemand gaat volgen. Maar dat is Robins constatering
|
|---|
| 14 | van 31-7 in één zin: **dat is informeren, geen gate.** De deur is al open op het
|
|---|
| 15 | moment dat het bericht aankomt, en een guardian die te laat kijkt kan alleen nog
|
|---|
| 16 | achteraf iets vinden van iets dat al gebeurd is.
|
|---|
| 17 |
|
|---|
| 18 | De asymmetrie is half te verdedigen. `Block` (Shaers "Orbit") is uitgaand ook
|
|---|
| 19 | ongated, en dat is de veilige richting: een kind dat zijn eigen wereld kleiner
|
|---|
| 20 | maakt heeft geen toestemming nodig. Volgen is de richting die hem opent.
|
|---|
| 21 |
|
|---|
| 22 | ## De regel
|
|---|
| 23 |
|
|---|
| 24 | **Per geval goedkeuring, met automatische goedkeuring bij wederkerigheid.**
|
|---|
| 25 |
|
|---|
| 26 | Volgt de ward iemand die de ward al volgt, dan hoeft er niemand meer naar te
|
|---|
| 27 | kijken. Die actor is namelijk al door de inkomende poort gekomen, en dat betekent
|
|---|
| 28 | dat een guardian er al ja tegen heeft gezegd. Nog een keer vragen is dezelfde
|
|---|
| 29 | vraag twee keer stellen, en elke overbodige vraag is er een die de volgende keer
|
|---|
| 30 | minder aandacht krijgt.
|
|---|
| 31 |
|
|---|
| 32 | Het predicaat is `target_uri ∈ ap_followers(slug)`. Kort, maar het klopt alleen
|
|---|
| 33 | zolang lidmaatschap van die tabel écht een guardian-besluit impliceert — zie de
|
|---|
| 34 | eerste open vraag.
|
|---|
| 35 |
|
|---|
| 36 | ## Beslissingen
|
|---|
| 37 |
|
|---|
| 38 | - **Een eigen tabel, niet `ap_pending_follows`.** Die tabel is gesleuteld op
|
|---|
| 39 | `(ward_slug, follower_uri)`: de ward is daar het *doel*. Uitgaand draait dat
|
|---|
| 40 | om. Een `direction`-kolom erbij zou elke bestaande query dubbelzinnig maken,
|
|---|
| 41 | inclusief `listForWard`, die nu simpelweg "wie wil mij volgen" betekent. Dus
|
|---|
| 42 | `ap_pending_outgoing_follows (id, ward_slug, target_uri, target_inbox,
|
|---|
| 43 | target_name, target_handle, target_icon, quorum, status, created_at)` met
|
|---|
| 44 | dezelfde vorm en dezelfde `decide()`-semantiek, maar apart.
|
|---|
| 45 |
|
|---|
| 46 | - **`ap_following.status` is al bezet en betekent iets anders.** Daar staat
|
|---|
| 47 | `pending` voor "wij hebben de Follow verstuurd, hun Accept moet nog komen".
|
|---|
| 48 | Een guardian-pending follow is nog helemaal niet verstuurd. Twee verschillende
|
|---|
| 49 | wachttoestanden op één kolom is precies het soort dubbelzinnigheid dat later
|
|---|
| 50 | een bug wordt. Daarom: **de `ap_following`-rij ontstaat pas bij goedkeuring**,
|
|---|
| 51 | op het moment dat de Follow daadwerkelijk uitgaat. Vóór die tijd bestaat het
|
|---|
| 52 | verzoek alleen in de nieuwe tabel.
|
|---|
| 53 |
|
|---|
| 54 | - **Wederkerigheid is een momentopname.** Getoetst bij het verzoek, niet
|
|---|
| 55 | doorlopend bewaakt. Ontvolgt de ander later, dan wordt een al goedgekeurde
|
|---|
| 56 | follow niet met terugwerkende kracht ingetrokken. Anders krijg je een relatie
|
|---|
| 57 | die stilletjes verdwijnt door een actie van een derde.
|
|---|
| 58 |
|
|---|
| 59 | - **Quorum: `any` hergebruiken, maar het eindelijk ergens vastleggen.** De kolom
|
|---|
| 60 | `quorum` bestaat op `ap_pending_follows`, maar de aanroep in
|
|---|
| 61 | `ActivityPubService.js` geeft hem nooit mee. Alles valt dus terug op de default
|
|---|
| 62 | `'any'`, en `'all'` en `'none'` zijn in de praktijk dood. Een uitgaand beleid
|
|---|
| 63 | heeft een echte plek nodig — per ward, niet per verzoek — en dat is het moment
|
|---|
| 64 | om de inkomende kant dezelfde plek te laten gebruiken.
|
|---|
| 65 |
|
|---|
| 66 | - **De wachtrij splitsen.** `shaer:queues.follows` staat al op het actor-document.
|
|---|
| 67 | Als beide richtingen daarin landen, kan een guardian "iemand wil Mee volgen"
|
|---|
| 68 | niet onderscheiden van "Mee wil iemand volgen" — twee vragen die in de
|
|---|
| 69 | interface verschillende woorden verdienen. Dus `follows` blijft inkomend
|
|---|
| 70 | (compatibel) en er komt `shaer:queues.outgoingFollows` naast.
|
|---|
| 71 |
|
|---|
| 72 | Let op: nieuwe termen moeten in `AP_CONTEXT` of `test/activitypub-as2.test.js`
|
|---|
| 73 | valt om. Die test eist dat elke uitgestuurde sleutel AS2-core is of in de
|
|---|
| 74 | federatie-context staat, en dat is precies de bedoeling ervan.
|
|---|
| 75 |
|
|---|
| 76 | - **Cross-instance: spiegel het `Offer(Follow)`-patroon.** Een lokale guardian
|
|---|
| 77 | leest `/guardian` rechtstreeks; een guardian op een andere server krijgt een
|
|---|
| 78 | Offer afgeleverd zodat zijn instance een kopie opslaat, net als bij de
|
|---|
| 79 | adoptie-offer en bij `ap_follow_reviews`. Voor `mee` (ward op `loop`) en
|
|---|
| 80 | `boiert` (guardian op `boiert`) zijn dat twee instances op dezelfde machine,
|
|---|
| 81 | dus die weg wordt meteen echt gelopen en niet gesimuleerd.
|
|---|
| 82 |
|
|---|
| 83 | - **Een tegengehouden follow mag er niet uitzien als een gelukte.** Dit is
|
|---|
| 84 | dezelfde les als in de bestaande `case 'Follow'`: *"De error REACHT de app
|
|---|
| 85 | (Robins melding, 31-7): het wegslikken maakte een mislukte follow precies
|
|---|
| 86 | gelijk aan een gelukte."* Een verzoek in de wacht is een derde uitkomst en de
|
|---|
| 87 | app moet die kunnen tonen. Voorstel: `202` met een expliciete status in de body
|
|---|
| 88 | (`{ ok: true, status: 'awaiting_guardian', id }`), zodat Shaer "wacht op
|
|---|
| 89 | toestemming" kan laten zien in plaats van een tegel die er al volgend uitziet.
|
|---|
| 90 |
|
|---|
| 91 | ## Status
|
|---|
| 92 |
|
|---|
| 93 | Gebouwd op 3-8-2026: `ap_pending_outgoing_follows` + `outgoing.js`, de poort in
|
|---|
| 94 | `ingestOutboxActivity` (`case 'Follow'`), de `outgoingFollows`-wachtrij, en het
|
|---|
| 95 | antwoord van de guardian op `POST /api/outgoing-follow/:id`. Negen tests in
|
|---|
| 96 | `test/outgoing-follow-gate.test.js`.
|
|---|
| 97 |
|
|---|
| 98 | ## Open vragen
|
|---|
| 99 |
|
|---|
| 100 | 1. **Beantwoord (Bart, 3-8): bestaande followers worden gegrandfatherd.**
|
|---|
| 101 | `ap_followers` heeft nu `gate_approved`, gezet zodra de inkomende poort
|
|---|
| 102 | iemand toelaat. Iedereen die al volgde op het moment dat de kolom erbij kwam,
|
|---|
| 103 | krijgt de markering eenmalig mee: de regel is exact vanaf dat moment, in
|
|---|
| 104 | plaats van met terugwerkende kracht wantrouwig tegen relaties die er al
|
|---|
| 105 | waren. Wie daarna binnenkomt zonder poort — de followers van een vrije actor
|
|---|
| 106 | die later ward wordt — telt niet mee voor de wederkerigheid.
|
|---|
| 107 |
|
|---|
| 108 | 2. **Nog open. Mag de ward zijn eigen verzoek intrekken** zolang het in de wacht
|
|---|
| 109 | staat? `outgoing.withdraw()` bestaat al, maar er is nog geen route en geen
|
|---|
| 110 | knop. Lijkt vanzelfsprekend ja, maar het is een Undo op iets dat nooit
|
|---|
| 111 | verstuurd is.
|
|---|
| 112 |
|
|---|
| 113 | 3. **Beantwoord: de guardian zelf als doel wacht niet.** Dezelfde uitzondering
|
|---|
| 114 | als inkomend, waar de Follow van een vastgelegde guardian de poort overslaat.
|
|---|
| 115 | Gebouwd en getest.
|
|---|
| 116 |
|
|---|
| 117 | 4. **Nog open. Interactie met Block/Orbit.** Blokkeert de ward iemand terwijl er
|
|---|
| 118 | nog een verzoek voor die persoon open staat, dan moet dat verzoek verdwijnen.
|
|---|
| 119 | `withdraw()` is er klaar voor; het wordt alleen nog nergens aangeroepen.
|
|---|
| 120 |
|
|---|
| 121 | 5. **Nog open. Emancipatie.** Wat gebeurt er met openstaande verzoeken als de
|
|---|
| 122 | guardianship eindigt? Automatisch goedkeuren of laten vervallen.
|
|---|
| 123 |
|
|---|
| 124 | 6. **Nieuw, uit het bouwen. Er zit geen venster op een verzoek.** Een uitgaande
|
|---|
| 125 | follow die niemand beantwoordt blijft staan tot iemand hem beantwoordt —
|
|---|
| 126 | dezelfde omissie die de handshake had voordat er een week op kwam (§3.5). Een
|
|---|
| 127 | kind dat vraagt of het iemand mag volgen en nooit antwoord krijgt, verdient
|
|---|
| 128 | een afloop.
|
|---|
| 129 |
|
|---|
| 130 | ## Raakvlakken met andere beads
|
|---|
| 131 |
|
|---|
| 132 | - **shaer-3kp** (gated features, guardian-overeengekomen instellingen) noemt
|
|---|
| 133 | "following approve" al met zoveel woorden. Dat is de plek waar het beleid
|
|---|
| 134 | hoort te wonen: één instellingen-object op de ward met de server als bron van
|
|---|
| 135 | waarheid. Dit ontwerp moet daar een veld in zijn, geen eigen mechaniek ernaast.
|
|---|
| 136 | - **shaer-h6u** (lokale guardian via de lijn i.p.v. de gedeelde database) raakt
|
|---|
| 137 | de bezorgweg die hier ook gebruikt wordt.
|
|---|
| 138 | - **shaer-zjt** bouwt het guardian-dashboard mét quorumteller; de nieuwe
|
|---|
| 139 | wachtrij moet daar meteen in passen.
|
|---|
| 140 |
|
|---|
| 141 | ### De quorum-kolom is nooit aangesloten
|
|---|
| 142 |
|
|---|
| 143 | Los van dit ontwerp, en niet in een bead gevonden: `ap_pending_follows.quorum`
|
|---|
| 144 | wordt door de aanroep in `ActivityPubService.js` nooit meegegeven, dus alles
|
|---|
| 145 | valt terug op de default `'any'`. `'all'` bestaat alleen in de vergelijking in
|
|---|
| 146 | `decide()` en wordt nergens geschreven; `'none'` heeft helemaal geen tak en zou
|
|---|
| 147 | zich als `'any'` gedragen in plaats van als "open". shaer-hxg is gesloten met
|
|---|
| 148 | "quorum any/all" als opgeleverd, en `test/follow-gating.test.js` slaagt omdat de
|
|---|
| 149 | test de waarde zelf meegeeft — precies het pad dat productie nooit neemt.
|
|---|
| 150 |
|
|---|
| 151 | Gevolg: een ward met drie guardians gaat vandaag open op één goedkeuring, ook
|
|---|
| 152 | als iemand dacht `all` te hebben ingesteld. Verdient een eigen bead, en de
|
|---|
| 153 | oplossing hangt samen met shaer-3kp: als het beleid daar komt te wonen, is dat
|
|---|
| 154 | meteen de plek waar de inkomende kant zijn quorum vandaan haalt.
|
|---|
| 155 |
|
|---|
| 156 | ## Daemon-pariteit
|
|---|
| 157 |
|
|---|
| 158 | De daemon-README is er stellig over: de apps moeten zich tegen beide backends
|
|---|
| 159 | hetzelfde gedragen, en als daemon en Klonkt uit elkaar lopen is de UI die je
|
|---|
| 160 | lokaal test een leugen. Wat hier landt, landt dus ook in `shaer-daemon`
|
|---|
| 161 | (`gate.rs` heeft nu alleen de inkomende kant), en de wachtrij-namen en de
|
|---|
| 162 | `awaiting_guardian`-status moeten letterlijk gelijk zijn.
|
|---|