oficina / suite Public

Revision 3: answer all sixteen findings, four of them as rulings

Paul took decisions A, B and C on Fable's recommendation after the GPT-6
review, and added a product requirement for four document sharing states.

Four new rulings:

- 12. Membership is its own record at the home, written only by an owner.
  Finding 1 showed deriving the roster from call-2 directory rows let any
  instance announce a member around §5.4's step-up. Verified against the
  home spec, it was worse than that: home §6's row has no owner role
  ("kind is member or guest") and home §7 says the home never learns who
  an owner is, so §5.3's wire shape had no source; and the row is scoped
  per (team, app) where the roster is per team, so a projection could
  express a state ruling 1 forbids.
- 13. A compromised home is three compromises, conceded separately: a
  leaked database, a compromised directory origin, a compromised
  credential origin. The third can sign in as anyone and is not new —
  home ruling 1's standing cost, cited not introduced.
- 14. Custody is bounded at acquisition and retired by the feed, not
  enforced by erasure. §0's inside test rewritten to promise what the
  design delivers, following the correction home §0 already made to
  itself.
- 15. Four sharing states, one ladder, and a link is not a rung. Only the
  team rung is team-visible; a link is a capability beside the ladder.
  Treating it as a rung would turn "whoever I send this URL to" into
  "everyone on the team, findable by search". This one is a proposal
  until Paul rules on it.

The other twelve answered in place: provenance envelopes on both write
paths, receipt and commit split in the hand-off, failed-refetch and
rewound-home behaviour, epoch-bound paging on call 8, the epoch claim's
own exception in home §5.4, the source-behind revision as UNKNOWN, all
nine gates rewritten so each can be written as stated, and Appendix A's
leftovers and misattributions — including keeping Sheets' local
same-commit rule, which revision 1 read as a cross-repository claim it
never made.

Nobody has reviewed revision 3. §12 step 1 waits on a third pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016XuAriz559moY27sTegqpt
Paul Campbell pushed by paul@keymail.dev 99a883847cd5d27d84e1cc5cd82d2c49faf0e2b6 parent 95155a8
2 files changed, +553 −167
  • README.md +12 −11
  • docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md +541 −156

Intent

Human

OK, what's next?

Human

I need the decisions asked much more plainer, with maybe an example. Also maybe a Fable or Alpha suggestion on each?

Human

<task-notification> <task-id>a27ebfc49ecbf1847</task-id> <tool-use-id>toolu_01L74sBL2jz4UTF7iuWwQKvN</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-oficina-suite/df6721cd-97ce-45b2-b664-3fea70beada0/tasks/a27ebfc49ecbf1847.output</output-file> <status>completed</status> <summary>Agent "Fable recommendation on three rulings" finished</summary> <note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note> <result>I've read all three documents and checked the load-bearing citations against the home spec on the trunk worktree (`5a42f2d`), the memoria spec, the Docs spec, the activity design and Pegamento's doctrine. Recommendations follow. ## A. Membership gets its own write authority (finding 1) **Recommendation.** Membership is a record of its own at the home: `(team, person, role)`, written only at the directory origin by a team owner behind step-up, or by the drain when it provisions the first owner. A directory row says where a person has things and never grants or revokes team standing; call 2 cannot write membership, and call 8 reads only the membership record. Ruling 4 holds; §5.1's derivation is retracted. **Why.** §5.1's "smaller than it reads" was my economy in revision 1, and it was a false one. I chose to derive the roster from existing directory rows because it added no table and let gate 8 keep the home §0 claim untouched. Verifying it now, it fails on three counts, not one: - The write path GPT-6 names is real. Home §9 call 2 is instance → home under a consumer token, via the outbox, and Docs §9.1 confirms the instance supplies `kind`. Pegamento §10.3 and home ruling 9 make that safe precisely because the row is a hint. §5.1 changed the reader without changing the writer. - The row cannot carry what call 8 returns. Home §6's directory row is `(person ref, app, team sqid, host, kind)` with `kind ∈ {member, guest}`. There is no owner role anywhere in the home's model; home §7 says outright "Who counts as an owner is the instance's business... the home never learns the answer." So `role: owner|member` in §5.3's wire shape has no source row. - The row is per `(team, app)`; the roster is per team. Under ruling 1 a member of Acme is a member of every Acme instance, so "member of acme.docs but not acme.sheets" is not a state the roster can express, but it is exactly what a projection of directory rows produces. §5.4 also needs the home to know who an owner is before it can gate a write on one. That is circular without an owner record the home itself holds. The bootstrap is already in home §7: the first owner arrives as sealed queue payload and the drain seeds it. **Rejected alternatives.** (1) Derivation from call-2 rows, revision 1's own choice, for the three reasons above. (2) Keeping call 2 as the membership write but requiring an owner step-up on each upsert: the home cannot verify "this instance's caller is an owner" without the roster it is trying to build, and it turns a consumer token plus a relayed person into a write capability over identity, which is the on-behalf-of shape ruling 9 rejected on the write side. (3) Instance-side rosters reconciled by asking siblings: already rejected in ruling 4 and by memoria decision 11's failure. **What it costs.** A `memberships` table at the home. Owner role is a new plaintext information class: publishing the database now identifies each team's owners, so home §0 and gate 8 change rather than stay "unchanged" (this is also finding 13). Adding a teammate becomes a home-origin act, not a Sheets or Docs share-dialog act, which is a product change every app spec must consume. A migration when call 8 lands: each instance's current member table becomes the proposed initial roster, confirmed by an owner, then demoted to a hint. Home §7's sentence that ownership is the instance's business is amended, so A.1 grows. Tests GPT-6 names: member upsert, revoke and delayed replay under an instance token, none of which may touch the roster; instance-scoped revoke deletes directory rows only. **Is GPT-6 right?** Yes, and it understates: the design was not only unguarded, it could not have been built as written, because the row lacks the role and the scope. ## B. What a compromised home means (finding 2) **Recommendation.** §5.5 names three compromises and concedes each. A leaked database reveals the graph, the roster, and who owns each team. A compromised directory origin can add a member, and a member reads everything team-visible in every instance of that team; the directory row and the notification are what honest code leaves, not what a compromise must. A compromised credential origin can sign in as anyone, owners included, and no instance can tell; that has been true since home ruling 1 put every credential on one origin, and ruling 4 adds nothing to it. What a compromised home still cannot do: read an individually granted or guest-only item, write as a person, or grant a per-item right. **Why.** I wrote "cannot read a document, cannot grant a sheet" by lifting home §2's "a compromised *directory* grants nothing... does not cost a document" and widening it to "home". Home §2 is careful to say directory; home §5 says "Sign-in is an authority, and says so"; home §3.1 keeps two threat models apart and records that its own first draft collapsed them and was wrong to. I repeated the collapse. Three specific corrections: - "Cannot read content" is false the moment a member can be added: the added account reads everything team-visible through every instance's ordinary front door. §9 item 1 already concedes this for a sibling; §5.5 has to concede it for the home. - "Cannot act silently" counts as evidence what honest code writes. Home §3.3 has the right idiom, "said plainly rather than counted as a fix", and the activity design §2.2(b) already concedes that after call 7 an exfiltrating home is indistinguishable from a working one. - Impersonation of an existing owner needs no membership row. The signing key is in platform config on the credential origin (home §5.3), and the instance's grants join is keyed on the `sub` the home asserted; home §5.4 says a hostile `sub` is account takeover. The instance's own grants are a bound on *what Ana may see*, never on *whether this is Ana*. Even a passkey does not help, because it also lives at the credential origin. The Pegamento filing changes shape with this. It is not "§5.5 diverges from §10.3 on membership"; it is that the home was never a §10.3 glue. §10.3's "a compromised glue grants nothing" is written for a registry, and the home is an identity authority by home ruling 1. Ruling 4 widens the *directory* origin's blast radius from the graph to membership, and that is the honest delta. **Rejected alternatives.** (1) Keep §5.5 and say step-up bounds it: step-up is performed by the compromised authority. (2) An independent audit mechanism, such as an instance-side rule that refuses a membership change or a call-7 arrival for a person with no recent session, or an owner countersignature on roster changes: real, but a new mechanism no spec has designed, and the activity design already says it belongs to instances. Record it in §13 with a trigger, not as a bound. (3) Retreat to Pegamento §10.3 by dropping ruling 4: returns to four unreconciled tables and the sentence memoria decision 11 could not write. **What it costs.** No code; honesty. Home §0's publish-the-database sentence gains "and who owns each team". §9 item 4 is rewritten to the three-tier form, and the Pegamento filing is refiled as "the home is an identity authority and §10.3 does not hold for one". The suite loses the comfortable sentence that a compromised home "does not cost a document". **Is GPT-6 right?** Yes on every point. The one thing to hold: the impersonation risk is not a defect *of this doctrine*. It is home ruling 1's known cost, and the correction is to cite it, not to design against it here. ## C. §0's "inside" test (finding 6) **Recommendation.** Custody is bounded at acquisition and retired by the feed; it is not enforced by erasure. Publish what a team's instance holds and the worst anyone learns is what that team could read in the owning app when the copy was taken. An honest consumer retires a copy within one feed pull plus its snapshot retention, and revocation never reaches a compromised one; that is §9 item 7 one layer down, stored bytes instead of a rendered page. **Why.** I wrote the inside test as an instantaneous statement because I wanted the one rule to be one clean sentence, and I knew better: §4.2 defines departure from visibility as a `gone` row a consumer pulls later, and §9 item 7 already concedes that revocation never reaches a rendered page. I did not carry that lag into §0. Home §0 records the identical mistake and its fix: "never stores your address" was "a claim the schema could not keep", so it became "never stores a *readable* address" with §3.2 carrying the window. The precedent was in the family and I did not follow it. The window is real and memoria already says so. memoria §7.4: "A snapshot is not a cache; it is a durable copy that outlives the deletion it predates", with retirement waiting out the backup retention window. memoria §3.2: a downgraded chunk "has already left" (to the provider). So a memoria bucket published with its DEK reveals content that Docs made private an hour ago, with no bug anywhere. And the compromised-consumer case is ruling 1's own meaning: a trust domain does not un-share. **Rejected alternatives.** (1) Making custody an erasure rule, with source-driven tombstone push or synchronous purge on `gone`: push is rejected on three grounds in memoria §6.1, no channel reaches a hibernating instance, and erasure cannot be enforced across a process boundary, which is exactly why ruling 1 made the team the boundary and not the process. (2) Sealing pulled content under a source-held key so revocation is key destruction: a consumer that has indexed has content in another costume (memoria §3.1, embeddings are content), so the key buys nothing against the copy that matters, and it is a new mechanism. (3) Keeping the sentence and calling it aspirational: §0 says falsifiable, and a test that is false for a window is not a test. **What it costs.** The clean sentence. Every consumer states its retirement bound by citation (memoria §7.4 already holds the number: snapshot retention plus the backup window), and the contract module carries that reason with its other constants. One gate: ingest, privatise at source, pull `gone`, advance a fake clock past the bound, assert the bytes are retired. §9 item 1 gains "or was team-visible when it was ingested", and memoria's README claim (§3.6) gains a sentence for sibling content matching the one it already has for connectors. The "across" half of §0 should also be scoped to paths the suite provides, as GPT-6 asks; a person exporting a file and uploading it elsewhere was never something the suite could prevent, and the sentence should not read as if it were. **Is GPT-6 right?** Yes on the window and on the compromised consumer. Slightly overstated in one place: "a compromised consumer retains permanently" is not a new leak class, it is §9 item 1 restated with a tense, and the fix is a clause, not a mechanism. The point that `gone` cannot erase knowledge that an item existed is true and minor; the id and revision history remain, never the body. ## One note across all three Each of these is a place where revision 1 borrowed a comforting sentence from a sibling spec and dropped the qualifier that made it true: home §2's "directory" became "home", home §0's "readable" became nothing, and ruling 9's "hint" became "authoritative" without changing who writes. The findings are right in each case, and the corrections are all in the direction the family's own specs already took.</result> <usage><subagent_tokens>104761</subagent_tokens><tool_uses>25</tool_uses><duration_ms>210229</duration_ms></usage> </task-notification>

Human

Yes, sounds good. I'm agreeing with these calls.

Human

We want any document to be: - private to me - shared with specific individuals - shared with the whole team - anyone with the link can [view|edit] etc.

Attributed to this commit: 65.9 g CO₂e (7123494 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on worktree-doctrine-review, never backward from it. A turn that produced more than one commit books all of its tokens to the last one, and a concurrent session on this branch can add to the same figure. Some of it was priced using the shared default rate, because no model-specific factor row exists yet for at least one model involved. The Carbon view has the repo total, which is never derived from this attribution.

diff --git a/README.md b/README.md
index e66aa2c..6c57aa6 100644
--- a/README.md
+++ b/README.md
@@ -25,14 +25,15 @@ the conformance suite in CI against the version it pins.
## Status
-The doctrine is a design, revision 2, and two models have now read it.
-Claude Opus 5 reviewed it on 2026-09-05 and found six defects, whose
-corrections are in the text. GPT-6 read it after that as a third hand and
-returned **request changes**: sixteen findings, six at the trust boundary,
-two of them against the first review's own reasoning. Those two are
-corrected; the rest are `CODEX-FINDINGS.md`, held open in the doctrine's
-§14 for Paul.
-
-So the doctrine is reviewed but not settled. §§0–2 and the rulings drew no
-finding. Do not adopt §§3–8 as final, and do not run §12 step 1, until
-§14's first item is answered. Read the status block first.
+The doctrine is a design, revision 3. Two reviews have run against it:
+Claude Opus 5 on revision 1, six defects; GPT-6 via Codex on revision 2,
+**request changes** with sixteen. Revision 3 answers all sixteen, four of
+them as new rulings — membership gets its own record at the home, a
+compromised home is conceded as three separate compromises, custody is
+bounded at acquisition rather than enforced by erasure, and sharing is a
+three-rung ladder with link-sharing as a capability beside it rather than
+a rung on it.
+
+Nobody has reviewed revision 3 yet, so §12 step 1 waits. Read the status
+block first; it says which rulings are Paul's and which one is still a
+proposal.
diff --git a/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md
index 6aa5cd0..8c62c3f 100644
--- a/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md
+++ b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md
@@ -3,32 +3,49 @@
How the apps of the suite share identity, membership and content with
each other, and where the line is that nothing crosses.
-**Status:** design, **revision 2**, 2026-09-05. Drafted by the suite
+**Status:** design, **revision 3**, 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.
-**Reviewed 2026-09-05 by Claude Opus 5**, in a session that did not write
-it, per AGENTS.md. The review pass §12 step 0 requires is done.
-**It found six defects, and their corrections are in the text below, each
-marked *(Review pass, 2026-09-05)*.** Three tighten a sentence: S4 and S5
-are accounted for (§6), the roster copy gains a maximum age (§5.3), and
-gate 9 gains a measurable unit (§11). Three change a rule: S2's answer is
-intersected with custody (§4.3), the S2 asker subject is typed on the wire
-(§3), and the hand-off gains an origin check and a confirmation step
-(§7.1). The review changed no ruling in §1; ruling 3's tightening to
-"never cached" is a separate change by the drafting session, merged in.
-**Read by a third hand, 2026-09-05: GPT-6 via Codex, verdict *request
-changes*.** It confirmed all three rule changes as answering real defects,
-and confirmed the fix for the typed asker subject outright. It found
-sixteen defects in the document as a whole, six of them at the trust
-boundary, and two of them in the revision-2 corrections' own reasoning —
-both since corrected here (§4.3, §7.1). Its findings are
-`CODEX-FINDINGS.md` beside this file, and they are **not** folded into the
-text: they are a third party's report, and §14 holds them for Paul.
-**So this is reviewed but not settled.** Two models have now read it and
-the second asks for changes. Do not adopt §§3–8 as final, and do not let
-§12 step 1 carry them into four app specs, before §14's first item is
-answered. §§0–2 and the §1 rulings drew no finding and stand as drafted.
+in §1 attributed to Paul was given in that conversation, in an earlier
+spec named in the row, or in the session that produced this revision.
+
+**Two reviews, both by sessions that did not write the draft.** Claude
+Opus 5 read revision 1 and found six defects; GPT-6 via Codex read
+revision 2 and returned **request changes** with sixteen, six of them at
+the trust boundary and two of them against the first review's own
+reasoning. GPT-6's report is `CODEX-FINDINGS.md` beside this file,
+unedited.
+
+**Revision 3 answers all sixteen.** Four of them became rulings, because
+they were decisions and not corrections:
+
+- **Ruling 12**, membership is its own record at the home, written only by
+ an owner. Finding 1 showed that deriving the roster from call-2
+ directory rows let any instance announce a member around §5.4's
+ step-up — and, worse, that it could not have been built: the row has no
+ owner role and is scoped per app, not per team.
+- **Ruling 13**, a compromised home is three compromises, conceded
+ separately. Finding 2. Revision 1 took home §2's sentence about a
+ compromised *directory* and widened it to the *home*.
+- **Ruling 14**, custody is bounded at acquisition and retired by the
+ feed, not enforced by erasure. Finding 6. §0's inside test is rewritten
+ to promise what the design delivers.
+- **Ruling 15**, four sharing states, one ladder, and a link is not a
+ rung. From Paul's product requirement, not from a finding.
+
+The other twelve are answered in place and cite where they landed:
+provenance envelopes on both write paths (§7), receipt and commit split in
+the hand-off (§7.1), the failed refetch and the rewound home (§5.3),
+epoch-bound paging on call 8 (§5.3), the epoch claim's own exception in
+home §5.4 (§5.3, A.1), the source-behind revision (§4.3), gate coverage
+(§11), and Appendix A's leftovers and misattributions (A.1–A.5).
+
+**Rulings 12, 13 and 14 are Paul's**, taken 2026-09-05 on Fable's
+recommendation after the GPT-6 review. **Ruling 15 is this session's
+proposal** from Paul's requirement the same day, and he has not ruled on
+it: it is the one row of §1 that is not settled.
+
+**Not yet read by anybody.** Revision 3 is substantially new text and no
+review has seen it. §12 step 1 waits on a third pass (§14).
**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
@@ -46,12 +63,31 @@ diverges, §14 says so and names the filing.
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.
+ them holds and the worst anyone learns is what that team could read in
+ the owning app **at the moment the copy was taken**. An honest instance
+ retires a copy that has left team-visibility within one feed pull plus
+ its stated retention (§4.2); a compromised one never retires anything.
+- **Across.** Take two teams. **There is no path the suite provides** 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. A person who exports a file and uploads it by hand has
+ left the paths the suite provides, and nothing here prevents that.
+
+**"At the moment the copy was taken" is the design, not a hedge.** Custody
+is bounded at acquisition and retired by the feed; it is never enforced by
+erasure, because no app can erase from another app's process — which is
+precisely why ruling 1 put the boundary at the team and not at the
+process. §9 item 7 already concedes this one layer up, for a page a
+browser has rendered. This is the same sentence for bytes at rest.
+
+Revision 1 stated the test instantaneously. That was false for the window
+between a source privatising an item and the consumer pulling its `gone`
+row, and false permanently against a consumer that has been broken into.
+The home made this same correction on its own §0: *"never stores your
+address"* was a claim the schema could not keep, so it became *"never
+stores a **readable** address"* with the window carried in its §3.2. The
+precedent was in the family and this document did not follow it. Ruling
+14.
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
@@ -75,6 +111,10 @@ happened to fall while every CARLOS app was one process.
| 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) |
+| 12 | **Membership is its own record at the home, written only by an owner.** `(team, person, role)`, written at the directory origin behind step-up, or seeded by the drain with the team's first owner. A directory row says where a person has things; it never grants or revokes team standing. Call 2 cannot write membership and call 8 reads only the membership record. | Deriving the roster from call-2 directory rows, which revision 1 chose. The row has no owner role to project (home §6: `kind` is member or guest; home §7: who counts as an owner "is the instance's business… and the home never learns the answer"); it is scoped per (team, app) where the roster is per team, so a projection can express "member of acme.docs but not acme.sheets", which ruling 1 forbids; and any instance writes it under a consumer token, so deriving from it routes around §5.4's step-up | Paul, 2026-09-05, on Fable's recommendation after the GPT-6 review (finding 1) |
+| 13 | **A compromised home is three compromises and each is conceded on its own.** A leaked database reveals the graph, the roster, and who owns each team. A compromised directory origin can add a member, and a member reads everything team-visible in every instance of that team. A compromised credential origin can sign in as anyone, owners included, and no instance can tell. | One undifferentiated "compromised home" that "cannot read a document", which took home §2's careful *directory* and widened it to *home*, and counted as evidence what only honest code leaves behind | Paul, 2026-09-05, on Fable's recommendation after the GPT-6 review (finding 2) |
+| 14 | **Custody is bounded at acquisition and retired by the feed. It is not enforced by erasure.** What an instance may *acquire* is team-visible at the time; what it may *render* is live-authorized (ruling 3); what it must *retire* is stated and timed. Three rules, not one. | §0's inside test stated instantaneously; source-driven tombstone push (home §6: no channel reaches a hibernating instance); sealing pulled content under a source-held key, which buys nothing once a consumer has indexed, since an embedding is content in another costume (memoria §3.1) | Paul, 2026-09-05, on Fable's recommendation after the GPT-6 review (finding 6) |
+| 15 | **Four sharing states, one ladder, and a link is not a rung.** An item is *private*, *granted* to named people, or *team*. Orthogonally it may carry a link capability, view or edit. **Only the `team` rung is team-visible**, so custody keys on the rung and never on the link. | Treating a link share as team-visible because "anyone can read it", which converts a capability held by whoever has the URL into a listing every team member can find through a sibling's search — strictly more exposure than the sharer chose | This session, 2026-09-05, from Paul's product requirement the same day. **A proposal until Paul rules on it** |
---
@@ -179,10 +219,42 @@ 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).
+**The ladder every app implements** (ruling 15). Four product states, three
+rungs and one capability:
+
+| State | Rung | Team-visible? |
+|---|---|---|
+| Private to me | `private` | No |
+| Shared with specific people — members, or guests from outside (§5.2) | `granted` | No |
+| Shared with the whole team | `team` | **Yes** |
+| Anyone with the link can view or edit | *a capability on any rung* | **No, by itself** |
+
+**Custody keys on the rung, never on the link.** The predicate is not "can
+somebody read this", it is "does team standing alone reach it". A link
+share is a capability held by whoever has the URL, and it is deliberately
+not a grant to anybody in particular.
+
+Getting this wrong is a live leak, not a technicality. Ana keeps a
+document private and link-shares it with one contractor. If the link made
+it team-visible, memoria would ingest it, and the first teammate to search
+for its subject would be handed a passage from a document nobody gave
+them. The sharer chose "whoever I send this URL to". Treating that as
+"everyone on the team, findable by search" is a different decision made on
+their behalf. An item that is *both* on the `team` rung and link-shared is
+team-visible because of the rung; the link adds nothing either way.
+
+**A link share still moves nothing** (ruling 2). Whoever holds the URL
+reads the item in the app that owns it, so no byte reaches another team's
+instance and §0's "across" half is untouched. A link holder is never on a
+roster, so no sibling ever names one as an S2 asker — siblings serve
+members, and a link holder is served by the owning app directly.
+
+Individually granted items, guest-only items and link-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). A
+move down the ladder — `team` to `granted` or `private` — is a departure
+like any other, and reads as `gone`.
### 4.3 Authorization
@@ -192,6 +264,17 @@ 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 revision may be behind in either direction, and only one is
+`STALE`.** memoria §4.1 defines `STALE` as the source having moved *past*
+the requested revision. A source restored from backup can be *behind* it —
+the consumer holds revision 7 and the app of record now has 5. `VISIBLE`
+would authorise a revision the app of record cannot vouch for; `NOT_VISIBLE`
+files a synchronisation failure under the one value that is allowed to be
+silent, which is the exact mistake `STALE` was introduced to fix. So a
+revision the app of record does not recognise is **`UNKNOWN`**: withheld
+loudly, the app named, the consumer free to re-ingest. memoria records
+this defect open in its §13; A.4 closes it. *(GPT-6 review, finding 11.)*
+
**Never cached, at any scope.** Not per session, not per page, not for
the life of a warm process. Caching authorization is what makes a stale
answer a leak (memoria §6.5), and the usability pressure to cache within
@@ -251,10 +334,29 @@ while keeping Ana's grant, query before the feed catches up.)*
- **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.
+ `member`, in a **membership record of its own** — `(team, person, role)`,
+ not a projection of anything else the home holds. This is ruling 4's
+ expansion and ruling 12's shape.
+
+**Revision 1 derived the roster from directory rows, and that was wrong
+three ways.** It is recorded because the economy was tempting and someone
+will propose it again: it adds no table, and it leaves the home's
+publish-the-database claim untouched.
+
+- **The row has no owner role to project.** Home §6's directory row is
+ `(person ref, app, team sqid, host, kind, sealed team name)` and "`kind`
+ is member or guest". Home §7 is explicit that who counts as an owner "is
+ the instance's business… and the home never learns the answer". §5.3's
+ wire shape had no source.
+- **The row is scoped per (team, app); the roster is per team.** A
+ projection can therefore express "a member of acme.docs but not of
+ acme.sheets", which ruling 1 forbids outright.
+- **Any instance can write it** under its consumer token, through call 2.
+ Deriving team standing from it routes around §5.4's step-up completely.
+
+Changing the reader without changing the writer is not a design. Ruling 9
+made a directory row a hint on purpose; revision 1 made it authoritative
+and left the writer alone.
### 5.2 What the home never owns
@@ -282,11 +384,21 @@ GET /roster?team=<sqid>&epoch=<n>
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.
+call-8 response, and in the assertion as a claim.
+
+**The epoch claim is an invalidation hint, and it needs its own exception
+in home §5.4.** Revision 2 said the claim passed that test unchanged. It
+does not: home §5.4 admits a non-identity claim when it is drawn from a
+closed enumerated set and fails cosmetically, and a monotonically rising
+epoch is neither of those. What makes it safe is a different argument and
+deserves its own words — **the instance decides nothing from the claim**. A
+claim it cannot parse is ignored. A claim that differs from what it holds
+buys exactly one call-8 refetch, and call 8's answer is what it enforces.
+So a hostile claim costs one request and can neither admit nor exclude
+anybody. A.1 records that exception in the home spec, which owns the test;
+this document cites it rather than asserting the old test already covered
+it. A person whose sign-in reveals a stale roster is admitted against the
+fresh roster, never against the claim. *(GPT-6 review, finding 12.)*
**The copy has a maximum age.** Every carrier of the epoch — call 2, call
5, call 8, the assertion — needs a person or a wake. Sibling calls carry no
@@ -299,9 +411,38 @@ contract module requires: it is the worst-case lag between a removal at the
home and its effect on an instance nobody signs in to. Measured with §14's
deadline rather than guessed here. *(Review pass, 2026-09-05.)*
-**Call 8 is paged**, with a cap and a cursor as S2 and S6 have them, and
-for the same reason: a response whose size is a team's membership is
-unbounded, and every other call in this contract states its cap.
+**Call 8 is paged, and a page is only meaningful inside one epoch.** S2
+and S6 cap a batch the *caller* supplies; call 8 is the first call whose
+response size the caller does not control, so it needs a cursor as well as
+a cap. The cursor is bound to the epoch it was issued in. Fetching page one
+at epoch 10 and page two at epoch 11 would otherwise compose a membership
+set that never existed at either — skipping a removal, or dropping an
+owner. So: the home refuses a cursor from a superseded epoch and says which
+epoch is current; the instance **stages every page and replaces its roster
+atomically only when a complete set for one epoch has arrived**; and a
+partial or failed fetch leaves the previous roster in place. A partial
+roster is never published, and an empty response is never read as "nobody
+is a member". *(GPT-6 review, finding 10.)*
+
+**A refetch that fails does not silently extend the copy.** The TTL says
+when to refresh; it has to say what happens when refreshing does not work,
+or "removed within the TTL" is a promise about the happy path only. Past
+the TTL with no successful fetch, the roster is **stale**: the instance
+keeps serving people it has already admitted for the length of their
+session, refuses to admit anyone new on team standing, and answers
+`UNKNOWN` on S2 rather than `NOT_VISIBLE` — the gap is named, per §4.3,
+because an unreachable home is a synchronisation failure and not a denial.
+This costs availability on purpose. The alternative is that an unreachable
+home lets a removed member keep their access for as long as the outage
+lasts.
+
+**A lower epoch is refused.** A home restored from backup can offer epoch
+10 to an instance that already applied 11, which would readmit everyone
+removed in between. An instance never accepts an epoch below the one it
+holds; it treats the regression as an unavailable home, per the paragraph
+above, until the home passes its own high-water mark. An instance restored
+from its own backup refreshes before it admits anyone against the restored
+roster. *(GPT-6 review, finding 5.)*
**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
@@ -310,10 +451,21 @@ 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.
+step-up (home §14 names it; this is its trigger), and **nowhere else**.
+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.
+
+**Call 2 cannot write membership** (ruling 12). It writes directory rows,
+which say where a person has things, and no directory row grants or
+revokes team standing however its `kind` reads. The membership record is
+the only source for call 8.
+
+**The first owner is seeded by the drain**, from the sealed queue payload
+home §7 already carries, because gating a membership write on an owner
+needs an owner to exist first, and that circularity has to break outside
+the roster.
**A member removed at the home is removed everywhere within one epoch**,
and on an instance that admits nobody, within the roster TTL (§5.3).
@@ -322,19 +474,48 @@ 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).
+Ruling 13. "A compromised home" is three different events and they cost
+three different things. Revision 1 ran them together, and it did so by
+taking home §2's careful sentence about a compromised **directory** and
+widening it to the **home**. Home §3.1 keeps the two apart and records
+that its own first draft collapsed them and was wrong to.
+
+**A leaked database** reveals the graph, the roster, and — new with ruling
+12 — who owns each team. It reveals no content.
+
+**A compromised directory origin** can add a member. A member reads
+everything team-visible in every instance of that team, through each app's
+ordinary front door, because that is what ruling 1 means. It cannot read
+an individually granted or guest-only item, write as a person, or grant a
+per-item right. This is the delta ruling 4 and ruling 12 actually
+introduce.
+
+**A compromised credential origin** can sign in as anyone, owners
+included. It holds the signing key (home §5.3), the instance's grants join
+is keyed on the `sub` the home asserted, and home §5.4 already calls a
+hostile `sub` account takeover. No membership row is needed and none is
+left behind. **This is not new and ruling 4 did not cause it**: it has been
+true since home ruling 1 put every credential on one origin, and a passkey
+does not help, because the passkey lives there too. It is cited here, not
+introduced here.
+
+**The step-up is not a bound on any of them**, because the compromised
+authority is the one performing it. Neither is the directory row or the
+notification: those are what honest code leaves, not what a compromise
+must. Home §3.3's idiom applies — said plainly rather than counted as a
+fix.
+
+**What is left as a real bound**: an instance's own grants decide what an
+admitted person may read, so a compromise of the home cannot reach an
+individually granted item; and the instance enforces from its own copy, so
+an outage is lag rather than admission (§5.3 says what a *failed* refetch
+does, which is a different question).
+
+**The Pegamento filing changes shape.** It is not that this document
+diverges from §10.3 on membership. It is that **the home was never a
+§10.3 glue**: "a compromised glue grants nothing" is written for a
+registry, and home ruling 1 makes the home an identity authority. §14
+files that, not the narrower claim.
### 5.6 Roles
@@ -425,6 +606,36 @@ call. Content leaving the team's hosts except under ruling 6.
The question "may memoria write a Doc" has two answers because it is two
questions.
+**Both answers carry the same envelope.** §4.3 requires a derived artefact
+to carry the provenance of every source it was built from, transitively.
+That rule had no mechanism at the one moment it matters — when content
+crosses from one app's store into another's — so it is stated here, for
+both write paths.
+
+A write carrying content derived from items in other apps carries a
+**provenance envelope**: per source, the app of record, the item id and the
+revision read. The receiver stores it as part of the item, not as metadata
+it may drop, and on every disclosure path afterwards — an ordinary read, an
+export, an activity row, an onward S3 fetch — it checks that the sources
+still authorize the asker, through S2 to each app of record, exactly as the
+first render did.
+
+Two consequences, both counter-intuitive enough to state:
+
+- **A new id is not a new authority.** Saving a memoria answer into Docs
+ gives the result a Docs id; it does not give it Docs' authority over a
+ Sheets cell it quotes. Ana's grant on the new Doc is necessary and not
+ sufficient. Revoke the sheet and the saved answer loses that passage, in
+ Docs, on the next read.
+- **A source the receiver cannot check is a source it must not accept.**
+ memoria's decision 7 and gate 17 forbid it serving a sibling-facing
+ endpoint at all, so today a hand-off carrying memoria-owned material has
+ no app of record to ask. Until A.4 opens that surface with its own
+ decision and leak entry, a write whose sources include one is refused,
+ rather than accepted with an envelope nobody can verify. **A person's
+ confirmation establishes their intent; it does not stand in for an
+ authorization the receiver cannot make.** *(GPT-6 review, finding 3.)*
+
### 7.1 A person is present: the hand-off
"Save this answer as a Doc" is Ana's act. memoria renders a form whose
@@ -471,11 +682,33 @@ origin guard, and one more round trip — and gate 7 has to run through that
middleware, since a handler-level fixture passes while a browser does not.
Still scriptless, which was the property worth protecting.
-**The commit step is also under-specified**, and this section does not yet
-fix it: nothing here binds the final mutation to the reviewed body, to the
-browser that reviewed it, or to a single redemption. *(Review pass,
-2026-09-05; corrected and extended from the GPT-6 review, findings 4 and
-9.)*
+**Receipt and commit are two requests with different rules**, and revision
+2 left the second unspecified. Split:
+
+- **Receipt.** The sibling's POST arrives, its `Origin` is checked against
+ the `consumers` allowlist, and the body is held as **pending state**:
+ bounded, expiring, redeemable once, bound to the browser and — after
+ sign-in — to the authenticated person. Nothing is created.
+- **Commit.** The person's confirmation POST comes from the *receiver's*
+ own origin, against that pending state, and what it commits is the body
+ the server already holds. The sibling-origin rule does not apply to this
+ request and would wrongly reject it. Because the body is fixed
+ server-side a second POST cannot substitute a different one, and because
+ the pending state redeems once, a replay creates nothing.
+
+Without the split, a bare second POST carrying a `confirm` flag can be sent
+by the same allowed sibling, and a screen appearing somewhere in the flow
+is not evidence that this browser saw this body.
+
+**The sending side has a boundary too, and it is earlier than the
+receiver's.** §4.1's rule that a client refuses a host whose `<team>` label
+differs governs the sibling HTTP client; an HTML form action is not that
+client. A form action misconfigured to another team's Docs host puts the
+body on the wire before any `Origin` check at the far end can refuse it,
+which breaks §0's "across" test literally: the byte arrives, and being
+refused on arrival is not the same as not arriving. So **the destination is
+validated against the sender's own `<team>` label before the form is
+rendered**, not after it is submitted. *(GPT-6 review, finding 4.)*
### 7.2 No person is present: write-as-self
@@ -495,10 +728,25 @@ suite tests:
- **Never a person-authored item.** Stated separately because it is the
attack the constraints exist to prevent.
+### 7.3 A link holder writes, and is named as one
+
+"Anyone with the link can edit" (ruling 15) is a third actor, and ruling
+9's requirement that a write name its real actor binds it too. A link
+holder has no person ref and may be on no roster, so the honest actor is
+*the link capability itself*: the item records that this revision came
+from link `<id>`, alongside whatever the app can attest, and never a
+person's name it does not have.
+
+Two consequences the receiving app enforces. A link write is confined to
+the item the link addresses, since a capability reaches exactly what it
+names and nothing adjacent. And a link holder never reaches a sibling
+call: S2's asker is a roster subject (§3), a link holder is not one, and
+no sibling serves an actor it cannot name to the app of record.
+
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
+### 7.4 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
@@ -526,8 +774,14 @@ chooses whether, and never whom: a team-configurable endpoint would let an
owner reinstate a proxy while every answer still printed "one named
provider" (memoria decision 13, Paul, 2026-09-05).
-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
+Docs §11.2's rejection of conversion APIs stands under the first clause.
+**Its own reason is that the egress sends customer documents to a third
+party**, which is what the passage says; "unavoidable and unnamed" is this
+document's *narrowing* of it under ruling 6, not a reading of what it
+already meant. A per-team optional converter with one named provider could
+satisfy §8 and would not have satisfied Docs §11.2 as written. Ruling 6
+authorises the change; the citation must not dress it as a discovery.
+*(GPT-6 review, finding 15.)* 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
@@ -542,9 +796,13 @@ 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.
+ across every app, through S3, **and everything that was team-visible
+ when it was ingested**. This is what "one trust domain" means and there
+ is no version of ruling 1 without it. Custody buys two things and not a
+ third: it cannot read an item that was never team-visible — private,
+ granted, or link-only (§4.2) — and an honest consumer retires what
+ leaves; a compromised one retires nothing, and no revocation reaches a
+ copy it already holds (ruling 14).
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. **The bound holds only because S2 is intersected
@@ -554,8 +812,17 @@ apps are a suite, and every one is a consequence of ruling 1 or 4.
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.
+4. **A compromised home is three events** (§5.5, ruling 13). A leaked
+ database gives up the graph, the roster, and who owns each team. A
+ compromised *directory origin* can add a member, and that member then
+ reads everything team-visible in every instance of the team. A
+ compromised *credential origin* can sign in as anyone, owners included,
+ with no membership row and nothing left behind — not new, and not
+ caused by ruling 4: it is home ruling 1's standing cost of one
+ credential origin. None of the three can read a private, granted or
+ link-only item, write as a person, or grant a per-item right. Neither
+ the step-up nor the notification bounds any of them, because the
+ compromised process is the one performing them.
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
@@ -569,6 +836,25 @@ apps are a suite, and every one is a consequence of ruling 1 or 4.
*next* request shows; nothing bounds what the last one already did.
Stated so nobody argues a surface is safe because it re-checks
(memoria §3.2).
+8. **Every instance of a team now holds the whole team's roster**, with
+ roles, rather than only the people it had heard of (§5.3). A
+ compromised instance therefore learns the membership and the owners of
+ its team, which under the old per-instance tables it could only partly
+ infer.
+9. **Owner is a new plaintext class at the home** (ruling 12). Publishing
+ the database now identifies each team's owners. Home §0's sentence and
+ gate 8 change to say so; the roster is not the no-op for that claim
+ revision 1 assumed.
+10. **The home learns when a person visits their front door**, at
+ per-visit granularity, and tells that person's instances so they can
+ answer call 7 (activity design §2.2). And once the home makes outbound
+ calls to instances as a matter of course, its outbound traffic stops
+ being a compromise signal (activity design §2.2(b)). Both were
+ disclosed there and belong here too.
+11. **A link capability is bearer-shaped** (ruling 15). Whoever holds the
+ URL reads or writes the item, the app cannot name them, and a
+ forwarded link is a grant nobody recorded. This is what "anyone with
+ the link" means and there is no version of it that is not this.
---
@@ -618,48 +904,86 @@ that cannot be written as stated is a spec bug.
with short, duplicate, malformed and non-2xx responses, and with an
asker subject of a kind the receiver does not key on, which is
`UNKNOWN` for the whole call and never `NOT_VISIBLE`.
-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".
- **S2 answers `NOT_VISIBLE` for those same three fixtures with a named
- asker who holds an individual grant on them**, and the answer is byte
- for byte the answer for the deleted one.
-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, and from an
- instance that admits nobody within the roster TTL. A lone
- instance, given a home hostname in a fixture, makes no request to it.
+3. **Custody, one fixture per rung.** S3 answers 200 for a `team` item and
+ 404 for `private`, `granted`, guest-only, **link-shared-but-not-team**
+ (ruling 15) and deleted. The status and body are identical for "not
+ team-visible" and "does not exist". **S2 answers `NOT_VISIBLE` for all
+ five with a named asker who does hold a grant on them**, byte for byte
+ the answer for the deleted one. A `granted` fixture means a live grant
+ held by a former or current member, never a dangling grant row an app's
+ schema would not permit.
+4. **No cross-team dial.** Zero connections to a host whose `<team>` label
+ differs — `acme-evil`, `acme.docs.oficina.works.evil` — and zero
+ *follow-up* connections when an allowed host answers with a redirect to
+ another label. The literal "without connecting" cannot be written for
+ the redirect case: a client must reach the allowed host to be told to
+ redirect. Two fixture servers, or an instrumented transport that counts
+ connections per host. The hand-off's destination check (§7.1) runs in
+ the same gate, before the form renders.
+5. **Roster**, on a fake clock. A member removed at the home, **holding no
+ other grant**, gets an empty call-7 list and `NOT_VISIBLE` on S2 from
+ every instance after one epoch, and within the roster TTL on an instance
+ that admits nobody. A **guest fixture** keeps exactly the activity its
+ own grant reaches: losing team standing does not revoke an
+ instance-side grant, and asserting an empty list for that person would
+ implement a complete-offboarding policy §5.2 has not agreed to. A
+ **failed refresh** past the TTL yields `UNKNOWN`, not `NOT_VISIBLE`, and
+ admits nobody new. A **home rewound** to a lower epoch is refused and
+ readmits nobody. 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. A POST whose `Origin` is not in the consumers
- allowlist is refused, and no fixture POST commits an item without the
- confirmation step.
-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.
+ when the team has designated no area. The actor is derived from the
+ authenticated consumer, never from the request body, and a spoofed
+ actor field is refused.
+6b. **Provenance survives the hop** (§7). Write an item into app B derived
+ from two sources in apps A and C; revoke the source in A; assert B's
+ ordinary read, its export and its S3 answer all lose that passage and
+ name the app. Assert a write is refused when a source's app of record
+ serves no S2.
+7. **The hand-off, through the real middleware.** A handler-level fixture
+ passes where a browser does not, so this runs against the app's actual
+ CSP and CSRF stack: receipt refused when `Origin` is outside the
+ consumers allowlist; a POST without a session lands in sign-in and
+ returns to the pending state by nonce; a body over the cap refused with
+ no partial write; commit refused when forged, replayed, sent with a
+ changed body, or sent from a different session; the absent and `null`
+ origin cases decided explicitly; a foreign-team destination refused
+ **before transmission**; and one successful same-team flow end to end.
+8. **Publish the home's database.** Counts, team names, memberships —
+ **and, new with ruling 12, who owns each team**. Home §0's sentence
+ changes rather than survives; revision 1's claim that the roster added
+ no column class was wrong. The test runs against the actual persisted
+ schema and its failure paths, not one roster response fixture, because a
+ fixture cannot prove what an entire database holds at rest, and a column
+ audit alone misses new information encoded in existing columns.
9. **Citation currency.** Each app spec's *Consumed suite rulings* section
- names a doctrine commit **and the doctrine revision it read**. The
- revision is in the status block and is bumped by any change to §1 or
- §6; the script fails when a spec is more than one revision behind
- `main`. *"One ruling behind" was not measurable — a change to §6 adds
- no ruling — and a gate that cannot be written as stated is a spec bug
- by this section's own first line. (Review pass, 2026-09-05.)*
+ names a doctrine commit **and the doctrine revision it read**, and the
+ script checks the revision actually belongs to that commit. The revision
+ is in the status block and is bumped by **any normative change,
+ wherever it lives** — §§3 to 8 bind as hard as §1 and §6. Revision 2
+ scoped the bump to §1 and §6, which let a changed hand-off rule or
+ stale-roster policy pass currency indefinitely. The script fails when a
+ spec is more than one revision behind `main`; that grace period is a
+ deliberate compatibility allowance and not evidence the consumer meets
+ current requirements. *("One ruling behind" was not measurable at all —
+ a change to §6 adds no ruling. Review pass 2026-09-05; scope corrected
+ from the GPT-6 review, finding 16.)*
---
## 12. Sequencing
-0. ~~**This spec, reviewed.**~~ **Done, 2026-09-05, Claude Opus 5.** See
- the status block. Its three rule changes are §14's first open item.
+0. ~~**This spec, reviewed.**~~ **Done twice, 2026-09-05: Claude Opus 5,
+ then GPT-6 via Codex.** See the status block. Revision 3 answers all
+ sixteen of the second review's findings; the third pass is §14's first
+ open item.
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.
+ code. **A.1 is now the largest of the four**, because ruling 12 gives
+ the home a table it did not have and amends home §7's sentence that
+ ownership is the instance's business.
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.
@@ -693,44 +1017,30 @@ change to where the list comes from.
| 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 |
+| An instance-side check on membership changes — refusing a roster addition, or a call-7 arrival, for a person with no recent session; an owner countersignature on roster writes | A designed mechanism. Ruling 13 concedes a compromised home rather than bounding it, and this is the shape a bound would take. Recorded so it reads as absent by decision, not by oversight |
+| A link share that a sibling can index | Never, under ruling 15. A link is a capability, and a capability that becomes searchable by the team is a different grant than the one made |
---
## 14. Open
-- **The GPT-6 review's sixteen findings**, for Paul. `CODEX-FINDINGS.md`,
- 2026-09-05, verdict *request changes*. It reads the three review-authored
- rule changes as answering real defects and passes the typed asker subject
- outright; its other findings are open. Six sit at the trust boundary and
- none is editorial:
- - **Directory rows carry an instance-token write path** (finding 1).
- Home call 2 lets an instance upsert its own directory rows. §5.1 makes
- those rows the authoritative roster without giving membership its own
- write authority, so an instance can announce a member around §5.4's
- owner step-up. This is the one that most looks like it wants a ruling.
- - **§5.5's compromised-home bound is too kind to itself** (finding 2). A
- home that mints assertions can impersonate an existing owner, which
- needs no membership row and predates ruling 4; and a compromised
- process is not made to leave the directory row or notification the
- section counts as evidence.
- - **Neither write path carries provenance** (finding 3), so §4.3's
- transitive-provenance rule has no mechanism at the moment content
- crosses an app.
- - **A roster refetch that fails has no stated behaviour** (finding 5),
- and a home rolled back to a lower epoch can readmit.
- - **Custody is a rule about rendering, not about erasure** (finding 6).
- A consumer keeps what it lawfully ingested after the item goes
- private, so §0's "inside" test and §9 item 1 promise more than the
- design delivers.
- - **The hand-off's commit step** (finding 4), above.
-
- Ten more at P2, including that Appendix A leaves contradictions standing
- in memoria and paraphrases where §10 asks for citations (finding 14),
- that §2 and §8 put words in Docs' and Sheets' mouths their own text does
- not say (finding 15), and that gate 9 still goes green across a change to
- §3, §5 or §7 (finding 16). Its gate-by-gate table marks 5 and 7 as not
- yet writable, and 4 as impossible literally — a client must connect to
- the first host to be told to redirect.
+- **A third review pass, on revision 3.** Two models have read this and
+ the second returned *request changes*; revision 3 answers all sixteen of
+ its findings and adds four rulings, so what needs reading now is
+ different text. Rulings 12, 13 and 14 are Paul's, taken on Fable's
+ recommendation. **Ruling 15 is this session's proposal from Paul's
+ product requirement and he has not ruled on it** — a reader should treat
+ it as the one rung of §1 that is not yet settled.
+- **What the second review said**, kept for the record. `CODEX-FINDINGS.md`
+ is unedited beside this file; the map from its findings to revision 3 is
+ in the status block. Its six trust-boundary findings were:
+ finding 1, the instance-token write path into the rows §5.1 made
+ authoritative → **ruling 12**; finding 2, the compromised-home bound →
+ **ruling 13**; finding 6, custody as rendering rather than erasure →
+ **ruling 14** and §0; finding 3, provenance missing from both write
+ paths → §7's envelope; finding 5, the failed refetch and the rewound
+ home → §5.3; finding 4, the commit protocol → §7.1. The ten at P2 are
+ answered in place and cited where they landed.
- **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
@@ -754,7 +1064,16 @@ change to where the list comes from.
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.
+rulings* section naming this document's commit **and revision** (gate 9).
+
+**Where a replacement below still paraphrases a rule, the paraphrase is
+the defect, not the fix.** §10 and AGENTS.md say a rule lives here once
+and an app spec names it. A replacement that puts a second copy of a
+boundary into an app spec and then adds a citation beneath it has left
+the second copy to drift, which is the failure §2 diagnosed. Read each
+amendment as *delete the restatement, name the ruling*; the wording given
+is there to identify what is being deleted, not to be pasted in as a new
+copy. *(GPT-6 review, finding 14.)*
### A.1 `oficina/home`, spec §1 and §2.2
@@ -771,11 +1090,30 @@ 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
+In §9 add call 8 per suite §5.3, paged with an epoch-bound cursor.
+
+**A.1 is the largest of the four, because ruling 12 gives the home a
+record it does not have.** Add a `memberships` table, `(team, person,
+role)`, written only at the directory origin by an owner behind step-up,
+and seeded by the drain with the first owner from the sealed queue
+payload §7 already carries. Call 8 reads only that table. **Amend §7's
+sentence** that who counts as an owner "is the instance's business… and
+the home never learns the answer": the home now learns it, for team
+standing only, and per-item roles stay the instance's. **Amend §6** so a
+directory row is explicitly a hint that never grants or revokes team
+standing, whatever its `kind` says, and **call 2 cannot write
+membership**. **Amend §0 and its gate**: publishing the database now
+identifies each team's owners, which is a new plaintext class (suite gate
+8).
+
+In §5.3's claims table add `roster_epoch`. In §5.4 record a **narrow
+invalidation-hint exception** rather than claiming the existing test
+covers it: the epoch is not a closed enumerated set and does not fail
+cosmetically, and what makes it admissible is that the instance decides
+nothing from it — an unparseable claim is ignored, a differing claim buys
+exactly one call-8 refetch, and call 8's answer is what is enforced. In
§14 move *step-up for instances* to built-when: *"roster writes (suite
-§5.4)"*.
+§5.4)"*. *(GPT-6 review, findings 1, 12 and 13.)*
### A.2 `oficina/docs`, spec §8.2, §9, §11.2
@@ -783,21 +1121,34 @@ mismatch costs one refetch and a failed match is treated as absent. In
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."*
+S1 to S3 and S8 **wherever sibling consumer tokens are configured**."*
+The sibling switch is independent of shape (suite §3): §12 steps 2 and 3
+have Docs and Sheets serving lone peers before the home exists, so "in
+suite shape" would forbid the sequencing this document depends on.
§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.
+§11.2: the rejection stands, and the reason **changes** rather than being
+restated. Docs' own words are that a conversion API sends customer
+documents to a third party. Suite §8 is narrower — a per-team optional
+converter with one named provider would satisfy it and would not have
+satisfied Docs §11.2 as written — so the amendment reads *"Rejected under
+suite §8. Ruling 6 governs; this is a qualification of the original
+rejection, not a restatement of it."* This closes discussion 33 by
+agreeing on the rule, not by claiming both specs always meant the same
+thing. *(GPT-6 review, finding 15.)*
Docs is the first receiver of the hand-off, so its create endpoint takes
-suite §7.1's two protections: the `Origin` check against the `consumers`
-allowlist, and the confirmation screen before the item is written. Gate 7
-is Docs' to run first. *(Review pass, 2026-09-05.)*
+suite §7.1 whole: the `Origin` check against the `consumers` allowlist at
+receipt, pending state, and a commit from Docs' own origin. Its pinned
+`csrf.Protect` refuses the receipt POST today and its route needs an
+explicit replacement guard; memoria's CSP needs a narrowly scoped
+`form-action` allowance for the sending side. Gate 7 is Docs' to run
+first, through that middleware rather than around it. *(Review pass
+2026-09-05; extended from the GPT-6 review, findings 4 and 9.)*
### A.3 `oficina/sheets`, PRODUCT.md principle 2, spec §8.2, §9, AGENTS.md
@@ -814,9 +1165,13 @@ roles stay here."*
§9: as A.2's §9.
-AGENTS.md: replace *"a contradiction gets fixed in the same commit"* with
+AGENTS.md: **keep** *"a contradiction gets fixed in the same commit"* and
+add a second rule beside it. The existing rule is about that file and the
+Sheets design spec, both of which live in Sheets, so it is a local
+consistency obligation that works and was never the thing that broke.
+Revision 1 read it as a cross-repository claim it does not make. Add:
*"a contradiction with the suite doctrine is a bug in this spec, fixed by
-citing it."*
+citing the ruling."* *(GPT-6 review, finding 15.)*
### A.4 `oficina/memoria`, spec §1, §1.3, §4.3, §6, §8.3, §13
@@ -857,10 +1212,40 @@ made it. §5.2 records the producing model alongside provenance so a
regenerated artefact never inherits the wrong name. Raised by the memoria
session, 2026-09-05.
+§4.1 also: define the **source-behind** case. `STALE` covers a source
+that has moved past the requested revision; a source restored from backup
+can be behind it, and memoria's own §13 records this open. A revision the
+app of record does not recognise is `UNKNOWN`, withheld loudly — never
+`VISIBLE`, which would authorise a revision nobody can vouch for, and
+never `NOT_VISIBLE`, which is the silent value and the exact mistake
+`STALE` was added to fix (suite §4.3). *(GPT-6 review, finding 11.)*
+
+§9.1 and gate 17: decision 7's "no sibling-facing endpoint" now blocks a
+hand-off whose sources include memoria-owned material, because suite §7
+requires the receiver to check every source with its app of record and
+memoria serves nobody to ask. Either open that surface with its own
+decision and leak entry, as memoria §12 requires, or record that such a
+hand-off is refused until it is. Not an editorial change: it decides
+whether "save this answer as a Doc" works for answers built on memoria's
+own uploads. *(GPT-6 review, finding 3.)*
+
+§11: memoria's person-ref and home dependency on slice 1 survives A.4 as
+written. Slice 1 runs in lone shape against sibling tokens (suite §3, §12
+step 3), so the dependency is on the typed asker subject, not on the
+home. Correct it, or slice 1 stays blocked on something that is not
+actually in its way.
+
+§4.3 and its admission fixtures: retiring S7 leaves the admission gate
+citing a call that no longer exists. Replace the fixtures too, not only
+the prose.
+
§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.
+rulings 1 to 15, with the doctrine's commit and revision, when calendar's
+first screens merge. Calendar has no commenter role (verified in
+`internal/calendar/models.go`), so §5.6's "not before" applies to it
+unchanged.