oficina / suite Public

Add the suite sharing doctrine, README and AGENTS.md

The doctrine that binds every Oficina app when it talks to another app
or to the home. The team is the boundary; inside it the apps are one
trust domain, across it sharing is membership and nothing moves. Eleven
rulings from the 2026-09-05 brainstorm with Paul, the roster of record
at the home with a lone shape for self-hosters, the suite contract
consolidating memoria's S1-S7 and the home's call 7, two-actor writes,
one egress sentence, a suite leak list, and the consuming amendments
each app adopts in Appendix A.

Review pass outstanding; the status block says so.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JLq4NnZuUNpykg5nnkWdQg
Paul Campbell pushed by paul@keymail.dev 609394e9135ab1c5358e8f5bf0d4b4964687960f parent 291d957
3 files changed, +731 −0
  • AGENTS.md +50 −0
  • README.md +29 −0
  • docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md +652 −0
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..6a7cb1e
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,50 @@
+# 🤖 suite
+
+The shared repository of the Oficina suite. See `README.md` for what it
+holds. This file is binding. Where it disagrees with a general skill, this
+file wins.
+
+---
+
+## The one rule
+
+> **Cite, never restate.**
+
+This repository exists because five app specs each restated one contract
+in their own words and the copies drifted. A rule that lives here is
+stated here once. An app spec names the ruling number and the commit it
+read; it does not paraphrase. A paraphrase in an app spec is a bug in that
+spec, fixed by replacing it with a citation.
+
+The same rule binds this repository in the other direction: the home's
+calls 1 to 6 belong to the home spec and are cited here, not restated.
+
+---
+
+## Authorship
+
+- Every ruling carries who made it and when. "This session" is a valid
+ author and is marked as such so a reader can tell a ruling from a
+ proposal.
+- A design here is not approved until a review pass by the
+ highest-capability model available, recorded by name in the status
+ block. The author of a draft cannot be its reviewer.
+- Rejected alternatives are recorded beside the ruling that rejected
+ them, so nobody proposes them again in good faith without seeing why.
+
+## The contract module
+
+- Additive only. A removed field is a new major version.
+- No rastrillo dependency. The module is types, fixtures and a thin
+ client, so the pin chain from an app is one deep.
+- Fixtures are the interface. A fixture is added with the first client
+ that needs it and corrected by the second implementation, never
+ designed from two specs at once.
+- Every constant carries its reason in a comment: a cap, a deadline, a
+ TTL.
+
+## Discussions
+
+Cross-repository requests go through `amadan discuss` on the repository
+whose text must change. A request asks for a boundary to be recorded, not
+for a ruling to be reversed, and says which of the two it is.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..197c57b
--- /dev/null
+++ b/README.md
@@ -0,0 +1,29 @@
+# 🤖 suite
+
+The Oficina suite's shared repository: the sharing doctrine that binds
+every app, and the contract module they implement against.
+
+The suite is a consolidation. Home, Docs, Sheets, memoria and calendar
+each keep their own repository, and mail and files arrive later as
+wrappers around keymail and slopbox. What they share lives here so no app
+owns the rule that binds the others.
+
+## What is here
+
+- `docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md`: the
+ doctrine. The team is the boundary. Inside it the apps are one thing;
+ across it, sharing is membership and nothing moves. Read §1 first.
+- The contract module, `amadan.net/oficina/suite`: types, golden
+ fixtures and a thin client for the calls apps make to each other and to
+ the home. Not written yet; the doctrine's §12 says when.
+
+## How an app uses it
+
+An app spec cites the doctrine and lists the rulings it consumes. It never
+restates one. An app imports the module, serves its server half, and runs
+the conformance suite in CI against the version it pins.
+
+## Status
+
+The doctrine is a design awaiting its review pass. See its status block
+before adopting anything from it.
diff --git a/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md
new file mode 100644
index 0000000..6adf872
--- /dev/null
+++ b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md
@@ -0,0 +1,652 @@
+# 🤖 The Oficina suite — sharing doctrine
+
+How the apps of the suite share identity, membership and content with
+each other, and where the line is that nothing crosses.
+
+**Status:** design, 2026-09-05. Drafted by the suite session (Fable 5.1)
+from a brainstorm with Paul the same day. Every ruling in §1 attributed to
+Paul was given in that conversation or in an earlier spec named in the row.
+**Review pass outstanding.** AGENTS.md requires a review by the
+highest-capability model available, recorded by name, before this is
+approved. The author is that model, so the review must come from a session
+that did not write this. Do not adopt on the strength of this line.
+**Scope:** the rules that bind every app in the suite when it talks to
+another app or to the home. Consumed by `oficina/home`, `oficina/docs`,
+`oficina/sheets`, `oficina/memoria`, `oficina/calendar`, and any wrapper
+that later joins (mail via keymail, files via slopbox).
+**Reads on top of** `carlos/pegamento:docs/doctrine.md`. Where it
+diverges, §14 says so and names the filing.
+
+---
+
+## 0. The one rule
+
+> **The team is the boundary. Inside it, the apps are one thing. Across
+> it, sharing is membership and nothing moves.**
+
+The falsifiable form, in two halves:
+
+- **Inside.** Take a team's instances of every app. Publish what any one of
+ them holds about the others and the worst anyone learns is what that
+ team could already see in the owning app. No instance holds a copy of a
+ thing its team could not read there.
+- **Across.** Take two teams. There is no path by which a byte of one
+ team's content reaches an instance of the other. A person in both teams
+ reaches both through the home and carries nothing between them.
+
+Pegamento §1 says the app declares its own boundary. The suite is the app
+(memoria decision 1, Paul, 2026-09-03), and it declares the team. The
+process is not the boundary and never was: it was where the boundary
+happened to fall while every CARLOS app was one process.
+
+---
+
+## 1. Settled decisions (don't relitigate casually)
+
+| # | Ruling | Rejected | Who, when |
+|---|---|---|---|
+| 1 | **The team is the boundary. An instance is a process.** A team's instances across every app are one trust domain at `<team>.<app>.oficina.works`. | One boundary per process, which left memoria holding person-initiated snapshots and made the home's activity feed a federation question | Paul, 2026-09-03 (memoria decision 1); reaffirmed suite-wide 2026-09-05 |
+| 2 | **Sharing is membership across teams, unchanged.** No data crosses a team label. A guest is a scoped member of the granting team. A link share moves nothing. | Any federation story; a "copy to my team" verb | Pegamento §3, applied |
+| 3 | **Custody and authorization are two rules.** Custody bounds what a sibling may *hold*: team-visible items only. Authorization is what a *person* may read: asked of the app of record, per person, per query, cached no longer than a session. A copy is never an authority. | One ACL copied between apps; "is a member" as the only question | Paul, 2026-09-03 (memoria decision 3); generalised 2026-09-05 |
+| 4 | **The home owns identity and the team, including the membership roster. It never owns a per-item grant.** | Per-instance rosters as the record, reconciled by asking siblings; the home holding document-level grants | Paul, 2026-09-05 |
+| 5 | **Two shapes, one switch.** A *lone* instance is its own identity authority and its own roster. A *suite* instance takes both from the home. Membership authority follows identity authority; an instance never has one without the other. | A suite instance half-trusting its local table; a lone instance dialling a home for a roster it does not need | Paul, 2026-08-31 (Docs §8.1, two shapes); extended to the roster 2026-09-05 |
+| 6 | **Egress is one suite sentence.** Egress that is a property of the product is forbidden. Egress that is per-team opt-in, to one named recipient, disclosed, with a working floor when off, is admissible. | Two contradicting texts, Docs §11.2 and memoria §8.3 | Paul, 2026-09-03 (memoria decision 4); made suite-wide 2026-09-05 |
+| 7 | **One contract, one Go module, both halves, fixtures as the interface.** Every app imports the module, serves its server half, and passes the conformance suite in CI. An app spec cites the doctrine and lists consumed rulings. It never restates one. | Each spec restating the contract in its own words, which is what drifted | Paul, 2026-09-05 |
+| 8 | **Two credentials, chosen by what the call proves.** A home-minted assertion proves a person. A consumer bearer token proves a same-team sibling. Custody calls carry the token and no asker. Per-person reads from a sibling carry the token and a named asker. | A token exchange putting the home in the loop of every search; one credential for everything | This session, 2026-09-05 |
+| 9 | **A write names its real actor. No sibling ever acts as a person.** A person writes through their own session, carried by their browser. A sibling writes as itself, into an area the team owner designated for it, touching only what it authored. | On-behalf-of writes, where a sibling token becomes a write capability over every member's identity; browser-carried only, which forbids unattended writes | Paul, 2026-09-05 |
+| 10 | **Separate repos. One shared repo for the doctrine and the contract.** The suite is a consolidation: mail and files arrive as wrappers around keymail and slopbox, so the contract must cross repository boundaries on day one. | A core monorepo for home, docs, sheets, memoria and estilo, which buys atomic spec edits for four apps and loses the property the day mail joins | Paul, 2026-09-05 |
+| 11 | **Activity is pulled at render and held nowhere at rest** — `oficina/home` ruling 18 and the `design-activity-pull` spec, adopted as suite contract call 7. | Push into the home; a contentless heat map | Paul, 2026-09-05 (home ruling 18) |
+
+---
+
+## 2. What the collision was
+
+Nothing inter-app is built. The home is a skeleton. memoria's slice 1 is
+blocked. The activity pull is an unmerged worktree. So this is a spec
+alignment, not a migration, and it is cheap now and expensive in a month.
+
+The collision was never five apps disagreeing about the boundary. It was
+five specs each restating the same rules in their own words:
+
+- Home §2.2, Sheets PRODUCT.md principle 2 and Docs §8.2 each say data
+ never crosses an *instance* boundary. memoria decision 1 says the
+ boundary is the *team*. All four were written in good faith from the
+ same Pegamento sentence, and the word "instance" carried a process
+ assumption nobody had examined.
+- Docs §9 restated the home's five-call contract and was written against
+ a superseded draft. The home spec now has six calls and the activity
+ design a seventh.
+- Docs §11.2 and memoria §8.3 give opposite verdicts on the same egress,
+ and memoria could only ask Docs to change (`oficina/docs` discussion 33)
+ because Sheets AGENTS.md's rule that a contradiction is fixed in the
+ same commit cannot be kept across repositories.
+- Four team models exist: the home's suite-wide `teams` table (ruling 17),
+ Sheets' owners list plus `TeamRole`, Docs' allowed-addresses list, and
+ calendar's `Kind = "team"` calendar everyone holds a grant on. memoria
+ decision 11 works around there being no roster of record by asking
+ every sibling and admitting on any yes.
+
+Restating is the defect. Co-location would not have fixed it; citation
+does. §10 is the mechanism.
+
+---
+
+## 3. Two shapes
+
+An instance is provisioned in exactly one shape and the shape is not a
+runtime mode.
+
+**Lone.** One team, self-hosted, no home. The instance signs people in
+itself with rastrillo's magic link, keeps its own owners list and member
+rows, and never dials a home. This is Docs §8.1's ruling and Factor VII's
+requirement, not a degraded tier. Seapointish is the family precedent:
+"none" is a supported answer to "what glue does this app need".
+
+**Suite.** The instance accepts home assertions (home §5.3) and pulls its
+roster from the home (§5).
+
+The switch is the presence of a home consumer token in platform config.
+With it, the instance is a suite instance in every respect. Without it, a
+lone one in every respect. Ruling 5's reason: an instance that trusted the
+home for identity and its own table for membership would have two rosters
+of record, and the test in §0 could not be written for it.
+
+**Sibling calls are a separate switch**, and it is independent of shape.
+An instance serves and makes the sibling contract (§6) when sibling
+consumer tokens are in its platform config, which the drain mints without
+the home's involvement (memoria §6.2). So a self-hoster running Sheets and
+memoria with no home gets the two talking, and a suite instance always
+may. Two instances that talk were provisioned together and share a shape,
+which is what lets S2's named asker mean one thing: **the subject the
+receiving instance keys its members on**, a person ref in suite shape and
+a lowercased address in lone shape. A pair of lone instances has two
+rosters, one per instance, and that is what lone means; custody and
+authorization work unchanged because neither depends on the roster being
+shared.
+
+---
+
+## 4. Boundaries
+
+### 4.1 The team
+
+The `<team>` DNS label, reserved suite-wide at the home (home ruling 19).
+Every barrier in this document is a barrier at that label. Two of them are
+structural:
+
+- **A sibling's `consumers` row** is both credential and origin allowlist.
+- **Every sibling client refuses a host whose `<team>` label differs from
+ its own**, test-enforced against a fixture of hostile hostnames, and
+ refuses to follow redirects.
+
+Neither is a boundary against code execution on an instance. They stop
+mis-provisioning and accidental cross-team dialling, and that is what
+they are claimed as (memoria §6.3).
+
+### 4.2 Custody
+
+What a sibling may *hold* a copy of. The predicate is **team-visible**:
+readable by every roster member by virtue of team standing alone. Each app
+defines it in one function, and the conformance suite (§11) proves that a
+content fetch for anything else answers 404, never 403.
+
+Individually granted items and guest-only items are never team-visible.
+The change feed therefore carries no visibility flag: departure from
+team-visibility is `gone`, indistinguishable from deletion, so a sibling
+cannot learn that a private item exists (memoria §6.1).
+
+### 4.3 Authorization
+
+What a *person* may read. Asked of the app of record with the asker
+named, per query, and answered from that app's grants join. Four values,
+none collapsible into another: `VISIBLE`, `NOT_VISIBLE`, `STALE`,
+`UNKNOWN`. Only `NOT_VISIBLE` may be silent. Everything else that is not
+`VISIBLE` withholds loudly and names the app (memoria §4.1).
+
+**The app of record for an item is the app whose store it was created
+in.** A copy never becomes an authority, and a derived artefact carries the
+provenance of every source it was built from, transitively, so two hops
+cannot launder what one cannot (memoria §13, now a suite rule).
+
+---
+
+## 5. Identity and the roster
+
+### 5.1 What the home owns
+
+- **The person**: an opaque person ref, the assertion's `sub` (home §5.3).
+- **The team**: sqid, DNS label, sealed display name (home ruling 17).
+- **The team's instances**: `team_instances`, one row per (team, app).
+- **The roster**: which person refs are on which team, as `owner` or
+ `member`. **This is ruling 4's expansion**, and it is smaller than it
+ reads: the directory already holds person↔team↔kind rows (home §6). The
+ roster is those rows with `kind = member`, made authoritative for team
+ standing.
+
+### 5.2 What the home never owns
+
+A per-item grant. Which sheets a member may edit, which document a guest
+may comment on, which calendar a viewer sees: all instance-side, in the
+app's own grants table, decided from there on every request. The home
+answers only facts about its own records, never "may this person" (home
+ruling 19). "Is this person on this team" is a fact about its records.
+"May this person open this sheet" is not.
+
+Guests are not on the roster. A guest grant is minted in the instance and
+announced to the directory as `kind = guest` (home call 2), exactly as
+today. Nothing about guests changes.
+
+### 5.3 How the roster reaches an instance
+
+**Call 8, roster pull**, instance → home, under the instance's consumer
+token, scoped to the instance's own team:
+
+```
+GET /roster?team=<sqid>&epoch=<n>
+→ { epoch, members: [ { person_ref, role: owner|member } ] }
+```
+
+The home cannot push (home §6: no channel reaches a hibernating instance),
+so an instance pulls on wake and whenever it learns its copy is stale. It
+learns that from a **roster epoch** carried in every call-2, call-5 and
+call-8 response, and in the assertion as a claim. The claim passes home
+§5.4's test: a mismatched epoch triggers one refetch and a failed match is
+treated as absent, so the instance makes no trust decision on it, and the
+home already holds it. A person whose sign-in reveals a stale roster is
+admitted against the fresh one.
+
+**The instance enforces from its own copy**, on every request, exactly as
+it does today. What changes is where the copy comes from: the home rather
+than an owners list in an environment variable.
+
+### 5.4 Writing the roster
+
+At the home's directory origin, by a team owner, behind a fresh-assertion
+step-up (home §14 names it; this is its trigger). Adding a person announces
+them to every `team_instances` row's directory entry, so their front door
+shows the whole team on first sign-in. Removing a person bumps the epoch,
+and every instance that wakes or admits anyone refetches.
+
+**A member removed at the home is removed everywhere within one epoch.**
+That is the sentence memoria decision 11 could not write and the reason
+its four unreconciled tables existed.
+
+### 5.5 What this costs, said plainly
+
+A compromised home can add a member to a team. Before ruling 4 it could
+not: "a compromised glue grants nothing" (Pegamento §10.3). It still
+cannot read a document, cannot grant a sheet, and cannot act without
+leaving a directory row and a notification behind. The blast radius grows
+from "the graph" to "the graph plus membership", and §9 records it.
+
+Three things bound it: the owner step-up, the instance's own copy being
+what is enforced (so an outage or a rollback is a lag, not an admission),
+and the leak test in §0 being unchanged, since a roster row is a directory
+row the home already held.
+
+**Recorded as a divergence from Pegamento §10.3**, to be filed at
+`carlos/pegamento` (§14).
+
+### 5.6 Roles
+
+The roster knows two: `owner` and `member`. Per-item roles stay the app's
+own. Sheets has `commenter`; calendar does not; Docs has none yet. The
+doctrine does not unify them, because a shared role type designed from
+three specs at once would fit none (estilo's one rule). When two apps have
+shipped the same per-item vocabulary, it may move to the contract module.
+Not before.
+
+---
+
+## 6. The suite contract
+
+One Go module, `amadan.net/oficina/suite`, with a client half, a server
+half that mounts beside an app's own router, and golden fixtures. memoria
+§6 specified S1 through S7 and the activity design specified call 7; both
+move here unchanged in substance. The home's calls 1 to 6 stay in the home
+spec, which owns them, and are cited here for completeness of direction.
+
+| Call | Direction | Credential | Proves | Notes |
+|---|---|---|---|---|
+| 1–6 | person → home, instance → home | session; consumer token | — | Home §9. Unchanged. |
+| 7 activity | home → instance | **assertion** for the person, `aud` = instance | who the front door is for | `design-activity-pull` §4–5. The home renders and discards; RAM cache only. |
+| 8 roster | instance → home | consumer token | which instance asks | §5.3. Scoped to the caller's team. |
+| S1 change feed | sibling → sibling | consumer token | same-team sibling | `(item_id, revision, gone, touched_at)`. No title, no content, no visibility flag. Per-consumer TTL; past it, full re-announce. |
+| S2 authorization | sibling → sibling | consumer token **+ named asker** | same-team sibling, asking for a person | Four values, total decoder, cap 256 pairs. Never cached across requests. |
+| S3 content | sibling → sibling | consumer token | same-team sibling | `(id, revision)`, **no person**. Team-visible only. 404, never 403. |
+| S6 aggregate | sibling → Sheets | consumer token + named asker | as S2 | Enumerated function over cell refs, never expression text. Cap 64. |
+| S8 write-as-self | sibling → sibling | consumer token | same-team sibling | §7.2. Into the caller's designated area only; only items the caller authored. |
+| hand-off | browser → instance | the **person's own session** | the person | §7.1. A redirect POST, the shape of home call 6. Not a server call. |
+
+**S7 membership is retired in suite shape.** It existed because there
+was no roster of record; call 8 is that record. A lone instance never
+needed it. memoria decision 11 is superseded (Appendix A.4).
+
+### 6.1 Why two credentials
+
+**Only the home can mint an assertion**, and putting the home in the loop
+of every memoria search would break memoria §6.4 and its 800 ms deadline
+at once. So a sibling asking "may Ana see these" sends its consumer token
+and names Ana. The sibling is trusted to name the asker because a
+compromised sibling already reads everything team-visible through S3; the
+naming adds nothing to what it can read. It would add something to what it
+can *write*, which is why S8 takes no asker (§7).
+
+**The home does hold the signing key**, so call 7 carries a real
+assertion and the answering instance decides from its grants with no
+trust in the home's word. Ruling 3 of the activity design, adopted.
+
+### 6.2 Authentication and rotation
+
+Per-consumer bearer tokens, minted by the operator drain, delivered as
+platform config, stored hashed by the receiver. Three-phase rotation:
+expand acceptance, switch the sender, contract acceptance (memoria §6.2).
+The home never sees a sibling token. Mutual TLS is unavailable because TLS
+terminates at the platform router.
+
+### 6.3 Availability
+
+Every pull carries a deadline, all siblings are called concurrently, and a
+sibling past its deadline is `UNKNOWN`: withheld loudly, the gap named
+with the app. Partial pages are the normal case. A cold sibling is a wake,
+and a wake is slower than any deadline, so a consumer pre-warms on its own
+wake (memoria §6.5, activity design §6). **A reliably slow sibling is a
+suite health problem the consumer surfaces, never one it routes around by
+caching authorization.**
+
+### 6.4 What the contract does not license
+
+Any hop across a `<team>` label. Any sibling presenting an assertion it
+did not receive from the home for its own `aud`. Any write except §7. Any
+item outside the custody predicate. The home in the loop of a sibling
+call. Content leaving the team's hosts except under ruling 6.
+
+---
+
+## 7. Writes
+
+The question "may memoria write a Doc" has two answers because it is two
+questions.
+
+### 7.1 A person is present: the hand-off
+
+"Save this answer as a Doc" is Ana's act. memoria renders a form whose
+action is Docs' create endpoint on Docs' own host; Ana's browser POSTs it
+with her own Docs session; Docs sees Ana and Ana owns the document.
+memoria holds no write credential and Docs trusts nothing memoria says.
+
+Same-site cookies make this a plain form: `<team>.memoria.oficina.works`
+and `<team>.docs.oficina.works` share a registrable domain, so a `Lax`
+session cookie rides the POST. Body size is capped by the receiver, with
+the number's reason in a comment. A person with no Docs session is sent
+through sign-in and returns to the pending state by nonce, the flow home
+§5.5 already specifies. This is scriptless and satisfies every app's
+zero-JS floor.
+
+### 7.2 No person is present: write-as-self
+
+A weekly digest. A Notion page ingested and written out as a Doc. That is
+memoria's act, and rastrillo already models a non-human actor. S8 lets a
+sibling create or modify items **as itself**, under its consumer token,
+subject to three constraints the receiver enforces and the conformance
+suite tests:
+
+- **Only into an area the team owner designated for that sibling.** A
+ folder, a tag, a calendar, whatever the app's own grouping is. The
+ designation is an instance-side grant, made in the receiving app's
+ share UI, and it is what makes S8 refusable per team.
+- **Only items the sibling authored.** Every S8 write carries the writer's
+ identity in the item's actor field, and an S8 modify is refused for any
+ item whose actor is not the caller.
+- **Never a person-authored item.** Stated separately because it is the
+ attack the constraints exist to prevent.
+
+The attribution rule holds: anything that writes is attributed, and
+"memoria" is a name a reader can act on.
+
+### 7.3 What was rejected, and why it stays rejected
+
+**On-behalf-of writes**: a sibling writing "as Ana" under its own token.
+It would make every consumer token a write capability over every member's
+identity, and the receiving app could not tell Ana's edit from a
+compromised sibling's. The read-side version of this trust (S2's named
+asker) is acceptable because it adds no reach; the write-side version adds
+all of it.
+
+**Browser-carried only**: forbids unattended writes, which the product
+wants, and would push implementers toward a person-present pretence.
+
+---
+
+## 8. Egress
+
+One sentence, binding on every app:
+
+> **Egress that is a property of the product is forbidden. Egress that is
+> per-team opt-in, to one named recipient, disclosed in the README's first
+> paragraph and on every artefact it produced, with a working floor when
+> it is off, is admissible.**
+
+Docs §11.2's rejection of conversion APIs stands under the first clause:
+the egress was unavoidable and unnamed. memoria §8.3's model provider
+stands under the second, and the comparison is like with like: memoria's
+embedding ingest is whole-corpus egress, comparable in volume to what
+Docs rejected, differing only in the three properties the second clause
+names (discussion 33). Both specs cite this section and delete their own
+wording (Appendix A).
+
+---
+
+## 9. The suite leak list
+
+Each app keeps its own. These are the costs that exist only because the
+apps are a suite, and every one is a consequence of ruling 1 or 4.
+
+1. **A compromised sibling reads everything team-visible in its team**,
+ across every app, through S3. This is what "one trust domain" means
+ and there is no version of ruling 1 without it. It cannot read
+ individually granted or guest-only items, which is what custody buys.
+2. **A compromised sibling can name any asker on S2**, and so learn which
+ team-visible items a given member may see. Bounded by item 1: it can
+ already read them all.
+3. **A compromised sibling can write into its own designated area** as
+ itself (S8). It cannot write as a person or touch a person-authored
+ item.
+4. **A compromised home can add a member to a team** (§5.5). It cannot
+ read content, grant an item, or do so silently.
+5. **The home page holds titles and actor names in RAM** for the length of
+ a response and a short cache (activity design §7). An XSS on the
+ directory origin reaches call 7's output, which is titles, never
+ documents. Never at rest.
+6. **The hand-off puts a document body in a POST** across two origins of
+ one site, in the person's browser. It is the person's own content
+ moving under the person's own session, and the receiver caps it.
+
+---
+
+## 10. Where things live
+
+**Repositories stay separate** (ruling 10). `oficina/suite` holds this
+doctrine and the contract module. Estilo stays the design system's home
+and is the precedent for a shared module every app pins.
+
+**Cite, never restate.** Each app spec gains a section, *Consumed suite
+rulings*, listing ruling numbers from §1 and the doctrine commit it was
+read against. Where an app spec today restates a boundary, contract, or
+egress rule, the restatement is replaced by a citation (Appendix A). Sheets
+AGENTS.md's "a contradiction is fixed in the same commit" becomes **"a
+contradiction with the suite doctrine is a bug in the app spec, fixed by
+citing"**, which can be kept across repositories, where the old rule
+could not.
+
+**Conformance is the interface.** The contract module ships golden
+request and response fixtures for every call and a test each app runs in
+CI against the version it pins. Estilo's rule: two specs are guesses, two
+implementations are facts. The fixtures are what memoria's server half and
+Docs' server half both have to satisfy, and they are corrected by the
+second adoption.
+
+**The two-pins trap is the stated cost.** Apps pin the contract module;
+the module pins nothing (it is types, fixtures and a thin client, with no
+rastrillo dependency), so the chain is one deep. Contract changes are
+additive-only, and a removed field is a new major version.
+
+**Docs moves its module path** from `github.com/carlosframework/docs` to
+`amadan.net/oficina/docs` before it imports the contract. Every other app
+is already there.
+
+---
+
+## 11. Testing gates
+
+Gates live in the contract module and each app runs them in CI. A gate
+that cannot be written as stated is a spec bug.
+
+1. **Fixture conformance.** Every server half reproduces the golden
+ response for every golden request, per call. A new fixture is added
+ with the first client that needs it.
+2. **S2 total decoder.** For any 256-pair request, the client yields
+ exactly one result per pair or `UNKNOWN` for the whole call. Tested
+ with short, duplicate, malformed and non-2xx responses.
+3. **Custody.** S3 answers 404 for a fixture item that is individually
+ granted, guest-only, or deleted, and 200 for a team-visible one. The
+ status is identical for "not team-visible" and "does not exist".
+4. **No cross-team dial.** The client refuses a hostile-hostname fixture
+ (`acme-evil`, `acme.docs.oficina.works.evil`, a redirect to another
+ label) without connecting.
+5. **Roster.** A member removed at the home receives an empty call-7 list
+ and `NOT_VISIBLE` on S2 from every instance after one epoch. A lone
+ instance, given a home hostname in a fixture, makes no request to it.
+6. **Write-as-self.** S8 is refused outside the caller's designated area,
+ refused for an item whose actor is not the caller, and refused entirely
+ when the team has designated no area.
+7. **The hand-off.** A POST without a session lands in sign-in and
+ returns to the pending state by nonce. A body over the cap is refused
+ with no partial write.
+8. **Publish the home's database.** Unchanged from home §0: counts, team
+ names, memberships. The roster adds no column class, and the test
+ proves it by running against a roster fixture.
+9. **Citation currency.** Each app spec's *Consumed suite rulings* section
+ names a doctrine commit; a script fails when that commit is more than
+ one ruling behind `main`.
+
+---
+
+## 12. Sequencing
+
+0. **This spec, reviewed.** A review pass by a model that did not write
+ it, recorded by name in the status block.
+1. **The four amendments** (Appendix A) land in home, Docs, Sheets and
+ memoria. This closes `oficina/docs` discussion 33 and unblocks
+ memoria slice 1, which its own §13 says is blocked on agreement, not
+ code.
+2. **The contract module**, types and fixtures only: S1, S2, S3, the
+ hostile-hostname fixture, gates 1 to 4. No server half yet. Docs moves
+ its module path.
+3. **memoria slice 1 against the module**, with Sheets and Docs serving
+ S1 to S3. The second adoption corrects the fixtures.
+4. **The home's identity layer** (home §13 step 4) and, on top of it,
+ call 8 and the roster (§5). Gates 5 and 8.
+5. **Call 7**, the activity pull, once the home can mint assertions
+ (home §13 step 10).
+6. **The hand-off** from memoria to Docs. Gate 7.
+7. **S8** and the designation UI in one receiving app. Gate 6.
+
+Steps 2 and 3 do not wait for the home. That is deliberate: the sibling
+contract has no home in the loop and its tokens are a separate switch
+(§3), so memoria can ship against Sheets and Docs in lone shape with
+sibling tokens and an owners list, and the roster arrives later as a
+change to where the list comes from.
+
+---
+
+## 13. Deliberately not built
+
+| Thing | Trigger |
+|---|---|
+| Cross-team anything | Never, under ruling 2 |
+| On-behalf-of writes | Never, under ruling 9 |
+| A unified per-item role vocabulary | Two apps ship the same one (§5.6) |
+| A core monorepo | Never while the suite is a consolidation (ruling 10) |
+| Home-side search or aggregation over pulled content | Never, under home §2.2 as amended |
+| A roster in lone shape that syncs anywhere | Never; lone means lone |
+| Push from an instance to the home | A hibernation model that runs timers, which the platform does not have |
+| A token exchange so siblings can hold assertions | The home's latency budget can afford being in every search, which §6.1 says it cannot |
+| Mail and files as suite apps | A wrapper around keymail or slopbox that serves S1 to S3 and takes a roster |
+
+---
+
+## 14. Open
+
+- **Review pass.** See status.
+- **Pegamento filing.** §5.5's divergence from Pegamento §10.3, and
+ ruling 1's application of §1 to a suite, both to `carlos/pegamento`.
+- **The `/auth/begin` dial.** rastrillo's sign-in classifier fetches
+ `.well-known/keymail` on the typed address's domain before the admission
+ gate, so an anonymous visitor makes any instance dial any host
+ (memoria QUESTIONS §5). It affects every app in the suite and belongs
+ upstream in `rastrillo/auth`. Recorded here because no single app owns
+ it.
+- **`rastrillo/rastrillo` #2**, the subject hook and sealed link store,
+ still gates the home's identity layer and therefore steps 4 and 5.
+- **Whether calendar joins the contract in slice 1** or after its first
+ screens merge. It has a spec and content; bodies stay off by default
+ when it does (memoria §12).
+- **The deadline number.** 800 ms per sibling is memoria's starting
+ assumption, unmeasured. It becomes the contract's default once measured
+ against a cold wake on the platform.
+
+---
+
+## Appendix A — the consuming amendments
+
+Each is the text an app spec adopts, replacing its own restatement. Each
+is one commit in its own repository, and each adds a *Consumed suite
+rulings* section naming this document's commit.
+
+### A.1 `oficina/home`, spec §1 and §2.2
+
+Add ruling 22: *"The suite sharing doctrine (`oficina/suite`) binds this
+spec. The team is the boundary; the home owns identity, the team and the
+membership roster; it never owns a per-item grant. Call 8, roster pull,
+is added to §9."*
+
+In §2.2 replace *"Never federates. Data does not cross an instance
+boundary."* with *"Never crosses a team. Data does not cross a team
+label; a team's instances across apps are one trust domain (suite ruling
+1)."* Complete ruling 18's pending change of *"never renders"* to *"never
+stores"*. Replace *"Never holds an authorization model"* with *"Never
+holds a per-item grant. It holds the membership roster (suite ruling 4),
+and answers only facts about its own records (ruling 19)."*
+
+In §9 add call 8 per suite §5.3. In §5.3's claims table add
+`roster_epoch`, and in §5.4 record that it passes the two-sided test: a
+mismatch costs one refetch and a failed match is treated as absent. In
+§14 move *step-up for instances* to built-when: *"roster writes (suite
+§5.4)"*.
+
+### A.2 `oficina/docs`, spec §8.2, §9, §11.2
+
+§8.2: replace *"'Sharing is membership' forbids data crossing the
+instance boundary."* with *"'Sharing is membership' forbids data crossing
+a team boundary (suite ruling 2). Same-team siblings may pull
+team-visible content under the suite contract (suite §6), and Docs serves
+S1 to S3 and S8 in suite shape."*
+
+§9: delete the restated five-call contract and the *consumed rulings*
+list. Replace with *"Docs consumes the home contract as the home spec
+states it, and the suite contract as `oficina/suite` states it. See
+Consumed suite rulings."*
+
+§11.2: replace the rejection's reason clause with *"Rejected under suite
+§8's first clause: unavoidable, unnamed egress. The second clause governs
+memoria, and Docs does not restate it."* This closes discussion 33.
+
+### A.3 `oficina/sheets`, PRODUCT.md principle 2, spec §8.2, §9, AGENTS.md
+
+PRODUCT.md principle 2: *"One instance per team. Sharing is membership
+across teams: the sheet never moves, and an outside person becomes a
+scoped member of the team that owns it. Within a team, Sheets is one of
+several apps sharing one trust domain (suite ruling 1), and same-team
+siblings may pull team-visible sheets under the suite contract. There is
+no federation story across teams and there will not be one."*
+
+§8.2: append *"Team standing comes from the roster the home holds (suite
+§5) in suite shape, and from `$SHEETS_OWNERS` in lone shape. Per-sheet
+roles stay here."*
+
+§9: as A.2's §9.
+
+AGENTS.md: replace *"a contradiction gets fixed in the same commit"* with
+*"a contradiction with the suite doctrine is a bug in this spec, fixed by
+citing it."*
+
+### A.4 `oficina/memoria`, spec §1, §1.3, §4.3, §6, §8.3, §13
+
+§1: add *"Decisions 1, 3 and 4 are now suite rulings 1, 3 and 6; this
+spec cites them. Decision 11 is superseded: admission is the home's roster
+(suite §5) in suite shape and the owners list in lone shape. S7 is
+retired."*
+
+§1.3: correct the S3 prose to match §6.1: S3 takes no person; the fix
+removed the asker rather than adding one.
+
+§4.3: replace the body with a citation of suite §5.3 and delete the
+"at least one sibling says MEMBER" rule, which is what made a guest a
+member.
+
+§6: move the S1 to S6 definitions to the contract module and cite them.
+Replace §6.4's *"Any write"* with *"Any write except suite §7: a
+person's own hand-off, and S8 as memoria itself into its designated
+area."*
+
+§8.3: replace the qualification of Docs §11.2 with a citation of suite
+§8.
+
+§13: close the blocker *"the source apps must adopt decision 1"* against
+A.1 to A.3 when they land.
+
+### A.5 `oficina/calendar`
+
+No restatement exists to replace. Add *Consumed suite rulings* naming
+rulings 1 to 9 when calendar's first screens merge.