Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision ea211b9cdb42e4c59da937c4c2ab5162251ce374)
+++ src/services/ActivityPubService.js	(revision 29eb7459b738dadb3363af52947ca6358844a4e9)
@@ -110,7 +110,37 @@
   return true; // not an IP literal we recognise → refuse
 }
+/**
+ * Uitzonderingen op de SSRF-poort, voor een testkudde op de eigen machine
+ * (shaer-6wt: honderd wards met een guardian, Barts opdracht 8-8).
+ *
+ * WAAROM DIT MAG BESTAAN. De bescherming hierboven is er omdat een actor-URI van
+ * een VREEMDE komt: een aanvaller die "http://169.254.169.254/" doorgeeft laat
+ * ons zijn werk doen. Deze lijst gaat niet over vreemden -- hij staat in de
+ * omgeving van deze server, wordt door de beheerder gezet, en is leeg tenzij
+ * iemand hem expliciet vult.
+ *
+ * WAAROM HIJ ZO SMAL IS. Geen vlag die "loopback is oke" zegt, maar een lijst
+ * van precieze host:poort-paren. `[::1]:3060` opent niet 127.0.0.1, niet poort
+ * 3061, en niets in het interne netwerk. Een brede vlag zou de bescherming in
+ * een dev-omgeving uitzetten, en dev-omgevingen worden productie.
+ *
+ *   AP_ALLOW_HOSTS="[::1]:3060,[::1]:3061"
+ */
+const AP_ALLOW_HOSTS = new Set(
+  String(process.env.AP_ALLOW_HOSTS || '').split(',').map((x) => x.trim().toLowerCase()).filter(Boolean),
+);
+function isAllowedTestHost(u) {
+  if (!AP_ALLOW_HOSTS.size) return false;
+  return AP_ALLOW_HOSTS.has(u.host.toLowerCase());
+}
 async function assertPublicHost(hostname) {
-  if (net.isIP(hostname)) { if (isBlockedIp(hostname)) throw new Error('ssrf-blocked-ip'); return; }
-  const addrs = await dns.promises.lookup(hostname, { all: true });
+  // URL.hostname geeft een IPv6-literal MET blokhaken ("[::1]"), en net.isIP
+  // herkent die vorm niet. Zonder strippen viel elk IPv6-adres door naar de
+  // DNS-tak, waar het strandde op ENOTFOUND: geweigerd, maar per ongeluk en met
+  // de verkeerde reden. isBlockedIp strippde ze al -- die verwachtte dus input
+  // die hier nooit aankwam.
+  const naakt = String(hostname || '').replace(/^\[|\]$/g, '');
+  if (net.isIP(naakt)) { if (isBlockedIp(naakt)) throw new Error('ssrf-blocked-ip'); return; }
+  const addrs = await dns.promises.lookup(naakt, { all: true });
   if (!addrs.length || addrs.some((a) => isBlockedIp(a.address))) throw new Error('ssrf-blocked-host');
 }
@@ -128,5 +158,7 @@
     const u = new URL(target); // throws on malformed → caller's catch
     if (u.protocol !== 'https:' && u.protocol !== 'http:') throw new Error('ssrf-bad-scheme');
-    await assertPublicHost(u.hostname);
+    // Alleen op de precieze host:poort uit AP_ALLOW_HOSTS, en per hop opnieuw:
+    // een omleiding naar een ANDER intern adres blijft geweigerd.
+    if (!isAllowedTestHost(u)) await assertPublicHost(u.hostname);
     const r = await fetch(target, {
       ...opts,
Index: src/services/guardianship/handshake.js
===================================================================
--- src/services/guardianship/handshake.js	(revision ea211b9cdb42e4c59da937c4c2ab5162251ce374)
+++ src/services/guardianship/handshake.js	(revision 29eb7459b738dadb3363af52947ca6358844a4e9)
@@ -44,4 +44,34 @@
   if (type !== 'Undo') return null;
   return parseRelationship(activity && activity.object);
+}
+
+/**
+ * De overgebleven guardians opnieuw vertellen of ZIJ nu de doorslag geven
+ * (shaer-8vt, Barts correctie 8-8).
+ *
+ * "Doorslaggevend" is geen eigenschap van een moment maar van een STAND: zodra
+ * er nog een stem nodig is, is iedereen die nog moet antwoorden het. Eenmalig
+ * berekenen bij het doorsturen bevriest een antwoord dat verandert.
+ *
+ * Nooit dragend: lukt de update niet, dan blijft de oude waarde staan. Die is
+ * dan te voorzichtig of te stil -- en juist daarom staat de FAALSTAND aan de
+ * kant van waarschuwen (isDecisive leest onbekend als "ja, jij beslist").
+ */
+function herzieDoorslag(site, offerId, gsOffer, laatsteStem) {
+  try {
+    const p = gated.gatedProgress(site.slug, gsOffer.feature);
+    if (!gated.isDecisive(p.votes, p.need)) return;   // nog niets veranderd
+    const me = deps.selfId(site.slug);
+    const gestemd = new Set([gsOffer.proposer, laatsteStem].filter(Boolean));
+    for (const g of relations.listGuardians(site.slug).map((x) => x.other_uri)) {
+      if (gestemd.has(g)) continue;
+      deps.deliverTo(site, g, {
+        id: offerId, type: 'Offer', actor: me, to: [g],
+        object: { type: 'shaer:GatedSetting', 'shaer:ward': me, 'shaer:feature': gsOffer.feature, 'shaer:value': !!gsOffer.value },
+        'shaer:proposer': gsOffer.proposer || undefined,
+        'shaer:decisive': true,
+      }).catch(() => { /* de bezorgwachtrij probeert opnieuw */ });
+    }
+  } catch { /* nooit dragend */ }
 }
 
@@ -537,4 +567,14 @@
     const r = gated.recordGatedVote(site.slug, gsOffer.feature, actor, value);
     if (r.state === 'settled') answerGatedProposer(site, offerId, r);
+    // DOORSLAGGEVEND SCHUIFT MEE (Barts correctie, 8-8). Ik berekende dit een
+    // keer bij het doorsturen en bevroor het. Bij vijf guardians staat er dan
+    // "je beslist niets" -- en zodra er een ja bij komt IS elk van de anderen de
+    // doorslag. Dat is precies de stille kant: het scherm zwijgt op het moment
+    // dat het moet spreken.
+    //
+    // Dus na elke stem die het open laat: de overgeblevenen opnieuw vertellen
+    // waar ze staan. Alleen wie NOG NIET geantwoord heeft, en alleen als het
+    // antwoord verandert -- anders is dit een bericht per stem per guardian.
+    else herzieDoorslag(site, offerId, gsOffer, actor);
     notify(site.slug, { kind: 'gated_setting', feature: gsOffer.feature, value, state: r.state });
     return true;
