Changeset 3d882bd in Klonkt for src/services


Ignore:
Timestamp:
08/04/2026 12:30:11 PM (5 weeks ago)
Author:
Bart <bart@…>
Branches:
main
Children:
fa33214
Parents:
30d0e2c
Message:

FEP-633c §4.1: een kapotte guardian kost een kind niet de goede

Een "guardian" met eigen guardians is er geen (§1), en een escalatie daarheen
komt nergens aan: er is geen grand-guardian om naar door te vertakken. Klonkt
handhaafde dat nergens. De daemon doet het vanaf het begin, en dat verschil is
precies waar shaer-6d9 voor bestaat.

Nu zacht falen zoals §4.1 vraagt: dat ene doelwit valt af, de rest krijgt de
hulpvraag gewoon. Andersom zou één verkeerd geconfigureerd account van een
volwassene de noodroep van een kind helemaal laten mislukken.

Alleen bij een hulpvraag. Een gewoon direct bericht is geen escalatie, en een
ward mag een andere ward best iets sturen — daar stilletjes ontvangers uit
slopen zou een bug zijn met een spec-verwijzing eromheen.

Als ELKE guardian kapot is, is er niets om naar door te leveren. §4 dekt dat
niet, want §4.1 gaat ervan uit dat er anderen zijn. Dan komt de hulpvraag bij
niemand aan, en dat is het enige wat deze FEP juist moet voorkomen: dat faalt
dus luid in de log in plaats van een aflevering te melden die niet gebeurde.

carriesGuardians() staat nu in context.js, waar de rest van het vocabulaire ook
woont: §3 en §5.2 stellen dezelfde vraag en moeten hem hetzelfde lezen.

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

Location:
src/services/guardianship
Files:
3 edited

Legend:

Unmodified
Added
Removed
  • src/services/guardianship/context.js

    r30d0e2c r3d882bd  
    2424}
    2525
    26 export default { SHAER_CONTEXT, GUARDIAN_RELATIONSHIP, GUARDIAN_RELATIONSHIP_COMPACT, isGuardianRelationship };
     26/**
     27 * True when an actor document carries `shaer:guardians` — i.e. it is a ward,
     28 * and therefore not a valid guardian (§1). The one question §4 asks, in both
     29 * places it asks it: before committing a guardianship (§4.2) and before
     30 * delivering an escalation to one (§4.1).
     31 *
     32 * §2.1 allows the list as an array of URIs, a single URI, or a Collection, so
     33 * all three are read here rather than in each caller.
     34 */
     35export function carriesGuardians(doc) {
     36  const g = doc && doc['shaer:guardians'];
     37  if (Array.isArray(g)) return g.length > 0;
     38  if (typeof g === 'string') return g.length > 0;
     39  if (g && typeof g === 'object') return Array.isArray(g.items) ? g.items.length > 0 : true;
     40  return false;
     41}
     42
     43export default { SHAER_CONTEXT, GUARDIAN_RELATIONSHIP, GUARDIAN_RELATIONSHIP_COMPACT, isGuardianRelationship, carriesGuardians };
  • src/services/guardianship/delivery.js

    r30d0e2c r3d882bd  
    1313import crypto from 'crypto';
    1414import db from '../../config/database.js';
     15import { carriesGuardians } from './context.js';
    1516
    1617const PUBLIC = 'https://www.w3.org/ns/activitystreams#Public';
     
    4950  // Resolve every recipient for a mention anchor + a delivery inbox.
    5051  const resolved = [];
     52  const teapots = [];
    5153  for (const uri of list) {
    5254    // An actor we host is read from our own database, not fetched from our own
     
    5658    const a = (localActor && localActor(uri)) || await fetchActor(uri).catch(() => null);
    5759    if (!a || !(a.inbox || (a.endpoints && a.endpoints.sharedInbox))) continue;
     60    // FEP-633c §4.1: an escalation addressed to a "guardian" that carries
     61    // guardians of its own goes nowhere. There is no grand-guardian, so we
     62    // MUST NOT recurse to that actor's guardians — and we fail SOFTLY: drop
     63    // this one target and keep delivering to the rest, because a malformed
     64    // guardian must never cost a child the guardians who are fine.
     65    //
     66    // Only for a call for help. An ordinary direct note is not an escalation,
     67    // and a ward is perfectly entitled to message another ward.
     68    if (helpRequest && carriesGuardians(a)) { teapots.push(uri); continue; }
    5869    resolved.push({ uri, inbox: (a.endpoints && a.endpoints.sharedInbox) || a.inbox, local: !!a.local, handle: deriveHandle(uri), url: a.url || uri });
    5970  }
    60   if (!resolved.length) return null;
     71  if (teapots.length) console.warn('[AP] not a teapot: escalation dropped for malformed guardian(s)', teapots.join(', '));
     72  if (!resolved.length) {
     73    // Every guardian was malformed. §4 does not say what to do here because
     74    // §4.1 assumes there are others to continue to — but a ward whose whole
     75    // safety net is broken has just called for help into nothing, which is the
     76    // one outcome this FEP exists to prevent. Say so loudly; the caller can
     77    // tell "nobody was reachable" from "nobody was valid".
     78    if (teapots.length) console.error('[AP] EVERY guardian of', site.slug, 'is malformed: the call for help reached no one');
     79    return null;
     80  }
    6181  const mention = resolved.map((r) => {
    6282    const disp = r.handle && r.handle[0] === '@' ? r.handle : '@' + (r.handle || '');
     
    104124  }
    105125  console.log('[AP] direct note', site.slug, '→', resolved.length, 'recipient(s), delivered', delivered);
    106   return { id, content, delivered };
     126  return { id, content, delivered, teapots };
    107127}
    108128
  • src/services/guardianship/handshake.js

    r30d0e2c r3d882bd  
    1616 * arrive once via wireHandshake(deps); nothing here imports ActivityPubService.
    1717 */
    18 import { isGuardianRelationship, GUARDIAN_RELATIONSHIP_COMPACT } from './context.js';
     18import { isGuardianRelationship, GUARDIAN_RELATIONSHIP_COMPACT, carriesGuardians } from './context.js';
    1919import * as offers from './offers.js';
    2020import * as relations from './relations.js';
     
    139139  const doc = await deps.fetchActor(candidateUri).catch(() => null);
    140140  if (!doc) return 'unverified';
    141   const g = doc['shaer:guardians'];
    142   const has = Array.isArray(g) ? g.length > 0
    143     : typeof g === 'string' ? g.length > 0
    144       : (g && typeof g === 'object') ? (Array.isArray(g.items) ? g.items.length > 0 : true)
    145         : false;
    146   return has ? 'malformed' : 'ok';
     141  return carriesGuardians(doc) ? 'malformed' : 'ok';
    147142}
    148143
Note: See TracChangeset for help on using the changeset viewer.