oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1# 🤖 Activity on the front door — design
2
3**Status:** design, 2026-09-05, revised three times the same day and once
4again to coordinate with the suite sharing doctrine (see below).
5Drafted by the activity-stream session (`claude-opus-5`).
6**The coordination revision was reviewed 2026-09-05 by Fable 5.1**, which
7returned CHANGES NEEDED on two blocking findings — both of them §1.1's own
8rule turned against it: an "is `main`" assertion that held for fifty-three
9seconds, and a second that was true for forty-five seconds and went false
10unnoticed. Both are fixed,
11and the wording moved to reachability. The four passes below predate that
12revision and are recorded separately.
13
14**Review passes: three, all by Fable 5.1, all returning CHANGES NEEDED.**
15The first found six blocking (a superseded §11, §7 contradicting itself on
16stale rows, §10's 404 rule inverting ruling 5, the shared platform edge cache
17upstream of both ends, the RAM cache understated as one person's, and two
18"open numbers" that were load-bearing). The second found three (the fan-out
19set stated two ways, one of which disclosed a guest's sibling apps; §2's
20framing backwards after the rescope; §2.1 answering an alternative nobody
21proposed). The third found that §2.1's credential argument was not the whole
22story — browser-mediated composition needs no amendment at all — which Paul
23then ruled on. **The fourth pass APPROVED**, and the nits it listed are
24applied in the commit that carries this line.
25**Peer pass, and why it is not the review pass.** The home-spec-compliance
26session (`claude-opus-5`) reviewed the first draft and found four things,
27all applied: the "no new knowledge" defence was true and beside the point
28(§2.2, now a retraction); gate 11 tested only the happy path while §6 makes
29failure the normal case (§9); ruling 3's safety rests entirely on `aud`
30binding and needed a written "never" against the obvious optimisation (§11);
31and one claim in §5 was falsified by §5.3's own last line. That session
32wrote enough of the resulting argument to no longer be independent, and it
33says so itself. **A fresh reviewer is still required.**
34
35**Coordinated with the suite sharing doctrine, 2026-09-05.** `oficina/suite`,
36`docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md`, revision 7 at
37`2b78680`, adopts this design as **suite contract call 7** (its ruling 11). Five points
38were settled with that session and are reflected below: the doctrine's §4.3
39now carves out call 7 explicitly, the wire shape stays here rather than moving
40into the contract module, the `roster_epoch` invalidation exception is scoped
41away from call 7, §1.1 records what this spec consumes, and §3 stops
42restating a clause the doctrine's Appendix A.1 owns. What changed here, and
43what changed there, is listed at §1.1.
44
45**Scope:** how the home's front door (§11 of the home design) shows a person
46what has been happening in the team they are looking at, across that
47team's apps that they personally have rows in, without the home
48learning or holding any of it.
49
50**Reads on top of** `docs/superpowers/specs/2026-08-30-home-design.md`. Every
51`§n` below refers to that spec unless it says otherwise. Nothing here
52replaces it; two clauses of it change, and §3 says which.
53
54---
55
56## 0. The short version
57
58The ask was "an activity stream API the other apps post to, so home shows a
59feed on sign-in." **The direction inverted during design, and the API is
60therefore not the one that was asked for.** Instances do not post. The home
61asks each of the viewer's own instances in the selected team "what's theirs, recently?" when it renders
62their front door, merges the answers, and holds them nowhere but memory (§7).
63
64**Scoped to the selected team — RULED 2026-09-05 by Paul**, and recorded in
65the home spec as ruling 18. The front door is one team's page (ruling 17,
66`?team=<sqid>` per ruling 22), so the feed under it is that team's, merged
67across the viewer's own rows in that team. An earlier draft specified a
68cross-team merged list and argued for it; that argument is retired in §2.1
69rather than deleted.
70
71That inversion is the whole design. It is what makes the feature possible
72inside the one rule instead of against it:
73
74- **The home stores nothing new.** §0's falsifiable claim — publish the
75 database and the worst anyone learns is counts, team names and memberships
76 — survives word for word.
77- **The app that owns the grants answers the question about the grants.** No
78 authorization model at the home (§2.2), no copied ACL to drift, no revoke
79 tail.
80- **Two clauses change** (§3): §2.2's verb, from *never renders* to *never
81 stores*, and §0's prose gloss, which now reads as *never retains*.
82
83What it costs is latency, honestly rendered. §6 and §7.
84
85---
86
87## 1. Settled decisions
88
89| # | Ruling | Rejected | Who, when |
90|---|---|---|---|
91| 1 | **A real feed with titles, not a contentless heat map** | "4 changes in acme·sheets" with no titles; no feed at all | Paul, 2026-09-05 |
92| 2 | **Pull at render. The home holds no activity at rest** | Push into the home (the parked shape); push-a-signal / pull-detail hybrid | Paul, 2026-09-05 |
93| 8 | **Server-side pull, and amend §2.2 — not browser-mediated composition** | An `<iframe>` per instance, which satisfies ruling 2 as written and would leave §2.2 unamended, at the cost of the ordered merge, the server deadline, and any rows at all for a viewer without a live session at that sibling — §2.1 | Paul, 2026-09-05 |
94| 3 | **Reuse the §5.3 assertion for the pull. No new home→instance credential** | A dedicated service token per instance | This session, 2026-09-05 |
95| 4 | **The last good pull is cached in RAM per person, never at rest** | No cache (every load pays cold wake); a cache on disk (stores content) | This session, 2026-09-05 |
96| 5 | **A silent instance renders as a named gap, never as an absent row** | Rendering only what arrived | This session, 2026-09-05 |
97| 6 | **Scoped to the selected team, following `?team=<sqid>`** — home spec ruling 18 | A cross-team merged list, which this design specified and argued for until 2026-09-05; the argument is retired in §2.1 rather than deleted | Paul, 2026-09-05 |
98| 7 | **Participation is a registry flag, never inferred from a `404`** — §10 | Treating a 404 as "does not participate", which makes a broken or hostile instance vanish silently and inverts ruling 5 | This session, 2026-09-05 |
99
100Rulings 3, 4, 5 and 7 are this session's and were flagged as such. Ruling 8
101is Paul's, taken after the third review found an alternative that would have
102needed no amendment at all (§2.1). Ruling 5 is not a courtesy: §2 explains why it is the
103load-bearing test in the document.
104
105---
106
107## 1.1 Consumed suite rulings
108
109The suite sharing doctrine's gate 9 requires a consuming spec to name the
110document, its commit **and its revision**, and to list the rulings it takes —
111and to *cite* rather than restate them, because a second copy of a boundary is
112the drift the doctrine exists to stop. This section is that citation. Nothing
113below is restated here in its own words.
114
115**Document:** `oficina/suite`, `docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md`,
116**revision 7**, commit **`2b78680`**, **reachable from `main`** in that
117repository. (Its checkout resolved the file when this was written on
1182026-09-05; that is an observation with a date on it, not a standing claim,
119for the reason the next paragraph gives.)
120
121**"Reachable from `main`", not "is `main`" — and that wording is the point.**
122An earlier draft of this section wrote "that commit is `main`", moved the
123citation *because* such an assertion had gone false, and then made the same
124assertion again: it was true for **fifty-three seconds** before revision 7
125landed. A second such claim, about the suite's own `AGENTS.md` at
126`47ae0e8`, was **true when it was written — by forty-five seconds — and went
127false twenty-six minutes later, unnoticed.** (An earlier draft of this
128paragraph said it was already false when written. It was not: the claim was
129made at 18:02:07Z and `47ae0e8` had been `main` since 18:01:22Z. That
130correction came from the review, was taken on report, and is only right here
131because it was checked at source afterwards — in a section about exactly that
132failure.) A present-tense claim about
133another repository's tip cannot survive in a document, and chasing it defeats
134gate 9's one-revision grace outright. **Reachability is durable** — true
135until somebody force-pushes — so that is what this spec asserts. Where a
136present-tense observation is genuinely wanted, it is dated and past tense.
137
138**Why the revision moved twice in an evening, when it did not have to.**
139Gate 9 lets a consumer sit one revision behind so it is not chased on every
140edit, and **every ruling this spec consumes is textually unchanged from
141revision 4 through 7** — verified step by step: revision 5 changed the status
142block and A.1's note; revision 6 gave S6 its own §12 step and added a §6
143paragraph and A.4's cite-as-specification note; revision 7 moved S6 and gave
144it a reason. **The doctrine's §1 rulings table is byte-identical between
145revision 4 and revision 7**, so none of it reaches §1.1's list. The citation moved
146for the wrong reason (chasing an "is `main`" assertion) and now sits at
147revision 7 for the right one: it is the current text, and the wording no
148longer makes a promise that expires.
149
150| Suite ruling | What this spec takes from it |
151|---|---|
152| 1 | The team is the boundary; a team's instances across apps are one trust domain. This is why ruling 6's per-team scoping is the correct shape and not a narrowing. |
153| 3 | Custody and authorization are two rules, and authorization is never cached. §7 is the one carve-out, and §4.3 of the doctrine now writes it. |
154| 4 | The home owns identity, the team and the membership roster, and never a per-item grant. §5.1's argument is re-derived against this. |
155| 7 | One contract, one module, fixtures as the interface, and a consuming spec cites rather than restates. This section exists because of it. |
156| 8 | Two credentials chosen by what the call proves. Call 7 carries an assertion because it must prove *which person the call is for*, which a consumer token cannot (§5). |
157| 11 | Activity is pulled at render and held nowhere at rest — this design, adopted as call 7. |
158| 15 | Three rungs and a read-only link. Only the `team` rung is team-visible. Relevant to §5.2's guests, and the reason call 7 never needs a visibility flag. |
159
160**What this spec gave back, recorded so the direction of each borrowing is
161legible:** the doctrine's §4.3 call-7 carve-out is this §7's two ages,
162reconciled with that session; the epoch scoping in its §5.3 was raised from
163here; and its leak-list item 11, a cacheable call-7 response as a
164*cross-person* leak at the platform edge, is gate 16, credited there. Its §6.3
165sentence — *"a reliably slow sibling is a suite health problem the consumer
166surfaces, never one it routes around by caching authorization"* — is ruling 5
167generalised, and is cited in §8 as the doctrine's wording of it.
168
169**Why the number was 5 and not 4 — history, kept because it explains the hazard. This spec's first coordination revision
170cited 4.** Two commits after revision 4 changed normative text without bumping
171the revision: `574cc84` and `bf0eeb8`, the second carrying the §4.3 carve-out
172and the §5.3 epoch scoping that this spec consumes. The doctrine's own gate 9
173requires a bump on any normative change, so revision 4 was being cited while
174its text moved underneath the citation. Raised from here after this spec was
175handed one commit for revision 4 while this repository's `suite-amendment`
176branch carried another, `210312f`, and neither could be confirmed; the doctrine
177session bumped to revision 5 and recorded the reason in its status block.
178
179**`210312f` was not an error where it appeared, and now it is.** Revision 4
180was correct when those citations were written and sat inside gate 9's
181one-revision grace. That grace has since been spent twice over: the home
182spec's own ruling 23 landed on `main` citing revision 4, and the Docs and
183Sheets amendments — unmerged, on their `suite-amendment` branches — still do.
184**Three revisions behind fails gate 9**, which allows one. This spec and the
185home spec move together in this commit; the other two are named here so they
186are not forgotten.
187
188**What made this hard to see, recorded because it will recur.** For most of
1892026-09-05 the doctrine existed only on that repository's origin and in its
190worktrees: every merge went to the remote, and nothing updated the shared
191checkout, which sat at `291d957` with an **empty specs directory**. A reader
192going to the repository on disk found no document at all, and the only
193readable text was a worktree — which is why the first coordination revision
194cited a working tree rather than a commit and said so. The checkout is now
195fast-forwarded and the file resolved when this was written on 2026-09-05 —
196dated, for the reason two paragraphs up.
197
198**The rule this produced now lives in `oficina/suite`'s `AGENTS.md`** (merged
199`47ae0e8`, reachable from that repository's `main`): fetch before reading a spec
200you did not just write, and fast-forward the shared checkout after you merge.
201It is cited here rather than restated, for the same reason §1.1 exists — this
202paragraph records why the citation in this spec was hard to get right, and
203that file records what everyone must now do about it. What the rule guards
204against is not the empty directory, which is loud, but the quiet version: a
205session reading a file a few commits behind, citing a hash that resolves, and
206being wrong about the text. Gate 9 checks that a citation's revision belongs
207to its commit; **nothing checks that the reader read the commit**, and that
208gap is open.
209
210---
211
212## 2. Why pull, and what it inherits from a parked design
213
214**This shape was proposed before, in the other direction, and parked.** On
2152026-09-02 Paul proposed `escritorio` — a per-team app aggregating sheets,
216docs, calendar and mail into one list, apps posting entries opt-in, scoped to
217pointers only. Three visibility options were put to a Fable review: **A**,
218the app posts the ACL and escritorio filters locally; **B**, escritorio asks
219each app at render time; **C**, A plus a priority push on revoke. The verdict
220was nine severity-ordered findings, each marked **confirmed** or
221**suspected**, and a **conditional** recommendation — not "park it", which
222was a later paraphrase: *"Do not build escritorio now; if a merged list must
223exist, it is Option B's mechanism without Option B's process."* Paul parked
224it and put the home's identity layer first.
225
226**Findings are quoted with their own confidence marking below.** The verdict
227distinguishes what it verified against a file from what it reasoned to, and
228flattening that distinction is how a citation degrades into a recollection.
229
230**Both sources are in this repo**, because the citation was otherwise
231uncheckable — they existed only as local files in a sweepable cache
232directory, and the verdict only inside a subagent transcript. No discussion
233was opened and no spec was written at the time.
234
235- `docs/superpowers/reviews/2026-09-02-escritorio-review-brief.md`
236- `docs/superpowers/reviews/2026-09-02-escritorio-fable-verdict.md`
237
238**Prefer those files to discussion #6.** #6 is a good-faith reconstruction
239written from a session's private memory before the originals were found; its
240wording is that session's, not Fable's. These two are the contemporaneous
241documents. Quote from them.
242
243The findings that bear on this design, and what happens to each:
244
245| # | The finding | What happens to it here |
246|---|---|---|
247| 1 | ~~**It answered a different question.**~~ **Overtaken by ruling 6: per team is now the shape.** Per-team aggregation gives a person in three teams three lists, which the verdict called the wrong answer and Paul has since ruled the right one — the verdict's second path, not its first. The §5.4 argument below stands on its own and is what still matters. The cross-team list is forbidden at the only place that could hold it — §5.4, *"'Which documents you can see' passes (a) and fails (b), handing the home a document-level graph it has no business holding."* **Ruling 6 took the verdict's SECOND path** — three lists, per team — so what was reopened is §2.2's verb, not §5.4. The verdict's own resolution: *"either that ruling is reopened deliberately or the want is declined."* | **§2.2's verb was reopened deliberately** (rulings 1 and 2, Paul, 2026-09-05). **§5.4 was not** — the cross-team want was declined instead, which is the verdict's *second* path, and ruling 6 is where that landed. §2.1 argues why a rendered page is not a held graph. |
248| 2 | **Titles are first-class sealed content, not cheap pointers.** Sheets §2.1 seals titles under its DEK; a mail "title" is a subject line — *"Re: your termination"*. Any indexer needs Sheets-grade custody. | **Survives, and is ruled through** (ruling 1). The instance decrypts under its own DEK and hands the title over for one render. The home stores nothing, so it never acquires the custody obligation — §3. **Also kills the stale-title leak** the verdict names ("Project Blue" still listed under its old name), which no cached copy can avoid and a pull cannot produce. |
249| 3 | **`visible_to` is not a list, it is the grants join** — team rows plus individual grants minus removals. Either the app fans out one membership change to N item pushes, or the indexer re-implements the rule and drifts, failing as *a plausible empty list, not an error*. | **Dissolved by the inversion.** Nobody posts an ACL because nobody posts. The instance evaluates its own join at read time for the person in the assertion. There is no second evaluator and no copy to drift from. |
250| 4 | **"Seconds" for a revoke is a median with an unbounded tail.** The carrier is *Sheets'* outbox, and Sheets §9.1.1 says *"for a quiet team a failed upsert may wait weeks for an organic wake, which is acceptable precisely because the directory is a hint"* — an argument that does not transfer to a rendered list. | **Dissolved by the inversion.** Revoke is instant because the answer is computed after it, every time. The only residue is the RAM cache TTL — §7. |
251| 5 | **Option B's real costs:** a scriptless page blocking on four cold wakes (*confirmed*), and a visibility endpoint that must not become an enumeration oracle (*suspected*). | **The first is designed for, in the shape the verdict itself pre-approved** — *"not disqualifying if escritorio renders what it has after a deadline and marks the rest 'waking', but that is a design the brief does not describe."* §6 and §8 describe it. **The second is answered in §5.1**, which the first draft of this spec missed. It is treated as real despite being marked suspected, because the cost of the answer is one gate. |
252| 8 | **Blocked on §13 step 4**, not started. | **Still true.** §10. |
253
254**Finding 6 does not evaporate — it argues for §3**, and an earlier draft of
255this spec wrongly filed it as escritorio-specific. Its principle, quoted
256there against escritorio's per-team defence:
257
258> **"Blast radius is bounded by what a service holds, not by how many
259> processes it runs in."**
260
261That is the same move §3's amendment makes, pointed the other way. Custody is
262what counts, not topology — which is exactly why *"never renders"* was never
263the protection and *"never stores"* is. It is the verdict's own principle
264doing this design's work, and it is the one sentence to cite if only one is
265cited.
266
267Finding 7 (guests) is handled in §5.2 and §8. Finding 9's fourth option is
268this design.
269
270**The verdict recommended this mechanism.** Its words: *"a pull-based,
271stateless page owned by one existing app… fetched from the sibling with the
272viewer's identity, fail-soft on cold wake, nothing stored"* — Option B's
273mechanism with the escritorio process deleted.
274
275**The one place this design departs from it**, named because a reviewer who
276knows the verdict will reach for it: the verdict put that page inside an
277existing app, and this puts it at the home. §2.1.
278
279### 2.1 Why the home, and why a rendered page is not a held graph
280
281The verdict's finding 1 said a per-team feed gives "three lists" and answers
282a different question than the one asked. **Ruling 18 accepts three lists**,
283so this design now takes the verdict's *second* path — the want is declined
284in its original form — rather than its first. An earlier draft of this
285section opened by calling finding 1 "the strongest objection available"; it
286is no longer an objection at all, it is the shape.
287
288What still needs answering is §5.4, and home spec §13 step 10 asks for that
289reopening to be revisited rather than left standing.
290
291**Why the home and not an app — re-justified, because Paul's per-team
292ruling retired the argument this section used to make.** That argument was:
293only the home knows every team you belong to, so only the home can answer
294*"my things"* rather than *"this team's things"*. Per-team scope concedes
295exactly that question, so the argument is gone and is recorded here as gone.
296
297What survives is narrower and still uniquely the home's — **and it is the
298credential, not the knowledge.**
299
300An earlier draft of this paragraph said "only the home knows which apps a
301team has instances in", and answered an alternative nobody proposed (Sheets
302holding Docs' grants, which would indeed be federation). Both are wrong. The
303knowledge is not exclusive: home spec §9's call 4b lets an instance learn a
304sibling exists, and the springboard shows the viewer anyway. And the
305verdict's actual recommendation was not grant-holding — it was "a
306pull-based, stateless page owned by one existing app… fetched from the
307sibling with the viewer's identity", which holds nothing.
308
309**That alternative fails on the credential.** "With the viewer's identity"
310means Sheets presenting something at `acme.docs` that speaks for *you* — so
311the home would have to mint an `aud=acme.docs` assertion and hand it to
312Sheets. §11's first row names that exactly: a cross-instance bearer is full
313impersonation, and it is the one thing this design refuses to build. Only
314the credential origin mints, so only a page at the home can hold a
315viewer-bound assertion per sibling without any instance ever holding one for
316another.
317
318**That is not the whole story, and an earlier draft said it was.** There are
319three shapes of "fetched from the sibling with the viewer's identity", and
320only one of them needs a cross-instance bearer. All three are named here,
321because §2.2 is a doctrine clause and amending it without weighing the
322alternative that needs no amendment would be the wrong way to get there.
323
324- **Browser-mediated composition.** The front door renders an
325 `<iframe src="https://acme.docs.oficina.works/activity">` per row the
326 viewer has. `oficina.works` is the registrable domain and the apex is not
327 on the public suffix list, so instance and front door are **same-site** and
328 the instance's own `SameSite=Lax` session cookie rides along. Zero JS, pull
329 at render, nothing at rest — **and the home never touches a title, so
330 §2.2 would stand unamended.** With it would go the RAM cache, §2.1's
331 cross-person residue, the swap and core-dump questions, the one-process
332 question, and gates 11 and 16's parsing surface. **RULED 2026-09-05 by
333 Paul: server-side pull, and amend §2.2.** What the frames cost is the
334 feature: no ordered merge across apps, only per-app panels; no server-side
335 deadline, so a cold instance is an empty frame under a heading rather than
336 ruling 5's named gap — the honesty mechanism replaced by a blank; a viewer
337 without a live session at that sibling gets a sign-in prompt where rows
338 should be, which is most guests and every first visit; a hostile instance
339 draws pixels on the suite's most public page; and §6's guest-wake billing
340 problem survives with **no home-side throttle that keeps rows on the page**
341 — the home authors the markup so it could withhold a frame, but having no
342 rows of its own it can only replace it with nothing. **And every instance
343 would have to relax its framing policy for the apex**: rastrillo sends
344 `frame-ancestors 'none'` and `X-Frame-Options: DENY` by default. The home
345 is not in the request path. Recorded as considered and costed, not as
346 overlooked.
347- **Instance → instance under the caller's own consumer credential**, passing
348 `sub`. Fails, but not on the bearer: it would have `acme.docs` trusting
349 `acme.sheets` for *who you are*, and home spec §5.4 reserves signed
350 identity claims to the credential origin. It also makes `acme.sheets` an
351 oracle over every person ref at `acme.docs`.
352- **A redirect chain** carrying rows. Not composition at all, and it puts
353 titles in URLs — history and referrers — which is exactly why §5.3 keeps
354 assertions out of query strings.
355
356**Per-team is also cheaper on every axis this design worries about.** §6's
357fan-out drops from every instance a person can reach to the apps in one
358team; §7's cache holds less; §5's oracle surface shrinks. The ruling cost
359this design its headline and improved everything else.
360
361**Why §5.4 is not breached.** §5.4 governs **what may ride an assertion into
362the home's custody**, and its two-sided test is about claims the home comes
363to hold. The failing example is a *held* document-level graph. Here:
364
365- **Nothing rides in.** The assertion carries `sub` and theme, exactly as
366 §5.3 already specifies. The activity flows the other way, in a response.
367- **Nothing is held.** §3's amendment and gate 11 are the mechanism, not the
368 intention — a grep proves it.
369- **The home learns nothing durable it did not already know.** It knew which
370 teams in which apps you belong to; that is the directory, §6. It does not
371 learn which documents you can see, because it never asks that question and
372 never records an answer.
373
374**The honest residue**, stated rather than argued away and larger than an
375earlier draft admitted: for the lifetime of one HTTP response, and for the
376RAM cache's TTL, the home's process memory holds a cross-app list of titles
377**for every person who rendered inside that window** — not for one person.
378That is a cross-person, cross-app set of recent titles, resident in the same
379process that mints sessions. §3's amendment lists cross-person aggregation
380as never licensed, and home spec §2.2 forbids the console it would build; this builds it as a data structure while forbidding it as a
381feature, and the distinction only holds because nothing reads across the
382per-person keys. **So the cache is bounded**: a maximum number of person
383entries and a maximum bytes per person, both named in §12, evicting oldest
384first.
385
386**What "in RAM" leans on, named rather than assumed.** Instances hibernate by
387process death (SIGTERM then SIGKILL), so they hold nothing. The home runs
388under a memory cap with reclaim before kill. Two platform facts were NOT
389established when this was checked on 2026-09-05: whether swap is configured,
390and whether core dumps are disabled. **Both must be settled before this
391ships** — a swapped or dumped page of titles is at rest, and §3's amendment
392is about storage, not intent. That is real, it is what
393ruling 1 bought, and it is the thing a reviewer should press on. The claim is
394not that this is nothing — it is that *transient and unreplicated* is a
395different category from *stored and shipped to a bucket*, and that §0's
396falsifiable test is the line that matters because it is the one with a test
397behind it.
398
399### 2.2 Two costs that are not knowledge, and are not covered by §2.1
400
401An earlier draft of this spec defended call 7 with "the same knowledge §6
402already gives the directory, not new knowledge". **That sentence is true and
403beside the point**, and it is retracted here rather than left for a reviewer
404to pull on — when it gives way it takes §3 with it. The accurate form:
405
406> **No new knowledge at the home; new emission to instances, and §9's uniform
407> call direction spent as a detection signal. Both disclosed.**
408
409**(a) A passive map becomes a broadcast.** §6's knowledge sits still today.
410Call 7 makes every front-door render fire an authenticated, timestamped
411request at the selected team's hosts that the viewer has rows in (§6). Each instance thereby learns
412*"this person opened their front door at 09:14:03"* — including on mornings
413they never open that app, and from a host they may have been removed from
414whose directory row has not drained.
415
416§3.3 coarsens published timestamps **to the day**, and says why: "timestamps
417correlate with events observable at instances." Call 7 hands every instance
418in the fleet a fine-grained, signed, unrepudiable version of the signal §3.3
419deliberately blunted. Note the shape: ruling 4 keeps the pepper off instances
420so one compromised instance cannot learn about the whole suite. This is a
421much weaker leak than the pepper, but it is **the same shape** — one instance
422learning about a person's life outside it — and it arrives by design rather
423than by compromise. **Mitigation is thin and named as thin:** the RAM cache
424(§7) collapses repeat renders within its TTL, so the emission is per-TTL
425rather than per-load. Nothing removes it.
426
427**(b) It spends a detection property.** home spec §9's calls, call 4b included, are uniformly
428person→home or instance→home; **zero run outward**. That uniformity is a
429security property rather than an accident of drafting: today a home making
430content-bearing requests to instances is anomalous *by construction*, so such
431traffic is evidence of compromise, visible to instances — the only parties
432positioned to see it. After call 7, an exfiltrating home is indistinguishable
433from a working home rendering front doors.
434
435§3.1's second threat model already concedes a great deal when the live host
436falls. This removes the one signal that would have told anyone it had. It is
437not recoverable by a mitigation in this spec; an instance-side anomaly rule
438(call 7 arriving for a person with no recent session) is the only lever, and
439it belongs to the instances, not here.
440
441**Neither cost is a reason not to build it** under rulings 1 and 2. Both are
442reasons the review pass (§12) should be run by someone who has read this
443section.
444
445**This is not escritorio revived.** No new app, no new process, no new store,
446no new repo. It is §11's front door — *"one page: a grid of apps, with a team switcher as
447a dropdown in the header"* (ruling 17) — growing a recency section.
448
449---
450
451## 3. The two clauses that change
452
453Nothing new goes at rest, so §0's **falsifiable** form is unamended —
454publish the database and you still learn only counts, team names,
455memberships and §3.2's list. **Its prose form does move**, and saying so is
456the honest version: "it never learns what is in anything" now has to be read
457as *never retains*. That reading belongs in §0's own gloss rather than left
458for the two documents to disagree about.
459
460§2.2's first never-do changes, and only its verb: *never renders* becomes
461***never stores***.
462
463**The replacement text is not reproduced here, and that is deliberate.** It
464lives in the home spec's §2.2 and in the suite doctrine's Appendix A.1, which
465owns the amendment as part of home ruling 23. An earlier revision of this
466section quoted the new clause in full; the doctrine's Appendix A preamble
467names exactly that as the defect — *"a replacement that puts a second copy of
468a boundary into an app spec and then adds a citation beneath it has left the
469second copy to drift"* — and it is right. **What this document owns is the
470argument for the change, below. What it does not own is the wording.** Read
471the clause where it is binding.
472
473**Landed, not proposed.** An earlier revision said "on approval, seven places
474change" and listed them. The approval came, and `main` carries the amendment
475at `2b68301`: the home spec's §2.2 and §0 gloss, and `AGENTS.md`'s "the one
476rule" and its four-things list, all read *never stores* as of 2026-09-05. The
477future tense is retired rather than merely updated, because a list of places
478still to edit, in a document whose edits have landed, is a reader's trap.
479`AGENTS.md` is no longer a binding file this design contradicts; it agrees.
480
481**§2.2's other two never-dos also change, and neither is this design's to
482land** — noted so the count in this section's title is not read as the count
483of changes to §2.2. §2.2's *never federates*
484becomes *never crosses a team*, and *never holds an authorization model*
485becomes *never holds a per-item grant* with the roster held explicitly —
486suite rulings 1 and 4, arriving as home ruling 23 on branch
487`suite-amendment`, not as part of this work. It is named here only because
488§5.1 rests on the old wording and had to be re-derived against the new (see
489§5.1), and because a reader comparing §2.2 against this document should know
490which of its changes came from where.
491
492**Why the amendment is small rather than a hole in the doctrine:**
493
494- **The escritorio verdict's own principle says custody is the thing**, not
495 topology: *"blast radius is bounded by what a service holds, not by how
496 many processes it runs in"* (finding 6, quoting home §2). It was written to
497 deny a per-team process the defence that its boundary made its holdings
498 safe. Read forward, it is the argument for this edit: if what a service
499 *holds* is what bounds the damage, then a clause about *rendering* was
500 never the protection, and one about *storing* is.
501- **The exposure is bounded by where it lands.** The front door is on the
502 **directory origin** (`oficina.works`), which already renders
503 account-authored strings — team names — and already escapes them. A title
504 is the same class of string arriving by a different route. §4's split
505 holds: **the credential origin renders none of this**, and gate 5 already
506 asserts it.
507- **"Renders" was doing custody's work.** The original clause was written to
508 stop the home *becoming a place documents live* — a support console, a
509 preview cache, an index. Storage is what makes it that. A string that
510 exists for the length of one HTTP response, in RAM, addressed to the one
511 person entitled to it, does not.
512- **It is testable.** "Never stores" has a grep behind it (home spec §12, gate 1).
513 "Never renders" had no test at all, which is a fair sign it was a
514 statement of intent rather than a mechanism.
515
516**What the amendment does not license**, recorded so it is not read wider
517than it is: no search over pulled content, no cross-person aggregation, no
518analytics, no operator view, no on-disk cache, no retry queue holding a
519response body. §11.
520
521---
522
523## 4. Call 7 — the activity pull
524
525The next call in §9's contract, and **the first that runs home →
526instance**. Every existing call is instance → home or person → home. That
527direction change is the main thing an implementer needs to hold in mind.
528
529```
530GET https://<team>.<app>.oficina.works/activity?limit=10&since=<rfc3339>
531Authorization: Bearer <assertion>
532Accept: application/json
533```
534
535**Request.** `limit` is advisory and the instance caps it. `since` is
536advisory and lets a warm instance answer cheaply; an instance that ignores it
537is still correct.
538
539**Response**, `200`:
540
541```json
542{
543 "items": [
544 {
545 "title": "Q3 Budget",
546 "url": "https://acme.sheets.oficina.works/s/9fk2",
547 "verb": "edited",
548 "actor": "Ana",
549 "at": "2026-09-04T09:12:00Z"
550 }
551 ],
552 "truncated": false
553}
554```
555
556| Field | Meaning |
557|---|---|
558| `title` | The object's display title, decrypted by the instance under its own DEK. The home renders and discards it. |
559| `url` | Absolute, and **on the answering instance's own host** — the home rejects any other, or an instance becomes an open redirector through the front door. |
560| `verb` | A **closed enumerated set**: `created`, `edited`, `commented`, `shared`. Anything else renders as `changed`, per §5.4's rule that a failed match is treated as *absent*, never as the raw value. |
561| `actor` | A display string the instance chooses. **Never an address** — the home does not hold addresses (§3.2) and must not receive one. An instance with no display name for the actor omits the field, and the row renders without an attribution. |
562| `at` | RFC 3339, from the instance's clock. Used for ordering only. |
563| `truncated` | The instance had more and capped. Rendered as "more in Sheets", never silently dropped. |
564
565**A hard body cap, with its number carrying its reason in a comment** —
566§9's contract item 6, which the home spec's first draft failed to adopt. The
567home reads at most that many bytes and treats an overrun as a failed pull
568(§7), not as a truncated parse. An instance is a consumer-supplied endpoint;
569a front door that will read an unbounded body from one has a memory-exhaustion
570bug on its most public page.
571
572**Empty is a first-class answer.** `{"items":[]}` with `200` is what a person
573with no grant gets, and what a quiet team gets. It is **not** what a broken
574team gets — see §7, which is the entire point of ruling 5.
575
576**This spec owns the wire shape, and the suite contract module does not
577duplicate it.** Suite ruling 7 puts every inter-app call in one Go module with
578golden fixtures as the interface, which raised the question of whether call 7
579moves there. Settled with the doctrine session on 2026-09-05: **it does not.**
580Call 7 is absent from the module's first step, which builds the
581sibling-to-sibling calls only, and the doctrine's contract table records that
582this section owns the shape. When call 7 is built, **the module's fixtures
583cite gates 11 to 21 rather than restate them** — the reason being the one
584suite §2 is about, that a restatement is a second copy free to drift, and a
585fixture that has drifted from the gate it was written for is worse than no
586fixture because it passes.
587
588Two consequences worth stating. **Gate 16's cacheable-response case is now a
589suite-level concern**, recorded as item 11 on the doctrine's leak list,
590because the failure lives in the platform edge *between* two apps and neither
591app's own tests would ever see it — the home's tests do not run the edge, and
592the instance's tests do not know a second person exists. And **an instance
593implementing call 7 is conforming to this document**, not to a module, until
594that module carries it; a spec is a poorer interface than a fixture, which is
595§12's problem and not a reason to move the shape somewhere it would be
596restated.
597
598---
599
600## 5. Authentication, and why it needs nothing new
601
602**Who mints, and where the key is.** Home spec §4 puts assertion minting on
603the **credential origin**, and the front door is on the **directory origin**,
604which obtains assertions like any other consumer. The fan-out therefore mints
605N assertions per render from a page that is not the minting origin. Say which
606this is: **one process serving two hostnames**, so the signing key's exposure
607is unchanged from §5.3 and no cross-origin mint call exists. **Nothing on
608main establishes that today** — AGENTS.md says one binary, and §5.3 gives the
609directory origin its own `jti` store as a verifier — so §12 carries it as a
610thing to settle. It is load-bearing: split the origins and the directory
611needs a mint call and a different key story — and note the
612consequence, that the directory origin is now also the surface parsing N
613consumer-supplied JSON bodies per render (gate 16).
614
615**The pull carries a §5.3 assertion, unchanged.** Ed25519, `aud` bound to the
616answering instance's app and host, `sub` the person ref, `iss` the credential
617origin, `kid`, 120-second expiry, single-use `jti`.
618
619The only difference from the existing flow is transport: it rides an
620`Authorization` header on a server-to-server request instead of an auto-POST
621through the browser.
622
623**Why that transport swap is safe, specifically.** An earlier draft claimed
624"every property §5.3 relies on is a property of the token, not of how it
625travelled." **That is false, and §5.3's own last line is the counter-example:**
626"Delivery is an auto-POST, not a query string, so assertions stay out of edge
627logs, browser history and referrers" (the pre-2026-09-05 wording; main now
628reads "browser history and referrers") — a property of travel, not of the
629token. The conclusion survives on the specific argument rather than the
630general one: an `Authorization` header achieves the same two things an
631auto-POST still claims — its third leg, edge logs, having been removed: §5.3's "edge logs" reason was
632retracted on main on 2026-09-05, since no platform request log carrying
633paths was found. There is no browser involved so there is no history, and there
634is no `Referer` on a server-to-server call. **What must never happen is the
635assertion moving into the query string** — the failure §5.3 was guarding
636against, which a header avoids for the same reason a POST body does.
637
638**Why not a service token.** A per-instance home→instance bearer would be a
639further credential to mint, store, rotate, revoke and leak, and it would
640prove only that the caller is the home — not *which person the call is for*.
641The assertion proves both in one artifact the suite already verifies. Ruling
6423.
643
644**The instance decides capability entirely from its own grants** — §5.5,
645verbatim and unmodified. A valid assertion for a person with no grant on that
646instance returns an empty list. **This is the answer to §2's objection 2**,
647and it is an answer precisely because the home does not participate in it.
648
649**Per-verifier `jti` stores stay sufficient.** Audience binding means an
650activity assertion for `acme.sheets` is not valid at `bigco.sheets`, so no
651shared store is needed — §5.3's reasoning carries over untouched.
652
653**The assertion will carry `roster_epoch`, and call 7 must decide nothing from
654it.** Home ruling 23 adds the epoch to §5.3's claims table as a narrow
655exception to §5.4's boundary test — narrow because the epoch is not a closed
656enumerated set and does not fail cosmetically, and admissible because the
657instance decides nothing from it: an unparseable claim is ignored, a differing
658claim buys exactly one call-8 refetch, and call 8's answer is what is
659enforced.
660
661Call 7 rides the same §5.3 assertion, so it **inherits the claim whether it
662wants it or not**. The rule for this path is therefore explicit: an instance
663answering call 7 must not withhold, include, filter or reorder a single row on
664the strength of the epoch. Refetching a roster is not a decision; withholding
665an activity row is. An instance that let the epoch change its answer would be
666an instance making a trust decision from a home-supplied claim, which is the
667exact thing §5.4 forbids and the reason the exception is written narrowly.
668The suite doctrine's §5.3 scopes the exception to the call-8 path and nothing
669else. **Gate 21.**
670
671**Rate limiting is the instance's.** The home is one caller and a
672well-behaved one, but an instance must not assume that; a compromised home is
673in scope for every instance's own limits.
674
675### 5.1 It must not become a visibility oracle
676
677The escritorio verdict's finding 5 named this against Option B, whose
678endpoint answered *"of these refs, which may person P see?"* — a shape that
679turns into an enumeration oracle the moment its caller's token leaks. **The
680first draft of this spec did not address it.**
681
682Call 7 is a narrower shape, and the narrowness is deliberate:
683
684- **The caller supplies no refs.** It asks "what is this person's, recently?"
685 and the instance chooses the rows. There is no set to probe with.
686- **The subject is not caller-chosen.** It rides a signed, audience-bound
687 assertion the home mints. Forging one means holding the credential
688 origin's Ed25519 private key, at which point the suite has lost anyway.
689- **No grant and no such person are the same answer** — `200` with an empty
690 list. An instance must not distinguish them, or the endpoint answers
691 membership questions about arbitrary person refs. **Gate 18.**
692- **Errors carry no detail.** "Not for you" and "no such thing" are one
693 response, per §12's gate 4 pattern of 404 rather than 403.
694
695**Re-derived against home ruling 23, because the ground it stood on moved.**
696§2.2 used to say the home *never holds an authorization model*, and an earlier
697reading of this section leaned on that: the home cannot be an oracle about
698who may see what, because it knows nothing about who may see what. Ruling 23
699replaces that clause with *never holds a per-item grant*, and gives the home a
700membership roster — so the home now does hold a fact about standing, and an
701argument from the home's ignorance is no longer available.
702
703The conclusion survives, on the two grounds above and only those: **the caller
704supplies no refs, and the subject is not caller-chosen.** Neither mentions
705what the home knows. This is a better footing than the one it replaces, not a
706weaker one — the old argument depended on a property the home might later
707acquire, and then the home acquired it. What the roster does add is a separate
708disclosure, priced in the suite leak list's item 9 and in home §0's amended
709gate rather than here: publishing the home's database now identifies each
710team's owners. That is a fact about *team standing*, which the home is now
711the record for; it is still not a fact about any item, and call 7 asks about
712items.
713
714**What a leaked home does yield**, said plainly: an attacker holding the
715signing key can mint an assertion for any person ref and read that person's
716recent titles from any instance they have a grant on. That is strictly less
717than what the same key already buys — it can mint a *session* at those
718instances — so call 7 widens no boundary. It is not a new hole; it is inside
719an existing one, and §2.1 of the home design already prices a compromised
720home.
721
722### 5.2 Guests
723
724A guest — `kind: guest` in the directory (§6) — has a person ref and a grant,
725so they get a front door and their guest teams appear on it. This needs no
726special case, which is itself the argument for the design: the instance
727answers from its own grants, and a guest's grants are exactly the rows it
728returns.
729
730What a guest sees is already disclosed: §6 rules that a guest grant discloses
731the granting team's name, "in the sharing app's share dialog, in words". No
732new disclosure is created here.
733
734**A link-share viewer has no person ref** (Sheets §8.3 — "a view link mints
735nothing"), so they have no front door and no rows. Correct by construction.
736
737---
738
739## 6. Fan-out, and the cost this design pays
740
741**The viewer's own directory rows in the selected team — not the team's
742apps.** The distinction is a leak, not a nicety, and an earlier draft of this
743line got it wrong. Home spec §11: "Tiles come from the viewer's own rows,
744never from the label… a guest in `acme.docs` would learn that Acme also uses
745Sheets, which is another person's row and what gate 4 forbids." Fanning out
746to the team's apps would ask `acme.sheets` on that guest's behalf, and
747ruling 5 would then render "acme · sheets — waking" on their front door —
748disclosing the very thing §11 forbids, through the honesty mechanism. §8.1
749already had this right; §6 and §2.1 did not.
750
751So a member of a team in three apps is three hosts; a guest with one row in
752that team is one host. The home fans out in
753parallel, with a per-request deadline, and renders what arrived.
754
755**Zero JS (§11).** One server-side fan-out, no progressive loading, no
756skeleton that fills in. The page is complete when it is sent.
757
758**The client is hardened explicitly, because Go's defaults are wrong for
759this.** `http.DefaultClient` has no timeout and follows redirects. Required:
760the deadline is a **context covering connect, TLS, headers and the full body
761read**, not just the response headers; **redirects are never followed** — a
7623xx is a failed pull, because an instance redirecting to a document URL would
763otherwise put that URL inside a `net/http` error string, which §3.4 of the
764home spec rules is storage; TLS verification on; and **bounded concurrency
765across all in-flight renders**, so one instance trickling bytes cannot
766exhaust the process that also serves sign-in (home spec §2.1 makes it the
767suite's single point of failure).
768
769**The cost, stated rather than buried: instances hibernate and the home does
770not.** §2.1 makes the home the one app that never sleeps; every instance is
771the opposite. §5.3 sizes assertion expiry at 120 seconds precisely because a
772cold wake is "process start plus decrypt plus migrate-check" and "can exceed
773a few seconds". So:
774
775- **The first front-door load of the day will mostly miss its deadline.** Not
776 an edge case — the normal morning.
777- **Every load warms the instances the viewer has rows in**, which changes the
778 hibernation economics the platform assumes — and **instance-hours are
779 billed to the instance's account, not to the person opening the door**. A
780 guest with one row in `acme.docs` would otherwise wake Acme's instance on
781 every front-door load, at Acme's expense. So there is a **minimum pull
782 interval per (person, instance), enforced even on an explicit refresh**;
783 the interval starts on a **completed** pull, not on an attempt, so a timed
784 out instance can be retried immediately — otherwise the refresh affordance
785 would be dead during exactly the window it exists for.
786
787**Three timers, and which governs.** The **minimum pull interval** decides
788whether a pull may be attempted at all. Within that, the **refresh TTL**
789decides whether the home bothers. The **hard maximum age** (§7) overrides
790both: past it the rows go, whatever the other two say.
791
792Neither the cold-wake latency nor the wake economics is fixed here. Both are **mitigated and disclosed**:
793
794- The RAM cache (§7) makes every load after the first instant.
795- The waking state is rendered honestly (§8), so a slow first load is legible
796 rather than mysterious or wrong.
797
798**A fan-out budget with no specified behaviour is not a budget.** §12 lists
799one as an open number. The number can wait; **what happens at the limit
800cannot**, because an un-asked instance must render as its own kind of gap or
801ruling 5 is breached silently — the person cannot tell "nothing to say" from
802"never asked". Un-asked instances render as a distinct gap with a refresh
803affordance, and the budget is per render, not per person.
804
805**Two absences, and they are not the same.** An app the registry does not
806mark as participating (§10) is **never asked and renders nothing at all** —
807it is not part of this feature. An instance dropped by the budget **would**
808have been asked and renders the distinct gap. A builder needs both sentences
809or they will collapse into one.
810
811**The named escape hatch**, if the morning experience proves miserable in
812use: the rejected hybrid — instances push a *contentless* ping (person ref,
813app, team, coarse timestamp, no title, no object ref) so the home knows which
814instances have news and wakes only those. It bounds the fan-out to instances
815that actually have something to say, at the cost of a small at-rest activity
816table the home does not have today. **Trigger: the first real complaint about
817front-door latency, or a measured p50 over one second on a warm fleet.** Not
818built now, because it adds custody to solve a problem we have not yet
819measured.
820
821---
822
823## 7. The cache is in RAM, and that is load-bearing
824
825Ruling 4. The home keeps, per person, the last successful pull per instance,
826in process memory, with a short TTL.
827
828- **Never at rest.** Not SQLite, not a file, not a platform cache, not an
829 HTTP cache header that would let an intermediary keep it. §3's amendment
830 permits rendering, not storing, and a disk cache is storing.
831- **The platform edge is a shared cache upstream of both ends, and it must
832 be closed on both.** It caches any 2xx GET whose response carries a
833 positive `max-age`, keyed on host, path and query, shared across
834 every visitor of a host — and it does **not** refuse on a request
835 `Cookie`. So: the front door sends `Cache-Control: no-store` (the home
836 spec's gate 10), **and call 7's contract requires instances to send
837 `no-store` too**. An instance answering `public, max-age=60` would have
838 the edge serve person A's rows to the home's next pull for person B with
839 the same query string, and the home's own cache key — person ref and
840 instance host — cannot see that happen. **A cacheable response is
841 therefore a failed pull**: the home discards it and renders a gap. Gate 16
842 covers it.
843- **Precedent:** §3.2 already handles readable addresses "transient in RAM
844 elsewhere". This is the same category, and it is the only category the one
845 rule leaves open.
846- **A restart empties it**, and that is correct rather than unfortunate.
847- **`since` comes only from this cache, and is never persisted.** A future
848 "remember the last pull per (person, instance) across restarts" is the
849 contentless activity table §11 forbids, arriving as an optimisation rather
850 than as a decision.
851- **Keyed by person ref and instance host**, so one person's cache can never
852 render on another's page — the same isolation gate 4 already asserts for
853 the directory.
854
855**Two ages, not one, because an earlier draft contradicted itself.** It said
856the TTL bounds staleness "by a number we choose, not by a hibernating fleet's
857wake schedule", and then said a failed pull never evicts a good cached
858answer. Both cannot hold: if failure preserves rows, a person removed from a
859team keeps seeing its titles until that instance next *answers*, which is
860exactly the hibernation tail the first sentence disclaims.
861
862- **Refresh TTL** — how long before the home tries again. Short.
863- **Hard maximum age** — after which rows are **discarded regardless of pull
864 outcome**, failure included. This is the one that bounds the lie, and it is
865 the number the removed-from-a-team case turns on.
866
867**A failed pull does not evict a good cached answer that is still inside the
868hard maximum.** Past it, the rows go and the gap is rendered (§8), because
869"here is what I knew" stops being honest once "when I knew it" is old enough
870that the grant may be gone. Gate 20 drives the hard maximum, not the refresh
871TTL — a test against the refresh TTL proves the easy half.
872
873**This section is the suite doctrine's one carve-out from "never cached", and
874the carve-out is worded the way it is because of what it costs.** Suite ruling
8753 holds that authorization is never cached at any scope — not per session, not
876per page, not for the life of a warm process. Revision 4 of that document
877reached the opposite verdict about this section in two different places, and
878the reconciliation, settled with the doctrine session on 2026-09-05, is in its
879§4.3: the rule governs an authorization **answer**, and an S2 `VISIBLE` held
880past its request is a standing capability that *grows*, because the holder
881re-uses it against items and people the app of record never spoke about. This
882cache is not that. It holds rendered output for one person, the home holds no
883grants, and it can only ever re-serve the same rows to the same person.
884
885**The distinction is real and it is not free, and both documents say so rather
886than resting on it.** Inside the cache's life a person removed from a team
887still sees titles; calling that "not cached authorization" would be word-play.
888That is what the hard maximum is for, and why the doctrine requires the bound
889to exist while refusing to carry the number — the number is a property of the
890consumer, and **this section is where the consumer states it**, which is why
891§12 lists it as one of the four that must be settled before this ships.
892
893**What this closes.** The doctrine listed *"the activity path's cached
894authorization after revocation"* among the things it openly had not answered.
895It is not open. It is bounded, the two ages are the bound, and the remaining
896work is a number rather than a rule.
897
898---
899
900## 8. What the front door renders, including when it cannot
901
902home spec §11 is now one page — a grid of apps for the selected team, with a team switcher in the header (ruling 17). This adds a recency
903section above or beside it — **layout is the front-door session's call, not
904this spec's.**
905
906Every row: title, app, team, verb, actor if known, relative time, linking to
907the instance's own URL. Merged across the viewer's own instances in the selected team,
908ordered by `at` descending,
909capped.
910
911**And then the part that matters.** Ruling 5:
912
913> **An instance that did not answer must be visually distinguishable from an
914> instance with nothing to say.** A quiet team and a broken team must never
915> render the same.
916
917This is the direct answer to the failure mode Fable named — *a plausible
918empty list, not an error*. A feed that silently drops what it could not fetch
919is the worst available shape, because it looks exactly like a slow week and
920nobody files a bug against a slow week.
921
922**The suite doctrine generalises this to every inter-app pull**, and its
923wording is worth having here because it names the pressure that breaks the
924rule rather than just the rule: *"a reliably slow sibling is a suite health
925problem the consumer surfaces, never one it routes around by caching
926authorization"* (suite §6.3). The temptation ruling 5 exists to resist is not
927laziness, it is kindness — an honest gap looks like a defect, a silent
928omission looks like a clean page, and the second one ships. That sentence is
929this ruling read back from the doctrine, and §7's hard maximum is the same
930principle applied to time rather than to a missing instance.
931
932So each instance that failed to answer gets a row of its own, in words:
933
934- `acme · sheets — waking, refresh` (deadline missed; likely cold)
935- `acme · calendar — unavailable` (error, refused, or unparseable)
936- `acme · sheets — showing what we had at 09:14` (cache served, live pull failed)
937
938Each with a refresh link, which is a plain `GET` and works with scripts off.
939
940**And when nothing at all arrives**, the page says so — *"we couldn't reach
941your apps just now"* — rather than rendering an empty feed. The empty state
942and the everything-failed state are different pages, and a person can tell
943which one they are looking at.
944
945### 8.1 A named gap discloses nothing, including to a guest
946
947Ruling 5 is the most visible thing in this document, so the objection it
948attracts is answered here rather than left to be derived. **A named gap
949renders a team and app name — does that leak, particularly to a guest?**
950
951The escritorio verdict's finding 7 (*suspected*) flags that guests fit no
952option cleanly: a guest must see their one item with no hint the team's other
953apps exist. Against ruling 5 that looks alarming. It is not:
954
955- **The fan-out set is the person's own directory rows** (§6), so the home
956 only ever names a team and app that person already has a row for.
957- **Call 3 already hands them the same list directly** — "every team across
958 every app, with `kind`". A named gap discloses strictly nothing the
959 directory would not answer if asked.
960
961So the gap is bounded by the directory, and the directory is the thing the
962front door is *for*.
963
964**One consequence to expect rather than fix.** The directory is a hint with a
965drain lag (§6), so the home will sometimes call an instance the person was
966removed from weeks ago, or one whose instance was deleted (home spec §9's call 2, second
967event). §5.5 makes that safe — the answer is an empty list, or a 404 — but
968ruling 5 then renders a named gap for a team they have left, or an
969"unavailable" for a host that no longer exists.
970
971**That is a wrong-looking page with a correct mechanism behind it**, which is
972precisely the shape that gets bug-reported and then "fixed" by widening
973something. It is recorded here so the fix, when someone reaches for it, is
974the right one: **prune the stale directory row**, never suppress the gap.
975Suppressing gaps is ruling 5 in reverse.
976
977---
978
979## 9. Testing gates
980
981Numbered from the home spec's **ten**. `main` has gates 1-10, the tenth
982being no-store (ruling 22), so these start at 11.
983
98411. **No pulled content at rest — driven through the failure paths, not the
985 happy one.** A full front-door render against seeded instances leaves no
986 title, URL or actor string in the SQLite file, the WAL, any log line,
987 error string, metric label or on-disk artifact. Extends gates 1 and 9;
988 gate 1's corpus grows a set of known titles.
989
990 **The happy path is the exception here, so testing it is close to
991 testing nothing.** §6 says the first load of the day will mostly miss its
992 deadline, which makes failure the normal morning. And the natural Go
993 spelling of a failed parse —
994 `fmt.Errorf("parsing activity from %s: %w", body, err)` — puts the entire
995 response body, every title in it, into an error string, which §3.4 rules
996 is storage. A gate that renders only well-formed responses stays green
997 while exactly that ships.
998
999 So the gate drives **canary titles through every failure path**:
1000 malformed JSON, a body over the cap, a bad `at`, a wrong content type, a
1001 `url` on a foreign host, a connection reset mid-body, and a timeout. The
1002 canary must appear in none of the places above. This is the class gate
1003 9's own review turned up three of, and each one was a guard that looked
1004 complete.
100512. **A silent instance is a visible gap** — timeout, empty body, **and a `404` from an app that DECLARED participation** (§10's registry flag), which must render as a gap rather than as nothing. With one instance timing out and
1006 one returning empty, the render distinguishes them. This gate fails if
1007 both produce the same output. **The most important test in this
1008 document.**
100913. **The activity assertion is audience-bound.** One minted for
1010 `acme.sheets.oficina.works`, replayed against `bigco.sheets.oficina.works`,
1011 fails. Extends gate 3.
101214. **Two-person isolation on the pull.** Person A's assertion returns only
1013 A's rows; a cached answer for A never renders on B's page. Extends
1014 gate 4.
1015
1016 **And the guest case, which is the one that bites.** A guest with a row
1017 in `acme.docs` only: assert the render fans out to `acme.docs` and to
1018 **nothing else in that team**, and that no gap naming `acme · sheets`
1019 appears. Fanning out by the team's apps rather than the viewer's rows
1020 would disclose that Acme uses Sheets through ruling 5's honesty
1021 mechanism — home spec §11's gate 4 concern, arriving by the back door.
1022
102315. **The credential origin renders no pulled content.** Extends gate 5.
102416. **A hostile instance response cannot hurt the front door.** Oversized
1025 body (cap enforced), a `url` on another host (rejected), an unknown
1026 `verb` (renders `changed`), HTML in `title` and `actor` (escaped), a
1027 malformed `at` (row dropped, instance marked, page still renders), **a
1028 cacheable response** (`public, max-age=…` — discarded and rendered as a
1029 gap, §7), and **a redirect** (never followed, treated as a failed pull,
1030 §6). An instance is a consumer-supplied endpoint and this page is the
1031 suite's most public surface.
103217. **Scriptless.** The recency section and every refresh link work with
1033 scripts stripped. Extends gate 6.
103418. **Not an oracle** (§5.1). A person ref with no grant and a person ref
1035 that does not exist produce byte-identical responses from an instance.
1036
103719. **One assertion, one host.** The fan-out mints a separate assertion per
1038 instance, and a test asserts that no two requests in one render carry the
1039 same `aud`. §11's first row is the reason this is a gate and not a note.
1040
104120. **The hard maximum age is enforced through failure.** Seed a cached
1042 answer, make the instance fail, advance past the hard maximum, and
1043 assert the rows are gone and a gap is rendered — not that they survive
1044 because the pull failed. A test against the refresh TTL proves the easy
1045 half; this is the half that bounds the lie (§7).
1046
104721. **`roster_epoch` changes no call-7 answer** (§5). Run the same pull twice
1048 against the same instance with the same grants, once with a matching
1049 epoch claim, once with an epoch far ahead of what the instance holds, and
1050 once with an unparseable one. All three responses must be byte-identical.
1051 An instance that filters, withholds or reorders on the epoch fails —
1052 that is a trust decision from a home-supplied claim, and §5.4 forbids it.
1053 The exception the epoch rides on is scoped to call 8; this gate is what
1054 keeps it there.
1055
1056---
1057
1058## 10. Sequencing, and what blocks it
1059
1060**Blocked, and not marginally.** Call 7 needs:
1061
1062- **§13 step 3** — assertion minting. The pull has no credential without it.
1063- **§13 step 4** — the directory and person refs. The home cannot know which
1064 instances to ask, and `sub` has nothing to carry.
1065
1066Step 2 (sign-in) has a reviewed plan on main at `docs/superpowers/plans/2026-09-03-home-signin.md` (`b92c9b8`) and cannot begin
1067until `HOME_DEK` is minted, which only Paul can do.
1068
1069**This is spec-now, build-later, and the slot is after step 4** — a step 10 in
1070§13's list. It has no claim on anything earlier, and it must not acquire one:
1071the identity layer is the suite's critical path and this is a front-door
1072nicety.
1073
1074**Consumers.** Call 7 is implemented by every instance that wants to appear.
1075Sheets is the near-term one — §10.2 there is already a recency list, so the
1076endpoint is a projection of a query it has. Docs likewise.
1077
1078**The projection is not free, and the endpoint should say so.** Sheets §2.1
1079seals titles under its DEK, so answering call 7 decrypts once per row rather
1080than reading a column. That is a real cost on a list query and it is the
1081argument for the instance capping `limit` hard and low — the front door wants
1082a handful of rows per app, not a page of them.
1083
1084**Participation is declared, not inferred.** An earlier draft said a `404`
1085from an instance means "this app does not participate", rendered as nothing.
1086That is ruling 5 exactly backwards: a participating app that loses its route
1087in a deploy, or a hostile one, would vanish silently from every front door —
1088"a plausible empty list, not an error", which is the failure ruling 5 exists
1089to prevent. `404` is already overloaded too: §5.1 uses it for "no such
1090thing" and §8.1 for a dead host rendered *unavailable*.
1091
1092So participation is **a flag in the home's compiled-in app registry** (home
1093spec §11), beside `provisionable` and `integrated`. An app that does not
1094declare it is never asked. An app that declares it and then answers `404`
1095is **a gap**, rendered as one, and gate 12 covers that case.
1096
1097---
1098
1099## 11. Deliberately not built
1100
1101| Thing | Trigger |
1102|---|---|
1103| **One assertion reused across the fan-out** | **Never.** Written here because someone will propose it in good faith, citing §6 — six signatures on a latency-critical path is exactly the optimisation §6 creates the incentive for. Ruling 3 is safe *only* because `aud` is app-plus-instance-host and §5.5 makes each instance decide from its own grants. An assertion valid at every host in a person's fleet is a cross-instance bearer, which is full impersonation everywhere they have a grant. Ed25519 signing is microseconds; the latency is the network and the cold wake. Gate 19. |
1104| The contentless-ping hybrid (§6) | A real latency complaint, or measured p50 over one second warm |
1105| Any at-rest store of pulled **content** — titles, URLs, actors | Never, under §3's amendment as written. Changing this reopens §0. |
1106| Any at-rest **contentless** activity table | Only as §6's escape hatch, on §6's trigger. It is new custody — a per-person, per-team timestamp series, which is §3.3's de-anonymisation vector at fine resolution — so it is a decision, not a performance tweak. |
1107| Search across the feed | Never at the home. `memoria` is where cross-corpus search lives, and it has its own custody story. |
1108| Filtering by app, team, person, or date | Someone asks. It is rendering, not custody, so it is cheap — but the front door is a front door. |
1109| Notifications, unread counts, marking read | Any of these needs per-person read state at the home, which is new custody. Argue it separately. |
1110| Calendar and mail as sources | `oficina/calendar` has a repo and no spec; `oficina/mail` has neither. And the structural reason, not just the missing spec: an `integrated: false` app verifies no §5.3 assertion (home §11, §14), so it cannot answer call 7 at all. |
1111| An operator view of anyone's feed | Never (§2.2, §0). |
1112
1113---
1114
1115## 12. Open
1116
1117- **The review pass.** AGENTS.md requires the highest-capability model
1118 available, recorded by name. **Run — four passes by Fable 5.1, the fourth
1119 approving.** The three that returned CHANGES NEEDED are summarised in the
1120 status block. What remains open below is design, not review. **Reopened in
1121 one respect:** all four passes predate the coordination revision, so §1.1,
1122 §3's retirement of the future tense, §4's module paragraph, §5's epoch rule,
1123 §5.1's re-derivation, §7's carve-out and gate 21 carry no review at all.
1124- ~~`AGENTS.md` is stale against the home spec on three counts.~~
1125 **Closed 2026-09-05:** `main` carries all three retractions at `5a42f2d`,
1126 and `AGENTS.md` there is current.
1127- **Numbers.** The per-instance deadline and the body cap are tunables with
1128 specified behaviour at the limit, and can be chosen from measurement. Three
1129 are **not** merely tunables and must be settled before this ships:
1130 - the **hard maximum cache age** (§7), which is what bounds how long a
1131 person removed from a team keeps seeing its titles — the refresh TTL
1132 does not;
1133 - the **cache bounds** (§2.1): maximum person entries and maximum bytes per
1134 person, since the cache is cross-person;
1135 - the **minimum pull interval** per (person, instance) (§6), which is what
1136 stops a guest's front door billing someone else's account;
1137 - the **refresh TTL** (§7), which home spec §15 lists as open here and
1138 which the earlier draft of this bullet dropped from both lists.
1139
1140 The fan-out budget's *number* can wait; its behaviour at the limit is
1141 specified in §6 and is not open.
1142- **An upstream discrepancy to file on `oficina/suite`.** Its call-7 row
1143 says fixtures cite this spec's gates **10–20**; this spec's gates run
1144 **11–21** — there is no gate 10 here (the home spec owns it) and gate 21
1145 is new. §4's "gates 11 to 21" is right; the doctrine's row is not.
1146- **Two upstream stalenesses, also to file.** The doctrine's §14 first
1147 bullet still calls ruling 15 unruled while its own §1 attributes it to
1148 Paul; and §1.1 consumes 15 as settled.
1149- **Whether the credential and directory origins are one process.** §5
1150 assumes they are, and the signing-key story depends on it. Nothing on
1151 main says either way.
1152- **Two platform facts** the "in RAM" claim leans on and which were not
1153 established: whether swap is configured, and whether core dumps are
1154 disabled. Both must be answered before this ships (§2.1). Both are
1155 questions for the platform, not a guess in a spec. *(An orphaned fragment
1156 of this bullet, left by an earlier edit, is repaired here rather than
1157 deleted, since deleting it would have silently dropped the clause.)*
1158- ~~**Which commit revision 4 of the suite doctrine is.**~~ **Closed
1159 2026-09-05.** It was neither of the two named. Revision 4 was `210312f`;
1160 **`574cc84` then changed a §14 open item and A.1's ruling number 22→23,
1161 and `bf0eeb8` changed §4.3's call-7 carve-out and §5.3's epoch scoping**
1162 — the second of those is squarely normative and this spec consumes it —
1163 **all without bumping the revision, which that document's own gate 9
1164 forbids.** It became revision 5 at `f627359`, and is revision 7 at
1165 `2b78680` as this lands; **§1.1 carries the current citation** rather
1166 than this bullet, which is why this bullet no longer names one. Verified against that
1167 repository's `main` and its checkout rather than taken on report. The
1168 cause is recorded in §1.1 because it is a repository hazard rather than a
1169 spec one. Checked and clear on this spec's side: its only memoria reference
1170 is a general one in §11 and does not touch memoria decisions 13 or 14, its
1171 gate 26 or its §8.4 — the four the doctrine found resolving only on an
1172 unmerged worktree, and which remain open at `oficina/memoria` discussion 1.
1173- **When call 7 joins the suite contract module** (§4). It is deliberately
1174 absent from the module's first step, so until it arrives an instance
1175 implementing call 7 conforms to a document rather than to a fixture. That is
1176 a worse interface, accepted for now because the alternative was restating
1177 the shape in two places.
1178- **Front-door layout** is the other session's. This spec deliberately does
1179 not draw it.
1180- **Whether `actor` is worth having at all.** It is the field most likely to
1181 be absent in practice (an instance needs a display name it may not hold),
1182 and a feed of "Q3 Budget — edited" reads fine without it.
1183