oficina / home Public

Revision after the escritorio sources surfaced, and a peer pass

The escritorio review turned out to exist only as local files in a
sweepable cache directory, with the Fable verdict buried in a subagent
transcript. Both are now in docs/superpowers/reviews/, so §2 cites
primaries instead of two sessions' memory. The nine real findings
replace the five that were recalled.

Reading the verdict strengthens the case rather than weakening it. Its
finding 1 says the cross-team list is forbidden at the home by §5.4 and
that the honest options are to reopen the ruling deliberately or decline
the want — Paul reopened it, which is the first of the two. Its
recommendation is this mechanism: a pull-based stateless page, fail-soft
on cold wake, nothing stored. Its finding 5 pre-approves the rescue this
design uses, rendering after a deadline and marking the rest waking.

Four things from a peer pass, all applied and all corrections to claims
I made:

- "No new knowledge at the home" was true and beside the point. It is
  retracted in a new §2.2, which prices the two costs it was hiding:
  call 7 turns §6's passive map into a per-render broadcast of a
  fine-grained timestamp to every instance — the signal §3.3 coarsens
  to the day on purpose — and it spends §9's uniform inbound call
  direction, after which an exfiltrating home looks like a working one.
- Gate 10 tested only the happy path, while §6 says failure is the
  normal morning. The natural Go error wrap puts the whole response
  body, titles included, into an error string, which §3.4 calls
  storage. The gate now drives canary titles through seven failure
  paths.
- Ruling 3 is safe only because of aud binding, and §6's latency
  pressure is an incentive to reuse one assertion across the fan-out —
  which would be a cross-instance bearer. Written into §11 as never,
  with gate 18 behind it.
- §5's claim that assertion safety is "a property of the token, not of
  how it travelled" is falsified by §5.3's own last line. The
  conclusion survives on a specific argument instead.

Also new: §5.1 answers the verdict's enumeration-oracle finding, which
the first draft missed; §5.2 covers guests and link viewers; §10 prices
the per-row DEK decrypt that answering call 7 costs Sheets.

Still not approved. The peer that found these wrote enough of the
argument to not be independent, and says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 64bfc70caad1293eae610897fcb9039b1ce1c489 parent 15d910d
3 files changed, +447 −41
  • docs/superpowers/reviews/2026-09-02-escritorio-fable-verdict.md +105 −0
  • docs/superpowers/reviews/2026-09-02-escritorio-review-brief.md +107 −0
  • docs/superpowers/specs/2026-09-05-activity-pull-design.md +235 −41
diff --git a/docs/superpowers/reviews/2026-09-02-escritorio-fable-verdict.md b/docs/superpowers/reviews/2026-09-02-escritorio-fable-verdict.md
new file mode 100644
index 0000000..1f12954
--- /dev/null
+++ b/docs/superpowers/reviews/2026-09-02-escritorio-fable-verdict.md
@@ -0,0 +1,105 @@
+# Review: escritorio's visibility model
+
+I read the home design spec in full, both AGENTS.md files, the relevant sections of the Sheets design spec (§2, §8.2–8.3, §9.1.1, §10.2), the home repo's actual state, and the memory notes on open Oficina decisions. Findings first, severity-ordered; recommendation at the end.
+
+## Two things the brief gets wrong about its sources
+
+**The outbox belongs to the instance, not the home, and the home does not hibernate.** The brief says "The home's spec drains *its* outbox 'on wake and on each share mutation, not on a ticker'". The spec's sentence (§6) is about the *instance's* outbox: "The outbox drains on wake and on each share mutation, not on a ticker — a hibernating instance runs no timers." And §2.1: "**The home does not hibernate.** It is the one app on the estate where cold-wake latency is a suite-wide outage." This matters for Option C: the thing that would carry a revoke is Sheets' outbox, whose stated semantics are "For a quiet team a failed upsert may wait weeks for an organic wake, which is acceptable precisely because the directory is a hint" (Sheets spec §9.1.1). **Confirmed.**
+
+**Aside, not the brief's fault:** `home/AGENTS.md` is stale against its own spec — it still says "never stores your address", "some unidentified person", and "Assertion expiry is **seconds**", all three of which the spec explicitly retracted (§0: "the first draft's absolute phrasing … was false"; §3.3: "§0 says 'some person' rather than 'some unidentified person'"; §5.3: "Expiry is 120 seconds, not 'seconds'"). AGENTS.md's own rule is "Where it disagrees with the design spec, the spec wins and this file is wrong — fix it in the same commit." Nobody did. **Confirmed.**
+
+## Findings
+
+### 1. Per-team escritorio answers a question nobody asked (Q6) — the most important finding
+
+The stated want was "a list of MY sheets and docs". A per-team escritorio gives a person in three teams three lists. That is not a partial answer; it is the *same* answer the suite already gives, one hop closer.
+
+What already exists, per the specs:
+- Sheets spec §10.2: "**Home is your sheets by recency and a New button.**" Every app instance already has a per-team recency list.
+- Home spec §11: "**The directory is the suite's front door**: your teams grouped by app, and a New button."
+
+So escritorio's job 1 (launch links) is the home's job by the home's own spec, and job 2 (per-team merged list) is a per-team union of four screens that each already exist. Its only new value is "one page instead of four, per team". That is a modest UI convenience, not a new capability.
+
+The thing actually asked for — one cross-team list — is exactly what the home spec forbids the home from holding, and says so in words: §5.4, "'Which documents you can see' passes (a) and fails (b), handing the home a document-level graph it has no business holding." A per-team escritorio does not solve this; it routes around the *appearance* of it while leaving the want unmet. If the want is real, the honest options are (i) relitigate ruling 11 / §2.2 at the home with eyes open, or (ii) tell the user the architecture rules it out and why. Building escritorio does neither. **Confirmed** against both specs.
+
+### 2. Titles are first-class sealed content in this family; "pointers only" is not a cheap concession (Q2)
+
+Sheets spec §2.1 lists under **Sealed**: "Sheet titles, tab names, comment text." Its README-published leak sentence (§2.2) is that publishing Sheets' database yields "who works with whom, how big their spreadsheets are, how often they edit and what colour they painted things. You learn **no value in any cell**" — and no title. `sheets-core/AGENTS.md` bans a sheet title from any log line alongside cell content.
+
+The escritorio index as settled ("title, app, item ref, URL, touched_at, actor person refs") therefore takes a value that Sheets seals under its own DEK and writes it into a second SQLite file, in a second process, replicated to a second bucket. And "Mail" is in the app list: an email's "title" is its subject line, which is body-grade content in anybody's threat model. "Q3 layoffs — final" was the brief's example; "Re: your termination" is Mail's.
+
+Implications:
+- Escritorio cannot be "thin, blind"; it is a content-holding app and must meet the sibling's one rule — its own DEK, sealed titles, the plaintext-grep gate, the no-title-in-logs rule. That is most of a Sheets-grade trust model for a listing page.
+- Every option's custody sentence (finding 6) includes titles unless escritorio seals them. The brief's framing treats sealing as out of scope; it is not.
+- A stale title is itself a leak shape the brief does not name: Docs renames "Acquisition of X — term sheet" to "Project Blue"; A/C show the old name until the next push. Only a revoke was considered.
+
+**Confirmed** against Sheets spec §2.1/§2.2 and AGENTS.md.
+
+### 3. Option A's `visible_to` is not a list, it is the grants join — so A and C make escritorio a second authorization evaluator (Q1, Q5)
+
+The Sheets spec says access is "a grants join, hand-written, expressing 'rows this team can see, plus rows granted to me individually, minus rows I have been removed from'" (§ on data). The brief's Option A ("each pushed entry carries the person refs who may see it") assumes visibility is a per-item property. It is not; it is a function of item grants **and team membership**. Two ways to make A work, both bad:
+
+- **The app pushes the fully expanded set per item.** Then a member joining or leaving the team changes the visibility of every team-visible item, so one `members` mutation fans out to N item pushes. And a membership change is not a "share mutation" — the outbox's only drain triggers are "wake and each share mutation", so unless the spec is widened, a removed member's list simply does not update until someone shares something. **Suspected** on the drain-trigger point (the wording is narrow; whether the implementation counts membership as a share mutation is not stated), **confirmed** on the fan-out.
+- **Escritorio holds team membership and evaluates the join itself.** Now it holds an authorization model, evaluated by a second implementation of the same rule, in a process that is not the authority. The Sheets AGENTS.md is explicit about why this rule is dangerous to re-implement: getting it wrong "fails as a plausible empty list, not as an error, which is the worst available failure shape". Two evaluators of one rule drift, and the drift shows a title to the wrong person or hides one from the right person, silently.
+
+Either way A/C hand escritorio the graph. Option C inherits all of this and adds a fast path for one of the several event types (revoke) while leaving membership removal, rename, and delete on the slow path. **Confirmed** against the grants-join wording.
+
+### 4. Option C's "seconds" is a median, and the tail is unbounded (Q4)
+
+Trace a revoke under C, using only the specs' own mechanics:
+
+1. Owner un-shares in Sheets. Sheets is awake (it is serving the click).
+2. Priority push = an HTTP call to escritorio. If escritorio is hibernating, that call **wakes it** — a request wakes an app — so the receiver hibernating is not by itself fatal. But the cold wake costs what §5.3 sizes it at: "process start plus decrypt plus migrate-check can exceed a few seconds", against a 120 s budget chosen for exactly that worst case. So the push either sits **in the request path** (the revoke response blocks on another app's cold wake — the Sheets spec forbids this shape: call 2 goes "through an outbox in the instance, never a call in the request path") or it is **asynchronous after the response**.
+3. If asynchronous, the goroutine dies at "a deploy, a hibernation eviction or a panic" (Sheets spec, verbatim on a different subsystem, same platform fact). The revoke then sits in the outbox.
+4. The outbox retries only "on wake and on each share mutation". For a quiet team that is "weeks", by the Sheets spec's own admission.
+5. Restore-from-backup: an escritorio restored from replication holds `visible_to` from before the revoke, with no mechanism to learn it missed one. The home spec had to invent a pepper epoch plus full re-announce for precisely this class of problem, and generalised it into a binding rule in both AGENTS.md files: "**Any 'prune when all consumers have caught up' is unbounded in a hibernating fleet.**"
+
+So C needs, to be honest: per-item version numbers, idempotent pushes, an epoch, a full re-announce verb, and a fail-closed rule when escritorio suspects it is stale. That is the home's directory machinery rebuilt per team — and unlike the directory, escritorio's copy is *rendered*, so the "acceptable because it's a hint" argument that makes the weeks-lag tolerable for the home does not transfer. "Seconds" describes the case where nothing goes wrong. **Confirmed** on every step's mechanics; the tail length is the Sheets spec's own number.
+
+### 5. Option B is not "secretly A"; its real problems are a new oracle endpoint and a blocking zero-JS page (Q3)
+
+The brief asks whether a short-TTL cache turns B into A. No — the two stalenesses are different in kind. A's is a persisted graph copy with an unbounded tail (finding 4). B's cache would be in-RAM, per viewer, bounded by TTL, and evicted at hibernation for free. Bounded-and-volatile is categorically better than unbounded-and-replicated. **Suspected** (reasoning).
+
+B's actual costs, which the brief does not list:
+
+- **Latency shape.** The front door for a quiet team — the team most likely to have all four apps asleep — blocks on the slowest of four parallel cold wakes, tens of seconds by §5.3's sizing, on every first load of the day. The zero-JS baseline (home spec §11, both AGENTS.md) means no progressive render: the page is blank until the last app answers. It is 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.
+- **A visibility oracle.** B requires each app to expose "of these refs, which may person P see?" to a bearer-authenticated caller. That is four new per-consumer tokens per team (Pegamento's contract: hashed bearer, allowlisted origin, body cap) and, if escritorio's token leaks, an endpoint that answers membership questions about arbitrary persons and refs. It must be designed to answer only refs the caller supplied and to 404 rather than distinguish "no such ref" from "not yours", or it becomes an enumeration oracle. Not fatal, but unpriced.
+- **B still holds titles.** Finding 2 applies in full. B fixes the ACL custody, not the content custody.
+
+**Suspected** on the oracle risk; **confirmed** on the zero-JS and cold-wake facts.
+
+### 6. The blast-radius claim is half right, and the half that is wrong matters (Q1, Q5)
+
+The claim: a per-team instance may hold what the home may not "because it shares a blast radius with the content it indexes". The home spec's own principle cuts against the framing: §2, "**Blast radius is bounded by what a service holds, not by how many processes it runs in.**" Per-team boundary is necessary but says nothing about what escritorio may hold. What it holds is titles from four apps plus (A/C) four sharing graphs. Compromising one escritorio yields **wider breadth than any single app instance** (every title across four apps) at shallower depth (no bodies). It does not exceed the team, so the cross-team aggregation §0 fears is genuinely avoided. But the home spec's feature test — "Every proposed feature is weighed against 'does this belong in an instance instead?'" — is not passed by escritorio; escritorio *is* the proposal to create a new instance whose sole purpose is aggregation.
+
+The "publish the database and see" sentence, per option:
+
+- **A and C:** *Publish a team's escritorio database and you learn the title of every sheet, document, event and mail subject the team has, who touched each and when, and for every item exactly which people — members and outside guests — may see it: the team's complete sharing graph across four apps in one file. Titles are readable unless escritorio seals them with its own DEK.*
+- **B:** *Publish a team's escritorio database and you learn the title of every item across four apps, who touched each and when, and four hashed bearer tokens. You learn nothing about who may see what.*
+
+Note Sheets already publishes its grants in plaintext (§2.2: "Membership, grants, and roles — including readable email addresses"), so A's per-item ACL is not a new *category* relative to one app; the new things are the union across four apps and the titles. **Confirmed** against §2 and Sheets §2.2.
+
+### 7. Guests and link viewers do not fit any option cleanly (Q1, Q5)
+
+Sheets §8.2: a guest "see[s] exactly the sheets granted to them and learn[s] nothing about the team beyond §9.5's disclosure". Sheets §8.3: "A view link mints nothing — read-only against a capability token; no member, no directory entry." So a link viewer has no person ref, cannot appear in `visible_to`, and cannot have an escritorio session — fine. But a guest with one grant *does* have a person ref and will land on escritorio: escritorio must produce a page showing that one item and nothing else, including no hint of the other three apps' existence in that team, and the guest needs a directory row of `kind: guest` for escritorio itself (or escritorio's sign-in must be derived from the granting app's row — unspecified). **Suspected**; the specs do not address a 5th app's sessions for guests.
+
+### 8. Escritorio is blocked on work that has not started, and on a decision Paul has not made (Q8)
+
+- The home repo at `/home/paulca/amadan.net/oficina/home` is four Go files, 191 lines, "one placeholder page" per its own commit message. §13 steps 2–4 (sign-in, assertions, directory with person refs) have not begun. Every escritorio option keys on person refs — `visible_to`, "may this person see", and escritorio's own sessions all need `sub` — and the home spec says "**step 4 must land before either ships**, because call 2's person ref is what their grant rows key on." Escritorio is a third consumer of a call that does not exist. **Confirmed.**
+- The memory note records an open question "asked twice, not yet answered": whether the central host is a *directory that links* or an *authenticator that mints assertions*. Escritorio's whole identity story depends on that answer. **Confirmed** against the memory file.
+- Of the four apps escritorio would index, `mail` has no repo and `calendar` has a directory with no spec. A merged list across four apps, two of which do not exist, is a merged list across two — and those two already have per-team recency screens (finding 1). **Confirmed** by listing the repos.
+
+### 9. A fourth option exists, and it is "don't build escritorio; put the merged list where the data already is" (Q7)
+
+Two concrete alternatives, both dropping the new app:
+
+- **The home's directory as the only front door** (already specified, §11), linking to each app's own per-team recency list (already specified, Sheets §10.2). Zero new custody, zero new contract calls, zero new evaluators of the grants rule. Cost: one extra click per app. This is what the specs already say the suite does.
+- If a per-team merged list is genuinely wanted later: **a pull-based, stateless page owned by one existing app** (e.g. Sheets' own home page gains a "Docs" section fetched from the sibling with the viewer's identity, fail-soft on cold wake, nothing stored). This is Option B with the escritorio process deleted — one fewer database to seal, one fewer blast radius, and B's oracle endpoint is still needed but at least the caller is an app that already meets the one rule.
+
+Neither yields the cross-team "MY sheets" list. Nothing in A, B, C or these does, because the architecture forbids it at the only place that could hold it. That should be said plainly to the user rather than absorbed into a new app.
+
+## Recommendation
+
+**Do not build escritorio now; if a merged list must exist, it is Option B's mechanism without Option B's process.** The per-team justification is technically sound for cross-team aggregation but does not survive the home spec's own "what does it hold, not how many processes" test: escritorio holds titles that Sheets seals as first-class content, and under A or C it also holds a second, drifting copy of the grants join whose revoke path has the same weeks-long tail the specs already accept only because the directory is a hint — and escritorio's copy is not a hint, it is rendered. B is the only option where a stale escritorio cannot show a title to the wrong person; its costs (four cold wakes on a scriptless page, a visibility endpoint that must not become an oracle) are real but bounded and priceable, and its residual custody is titles only. But B still needs the home's person refs (§13 step 4, not started) and still does not give the user the cross-team list they asked for. So: answer the actual question first — the suite's architecture rules out "one list of MY things" at the home by §5.4, and either that ruling is reopened deliberately or the want is declined — and until the home's steps 2–4 land, point people at the directory the home spec already calls "the suite's front door".
+
+**The strongest argument against this:** the directory-plus-one-click answer scales linearly in clicks with the number of apps, and "your stuff, in one place" is the single thing every office suite's users actually use the front door for. If Calendar and Mail do ship, four separate recency screens per team is a real usability cost that no amount of architectural tidiness pays for, and a stateless B-shaped page will feel slow on exactly the quiet teams that are the product's likely customers. That cost is genuine; my position is only that it should be paid once the identity layer exists, not designed around before it does.
\ No newline at end of file
diff --git a/docs/superpowers/reviews/2026-09-02-escritorio-review-brief.md b/docs/superpowers/reviews/2026-09-02-escritorio-review-brief.md
new file mode 100644
index 0000000..cce17db
--- /dev/null
+++ b/docs/superpowers/reviews/2026-09-02-escritorio-review-brief.md
@@ -0,0 +1,107 @@
+# Review brief: escritorio's visibility model
+
+You are reviewing a **design decision that has not been made yet**, not code.
+Your job is to attack three options, then recommend one. Be adversarial. A
+"looks fine" review is a failed review.
+
+## Read these first (do not trust this brief's summary of them)
+
+- `/home/paulca/amadan.net/oficina/home/docs/superpowers/specs/2026-08-30-home-design.md`
+ — the suite home's design. §0, §2.2, §3, §6 and ruling 9 are the load-bearing
+ parts. **Quote it rather than paraphrasing; do not invent rules it does not state.**
+- `/home/paulca/amadan.net/oficina/home/AGENTS.md`
+- `/home/paulca/amadan.net/oficina/sheets-core/AGENTS.md` — the sibling app's
+ binding rules, for house conventions.
+
+## What is being designed
+
+**escritorio** — a proposed new app in the Oficina suite (sheets, docs,
+calendar, mail + the home). It is **one instance per team**, like Sheets is.
+It is the team's front door. It does two things:
+
+1. Launch links out to Sheets, Docs, Calendar, Mail for that team.
+2. Show **one merged list** of that team's items across those apps.
+
+**Settled already:** the index is **pointers only** — title, app, item ref,
+URL, touched_at, actor person refs. No body text, no snippets, no
+cross-app full-text search. Adding body text later is a separate decision.
+
+**Why this app exists at all:** the home is centralised and must stay blind
+(§0: "it never learns what is in anything"; §2.2: "Never renders content from
+an instance"). So the home can never show a merged document list. The claim
+being tested is that a **per-team instance** may hold what the centralised home
+may not, because it shares a blast radius with the content it indexes.
+**Attack that claim first.** If it is wrong, the whole app is wrong.
+
+## Platform constraints (verify against the specs, don't take my word)
+
+- Apps are rastrillo apps and **hibernate**. A request wakes them; there are no
+ timers while asleep. The home's spec drains its outbox "on wake and on each
+ share mutation, not on a ticker" for exactly this reason.
+- Identity comes from the home as an assertion; `sub` is an opaque **person
+ ref**, stable across email change. Instances store person refs against grants.
+- The home's directory is "a hint, never an authority" (ruling 9). Instances
+ re-check authorization on every request.
+
+## The question
+
+Escritorio must not show you the title of something you cannot see. Sheets and
+Docs are the authority on sharing. How does escritorio filter?
+
+**Option A — app posts the ACL; escritorio renders locally.**
+Each pushed entry carries the person refs who may see it. Escritorio filters
+from its own copy. No network on page load. Escritorio holds the sharing graph.
+Between a revoke and the next push it can show a title to someone just
+un-shared.
+
+**Option B — escritorio asks the app at render time.**
+Escritorio stores entries with no ACL. On each page load it asks each app "of
+these refs, which may this person see?" The app stays sole authority; a stale
+escritorio cannot leak. Page load fans out to ~4 apps, each of which may be
+hibernating.
+
+**Option C — Option A, plus revoke is a priority push.**
+A revoke jumps the outbox rather than waiting for the next ordinary push. Lag
+becomes seconds. Escritorio still holds the graph.
+
+## Attack these specifically
+
+1. **Is the per-team-instance justification sound**, or does escritorio just
+ re-create the aggregation risk the home was designed to avoid, one team at a
+ time? Does a team's escritorio have a *larger* blast radius than any single
+ app instance, since it spans four of them?
+2. **Are titles actually low-value?** "Q3 layoffs — final", "Acquisition of
+ X — term sheet". Argue the case that pointers-only is not the cheap
+ concession it appears to be, and say what that implies for the choice.
+3. **Option B and hibernation.** Is a cold wake of four apps on every front-door
+ load merely slow, or actually disqualifying? Does a short-TTL cache rescue
+ it, and if so does the cache re-introduce exactly Option A's staleness — i.e.
+ is B secretly A with worse latency?
+4. **Option C's priority push.** Does it work when the *receiver* hibernates?
+ Trace it. What happens to a revoke if escritorio is asleep, if the push
+ fails, or if the pushing app dies mid-queue? Is "seconds" defensible or
+ marketing?
+5. **Option A/C custody.** Escritorio holding `visible_to` person-ref lists is a
+ new copy of the sharing graph, per team. What does publishing escritorio's
+ database reveal, in the home spec's own "publish the database and see" form?
+ Write that sentence for each option.
+6. **The unit is per-TEAM, but the user's stated want was "a list of MY sheets
+ and docs".** A person in three teams gets three escritorios and still has no
+ single list. Is per-team the right unit, or does this design answer a
+ different question than the one asked? This may be the most important finding
+ available to you — do not skip it.
+7. **Anything a fourth option would fix** that none of A/B/C do. Propose it if
+ you find one; say so plainly if you don't.
+8. **YAGNI.** Should any of this be built now, or does the suite need the home's
+ sign-in and directory (spec §13 steps 2–4) to exist first? Is escritorio
+ blocked on work that has not started?
+
+## Output
+
+- Findings, ordered by severity. For each: the claim, why it is wrong or risky,
+ and what it costs. Mark each **confirmed** (you verified it against a file)
+ or **suspected** (reasoning only).
+- Then: **one recommendation** — A, B, C, or a fourth option — in a short
+ paragraph, with the single strongest argument against your own choice stated
+ honestly.
+- Do not write code. Do not edit any file.
diff --git a/docs/superpowers/specs/2026-09-05-activity-pull-design.md b/docs/superpowers/specs/2026-09-05-activity-pull-design.md
index 77531c8..b2aef71 100644
--- a/docs/superpowers/specs/2026-09-05-activity-pull-design.md
+++ b/docs/superpowers/specs/2026-09-05-activity-pull-design.md
@@ -1,9 +1,20 @@
# 🤖 Activity on the front door — design
-**Status:** design, 2026-09-05. Drafted by the activity-stream session
-(`claude-opus-5`). **Review pass outstanding** — AGENTS.md requires the
-highest-capability model available, recorded here by name, before this is
-approved. It has not run. Do not merge on the strength of this line.
+**Status:** design, 2026-09-05, revised the same day after a peer pass.
+Drafted by the activity-stream session (`claude-opus-5`).
+**Review pass outstanding** — AGENTS.md requires the highest-capability model
+available, recorded here by name, before this is approved. It has not run.
+Do not merge on the strength of this line.
+
+**Peer pass, and why it is not the review pass.** The home-spec-compliance
+session (`claude-opus-5`) reviewed the first draft and found four things,
+all applied: the "no new knowledge" defence was true and beside the point
+(§2.2, now a retraction); gate 10 tested only the happy path while §6 makes
+failure the normal case (§9); ruling 3's safety rests entirely on `aud`
+binding and needed a written "never" against the obvious optimisation (§11);
+and one claim in §5 was falsified by §5.3's own last line. That session
+wrote enough of the resulting argument to no longer be independent, and it
+says so itself. **A fresh reviewer is still required.**
**Scope:** how the home's front door (§11 of the home design) shows a person
what has been happening across their teams and apps, without the home
learning or holding any of it.
@@ -59,35 +70,128 @@ load-bearing test in the document.
**This shape was proposed before, in the other direction, and parked.** On
2026-09-02 Paul proposed `escritorio` — a per-team app aggregating sheets,
docs, calendar and mail into one list, apps posting entries opt-in, scoped to
-pointers only. A Fable review returned REQUEST CHANGES on five counts and
-Paul's call was to park it and build the home's identity layer first.
-
-**Provenance, stated because it is weaker than it looks.** Two sessions
-independently reported that review, in agreeing detail. The primary record
-was not findable: it is not a discussion on `oficina/home`, `oficina/sheets`
-or `oficina/docs`, and not a markdown file in the `oficina` tree.
-Corroboration is indirect — `memoria`'s README lists `escritorio` and
-`plausible empty list` among the phrases you can search *in the suite's own
-specs*, which is where both sessions' language came from. **Treat the five
-findings as real and the citation as unverified.** If the record surfaces,
-check this section against it.
-
-The five, and what this design does with each:
-
-| # | The objection | What happens to it here |
+pointers only. Three visibility options were put to a Fable review: **A**,
+the app posts the ACL and escritorio filters locally; **B**, escritorio asks
+each app at render time; **C**, A plus a priority push on revoke. The verdict
+was nine severity-ordered findings and a recommendation of *don't build it*.
+Paul parked it and put the home's identity layer first.
+
+**Both sources are in this repo**, because the citation was otherwise
+uncheckable — they existed only as local files in a sweepable cache
+directory, and the verdict only inside a subagent transcript. No discussion
+was opened and no spec was written at the time.
+
+- `docs/superpowers/reviews/2026-09-02-escritorio-review-brief.md`
+- `docs/superpowers/reviews/2026-09-02-escritorio-fable-verdict.md`
+
+**Prefer those files to discussion #6.** #6 is a good-faith reconstruction
+written from a session's private memory before the originals were found; its
+wording is that session's, not Fable's. These two are the contemporaneous
+documents. Quote from them.
+
+The findings that bear on this design, and what happens to each:
+
+| # | The finding | What happens to it here |
|---|---|---|
-| 1 | **Titles are sealed content, not pointers.** Sheets §2.1 seals sheet titles under its DEK; a mail "title" is a subject line. The source app cannot hand one out in the clear either. | **Survives, and is ruled through** (ruling 1). The instance decrypts under its own DEK and hands the title to the home for one render. The home never stores it, so it never needs Sheets-grade custody — §3. |
-| 2 | **A per-item ACL is not expressible by the poster.** Sheets access is a grants join — team rows plus individual grants minus removals — so no app can post "who may see this" as a value. A consumer re-evaluating the rule drifts, and fails 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 named in the assertion. There is no copy to drift from. |
-| 3 | **Push-based revoke has a weeks-long tail.** The outbox in §6 is the *instance's*, and drains on wake and on share mutation only. Tolerable for a hint; not for a rendered list. | **Dissolved by the inversion.** Revoke is instant because the answer is computed after the revoke, every time. |
-| 4 | **It answered a different question.** Per-team aggregation gives a person in three teams three lists; the cross-team list is forbidden at the only place that could hold it (§5.4 — "handing the home a document-level graph it has no business holding"). | **Answered, differently.** The cross-team list exists only as a rendered page, assembled per request and discarded. §5.4 governs what may *ride an assertion* into the home's custody; nothing rides in and nothing is held. §3 covers the clause that does change. |
-| 5 | **Blocked on person refs** from §13 step 4, not started. | **Still true.** §10. |
-
-**The review's own recommendation was this shape**: *a stateless pull inside
-an existing app, not a new process posting to a central store.* This design
-is that, with one difference worth naming — the review imagined the pull
-living inside an existing app, and here it lives in the home, which is the
-only place that knows all of a person's teams. That is the same knowledge
-§6 already gives the directory, not new knowledge.
+| 1 | **It answered a different question.** Per-team aggregation gives a person in three teams three lists. 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."* The verdict's own resolution: *"either that ruling is reopened deliberately or the want is declined."* | **The ruling was reopened deliberately** (rulings 1 and 2, Paul, 2026-09-05) — the first of the two paths the verdict named, taken with the sentence quoted in front of us. §2.1 argues why a rendered page is not a held graph. |
+| 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. |
+| 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. |
+| 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. |
+| 5 | **Option B's real costs:** a scriptless page blocking on four cold wakes, and a visibility endpoint that must not become an enumeration oracle. | **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. |
+| 8 | **Blocked on §13 step 4**, not started. | **Still true.** §10. |
+
+Findings 6, 7 and 9 are about escritorio-as-a-process and mostly evaporate
+with the process: 6 (blast radius) has nothing to bound because no new store
+exists; 7 (guests) is handled in §5.2; 9's fourth option is this design.
+
+**The verdict recommended this mechanism.** Its words: *"a pull-based,
+stateless page owned by one existing app… fetched from the sibling with the
+viewer's identity, fail-soft on cold wake, nothing stored"* — Option B's
+mechanism with the escritorio process deleted.
+
+**The one place this design departs from it**, named because a reviewer who
+knows the verdict will reach for it: the verdict put that page inside an
+existing app, and this puts it at the home. §2.1.
+
+### 2.1 Why the home, and why a rendered page is not a held graph
+
+The verdict's finding 1 is the strongest objection available to this design,
+so it gets answered rather than routed around.
+
+**Why the home and not an app.** The verdict's own alternative — Sheets'
+home page gaining a Docs section — gives a per-team merged list, which is the
+thing finding 1 says nobody asked for. Only the home knows every team a
+person belongs to, so only the home can answer *"my things"* rather than
+*"this team's things"*. Putting the page anywhere else re-answers the wrong
+question.
+
+**Why §5.4 is not breached.** §5.4 governs **what may ride an assertion into
+the home's custody**, and its two-sided test is about claims the home comes
+to hold. The failing example is a *held* document-level graph. Here:
+
+- **Nothing rides in.** The assertion carries `sub` and theme, exactly as
+ §5.3 already specifies. The activity flows the other way, in a response.
+- **Nothing is held.** §3's amendment and gate 10 are the mechanism, not the
+ intention — a grep proves it.
+- **The home learns nothing durable it did not already know.** It knew which
+ teams in which apps you belong to; that is the directory, §6. It does not
+ learn which documents you can see, because it never asks that question and
+ never records an answer.
+
+**The honest residue**, stated rather than argued away: for the lifetime of
+one HTTP response, and for the RAM cache's TTL, the home's process memory
+holds a cross-app list of titles for one person. That is real, it is what
+ruling 1 bought, and it is the thing a reviewer should press on. The claim is
+not that this is nothing — it is that *transient and unreplicated* is a
+different category from *stored and shipped to a bucket*, and that §0's
+falsifiable test is the line that matters because it is the one with a test
+behind it.
+
+### 2.2 Two costs that are not knowledge, and are not covered by §2.1
+
+An earlier draft of this spec defended call 7 with "the same knowledge §6
+already gives the directory, not new knowledge". **That sentence is true and
+beside the point**, and it is retracted here rather than left for a reviewer
+to pull on — when it gives way it takes §3 with it. The accurate form:
+
+> **No new knowledge at the home; new emission to instances, and §9's uniform
+> call direction spent as a detection signal. Both disclosed.**
+
+**(a) A passive map becomes a broadcast.** §6's knowledge sits still today.
+Call 7 makes every front-door render fire an authenticated, timestamped
+request at all of a person's hosts at once. Each instance thereby learns
+*"this person opened their front door at 09:14:03"* — including on mornings
+they never open that app, and from a host they may have been removed from
+whose directory row has not drained.
+
+§3.3 coarsens published timestamps **to the day**, and says why: "timestamps
+correlate with events observable at instances." Call 7 hands every instance
+in the fleet a fine-grained, signed, unrepudiable version of the signal §3.3
+deliberately blunted. Note the shape: ruling 4 keeps the pepper off instances
+so one compromised instance cannot learn about the whole suite. This is a
+much weaker leak than the pepper, but it is **the same shape** — one instance
+learning about a person's life outside it — and it arrives by design rather
+than by compromise. **Mitigation is thin and named as thin:** the RAM cache
+(§7) collapses repeat renders within its TTL, so the emission is per-TTL
+rather than per-load. Nothing removes it.
+
+**(b) It spends a detection property.** §9's six calls are uniformly
+person→home or instance→home; **zero run outward**. That uniformity is a
+security property rather than an accident of drafting: today a home making
+content-bearing requests to instances is anomalous *by construction*, so such
+traffic is evidence of compromise, visible to instances — the only parties
+positioned to see it. After call 7, an exfiltrating home is indistinguishable
+from a working home rendering front doors.
+
+§3.1's second threat model already concedes a great deal when the live host
+falls. This removes the one signal that would have told anyone it had. It is
+not recoverable by a mitigation in this spec; an instance-side anomaly rule
+(call 7 arriving for a person with no recent session) is the only lever, and
+it belongs to the instances, not here.
+
+**Neither cost is a reason not to build it** under rulings 1 and 2. Both are
+reasons the review pass (§12) should be run by someone who has read this
+section.
**This is not escritorio revived.** No new app, no new process, no new store,
no new repo. It is §11's front door — *"your teams grouped by app, and a New
@@ -203,8 +307,20 @@ origin, `kid`, 120-second expiry, single-use `jti`.
The only difference from the existing flow is transport: it rides an
`Authorization` header on a server-to-server request instead of an auto-POST
-through the browser. Every property §5.3 relies on is a property of the
-token, not of how it travelled.
+through the browser.
+
+**Why that transport swap is safe, specifically.** An earlier draft claimed
+"every property §5.3 relies on is a property of the token, not of how it
+travelled." **That is false, and §5.3's own last line is the counter-example:**
+"Delivery is an auto-POST, not a query string, so assertions stay out of edge
+logs, browser history and referrers" — a property of travel, not of the
+token. The conclusion survives on the specific argument rather than the
+general one: an `Authorization` header achieves the same three things an
+auto-POST does. Headers are not written to edge access logs the way query
+strings are, there is no browser involved so there is no history, and there
+is no `Referer` on a server-to-server call. **What must never happen is the
+assertion moving into the query string** — the failure §5.3 was guarding
+against, which a header avoids for the same reason a POST body does.
**Why not a service token.** A per-instance home→instance bearer would be a
seventh credential to mint, store, rotate, revoke and leak, and it would
@@ -225,6 +341,49 @@ shared store is needed — §5.3's reasoning carries over untouched.
well-behaved one, but an instance must not assume that; a compromised home is
in scope for every instance's own limits.
+### 5.1 It must not become a visibility oracle
+
+The escritorio verdict's finding 5 named this against Option B, whose
+endpoint answered *"of these refs, which may person P see?"* — a shape that
+turns into an enumeration oracle the moment its caller's token leaks. **The
+first draft of this spec did not address it.**
+
+Call 7 is a narrower shape, and the narrowness is deliberate:
+
+- **The caller supplies no refs.** It asks "what is this person's, recently?"
+ and the instance chooses the rows. There is no set to probe with.
+- **The subject is not caller-chosen.** It rides a signed, audience-bound
+ assertion the home mints. Forging one means holding the credential
+ origin's Ed25519 private key, at which point the suite has lost anyway.
+- **No grant and no such person are the same answer** — `200` with an empty
+ list. An instance must not distinguish them, or the endpoint answers
+ membership questions about arbitrary person refs. **Gate 17.**
+- **Errors carry no detail.** "Not for you" and "no such thing" are one
+ response, per §12's gate 4 pattern of 404 rather than 403.
+
+**What a leaked home does yield**, said plainly: an attacker holding the
+signing key can mint an assertion for any person ref and read that person's
+recent titles from any instance they have a grant on. That is strictly less
+than what the same key already buys — it can mint a *session* at those
+instances — so call 7 widens no boundary. It is not a new hole; it is inside
+an existing one, and §2.1 of the home design already prices a compromised
+home.
+
+### 5.2 Guests
+
+A guest — `kind: guest` in the directory (§6) — has a person ref and a grant,
+so they get a front door and their guest teams appear on it. This needs no
+special case, which is itself the argument for the design: the instance
+answers from its own grants, and a guest's grants are exactly the rows it
+returns.
+
+What a guest sees is already disclosed: §6 rules that a guest grant discloses
+the granting team's name, "in the sharing app's share dialog, in words". No
+new disclosure is created here.
+
+**A link-share viewer has no person ref** (Sheets §8.3 — "a view link mints
+nothing"), so they have no front door and no rows. Correct by construction.
+
---
## 6. Fan-out, and the cost this design pays
@@ -333,10 +492,27 @@ which one they are looking at.
Numbered from §12's nine.
-10. **No pulled content at rest.** A full front-door render against seeded
- instances leaves no title, URL or actor string in the SQLite file, the
- WAL, any log line, error string, metric label or on-disk artifact.
- Extends gates 1 and 9; gate 1's corpus grows a set of known titles.
+10. **No pulled content at rest — driven through the failure paths, not the
+ happy one.** A full front-door render against seeded instances leaves no
+ title, URL or actor string in the SQLite file, the WAL, any log line,
+ error string, metric label or on-disk artifact. Extends gates 1 and 9;
+ gate 1's corpus grows a set of known titles.
+
+ **The happy path is the exception here, so testing it is close to
+ testing nothing.** §6 says the first load of the day will mostly miss its
+ deadline, which makes failure the normal morning. And the natural Go
+ spelling of a failed parse —
+ `fmt.Errorf("parsing activity from %s: %w", body, err)` — puts the entire
+ response body, every title in it, into an error string, which §3.4 rules
+ is storage. A gate that renders only well-formed responses stays green
+ while exactly that ships.
+
+ So the gate drives **canary titles through every failure path**:
+ malformed JSON, a body over the cap, a bad `at`, a wrong content type, a
+ `url` on a foreign host, a connection reset mid-body, and a timeout. The
+ canary must appear in none of the places above. This is the class gate
+ 9's own review turned up three of, and each one was a guard that looked
+ complete.
11. **A silent instance is a visible gap.** With one instance timing out and
one returning empty, the render distinguishes them. This gate fails if
both produce the same output. **The most important test in this
@@ -356,6 +532,11 @@ Numbered from §12's nine.
most public surface.
16. **Scriptless.** The recency section and every refresh link work with
scripts stripped. Extends gate 6.
+17. **Not an oracle** (§5.1). A person ref with no grant and a person ref
+ that does not exist produce byte-identical responses from an instance.
+18. **One assertion, one host.** The fan-out mints a separate assertion per
+ instance, and a test asserts that no two requests in one render carry the
+ same `aud`. §11's first row is the reason this is a gate and not a note.
---
@@ -377,7 +558,13 @@ nicety.
**Consumers.** Call 7 is implemented by every instance that wants to appear.
Sheets is the near-term one — §10.2 there is already a recency list, so the
-endpoint is a projection of a query it has. Docs likewise. **Not appearing is
+endpoint is a projection of a query it has. Docs likewise.
+
+**The projection is not free, and the endpoint should say so.** Sheets §2.1
+seals titles under its DEK, so answering call 7 decrypts once per row rather
+than reading a column. That is a real cost on a list query and it is the
+argument for the instance capping `limit` hard and low — the front door wants
+a handful of rows per app, not a page of them. **Not appearing is
a valid state**: an app that has not implemented `/activity` simply
contributes no rows, and the front door renders the rest. There is no
registration step and no flag; a `404` from an instance is "this app does not
@@ -389,6 +576,7 @@ participate", rendered as nothing rather than as a failure.
| Thing | Trigger |
|---|---|
+| **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 18. |
| The contentless-ping hybrid (§6) | A real latency complaint, or measured p50 over one second warm |
| Any at-rest store of pulled **content** — titles, URLs, actors | Never, under §3's amendment as written. Changing this reopens §0. |
| 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. |
@@ -404,8 +592,14 @@ participate", rendered as nothing rather than as a failure.
- **The review pass.** AGENTS.md requires the highest-capability model
available, recorded by name. Not run. This spec is not approved.
-- **The escritorio record.** §2's provenance is second-hand. Someone who
- knows where it lives should confirm the five findings against the primary.
+- **`AGENTS.md` is stale against the home spec on three counts** the spec
+ explicitly retracted — "never stores your address", "some unidentified
+ person", and "assertion expiry is **seconds**" where §5.3 says **120
+ seconds**. The escritorio verdict confirmed all three. **Already tracked**
+ on discussion #1 / branch `fix/agents-md-matches-spec`, so it is not this
+ branch's to fix — but call 7 authenticates with the §5.3 assertion, and an
+ implementer who takes the expiry from `AGENTS.md` gets a number the spec
+ retracted. **Take 120 seconds from the spec.**
- **Numbers.** The per-instance deadline, the total fan-out budget, the RAM
cache TTL and the body cap are all named as decisions and none is chosen.
They want measurement against a real canary, not a guess in a spec.