| 1 | # 🤖 Activity on the front door — design |
| 2 | |
| 3 | **Status:** design, 2026-09-05, revised three times the same day and once |
| 4 | again to coordinate with the suite sharing doctrine (see below). |
| 5 | Drafted by the activity-stream session (`claude-opus-5`). |
| 6 | **The coordination revision was reviewed 2026-09-05 by Fable 5.1**, which |
| 7 | returned CHANGES NEEDED on two blocking findings — both of them §1.1's own |
| 8 | rule turned against it: an "is `main`" assertion that held for fifty-three |
| 9 | seconds, and a second that was true for forty-five seconds and went false |
| 10 | unnoticed. Both are fixed, |
| 11 | and the wording moved to reachability. The four passes below predate that |
| 12 | revision and are recorded separately. |
| 13 | |
| 14 | **Review passes: three, all by Fable 5.1, all returning CHANGES NEEDED.** |
| 15 | The first found six blocking (a superseded §11, §7 contradicting itself on |
| 16 | stale rows, §10's 404 rule inverting ruling 5, the shared platform edge cache |
| 17 | upstream 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 |
| 19 | set stated two ways, one of which disclosed a guest's sibling apps; §2's |
| 20 | framing backwards after the rescope; §2.1 answering an alternative nobody |
| 21 | proposed). The third found that §2.1's credential argument was not the whole |
| 22 | story — browser-mediated composition needs no amendment at all — which Paul |
| 23 | then ruled on. **The fourth pass APPROVED**, and the nits it listed are |
| 24 | applied in the commit that carries this line. |
| 25 | **Peer pass, and why it is not the review pass.** The home-spec-compliance |
| 26 | session (`claude-opus-5`) reviewed the first draft and found four things, |
| 27 | all 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 |
| 29 | failure the normal case (§9); ruling 3's safety rests entirely on `aud` |
| 30 | binding and needed a written "never" against the obvious optimisation (§11); |
| 31 | and one claim in §5 was falsified by §5.3's own last line. That session |
| 32 | wrote enough of the resulting argument to no longer be independent, and it |
| 33 | says 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 |
| 38 | were settled with that session and are reflected below: the doctrine's §4.3 |
| 39 | now carves out call 7 explicitly, the wire shape stays here rather than moving |
| 40 | into the contract module, the `roster_epoch` invalidation exception is scoped |
| 41 | away from call 7, §1.1 records what this spec consumes, and §3 stops |
| 42 | restating a clause the doctrine's Appendix A.1 owns. What changed here, and |
| 43 | what changed there, is listed at §1.1. |
| 44 | |
| 45 | **Scope:** how the home's front door (§11 of the home design) shows a person |
| 46 | what has been happening in the team they are looking at, across that |
| 47 | team's apps that they personally have rows in, without the home |
| 48 | learning 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 |
| 52 | replaces it; two clauses of it change, and §3 says which. |
| 53 | |
| 54 | --- |
| 55 | |
| 56 | ## 0. The short version |
| 57 | |
| 58 | The ask was "an activity stream API the other apps post to, so home shows a |
| 59 | feed on sign-in." **The direction inverted during design, and the API is |
| 60 | therefore not the one that was asked for.** Instances do not post. The home |
| 61 | asks each of the viewer's own instances in the selected team "what's theirs, recently?" when it renders |
| 62 | their 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 |
| 65 | the 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 |
| 67 | across the viewer's own rows in that team. An earlier draft specified a |
| 68 | cross-team merged list and argued for it; that argument is retired in §2.1 |
| 69 | rather than deleted. |
| 70 | |
| 71 | That inversion is the whole design. It is what makes the feature possible |
| 72 | inside 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 | |
| 83 | What 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 | |
| 100 | Rulings 3, 4, 5 and 7 are this session's and were flagged as such. Ruling 8 |
| 101 | is Paul's, taken after the third review found an alternative that would have |
| 102 | needed no amendment at all (§2.1). Ruling 5 is not a courtesy: §2 explains why it is the |
| 103 | load-bearing test in the document. |
| 104 | |
| 105 | --- |
| 106 | |
| 107 | ## 1.1 Consumed suite rulings |
| 108 | |
| 109 | The suite sharing doctrine's gate 9 requires a consuming spec to name the |
| 110 | document, its commit **and its revision**, and to list the rulings it takes — |
| 111 | and to *cite* rather than restate them, because a second copy of a boundary is |
| 112 | the drift the doctrine exists to stop. This section is that citation. Nothing |
| 113 | below 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 |
| 117 | repository. (Its checkout resolved the file when this was written on |
| 118 | 2026-09-05; that is an observation with a date on it, not a standing claim, |
| 119 | for the reason the next paragraph gives.) |
| 120 | |
| 121 | **"Reachable from `main`", not "is `main`" — and that wording is the point.** |
| 122 | An earlier draft of this section wrote "that commit is `main`", moved the |
| 123 | citation *because* such an assertion had gone false, and then made the same |
| 124 | assertion again: it was true for **fifty-three seconds** before revision 7 |
| 125 | landed. 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 |
| 127 | false twenty-six minutes later, unnoticed.** (An earlier draft of this |
| 128 | paragraph said it was already false when written. It was not: the claim was |
| 129 | made at 18:02:07Z and `47ae0e8` had been `main` since 18:01:22Z. That |
| 130 | correction came from the review, was taken on report, and is only right here |
| 131 | because it was checked at source afterwards — in a section about exactly that |
| 132 | failure.) A present-tense claim about |
| 133 | another repository's tip cannot survive in a document, and chasing it defeats |
| 134 | gate 9's one-revision grace outright. **Reachability is durable** — true |
| 135 | until somebody force-pushes — so that is what this spec asserts. Where a |
| 136 | present-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.** |
| 139 | Gate 9 lets a consumer sit one revision behind so it is not chased on every |
| 140 | edit, and **every ruling this spec consumes is textually unchanged from |
| 141 | revision 4 through 7** — verified step by step: revision 5 changed the status |
| 142 | block and A.1's note; revision 6 gave S6 its own §12 step and added a §6 |
| 143 | paragraph and A.4's cite-as-specification note; revision 7 moved S6 and gave |
| 144 | it a reason. **The doctrine's §1 rulings table is byte-identical between |
| 145 | revision 4 and revision 7**, so none of it reaches §1.1's list. The citation moved |
| 146 | for the wrong reason (chasing an "is `main`" assertion) and now sits at |
| 147 | revision 7 for the right one: it is the current text, and the wording no |
| 148 | longer 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 |
| 161 | legible:** the doctrine's §4.3 call-7 carve-out is this §7's two ages, |
| 162 | reconciled with that session; the epoch scoping in its §5.3 was raised from |
| 163 | here; 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 |
| 165 | sentence — *"a reliably slow sibling is a suite health problem the consumer |
| 166 | surfaces, never one it routes around by caching authorization"* — is ruling 5 |
| 167 | generalised, 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 |
| 170 | cited 4.** Two commits after revision 4 changed normative text without bumping |
| 171 | the revision: `574cc84` and `bf0eeb8`, the second carrying the §4.3 carve-out |
| 172 | and the §5.3 epoch scoping that this spec consumes. The doctrine's own gate 9 |
| 173 | requires a bump on any normative change, so revision 4 was being cited while |
| 174 | its text moved underneath the citation. Raised from here after this spec was |
| 175 | handed one commit for revision 4 while this repository's `suite-amendment` |
| 176 | branch carried another, `210312f`, and neither could be confirmed; the doctrine |
| 177 | session 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 |
| 180 | was correct when those citations were written and sat inside gate 9's |
| 181 | one-revision grace. That grace has since been spent twice over: the home |
| 182 | spec's own ruling 23 landed on `main` citing revision 4, and the Docs and |
| 183 | Sheets amendments — unmerged, on their `suite-amendment` branches — still do. |
| 184 | **Three revisions behind fails gate 9**, which allows one. This spec and the |
| 185 | home spec move together in this commit; the other two are named here so they |
| 186 | are not forgotten. |
| 187 | |
| 188 | **What made this hard to see, recorded because it will recur.** For most of |
| 189 | 2026-09-05 the doctrine existed only on that repository's origin and in its |
| 190 | worktrees: every merge went to the remote, and nothing updated the shared |
| 191 | checkout, which sat at `291d957` with an **empty specs directory**. A reader |
| 192 | going to the repository on disk found no document at all, and the only |
| 193 | readable text was a worktree — which is why the first coordination revision |
| 194 | cited a working tree rather than a commit and said so. The checkout is now |
| 195 | fast-forwarded and the file resolved when this was written on 2026-09-05 — |
| 196 | dated, 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 |
| 200 | you did not just write, and fast-forward the shared checkout after you merge. |
| 201 | It is cited here rather than restated, for the same reason §1.1 exists — this |
| 202 | paragraph records why the citation in this spec was hard to get right, and |
| 203 | that file records what everyone must now do about it. What the rule guards |
| 204 | against is not the empty directory, which is loud, but the quiet version: a |
| 205 | session reading a file a few commits behind, citing a hash that resolves, and |
| 206 | being wrong about the text. Gate 9 checks that a citation's revision belongs |
| 207 | to its commit; **nothing checks that the reader read the commit**, and that |
| 208 | gap 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 |
| 215 | 2026-09-02 Paul proposed `escritorio` — a per-team app aggregating sheets, |
| 216 | docs, calendar and mail into one list, apps posting entries opt-in, scoped to |
| 217 | pointers only. Three visibility options were put to a Fable review: **A**, |
| 218 | the app posts the ACL and escritorio filters locally; **B**, escritorio asks |
| 219 | each app at render time; **C**, A plus a priority push on revoke. The verdict |
| 220 | was nine severity-ordered findings, each marked **confirmed** or |
| 221 | **suspected**, and a **conditional** recommendation — not "park it", which |
| 222 | was a later paraphrase: *"Do not build escritorio now; if a merged list must |
| 223 | exist, it is Option B's mechanism without Option B's process."* Paul parked |
| 224 | it and put the home's identity layer first. |
| 225 | |
| 226 | **Findings are quoted with their own confidence marking below.** The verdict |
| 227 | distinguishes what it verified against a file from what it reasoned to, and |
| 228 | flattening that distinction is how a citation degrades into a recollection. |
| 229 | |
| 230 | **Both sources are in this repo**, because the citation was otherwise |
| 231 | uncheckable — they existed only as local files in a sweepable cache |
| 232 | directory, and the verdict only inside a subagent transcript. No discussion |
| 233 | was 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 |
| 239 | written from a session's private memory before the originals were found; its |
| 240 | wording is that session's, not Fable's. These two are the contemporaneous |
| 241 | documents. Quote from them. |
| 242 | |
| 243 | The 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 |
| 255 | this spec wrongly filed it as escritorio-specific. Its principle, quoted |
| 256 | there 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 | |
| 261 | That is the same move §3's amendment makes, pointed the other way. Custody is |
| 262 | what counts, not topology — which is exactly why *"never renders"* was never |
| 263 | the protection and *"never stores"* is. It is the verdict's own principle |
| 264 | doing this design's work, and it is the one sentence to cite if only one is |
| 265 | cited. |
| 266 | |
| 267 | Finding 7 (guests) is handled in §5.2 and §8. Finding 9's fourth option is |
| 268 | this design. |
| 269 | |
| 270 | **The verdict recommended this mechanism.** Its words: *"a pull-based, |
| 271 | stateless page owned by one existing app… fetched from the sibling with the |
| 272 | viewer's identity, fail-soft on cold wake, nothing stored"* — Option B's |
| 273 | mechanism with the escritorio process deleted. |
| 274 | |
| 275 | **The one place this design departs from it**, named because a reviewer who |
| 276 | knows the verdict will reach for it: the verdict put that page inside an |
| 277 | existing 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 | |
| 281 | The verdict's finding 1 said a per-team feed gives "three lists" and answers |
| 282 | a different question than the one asked. **Ruling 18 accepts three lists**, |
| 283 | so this design now takes the verdict's *second* path — the want is declined |
| 284 | in its original form — rather than its first. An earlier draft of this |
| 285 | section opened by calling finding 1 "the strongest objection available"; it |
| 286 | is no longer an objection at all, it is the shape. |
| 287 | |
| 288 | What still needs answering is §5.4, and home spec §13 step 10 asks for that |
| 289 | reopening to be revisited rather than left standing. |
| 290 | |
| 291 | **Why the home and not an app — re-justified, because Paul's per-team |
| 292 | ruling retired the argument this section used to make.** That argument was: |
| 293 | only 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 |
| 295 | exactly that question, so the argument is gone and is recorded here as gone. |
| 296 | |
| 297 | What survives is narrower and still uniquely the home's — **and it is the |
| 298 | credential, not the knowledge.** |
| 299 | |
| 300 | An earlier draft of this paragraph said "only the home knows which apps a |
| 301 | team has instances in", and answered an alternative nobody proposed (Sheets |
| 302 | holding Docs' grants, which would indeed be federation). Both are wrong. The |
| 303 | knowledge is not exclusive: home spec §9's call 4b lets an instance learn a |
| 304 | sibling exists, and the springboard shows the viewer anyway. And the |
| 305 | verdict's actual recommendation was not grant-holding — it was "a |
| 306 | pull-based, stateless page owned by one existing app… fetched from the |
| 307 | sibling with the viewer's identity", which holds nothing. |
| 308 | |
| 309 | **That alternative fails on the credential.** "With the viewer's identity" |
| 310 | means Sheets presenting something at `acme.docs` that speaks for *you* — so |
| 311 | the home would have to mint an `aud=acme.docs` assertion and hand it to |
| 312 | Sheets. §11's first row names that exactly: a cross-instance bearer is full |
| 313 | impersonation, and it is the one thing this design refuses to build. Only |
| 314 | the credential origin mints, so only a page at the home can hold a |
| 315 | viewer-bound assertion per sibling without any instance ever holding one for |
| 316 | another. |
| 317 | |
| 318 | **That is not the whole story, and an earlier draft said it was.** There are |
| 319 | three shapes of "fetched from the sibling with the viewer's identity", and |
| 320 | only one of them needs a cross-instance bearer. All three are named here, |
| 321 | because §2.2 is a doctrine clause and amending it without weighing the |
| 322 | alternative 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 |
| 357 | fan-out drops from every instance a person can reach to the apps in one |
| 358 | team; §7's cache holds less; §5's oracle surface shrinks. The ruling cost |
| 359 | this design its headline and improved everything else. |
| 360 | |
| 361 | **Why §5.4 is not breached.** §5.4 governs **what may ride an assertion into |
| 362 | the home's custody**, and its two-sided test is about claims the home comes |
| 363 | to 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 |
| 375 | earlier draft admitted: for the lifetime of one HTTP response, and for the |
| 376 | RAM 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. |
| 378 | That is a cross-person, cross-app set of recent titles, resident in the same |
| 379 | process that mints sessions. §3's amendment lists cross-person aggregation |
| 380 | as 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 |
| 381 | feature, and the distinction only holds because nothing reads across the |
| 382 | per-person keys. **So the cache is bounded**: a maximum number of person |
| 383 | entries and a maximum bytes per person, both named in §12, evicting oldest |
| 384 | first. |
| 385 | |
| 386 | **What "in RAM" leans on, named rather than assumed.** Instances hibernate by |
| 387 | process death (SIGTERM then SIGKILL), so they hold nothing. The home runs |
| 388 | under a memory cap with reclaim before kill. Two platform facts were NOT |
| 389 | established when this was checked on 2026-09-05: whether swap is configured, |
| 390 | and whether core dumps are disabled. **Both must be settled before this |
| 391 | ships** — a swapped or dumped page of titles is at rest, and §3's amendment |
| 392 | is about storage, not intent. That is real, it is what |
| 393 | ruling 1 bought, and it is the thing a reviewer should press on. The claim is |
| 394 | not that this is nothing — it is that *transient and unreplicated* is a |
| 395 | different category from *stored and shipped to a bucket*, and that §0's |
| 396 | falsifiable test is the line that matters because it is the one with a test |
| 397 | behind it. |
| 398 | |
| 399 | ### 2.2 Two costs that are not knowledge, and are not covered by §2.1 |
| 400 | |
| 401 | An earlier draft of this spec defended call 7 with "the same knowledge §6 |
| 402 | already gives the directory, not new knowledge". **That sentence is true and |
| 403 | beside the point**, and it is retracted here rather than left for a reviewer |
| 404 | to 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. |
| 410 | Call 7 makes every front-door render fire an authenticated, timestamped |
| 411 | request 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 |
| 413 | they never open that app, and from a host they may have been removed from |
| 414 | whose directory row has not drained. |
| 415 | |
| 416 | §3.3 coarsens published timestamps **to the day**, and says why: "timestamps |
| 417 | correlate with events observable at instances." Call 7 hands every instance |
| 418 | in the fleet a fine-grained, signed, unrepudiable version of the signal §3.3 |
| 419 | deliberately blunted. Note the shape: ruling 4 keeps the pepper off instances |
| 420 | so one compromised instance cannot learn about the whole suite. This is a |
| 421 | much weaker leak than the pepper, but it is **the same shape** — one instance |
| 422 | learning about a person's life outside it — and it arrives by design rather |
| 423 | than 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 |
| 425 | rather than per-load. Nothing removes it. |
| 426 | |
| 427 | **(b) It spends a detection property.** home spec §9's calls, call 4b included, are uniformly |
| 428 | person→home or instance→home; **zero run outward**. That uniformity is a |
| 429 | security property rather than an accident of drafting: today a home making |
| 430 | content-bearing requests to instances is anomalous *by construction*, so such |
| 431 | traffic is evidence of compromise, visible to instances — the only parties |
| 432 | positioned to see it. After call 7, an exfiltrating home is indistinguishable |
| 433 | from a working home rendering front doors. |
| 434 | |
| 435 | §3.1's second threat model already concedes a great deal when the live host |
| 436 | falls. This removes the one signal that would have told anyone it had. It is |
| 437 | not 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 |
| 439 | it belongs to the instances, not here. |
| 440 | |
| 441 | **Neither cost is a reason not to build it** under rulings 1 and 2. Both are |
| 442 | reasons the review pass (§12) should be run by someone who has read this |
| 443 | section. |
| 444 | |
| 445 | **This is not escritorio revived.** No new app, no new process, no new store, |
| 446 | no new repo. It is §11's front door — *"one page: a grid of apps, with a team switcher as |
| 447 | a dropdown in the header"* (ruling 17) — growing a recency section. |
| 448 | |
| 449 | --- |
| 450 | |
| 451 | ## 3. The two clauses that change |
| 452 | |
| 453 | Nothing new goes at rest, so §0's **falsifiable** form is unamended — |
| 454 | publish the database and you still learn only counts, team names, |
| 455 | memberships and §3.2's list. **Its prose form does move**, and saying so is |
| 456 | the honest version: "it never learns what is in anything" now has to be read |
| 457 | as *never retains*. That reading belongs in §0's own gloss rather than left |
| 458 | for 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 |
| 464 | lives in the home spec's §2.2 and in the suite doctrine's Appendix A.1, which |
| 465 | owns the amendment as part of home ruling 23. An earlier revision of this |
| 466 | section quoted the new clause in full; the doctrine's Appendix A preamble |
| 467 | names exactly that as the defect — *"a replacement that puts a second copy of |
| 468 | a boundary into an app spec and then adds a citation beneath it has left the |
| 469 | second copy to drift"* — and it is right. **What this document owns is the |
| 470 | argument for the change, below. What it does not own is the wording.** Read |
| 471 | the clause where it is binding. |
| 472 | |
| 473 | **Landed, not proposed.** An earlier revision said "on approval, seven places |
| 474 | change" and listed them. The approval came, and `main` carries the amendment |
| 475 | at `2b68301`: the home spec's §2.2 and §0 gloss, and `AGENTS.md`'s "the one |
| 476 | rule" and its four-things list, all read *never stores* as of 2026-09-05. The |
| 477 | future tense is retired rather than merely updated, because a list of places |
| 478 | still 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 |
| 482 | land** — noted so the count in this section's title is not read as the count |
| 483 | of changes to §2.2. §2.2's *never federates* |
| 484 | becomes *never crosses a team*, and *never holds an authorization model* |
| 485 | becomes *never holds a per-item grant* with the roster held explicitly — |
| 486 | suite 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 |
| 490 | which 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 |
| 517 | than it is: no search over pulled content, no cross-person aggregation, no |
| 518 | analytics, no operator view, no on-disk cache, no retry queue holding a |
| 519 | response body. §11. |
| 520 | |
| 521 | --- |
| 522 | |
| 523 | ## 4. Call 7 — the activity pull |
| 524 | |
| 525 | The next call in §9's contract, and **the first that runs home → |
| 526 | instance**. Every existing call is instance → home or person → home. That |
| 527 | direction change is the main thing an implementer needs to hold in mind. |
| 528 | |
| 529 | ``` |
| 530 | GET https://<team>.<app>.oficina.works/activity?limit=10&since=<rfc3339> |
| 531 | Authorization: Bearer <assertion> |
| 532 | Accept: application/json |
| 533 | ``` |
| 534 | |
| 535 | **Request.** `limit` is advisory and the instance caps it. `since` is |
| 536 | advisory and lets a warm instance answer cheaply; an instance that ignores it |
| 537 | is 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 |
| 567 | home 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; |
| 569 | a front door that will read an unbounded body from one has a memory-exhaustion |
| 570 | bug on its most public page. |
| 571 | |
| 572 | **Empty is a first-class answer.** `{"items":[]}` with `200` is what a person |
| 573 | with no grant gets, and what a quiet team gets. It is **not** what a broken |
| 574 | team 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 |
| 577 | duplicate it.** Suite ruling 7 puts every inter-app call in one Go module with |
| 578 | golden fixtures as the interface, which raised the question of whether call 7 |
| 579 | moves there. Settled with the doctrine session on 2026-09-05: **it does not.** |
| 580 | Call 7 is absent from the module's first step, which builds the |
| 581 | sibling-to-sibling calls only, and the doctrine's contract table records that |
| 582 | this section owns the shape. When call 7 is built, **the module's fixtures |
| 583 | cite gates 11 to 21 rather than restate them** — the reason being the one |
| 584 | suite §2 is about, that a restatement is a second copy free to drift, and a |
| 585 | fixture that has drifted from the gate it was written for is worse than no |
| 586 | fixture because it passes. |
| 587 | |
| 588 | Two consequences worth stating. **Gate 16's cacheable-response case is now a |
| 589 | suite-level concern**, recorded as item 11 on the doctrine's leak list, |
| 590 | because the failure lives in the platform edge *between* two apps and neither |
| 591 | app's own tests would ever see it — the home's tests do not run the edge, and |
| 592 | the instance's tests do not know a second person exists. And **an instance |
| 593 | implementing call 7 is conforming to this document**, not to a module, until |
| 594 | that 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 |
| 596 | restated. |
| 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 |
| 603 | the **credential origin**, and the front door is on the **directory origin**, |
| 604 | which obtains assertions like any other consumer. The fan-out therefore mints |
| 605 | N assertions per render from a page that is not the minting origin. Say which |
| 606 | this is: **one process serving two hostnames**, so the signing key's exposure |
| 607 | is unchanged from §5.3 and no cross-origin mint call exists. **Nothing on |
| 608 | main establishes that today** — AGENTS.md says one binary, and §5.3 gives the |
| 609 | directory origin its own `jti` store as a verifier — so §12 carries it as a |
| 610 | thing to settle. It is load-bearing: split the origins and the directory |
| 611 | needs a mint call and a different key story — and note the |
| 612 | consequence, that the directory origin is now also the surface parsing N |
| 613 | consumer-supplied JSON bodies per render (gate 16). |
| 614 | |
| 615 | **The pull carries a §5.3 assertion, unchanged.** Ed25519, `aud` bound to the |
| 616 | answering instance's app and host, `sub` the person ref, `iss` the credential |
| 617 | origin, `kid`, 120-second expiry, single-use `jti`. |
| 618 | |
| 619 | The 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 |
| 621 | through 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 |
| 625 | travelled." **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 |
| 627 | logs, browser history and referrers" (the pre-2026-09-05 wording; main now |
| 628 | reads "browser history and referrers") — a property of travel, not of the |
| 629 | token. The conclusion survives on the specific argument rather than the |
| 630 | general one: an `Authorization` header achieves the same two things an |
| 631 | auto-POST still claims — its third leg, edge logs, having been removed: §5.3's "edge logs" reason was |
| 632 | retracted on main on 2026-09-05, since no platform request log carrying |
| 633 | paths was found. There is no browser involved so there is no history, and there |
| 634 | is no `Referer` on a server-to-server call. **What must never happen is the |
| 635 | assertion moving into the query string** — the failure §5.3 was guarding |
| 636 | against, 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 |
| 639 | further credential to mint, store, rotate, revoke and leak, and it would |
| 640 | prove only that the caller is the home — not *which person the call is for*. |
| 641 | The assertion proves both in one artifact the suite already verifies. Ruling |
| 642 | 3. |
| 643 | |
| 644 | **The instance decides capability entirely from its own grants** — §5.5, |
| 645 | verbatim and unmodified. A valid assertion for a person with no grant on that |
| 646 | instance returns an empty list. **This is the answer to §2's objection 2**, |
| 647 | and 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 |
| 650 | activity assertion for `acme.sheets` is not valid at `bigco.sheets`, so no |
| 651 | shared 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 |
| 654 | it.** Home ruling 23 adds the epoch to §5.3's claims table as a narrow |
| 655 | exception to §5.4's boundary test — narrow because the epoch is not a closed |
| 656 | enumerated set and does not fail cosmetically, and admissible because the |
| 657 | instance decides nothing from it: an unparseable claim is ignored, a differing |
| 658 | claim buys exactly one call-8 refetch, and call 8's answer is what is |
| 659 | enforced. |
| 660 | |
| 661 | Call 7 rides the same §5.3 assertion, so it **inherits the claim whether it |
| 662 | wants it or not**. The rule for this path is therefore explicit: an instance |
| 663 | answering call 7 must not withhold, include, filter or reorder a single row on |
| 664 | the strength of the epoch. Refetching a roster is not a decision; withholding |
| 665 | an activity row is. An instance that let the epoch change its answer would be |
| 666 | an instance making a trust decision from a home-supplied claim, which is the |
| 667 | exact thing §5.4 forbids and the reason the exception is written narrowly. |
| 668 | The suite doctrine's §5.3 scopes the exception to the call-8 path and nothing |
| 669 | else. **Gate 21.** |
| 670 | |
| 671 | **Rate limiting is the instance's.** The home is one caller and a |
| 672 | well-behaved one, but an instance must not assume that; a compromised home is |
| 673 | in scope for every instance's own limits. |
| 674 | |
| 675 | ### 5.1 It must not become a visibility oracle |
| 676 | |
| 677 | The escritorio verdict's finding 5 named this against Option B, whose |
| 678 | endpoint answered *"of these refs, which may person P see?"* — a shape that |
| 679 | turns into an enumeration oracle the moment its caller's token leaks. **The |
| 680 | first draft of this spec did not address it.** |
| 681 | |
| 682 | Call 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 |
| 697 | reading of this section leaned on that: the home cannot be an oracle about |
| 698 | who may see what, because it knows nothing about who may see what. Ruling 23 |
| 699 | replaces that clause with *never holds a per-item grant*, and gives the home a |
| 700 | membership roster — so the home now does hold a fact about standing, and an |
| 701 | argument from the home's ignorance is no longer available. |
| 702 | |
| 703 | The conclusion survives, on the two grounds above and only those: **the caller |
| 704 | supplies no refs, and the subject is not caller-chosen.** Neither mentions |
| 705 | what the home knows. This is a better footing than the one it replaces, not a |
| 706 | weaker one — the old argument depended on a property the home might later |
| 707 | acquire, and then the home acquired it. What the roster does add is a separate |
| 708 | disclosure, priced in the suite leak list's item 9 and in home §0's amended |
| 709 | gate rather than here: publishing the home's database now identifies each |
| 710 | team's owners. That is a fact about *team standing*, which the home is now |
| 711 | the record for; it is still not a fact about any item, and call 7 asks about |
| 712 | items. |
| 713 | |
| 714 | **What a leaked home does yield**, said plainly: an attacker holding the |
| 715 | signing key can mint an assertion for any person ref and read that person's |
| 716 | recent titles from any instance they have a grant on. That is strictly less |
| 717 | than what the same key already buys — it can mint a *session* at those |
| 718 | instances — so call 7 widens no boundary. It is not a new hole; it is inside |
| 719 | an existing one, and §2.1 of the home design already prices a compromised |
| 720 | home. |
| 721 | |
| 722 | ### 5.2 Guests |
| 723 | |
| 724 | A guest — `kind: guest` in the directory (§6) — has a person ref and a grant, |
| 725 | so they get a front door and their guest teams appear on it. This needs no |
| 726 | special case, which is itself the argument for the design: the instance |
| 727 | answers from its own grants, and a guest's grants are exactly the rows it |
| 728 | returns. |
| 729 | |
| 730 | What a guest sees is already disclosed: §6 rules that a guest grant discloses |
| 731 | the granting team's name, "in the sharing app's share dialog, in words". No |
| 732 | new disclosure is created here. |
| 733 | |
| 734 | **A link-share viewer has no person ref** (Sheets §8.3 — "a view link mints |
| 735 | nothing"), 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 |
| 742 | apps.** The distinction is a leak, not a nicety, and an earlier draft of this |
| 743 | line got it wrong. Home spec §11: "Tiles come from the viewer's own rows, |
| 744 | never from the label… a guest in `acme.docs` would learn that Acme also uses |
| 745 | Sheets, which is another person's row and what gate 4 forbids." Fanning out |
| 746 | to the team's apps would ask `acme.sheets` on that guest's behalf, and |
| 747 | ruling 5 would then render "acme · sheets — waking" on their front door — |
| 748 | disclosing the very thing §11 forbids, through the honesty mechanism. §8.1 |
| 749 | already had this right; §6 and §2.1 did not. |
| 750 | |
| 751 | So a member of a team in three apps is three hosts; a guest with one row in |
| 752 | that team is one host. The home fans out in |
| 753 | parallel, with a per-request deadline, and renders what arrived. |
| 754 | |
| 755 | **Zero JS (§11).** One server-side fan-out, no progressive loading, no |
| 756 | skeleton 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 |
| 759 | this.** `http.DefaultClient` has no timeout and follows redirects. Required: |
| 760 | the deadline is a **context covering connect, TLS, headers and the full body |
| 761 | read**, not just the response headers; **redirects are never followed** — a |
| 762 | 3xx is a failed pull, because an instance redirecting to a document URL would |
| 763 | otherwise put that URL inside a `net/http` error string, which §3.4 of the |
| 764 | home spec rules is storage; TLS verification on; and **bounded concurrency |
| 765 | across all in-flight renders**, so one instance trickling bytes cannot |
| 766 | exhaust the process that also serves sign-in (home spec §2.1 makes it the |
| 767 | suite's single point of failure). |
| 768 | |
| 769 | **The cost, stated rather than buried: instances hibernate and the home does |
| 770 | not.** §2.1 makes the home the one app that never sleeps; every instance is |
| 771 | the opposite. §5.3 sizes assertion expiry at 120 seconds precisely because a |
| 772 | cold wake is "process start plus decrypt plus migrate-check" and "can exceed |
| 773 | a 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 |
| 788 | whether a pull may be attempted at all. Within that, the **refresh TTL** |
| 789 | decides whether the home bothers. The **hard maximum age** (§7) overrides |
| 790 | both: past it the rows go, whatever the other two say. |
| 791 | |
| 792 | Neither 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 |
| 799 | one as an open number. The number can wait; **what happens at the limit |
| 800 | cannot**, because an un-asked instance must render as its own kind of gap or |
| 801 | ruling 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 |
| 803 | affordance, 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 |
| 806 | mark as participating (§10) is **never asked and renders nothing at all** — |
| 807 | it is not part of this feature. An instance dropped by the budget **would** |
| 808 | have been asked and renders the distinct gap. A builder needs both sentences |
| 809 | or they will collapse into one. |
| 810 | |
| 811 | **The named escape hatch**, if the morning experience proves miserable in |
| 812 | use: the rejected hybrid — instances push a *contentless* ping (person ref, |
| 813 | app, team, coarse timestamp, no title, no object ref) so the home knows which |
| 814 | instances have news and wakes only those. It bounds the fan-out to instances |
| 815 | that actually have something to say, at the cost of a small at-rest activity |
| 816 | table the home does not have today. **Trigger: the first real complaint about |
| 817 | front-door latency, or a measured p50 over one second on a warm fleet.** Not |
| 818 | built now, because it adds custody to solve a problem we have not yet |
| 819 | measured. |
| 820 | |
| 821 | --- |
| 822 | |
| 823 | ## 7. The cache is in RAM, and that is load-bearing |
| 824 | |
| 825 | Ruling 4. The home keeps, per person, the last successful pull per instance, |
| 826 | in 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 |
| 856 | the TTL bounds staleness "by a number we choose, not by a hibernating fleet's |
| 857 | wake schedule", and then said a failed pull never evicts a good cached |
| 858 | answer. Both cannot hold: if failure preserves rows, a person removed from a |
| 859 | team keeps seeing its titles until that instance next *answers*, which is |
| 860 | exactly 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 |
| 868 | hard 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 |
| 870 | that the grant may be gone. Gate 20 drives the hard maximum, not the refresh |
| 871 | TTL — 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 |
| 874 | the carve-out is worded the way it is because of what it costs.** Suite ruling |
| 875 | 3 holds that authorization is never cached at any scope — not per session, not |
| 876 | per page, not for the life of a warm process. Revision 4 of that document |
| 877 | reached the opposite verdict about this section in two different places, and |
| 878 | the 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 |
| 880 | past its request is a standing capability that *grows*, because the holder |
| 881 | re-uses it against items and people the app of record never spoke about. This |
| 882 | cache is not that. It holds rendered output for one person, the home holds no |
| 883 | grants, 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 |
| 886 | than resting on it.** Inside the cache's life a person removed from a team |
| 887 | still sees titles; calling that "not cached authorization" would be word-play. |
| 888 | That is what the hard maximum is for, and why the doctrine requires the bound |
| 889 | to exist while refusing to carry the number — the number is a property of the |
| 890 | consumer, 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 |
| 894 | authorization after revocation"* among the things it openly had not answered. |
| 895 | It is not open. It is bounded, the two ages are the bound, and the remaining |
| 896 | work is a number rather than a rule. |
| 897 | |
| 898 | --- |
| 899 | |
| 900 | ## 8. What the front door renders, including when it cannot |
| 901 | |
| 902 | home 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 |
| 903 | section above or beside it — **layout is the front-door session's call, not |
| 904 | this spec's.** |
| 905 | |
| 906 | Every row: title, app, team, verb, actor if known, relative time, linking to |
| 907 | the instance's own URL. Merged across the viewer's own instances in the selected team, |
| 908 | ordered by `at` descending, |
| 909 | capped. |
| 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 | |
| 917 | This is the direct answer to the failure mode Fable named — *a plausible |
| 918 | empty list, not an error*. A feed that silently drops what it could not fetch |
| 919 | is the worst available shape, because it looks exactly like a slow week and |
| 920 | nobody files a bug against a slow week. |
| 921 | |
| 922 | **The suite doctrine generalises this to every inter-app pull**, and its |
| 923 | wording is worth having here because it names the pressure that breaks the |
| 924 | rule rather than just the rule: *"a reliably slow sibling is a suite health |
| 925 | problem the consumer surfaces, never one it routes around by caching |
| 926 | authorization"* (suite §6.3). The temptation ruling 5 exists to resist is not |
| 927 | laziness, it is kindness — an honest gap looks like a defect, a silent |
| 928 | omission looks like a clean page, and the second one ships. That sentence is |
| 929 | this ruling read back from the doctrine, and §7's hard maximum is the same |
| 930 | principle applied to time rather than to a missing instance. |
| 931 | |
| 932 | So 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 | |
| 938 | Each 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 |
| 941 | your apps just now"* — rather than rendering an empty feed. The empty state |
| 942 | and the everything-failed state are different pages, and a person can tell |
| 943 | which one they are looking at. |
| 944 | |
| 945 | ### 8.1 A named gap discloses nothing, including to a guest |
| 946 | |
| 947 | Ruling 5 is the most visible thing in this document, so the objection it |
| 948 | attracts is answered here rather than left to be derived. **A named gap |
| 949 | renders a team and app name — does that leak, particularly to a guest?** |
| 950 | |
| 951 | The escritorio verdict's finding 7 (*suspected*) flags that guests fit no |
| 952 | option cleanly: a guest must see their one item with no hint the team's other |
| 953 | apps 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 | |
| 961 | So the gap is bounded by the directory, and the directory is the thing the |
| 962 | front door is *for*. |
| 963 | |
| 964 | **One consequence to expect rather than fix.** The directory is a hint with a |
| 965 | drain lag (§6), so the home will sometimes call an instance the person was |
| 966 | removed from weeks ago, or one whose instance was deleted (home spec §9's call 2, second |
| 967 | event). §5.5 makes that safe — the answer is an empty list, or a 404 — but |
| 968 | ruling 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 |
| 972 | precisely the shape that gets bug-reported and then "fixed" by widening |
| 973 | something. It is recorded here so the fix, when someone reaches for it, is |
| 974 | the right one: **prune the stale directory row**, never suppress the gap. |
| 975 | Suppressing gaps is ruling 5 in reverse. |
| 976 | |
| 977 | --- |
| 978 | |
| 979 | ## 9. Testing gates |
| 980 | |
| 981 | Numbered from the home spec's **ten**. `main` has gates 1-10, the tenth |
| 982 | being no-store (ruling 22), so these start at 11. |
| 983 | |
| 984 | 11. **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. |
| 1005 | 12. **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.** |
| 1009 | 13. **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. |
| 1012 | 14. **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 | |
| 1023 | 15. **The credential origin renders no pulled content.** Extends gate 5. |
| 1024 | 16. **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. |
| 1032 | 17. **Scriptless.** The recency section and every refresh link work with |
| 1033 | scripts stripped. Extends gate 6. |
| 1034 | 18. **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 | |
| 1037 | 19. **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 | |
| 1041 | 20. **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 | |
| 1047 | 21. **`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 | |
| 1066 | Step 2 (sign-in) has a reviewed plan on main at `docs/superpowers/plans/2026-09-03-home-signin.md` (`b92c9b8`) and cannot begin |
| 1067 | until `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: |
| 1071 | the identity layer is the suite's critical path and this is a front-door |
| 1072 | nicety. |
| 1073 | |
| 1074 | **Consumers.** Call 7 is implemented by every instance that wants to appear. |
| 1075 | Sheets is the near-term one — §10.2 there is already a recency list, so the |
| 1076 | endpoint 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 |
| 1079 | seals titles under its DEK, so answering call 7 decrypts once per row rather |
| 1080 | than reading a column. That is a real cost on a list query and it is the |
| 1081 | argument for the instance capping `limit` hard and low — the front door wants |
| 1082 | a handful of rows per app, not a page of them. |
| 1083 | |
| 1084 | **Participation is declared, not inferred.** An earlier draft said a `404` |
| 1085 | from an instance means "this app does not participate", rendered as nothing. |
| 1086 | That is ruling 5 exactly backwards: a participating app that loses its route |
| 1087 | in 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 |
| 1089 | to prevent. `404` is already overloaded too: §5.1 uses it for "no such |
| 1090 | thing" and §8.1 for a dead host rendered *unavailable*. |
| 1091 | |
| 1092 | So participation is **a flag in the home's compiled-in app registry** (home |
| 1093 | spec §11), beside `provisionable` and `integrated`. An app that does not |
| 1094 | declare it is never asked. An app that declares it and then answers `404` |
| 1095 | is **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 | |