Index: src/routes/guardian.js
===================================================================
--- src/routes/guardian.js	(revision c8e03c6553401711c2e513b26dd53e24cc5a9486)
+++ src/routes/guardian.js	(revision 6d5ce0cde5b3d2911f42fcf2b4cd343d687c8c3b)
@@ -309,7 +309,10 @@
 //    handshake module decides when it commits (§3.1).
 // ── Step away (FEP-633c 3.6.1): the guardian declares itself unavailable ──
-// One direct note with shaer:away and an endTime to every ward, the same
-// path Shaer takes over C2S. Wards on this instance are applied directly (a
-// local inbox never receives its own delivery); the rest travels S2S.
+// One direct note with shaer:away and an endTime to every ward, the same path
+// Shaer takes over C2S, and the only path: a ward on this instance receives
+// that note through the loopback and applies the absence in its own inbox
+// handler, exactly as a ward elsewhere does. This route used to write the
+// local wards itself as well, which meant the wire version could break without
+// anyone here noticing.
 router.post('/api/away', requireAuth, express.json({ limit: '2kb' }), async (req, res) => {
   const site = siteForUser(req);
@@ -320,18 +323,8 @@
   if (!wards.length) return res.status(409).json({ error: 'no_wards' });
   const until = Date.now() + days * 24 * 3600 * 1000;
-  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
-  const me = AP.actorId(base, site.slug);
-  let applied = 0;
-  for (const uri of wards) {
-    const wslug = uri.startsWith(`${base}/`) ? uri.replace(/\/+$/, '').split('/').pop() : null;
-    if (wslug && Guardianship.listGuardians(wslug).some((g) => g.other_uri === me)) {
-      Guardianship.availability.declareAway(wslug, me, until);
-      applied++;
-    }
-  }
   const L = resolveLang(req);
   const text = i18nT(L, 'guardian.away_msg', { date: new Date(until).toLocaleDateString('nl-NL') });
   const r = await AP.deliverDirectNote(site, { recipients: wards, text, awayUntil: until }).catch(() => null);
-  if (!applied && !(r && r.id)) return res.status(502).json({ error: 'away_failed' });
+  if (!(r && r.id)) return res.status(502).json({ error: 'away_failed' });
   res.json({ ok: true, until });
 });
@@ -508,28 +501,14 @@
   const offerId = `${me}/gated/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
   const offer = Guardianship.gated.buildGatedOffer(offerId, me, uri, feature, allow);
+  // ONE path, whether the ward lives here or on the other side of the world
+  // (Robins regel, 29-7): propose over the wire and let the ward's server do
+  // what it does for everyone. deliverToActor loops a local recipient back
+  // into the same inbox handler, so co-location changes the transport and
+  // nothing else. The old shortcut recorded the vote here directly, which is
+  // how the remote path stayed broken for a month without anyone noticing.
+  AP.deliverToActor(site, uri, offer).catch(() => { /* queued, best-effort */ });
   const localSlug = (base && uri.startsWith(`${base}/`)) ? uri.replace(/\/+$/, '').split('/').pop() : null;
-  const localWard = localSlug ? db.prepare('SELECT slug FROM sites WHERE slug = ?').get(localSlug) : null;
-  if (localWard) {
-    Guardianship.gated.rememberGatedOffer(offerId, localWard.slug, feature, allow);
-    const r = Guardianship.gated.recordGatedVote(localWard.slug, feature, me, allow);
-    // Same forward as the S2S path: without it the other guardians never learn
-    // the proposal exists and a threshold of two can never be met.
-    if (r.state === 'open') {
-      const wardActor = AP.actorId(base, localWard.slug);
-      for (const g of Guardianship.listGuardians(localWard.slug).map((x) => x.other_uri)) {
-        if (g === me) continue;
-        // Signed by the ward, so the body must say the ward: anything else is
-        // a signer mismatch and the receiver answers 401 (as it should).
-        AP.deliverToActor(
-          db.prepare('SELECT * FROM sites WHERE slug = ?').get(localWard.slug),
-          g,
-          { ...offer, actor: wardActor, to: [g], 'shaer:proposer': me },
-        ).catch(() => { /* queued */ });
-      }
-    }
-    return res.json({ ok: true, allow, state: r.state, need: r.need, of: r.of });
-  }
-  AP.deliverToActor(site, uri, offer).catch(() => { /* queued, best-effort */ });
-  res.json({ ok: true, allow, state: 'open', federated: true });
+  const progress = localSlug ? Guardianship.gated.gatedProgress(localSlug, feature) : null;
+  res.json({ ok: true, allow, state: 'open', ...(progress || { federated: true }) });
 }
 
Index: src/services/ActivityPubService.js
===================================================================
--- src/services/ActivityPubService.js	(revision c8e03c6553401711c2e513b26dd53e24cc5a9486)
+++ src/services/ActivityPubService.js	(revision 6d5ce0cde5b3d2911f42fcf2b4cd343d687c8c3b)
@@ -1345,5 +1345,5 @@
 
 // Handle an incoming inbox POST. slugParam = null for the shared /ap/inbox.
-export async function handleInbox(req, slugParam) {
+export async function handleInbox(req, slugParam, preVerified = null) {
   const act = req.body || {};
   const type = act.type;
@@ -1352,5 +1352,10 @@
   const ip = req.ip || (req.connection && req.connection.remoteAddress) || '?';
   const base = (process.env.PUBLIC_BASE_URL || `${req.protocol}://${req.get('host')}`).replace(/\/+$/, '');
-  const verified = await verifyRequest(req).catch(() => null);
+  // preVerified is the loopback (see deliverToActor): a delivery between two
+  // actors on THIS instance never crosses a socket, so there is no signature to
+  // check — but we do know who signed, because we signed it. Handing that in
+  // keeps everything below identical, including the actor-versus-signer check,
+  // which is exactly the check that must not be skipped for being local.
+  const verified = preVerified || await verifyRequest(req).catch(() => null);
 
   // ENFORCE HTTP signatures: a data-affecting activity must be signed by the very
@@ -1534,6 +1539,12 @@
   const actorUri = typeof act.actor === 'string' ? act.actor : (act.actor && act.actor.id);
   const resolveActor = async (uri) => ((verified && verified.id === uri) ? verified : await fetchActor(uri).catch(() => null));
-  // Activities from our OWN actors are already stored via ap_outbox — don't re-store.
-  const isLocalActor = !!(base && actorUri && actorUri.startsWith(`${base}/ap/users/`));
+  // Our OWN activity is already stored via ap_outbox: don't store it twice.
+  // "Our own" means THIS inbox's owner, not "anyone who happens to live on this
+  // machine". The old reading dropped every activity between two sites on one
+  // instance, so a note from a co-located guardian to its ward was accepted
+  // with a 202 and then quietly thrown away: no mention, no away, no help
+  // request. Neighbours are not us (Robins regel, 29-7: on this machine
+  // everything behaves as if every Klonkt were somewhere else).
+  const isLocalActor = !!(actorUri && slugParam && actorUri === actorId(base, slugParam));
 
   // Inbound reply: a Create whose object replies to one of our notes (post OR comment).
@@ -2253,14 +2264,7 @@
             awayUntil = Guardianship.availability.parseEndTime(object.endTime);
             if (!awayUntil || awayUntil <= Date.now()) return { status: 400, error: 'away_needs_an_end' };
-            // A ward we host ourselves never receives its own delivery
-            // (private ranges, loopback): apply locally, the way the
-            // handshake commit does.
-            const meUri = selfActorId(site.slug);
-            for (const uri of recipients) {
-              const wslug = uri.startsWith(`${base}/`) ? slugFromActorUrl(uri) : null;
-              if (wslug && Guardianship.listGuardians(wslug).some((g) => g.other_uri === meUri)) {
-                Guardianship.availability.declareAway(wslug, meUri, awayUntil);
-              }
-            }
+            // No local shortcut here: the note below reaches a ward on this
+            // instance through the loopback, and its inbox handler applies the
+            // absence like it does for a ward anywhere else. One path.
           }
           const r = await deliverDirectNote(site, { recipients, text: plain, language: object.language || null, inReplyTo: typeof object.inReplyTo === 'string' ? object.inReplyTo : null, attachments: atts, helpRequest: help, awayUntil });
@@ -3807,4 +3811,23 @@
   const keys = getOrCreateKeys(site.slug);
   const payload = { '@context': AP_CONTEXT, ...activity };
+  // Co-location is a TRANSPORT detail, never a decision path (Robins regel,
+  // 29-7). An inbox on this machine is not reachable over HTTP from this
+  // machine, and should not be, so a local recipient is handed the activity
+  // straight into the same inbox handler the wire would reach. Everything
+  // above this line therefore behaves as if every Klonkt were remote: one code
+  // path, exercised by every deployment, including the checks. Two bugs in one
+  // day came from having a second, local-only path that hid a broken remote
+  // one.
+  const localSlug = localSlugOf(actorUri);
+  if (localSlug && db.prepare('SELECT 1 FROM sites WHERE slug = ?').get(localSlug)) {
+    const host = (() => { try { return new URL(selfActorId(site.slug)).host; } catch { return ''; } })();
+    const req = { body: payload, ip: 'loopback', protocol: 'https', get: () => host, headers: {} };
+    // The signer is us, and we say so: the actor-versus-signer check runs
+    // exactly as it does over the wire, so a mismatch fails here too.
+    const status = await handleInbox(req, localSlug, { id: me }).catch(() => 500);
+    const ok = status >= 200 && status < 300;
+    console.log('[AP]', activity.type, ok ? 'delivered (loopback) →' : `got ${status} (loopback) from`, actorUri);
+    return { delivered: ok, inbox: `${actorUri}/inbox`, loopback: true, status };
+  }
   const a = await fetchActor(actorUri).catch(() => null);
   const inbox = a && (a.inbox || (a.endpoints && a.endpoints.sharedInbox));
@@ -3822,8 +3845,25 @@
 }
 Guardianship.wireDelivery({
-  actorId, fetchActor, deriveHandle, escHtml, linkUrls, linkHashtags,
+  actorId, fetchActor, localActor, deliverTo: deliverToActor, deriveHandle, escHtml, linkUrls, linkHashtags,
   getOutboxRow: (id) => iStmts().getO.get(id),
   buildReplyNote, AP_CONTEXT, getOrCreateKeys, deliver, enqueueDelivery,
 });
+/**
+ * The actor document of a site WE host, read straight from the database.
+ * Same shape fetchActor returns for anyone else, plus `local: true` so the
+ * caller can take the loopback instead of a POST to our own hostname.
+ * Null for an actor we do not host: that one really is fetched.
+ */
+function localActor(actorUri) {
+  const slug = localSlugOf(actorUri);
+  if (!slug) return null;
+  const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
+  const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(slug);
+  if (!site) return null;
+  // primary_slug is what buildActor uses to pick '/' over '/user/<slug>'; the
+  // actor route sets it the same way before building.
+  const p = db.prepare('SELECT slug FROM sites WHERE is_primary = 1').get();
+  try { return { ...buildActor(base, { ...site, primary_slug: p && p.slug }), local: true }; } catch { return null; }
+}
 // Which local site (if any) hosts this actor URI — used by the handshake to
 // apply the local side of a commit and to derive a ward's existing guardians.
Index: src/services/guardianship/delivery.js
===================================================================
--- src/services/guardianship/delivery.js	(revision c8e03c6553401711c2e513b26dd53e24cc5a9486)
+++ src/services/guardianship/delivery.js	(revision 6d5ce0cde5b3d2911f42fcf2b4cd343d687c8c3b)
@@ -41,5 +41,5 @@
 // call-for-help path).
 export async function deliverDirectNote(site, { recipients, text, language, inReplyTo, attachments, helpRequest, wave, awayUntil }) {
-  const { actorId, fetchActor, deriveHandle, escHtml, linkUrls, linkHashtags,
+  const { actorId, fetchActor, localActor, deliverTo, deriveHandle, escHtml, linkUrls, linkHashtags,
           getOutboxRow, buildReplyNote, AP_CONTEXT, getOrCreateKeys, deliver, enqueueDelivery } = deps;
   const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
@@ -50,7 +50,11 @@
   const resolved = [];
   for (const uri of list) {
-    const a = await fetchActor(uri).catch(() => null);
+    // An actor we host is read from our own database, not fetched from our own
+    // hostname: that request has to leave the machine and come back, and when
+    // it does not, the recipient is silently dropped from the note. Everything
+    // that decides anything still runs below, for local and remote alike.
+    const a = (localActor && localActor(uri)) || await fetchActor(uri).catch(() => null);
     if (!a || !(a.inbox || (a.endpoints && a.endpoints.sharedInbox))) continue;
-    resolved.push({ uri, inbox: (a.endpoints && a.endpoints.sharedInbox) || a.inbox, handle: deriveHandle(uri), url: a.url || uri });
+    resolved.push({ uri, inbox: (a.endpoints && a.endpoints.sharedInbox) || a.inbox, local: !!a.local, handle: deriveHandle(uri), url: a.url || uri });
   }
   if (!resolved.length) return null;
@@ -83,5 +87,15 @@
   const keyId = `${me}#main-key`;
   let delivered = 0;
-  for (const inbox of [...new Set(resolved.map((r) => r.inbox))]) {
+  // A recipient on this machine takes the loopback (deliverToActor), which
+  // hands the Create to the same inbox handler an HTTP POST would reach: the
+  // note is stored, the mention is stored, and a shaer:away on it is applied,
+  // all by the code that does it for everyone else. A hairpin POST to our own
+  // hostname is not that code path, it is a second one that only appears to be.
+  for (const r of resolved.filter((x) => x.local)) {
+    const res = await deliverTo(site, r.uri, create).catch(() => null);
+    if (res && res.delivered) delivered++;
+  }
+  // Remote: one POST per inbox, so two guardians on the same server share it.
+  for (const inbox of [...new Set(resolved.filter((x) => !x.local).map((r) => r.inbox))]) {
     let ok = false;
     try { const st = await deliver(inbox, create, keyId, keys.private_pem); ok = st >= 200 && st < 300; } catch { ok = false; }
Index: src/services/guardianship/handshake.js
===================================================================
--- src/services/guardianship/handshake.js	(revision c8e03c6553401711c2e513b26dd53e24cc5a9486)
+++ src/services/guardianship/handshake.js	(revision 6d5ce0cde5b3d2911f42fcf2b4cd343d687c8c3b)
@@ -219,13 +219,9 @@
     const lp = availability.parseLapse(activity.object);
     if (lp) {
+      // ONE path (Robins regel, 29-7): the ward's server opens, tallies and
+      // enforces, wherever it lives. A local ward is reached by the same
+      // deliverTo, which loops back into the inbox handler; co-location is a
+      // transport detail and never a shortcut past the decision.
       const id = `${me}/lapses/${Date.now().toString(36)}${Math.floor(Math.random() * 1e4).toString(36)}`;
-      const wardSlug = deps.localSlug(lp.ward);
-      if (wardSlug) {
-        const r = availability.openLapse({ id, wardSlug, wardUri: lp.ward, target: lp.target, openedBy: me, now: Date.now() });
-        if (r.error) return { status: r.error === 'not_in_available_set' ? 403 : 409, error: r.error };
-        deps.deliverTo(site, lp.target, { id, type: 'Offer', actor: me, to: [lp.target], object: { type: 'shaer:Lapse', 'shaer:ward': lp.ward, object: lp.target } }).catch(() => { /* best-effort */ });
-        notify(wardSlug, { kind: 'lapse_opened', lapse: id, target: lp.target, set: r.set });
-        return { status: 202, id, url: id, 'shaer:set': r.set, 'shaer:threshold': r.threshold };
-      }
       const offer = { id, type: 'Offer', actor: me, to: [lp.ward], object: { type: 'shaer:Lapse', 'shaer:ward': lp.ward, object: lp.target } };
       const delivered = await fanout(site, [lp.ward], offer);
Index: test/co-location.test.js
===================================================================
--- test/co-location.test.js	(revision 6d5ce0cde5b3d2911f42fcf2b4cd343d687c8c3b)
+++ test/co-location.test.js	(revision 6d5ce0cde5b3d2911f42fcf2b4cd343d687c8c3b)
@@ -0,0 +1,250 @@
+// Everything on this machine behaves as if every Klonkt were somewhere else.
+//
+// Co-location is a TRANSPORT detail: a delivery to a local actor is looped back
+// into the same inbox handler instead of crossing a socket, and nothing above
+// that line knows the difference. The rule exists because two bugs in one week
+// came from a second, local-only path hiding a broken remote one:
+//   - the Undo never reached a co-located ward (you cannot HTTP your own inbox);
+//   - the gated proposal was broken over the wire, while the local shortcut
+//     recorded the vote directly and looked perfectly fine on the dashboard.
+//
+// So the scenario below runs TWICE, all-local and all-remote, and both runs must
+// land in the same place. A shortcut that decides something for a local party
+// shows up here as a difference between the two.
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'fs';
+
+process.env.DATABASE_PATH = ':memory:';
+process.env.PUBLIC_BASE_URL = 'https://test.example';
+
+const dbMod = await import('../src/config/database.js');
+const db = dbMod.default;
+dbMod.initializeDatabase();
+const AP = (await import('../src/services/ActivityPubService.js')).default;
+const G = await import('../src/services/guardianship/index.js');
+
+const BASE = 'https://test.example';
+const local = (slug) => `${BASE}/ap/users/${slug}`;
+const remote = (name) => `https://${name}.test/u/${name}`;
+
+db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
+  .run('u1', 'u1', 'u1@t', 'x', 'god');
+let n = 0;
+function site(slug) {
+  db.prepare('INSERT OR IGNORE INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,?)')
+    .run(`s${++n}`, slug, slug, 'u1', n === 1 ? 1 : 0);
+  return db.prepare('SELECT * FROM sites WHERE slug = ?').get(slug);
+}
+const guardianship = (slug, role, other) =>
+  db.prepare(`INSERT OR IGNORE INTO ap_guardianships (slug, role, other_uri, status, offer_id)
+              VALUES (?,?,?,'accepted','o')`).run(slug, role, other);
+
+/**
+ * One ward with three guardians, in a chosen topology. `local` puts all three
+ * on this instance, `remote` puts them elsewhere. The ward is local either way:
+ * the ward's server is the one under test, because it is the one that tallies.
+ */
+function world(prefix, topology) {
+  const wardSlug = `${prefix}ward`;
+  const wardSite = site(wardSlug);
+  const guardians = ['a', 'b', 'c'].map((g) => {
+    if (topology !== 'local') return remote(`${prefix}${g}`);
+    site(`${prefix}${g}`);                                 // a real site, so the loopback lands somewhere
+    guardianship(`${prefix}${g}`, 'guardian', local(wardSlug));
+    return local(`${prefix}${g}`);
+  });
+  for (const g of guardians) guardianship(wardSlug, 'ward', g);
+  return { wardSlug, wardSite, guardians, topology };
+}
+
+// Deliveries are recorded and then handed to the real deliverToActor, so a
+// local recipient travels the same loopback the deployment uses. That is the
+// point: the test drives the road production drives.
+const sent = [];
+G.wireHandshake({
+  selfId: (slug) => local(slug),
+  localSlug: (u) => (u && u.startsWith(`${BASE}/ap/users/`) ? u.split('/').pop() : null),
+  deriveHandle: (u) => `@${String(u).split('/').pop()}`,
+  fetchActor: async (u) => ({ id: u, inbox: `${u}/inbox` }),
+  deliverTo: async (fromSite, toUri, activity) => {
+    sent.push({ from: fromSite.slug, to: toUri, activity });
+    return AP.deliverToActor(fromSite, toUri, activity);
+  },
+  onEvent: null,
+});
+
+/** What the ward's server holds about one gated decision. */
+function gatedState(wardSlug, feature) {
+  const votes = db.prepare('SELECT guardian_uri, value FROM ap_gated_votes WHERE slug = ? AND feature = ?')
+    .all(wardSlug, feature);
+  return {
+    voted: votes.length,
+    yes: votes.filter((v) => v.value === 1).length,
+    setting: db.prepare('SELECT external_embeds FROM sites WHERE slug = ?').get(wardSlug).external_embeds,
+  };
+}
+
+/**
+ * Guardian A proposes link previews. The ward's server records A's own answer
+ * (§3.1's one-step clause) and forwards to B and C. Then B agrees, and two of
+ * three settles it.
+ */
+async function proposeAndSettle({ wardSlug, wardSite, guardians }) {
+  const [A, B] = guardians;
+  const offerId = `${A}/gated/1`;
+  const offer = G.gated.buildGatedOffer(offerId, A, local(wardSlug), 'shaer:externalEmbeds', true);
+  await G.handleGuardianshipInbox(wardSite, offer);
+  const afterPropose = gatedState(wardSlug, 'shaer:externalEmbeds');
+  await G.handleGuardianshipInbox(wardSite, { id: `${B}/accept/1`, type: 'Accept', actor: B, object: offerId });
+  return { afterPropose, afterSecond: gatedState(wardSlug, 'shaer:externalEmbeds') };
+}
+
+const localRun = await proposeAndSettle(world('loc', 'local'));
+const remoteRun = await proposeAndSettle(world('rem', 'remote'));
+
+test('a gated decision reaches the same state whether the guardians are local or remote', () => {
+  assert.deepEqual(localRun.afterPropose, remoteRun.afterPropose,
+    'the tally after one proposal must not depend on where the guardians live');
+  assert.deepEqual(localRun.afterSecond, remoteRun.afterSecond,
+    'nor the outcome after the second answer');
+  // And the shared state is the RIGHT one, so "identical" cannot be satisfied
+  // by both sides being equally broken.
+  assert.equal(localRun.afterPropose.voted, 1, 'one voice after the proposal');
+  assert.equal(localRun.afterPropose.setting, null, 'one voice does not open a gate');
+  assert.equal(localRun.afterSecond.setting, 1, 'two of three does');
+  assert.equal(localRun.afterSecond.voted, 0, 'and the settled decision is cleared');
+});
+
+test('the other guardians are told, local ones no less than remote ones', () => {
+  const fwd = sent.filter((x) => x.activity.object && x.activity.object['shaer:feature']);
+  assert.equal(fwd.filter((x) => x.to.startsWith(BASE)).length, 2,
+    'a guardian on this machine is forwarded to, not skipped for being nearby');
+  assert.equal(fwd.filter((x) => !x.to.startsWith(BASE)).length, 2, 'and so is one elsewhere');
+  for (const x of fwd) {
+    // The forward is signed by the ward's key, so the body must name the ward.
+    // Its absence is what made boiert.eu answer 401 to every forward.
+    assert.match(x.activity.actor, /ward$/, 'the ward relays it under its own name');
+    assert.ok(x.activity['shaer:proposer'], 'with the proposer carried alongside');
+  }
+});
+
+test('a local guardian ends up with a review it can actually answer', () => {
+  // The loopback is only worth having if it produces the same effect on the
+  // receiving side as an HTTP delivery would: a stored proposal, on the
+  // guardian's own dashboard.
+  for (const g of ['a', 'b', 'c'].slice(1)) {
+    const reviews = G.gated.listGatedReviews(`loc${g}`);
+    assert.equal(reviews.length, 1, `guardian loc${g} holds the forwarded proposal`);
+    assert.equal(reviews[0].feature, 'shaer:externalEmbeds');
+    assert.equal(reviews[0].proposer, local('loca'), 'and can see who proposed it');
+  }
+});
+
+// ── §3.6.1 away: the other activity that has to cross the same gap ──
+// A guardian declaring itself away rides a direct note. Direct notes did NOT
+// take the loopback: they resolved an inbox and POSTed to it, so a note to a
+// ward on this machine went out to our own hostname and back, or nowhere. Two
+// hand-written shortcuts existed to paper over it (one in the C2S outbox, one
+// in the Guardian PWA route), which is the pattern this file is about.
+const awaySite = site('awguard');
+const awayWard = site('awward');
+guardianship('awward', 'ward', local('awguard'));
+guardianship('awguard', 'guardian', local('awward'));
+const AWAY_UNTIL = Date.now() + 9 * 24 * 3600 * 1000;
+const awayNote = await AP.deliverDirectNote(awaySite, {
+  recipients: [local('awward')], text: 'even weg', awayUntil: AWAY_UNTIL,
+});
+
+test('a guardian on this machine can declare itself away to a ward on this machine', () => {
+  assert.ok(awayNote && awayNote.id, 'the note was built');
+  assert.equal(awayNote.delivered, 1, 'and it was delivered, not silently dropped for being local');
+  assert.equal(G.availability.effective('awward', local('awguard'), Date.now()), 'away',
+    'the ward server recorded the absence, through the inbox handler like anyone else');
+});
+
+test('the same note read from the wire produces the same state for a remote guardian', () => {
+  // The body the loopback carried is the body an HTTP POST would carry, so
+  // replaying it from a remote actor must land in exactly the same place. This
+  // is what makes the two paths one path rather than two that agree today.
+  const row = db.prepare('SELECT * FROM ap_outbox WHERE id = ?').get(awayNote.id);
+  const note = AP.buildReplyNote(BASE, awaySite, row);
+  assert.equal(note['shaer:away'], true, 'shaer:away rides on the note itself (§3.6.1)');
+  assert.ok(note.endTime, 'with an end: an absence without one is dropped, never guessed');
+
+  const rw = site('awward2');
+  const far = remote('farg');
+  guardianship('awward2', 'ward', far);
+  // The same note, re-addressed to the second ward and sent by a guardian
+  // elsewhere: the mention tag is how a recipient recognises itself, so it
+  // travels along. Nothing else about the body changes.
+  const readdressed = JSON.parse(JSON.stringify(note).split(local('awward')).join(local('awward2')));
+  const create = { type: 'Create', actor: far, to: [local('awward2')], object: { ...readdressed, id: `${note.id}#2`, attributedTo: far } };
+  return AP.handleInbox(
+    { body: create, ip: '1.2.3.4', protocol: 'https', get: () => 'test.example', headers: {} },
+    rw.slug, { id: far },
+  ).then(() => {
+    assert.equal(G.availability.effective('awward2', far, Date.now()),
+      G.availability.effective('awward', local('awguard'), Date.now()),
+      'a remote guardian and a co-located one leave the ward in the same state');
+  });
+});
+
+test('the loopback still checks the signer against the actor', async () => {
+  // The loopback hands the inbox a verified signer instead of a signature. If
+  // that were taken on faith, a local delivery would be the one place where a
+  // forged actor passes. It is not: the same check runs.
+  site('mmward'); const b = site('mmguard');
+  const status = await AP.handleInbox(
+    { body: { type: 'Offer', actor: local('someone-else'), object: {} }, ip: 'loopback', protocol: 'https', get: () => 'test.example', headers: {} },
+    b.slug,
+    { id: local('mmward') },              // signed as mmward, body claims someone else
+  );
+  assert.equal(status, 401, 'signer mismatch is refused on the loopback too');
+});
+
+test('no guardianship decision takes a shortcut for a local party', () => {
+  // A guard, not a proof. Every place that asks "is this actor one of ours?"
+  // must sit in a function that is allowed to ask: one that WRITES what this
+  // instance hosts after a decision, or READS local state for display. Never
+  // one that decides instead of delivering.
+  //
+  // Adding a name here should feel like a decision. If a new function needs a
+  // local branch in a decision path, that is the bug, not this list.
+  const allowed = {
+    existingGuardiansOf: 'reads our own guardian list instead of fetching our own actor doc',
+    applyCommitLocally: '§3.1.4: each instance writes the side of the commit it hosts',
+    endGuardianship: '§3.2: same, for the ward side of the Undo, after the fanout',
+    proposeGated: 'reads the tally back for the screen, after delivering',
+    wardGuardianStatuses: 'availability of a ward we host: our own state, for our own screen',
+    wardGateSetting: 'the gate of a ward we host: our own column, for our own screen',
+    'route /wards/release-check': 'counts a ward\'s guardians: ours from the table, someone else\'s from their actor doc',
+    // Known second path, NOT blessed: a gated follow reaches a co-located
+    // guardian through the shared database instead of an Offer, and the answer
+    // travels back the same way. Listed so this guard keeps passing while it
+    // exists, not so it can be forgotten. Removing this line is the definition
+    // of that job being done.
+    wardSlugsOf: 'TODO: gated follows still take the shared-database path for a local guardian',
+  };
+  const files = ['src/services/guardianship/handshake.js', 'src/routes/guardian.js'];
+  // Only top-level declarations name a scope; an indented `const base = ...` is
+  // a local and would otherwise take the blame for its enclosing function. A
+  // route handler is an anonymous arrow, so it is named after its path.
+  const declares = /^(?:export\s+)?(?:async\s+)?function\s+(\w+)|^(?:export\s+)?(?:const|let)\s+(\w+)\s*=\s*(?:async\s*)?(?:function|\()/;
+  const routes = /^router\.\w+\(\s*['"`]([^'"`]+)/;
+  for (const f of files) {
+    const lines = fs.readFileSync(f, 'utf8').split('\n');
+    let fn = '<top level>';
+    for (const [i, line] of lines.entries()) {
+      const d = line.match(declares);
+      const r = line.match(routes);
+      if (d) fn = d[1] || d[2];
+      else if (r) fn = `route ${r[1]}`;
+      // The idioms for "this URI is on this machine": ask the helper, or
+      // compare the URI against our own base.
+      if (!/localSlug\b|\.startsWith\(\s*(?:`\$\{base\}|base\b)/.test(line)) continue;
+      if (/^\s*(\*|\/\/)/.test(line)) continue;                 // a comment about it is fine
+      assert.ok(allowed[fn], `${f}:${i + 1} branches on co-location inside ${fn}(), which is not on the list:\n    ${line.trim()}`);
+    }
+  }
+});
