source: Klonkt/docs/ward-outbound-follows-design.md

main
Last change on this file was fa33214, checked in by Bart <bart@…>, 5 weeks ago

FEP-633c §5.3 andersom: een ward vraagt eerst of het iemand mag volgen

Uitgaande follows gingen ongehinderd de deur uit; de guardians kregen achteraf
een bericht (1a2f206). Dat is informeren, niet gaten — de deur staat al open als
het bericht aankomt. Bead shaer-p729, ontwerp in
docs/ward-outbound-follows-design.md.

De regel: per geval goedkeuring, met twee uitzonderingen die geen gunst zijn
maar dezelfde beslissing die al genomen is. Je eigen guardian volgen is geen
vraag. En iemand die de ward al volgt DOOR DE POORT heen is door een guardian
bij naam goedgekeurd; die vraag nog eens stellen leert mensen alleen om de vraag
niet meer te lezen.

Daarvoor moet je weten wie er door de poort kwam, dus ap_followers krijgt
gate_approved, gezet bij acceptGatedFollow. Iedereen die al volgde toen die
kolom erbij kwam wordt eenmalig gegrandfatherd (Barts besluit): exact vanaf nu,
in plaats van met terugwerkende kracht wantrouwig tegen wat er al was.

Eigen tabel, want ap_pending_follows is gesleuteld met de ward als DOEL. Eigen
wachtrij (outgoingFollows), want een guardian moet "iemand wil je ward volgen"
kunnen onderscheiden van "je ward wil iemand volgen" — de AS2-test ving netjes
dat de nieuwe term aangemeld moest worden. En een tegengehouden follow reist als
derde uitkomst naar de app (state: awaiting_guardian), zodat Shaer "wacht op
toestemming" kan tonen in plaats van een tegel die er al volgend uitziet.

Co-Authored-By: Claude Opus 5 <claude@…>

  • Property mode set to 100644
File size: 8.7 KB
Line 
1# Uitgaande follows van een ward — ontwerp
2
3Bijbehorende beads: **shaer-p729** (bouwen) en **shaer-yeo5** (de spec-vraag:
4informeren of gaten?). Dit document beantwoordt yeo5 en beschrijft wat p729
5inhoudt.
6
7FEP-633c §5.3 houdt follows *naar* een ward tegen tot de guardians beslissen.
8Follows *van* een ward gaan ongehinderd de deur uit: `ingestOutboxActivity`
9roept bij `case 'Follow'` meteen `followActor()` aan, zonder ward-check en
10zonder wachtrij.
11
12De guardians blijven niet in het duister. Sinds 1a2f206 krijgt elke guardian een
13directe note zodra zijn ward iemand gaat volgen. Maar dat is Robins constatering
14van 31-7 in één zin: **dat is informeren, geen gate.** De deur is al open op het
15moment dat het bericht aankomt, en een guardian die te laat kijkt kan alleen nog
16achteraf iets vinden van iets dat al gebeurd is.
17
18De asymmetrie is half te verdedigen. `Block` (Shaers "Orbit") is uitgaand ook
19ongated, en dat is de veilige richting: een kind dat zijn eigen wereld kleiner
20maakt 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
26Volgt de ward iemand die de ward al volgt, dan hoeft er niemand meer naar te
27kijken. Die actor is namelijk al door de inkomende poort gekomen, en dat betekent
28dat een guardian er al ja tegen heeft gezegd. Nog een keer vragen is dezelfde
29vraag twee keer stellen, en elke overbodige vraag is er een die de volgende keer
30minder aandacht krijgt.
31
32Het predicaat is `target_uri ∈ ap_followers(slug)`. Kort, maar het klopt alleen
33zolang lidmaatschap van die tabel écht een guardian-besluit impliceert — zie de
34eerste 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
93Gebouwd op 3-8-2026: `ap_pending_outgoing_follows` + `outgoing.js`, de poort in
94`ingestOutboxActivity` (`case 'Follow'`), de `outgoingFollows`-wachtrij, en het
95antwoord van de guardian op `POST /api/outgoing-follow/:id`. Negen tests in
96`test/outgoing-follow-gate.test.js`.
97
98## Open vragen
99
1001. **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
1082. **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
1133. **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
1174. **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
1215. **Nog open. Emancipatie.** Wat gebeurt er met openstaande verzoeken als de
122 guardianship eindigt? Automatisch goedkeuren of laten vervallen.
123
1246. **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
143Los van dit ontwerp, en niet in een bead gevonden: `ap_pending_follows.quorum`
144wordt door de aanroep in `ActivityPubService.js` nooit meegegeven, dus alles
145valt 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
147zich 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
149test de waarde zelf meegeeft — precies het pad dat productie nooit neemt.
150
151Gevolg: een ward met drie guardians gaat vandaag open op één goedkeuring, ook
152als iemand dacht `all` te hebben ingesteld. Verdient een eigen bead, en de
153oplossing hangt samen met shaer-3kp: als het beleid daar komt te wonen, is dat
154meteen de plek waar de inkomende kant zijn quorum vandaan haalt.
155
156## Daemon-pariteit
157
158De daemon-README is er stellig over: de apps moeten zich tegen beide backends
159hetzelfde gedragen, en als daemon en Klonkt uit elkaar lopen is de UI die je
160lokaal 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.
Note: See TracBrowser for help on using the repository browser.