Review pass on the sharing doctrine: six defects, corrected
Claude Opus 5, in a session that did not write the draft, as AGENTS.md and §12 step 0 require. Recorded by name in the status block; the doctrine is now revision 2. Three corrections change a rule: - S2's answer is intersected with custody (§4.3). memoria §4.1 answers from the full grants join, "plus rows granted to me individually", so S2 would answer VISIBLE for an item that had left team-visibility and NOT_VISIBLE for one that was deleted — distinguishing exactly what §4.2 requires to be indistinguishable, and reopening on the third call the leak that removing S1's visibility flag and giving S3 "404, never 403" had closed on the first two. - The S2/S6 asker subject is typed on the wire (§3). Its meaning is shape-dependent, nothing else on the wire says which shape, and a mismatched pair fails to NOT_VISIBLE: the one value §4.3 lets be silent, returned for a mis-provisioning. - The hand-off checks Origin and asks the person to commit (§7.1). Same registrable domain is what carries the cookie and also what lets any same-site page drive the write; home call 6, whose shape this borrows, validates against the consumers allowlist and binds to the browser (home §5.6). Without a commit step §7.2's constraints are reachable through §7.1. Three tighten a sentence: S4 and S5 are accounted for as non-calls (§6), the roster copy gains a maximum age and call 8 a page cap (§5.3, §5.4), and gate 9 gains a measurable unit — a doctrine revision, since "one ruling behind" cannot see a change to §6 (§11). Gates 2, 3, 5 and 7 extended to cover the three rule changes. A.2 and A.4 carry the corrections into Docs and memoria. The three rule changes are review-authored and unread by a third hand. §14 holds them open for Paul.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016XuAriz559moY27sTegqpt
2 files changed,
+142
−22
README.md+6 −2docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md+136 −20
Intent
Attributed to this commit: 51.8 g CO₂e (4861222 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 197c57b..efa9330 100644 |
| --- a/README.md |
| +++ b/README.md |
| @@ -25,5 +25,9 @@ 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. |
| +The doctrine is a design, revision 2, reviewed 2026-09-05 by Claude Opus |
| +5 in a session that did not write it. The review found six defects and its |
| +corrections are in the text. Three of them change a rule rather than |
| +tighten a sentence, and those are review-authored and open for Paul — the |
| +doctrine's §14 says which. Read the 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| index 6adf872..860fbb8 100644 |
| --- a/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md |
| +++ b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md |
| @@ -3,13 +3,22 @@ |
| 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. |
| +**Status:** design, **revision 2**, 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). No ruling in §1 changed. |
| +**The three rule changes are review-authored and have not themselves been |
| +read by a third hand.** They are the review's own text, not Paul's, and |
| +§14 holds them open for his word. Everything else stands as drafted. |
| **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 |
| @@ -124,6 +133,16 @@ rosters, one per instance, and that is what lone means; custody and |
| authorization work unchanged because neither depends on the roster being |
| shared. |
| +**The asker subject is typed on the wire** — `{kind: person_ref | address, |
| +value}` — because its meaning is shape-dependent and nothing else in an S2 |
| +or S6 call says which shape the pair is in. "Provisioned together" is an |
| +operational habit, not an enforced property, and a mismatched pair would |
| +otherwise send a value the receiver keys nothing on and get back |
| +`NOT_VISIBLE`: the one value §4.3 permits to be silent, returned for a |
| +mis-provisioning. With the kind on the wire, a receiver that does not key |
| +on it answers `UNKNOWN` and the gap is named, which is what §4.3 requires |
| +of everything that is not a denial. *(Review pass, 2026-09-05.)* |
| + |
| --- |
| ## 4. Boundaries |
| @@ -168,6 +187,27 @@ 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). |
| +**S2's answer is intersected with custody.** The app of record runs its |
| +grants join for the named asker, and then answers `NOT_VISIBLE` for any |
| +item that is not team-visible, whatever the asker's individual grants say. |
| + |
| +Without the intersection §4.2's guarantee is false. A sibling holds ids for |
| +items it ingested, and an item that later leaves team-visibility appears in |
| +the feed as `gone`, which §4.2 requires to be indistinguishable from |
| +deletion. An S2 answering from the full join would distinguish them |
| +perfectly: `VISIBLE` for the one that went private, when a grantee is the |
| +named asker, and `NOT_VISIBLE` for the one that was deleted. The leak the |
| +S1 visibility flag was removed to prevent, and the leak S3's "404, never |
| +403" was designed to prevent, would arrive on the third call. |
| + |
| +The intersection costs a correct consumer nothing. It speaks only items it |
| +ingested, and S3 gave it none that were not team-visible. It costs an |
| +incorrect one exactly what decision 3 says it should: custody bounds what a |
| +sibling may hold, and a sibling has no business asking about what it may |
| +not hold. *(Review pass, 2026-09-05. memoria §4.1 states the join |
| +unintersected — "plus rows granted to me individually" — so A.4 carries |
| +the correction.)* |
| + |
| --- |
| ## 5. Identity and the roster |
| @@ -215,6 +255,21 @@ 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 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 |
| +epoch, deliberately, since a sibling has no business knowing the roster's |
| +version. So an instance kept warm by sibling traffic alone, admitting |
| +nobody and never sleeping, has nothing to tell it that its roster moved, |
| +and §5.4's sentence would not be true of it. The copy therefore carries a |
| +TTL and is refetched on expiry. The number's reason, in the comment the |
| +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. |
| + |
| **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. |
| @@ -227,7 +282,8 @@ 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.** |
| +**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). |
| That is the sentence memoria decision 11 could not write and the reason |
| its four unreconciled tables existed. |
| @@ -270,11 +326,11 @@ spec, which owns them, and are cited here for completeness of direction. |
| |---|---|---|---|---| |
| | 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. | |
| +| 8 roster | instance → home | consumer token | which instance asks | §5.3. Scoped to the caller's team. Paged; the copy has a TTL. | |
| | 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. | |
| +| 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. Asker is a typed subject (§3); the answer is intersected with custody (§4.3). | |
| | 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. | |
| +| S6 aggregate | sibling → Sheets | consumer token + named asker | as S2 | Enumerated function over cell refs, never expression text. Cap 64. Typed subject and the custody intersection, as S2. | |
| | 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. | |
| @@ -282,6 +338,14 @@ spec, which owns them, and are cited here for completeness of direction. |
| was no roster of record; call 8 is that record. A lone instance never |
| needed it. memoria decision 11 is superseded (Appendix A.4). |
| +**S4 and S5 are not calls, and no number is reserved for them.** memoria |
| +§6.1 resolved S4, the tombstone, into a feed row — `gone` covers deletion |
| +and departure from team-visibility indistinguishably — and S5, consumer |
| +expiry, into the per-consumer TTL, since expiry is the decommission verb. |
| +Both are settled; they are recorded here because a table that runs S1, S2, |
| +S3, S6, S8 leaves two holes, and a hole in a numbered contract is somewhere |
| +a later call falls. *(Review pass, 2026-09-05.)* |
| + |
| ### 6.1 Why two credentials |
| **Only the home can mint an assertion**, and putting the home in the loop |
| @@ -343,6 +407,26 @@ 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. |
| +**The receiver checks the origin and asks the person to commit.** What |
| +makes the cookie ride the POST — same registrable domain — is also what |
| +makes this a write any same-site page can drive without the person meaning |
| +it, and a create endpoint that commits on receipt is a drive-by. Home call |
| +6, whose shape this borrows, carries two protections this borrowed without |
| +(home §5.6): it validates hosts against the consumers allowlist, and it |
| +binds the flow to the browser that began it. So: |
| + |
| +- **The `Origin` header must match a row in the receiver's `consumers` |
| + table**, which §4.1 already makes an origin allowlist. No new machinery, |
| + and it holds the §4.1 barrier on a path that otherwise bypasses it. |
| +- **The receiver renders what it received and the person commits it.** One |
| + screen, one button, the person's own session. This is what makes the |
| + document Ana's act rather than memoria's, which is the entire claim of |
| + §7.1; without it §7.2's three constraints are reachable by any sibling |
| + through §7.1, and the distinction between the two sections collapses. |
| + |
| +Neither is a script and neither costs a round trip the flow did not |
| +already have. *(Review pass, 2026-09-05.)* |
| + |
| ### 7.2 No person is present: write-as-self |
| A weekly digest. A Notion page ingested and written out as a Doc. That is |
| @@ -408,7 +492,10 @@ apps are a suite, and every one is a consequence of ruling 1 or 4. |
| 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. |
| + already read them all. **The bound holds only because S2 is intersected |
| + with custody** (§4.3); without the intersection it would also learn |
| + which private items exist, which item 1 does not cover and §4.2 |
| + forbids. |
| 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. |
| @@ -467,35 +554,47 @@ that cannot be written as stated is a spec bug. |
| 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. |
| + 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. A lone |
| + 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. |
| 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. |
| + 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. |
| 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`. |
| + 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.)* |
| --- |
| ## 12. Sequencing |
| -0. **This spec, reviewed.** A review pass by a model that did not write |
| - it, recorded by name in the status block. |
| +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. |
| 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 |
| @@ -538,7 +637,13 @@ change to where the list comes from. |
| ## 14. Open |
| -- **Review pass.** See status. |
| +- **The review's three rule changes**, for Paul. The review pass is done |
| + and recorded, and it authored three rules nobody else has read: S2's |
| + custody intersection (§4.3), the typed asker subject (§3), and the |
| + hand-off's origin check and confirmation step (§7.1). Each closes a |
| + defect stated beside it, and each is the reviewer's own text, so each |
| + wants his word before step 1 carries it into four app specs. The other |
| + three corrections tighten sentences and need nobody. |
| - **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 |
| @@ -602,6 +707,11 @@ Consumed suite rulings."* |
| §8's first clause: unavoidable, unnamed egress. The second clause governs |
| memoria, and Docs does not restate it."* This closes discussion 33. |
| +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.)* |
| + |
| ### 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 |
| @@ -631,6 +741,12 @@ 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.1: intersect S2's answer with the custody predicate (suite §4.3), and |
| +type the asker subject (suite §3). The grants join as §4.1 states it — |
| +Sheets §4.5's *"plus rows granted to me individually"* — answers `VISIBLE` |
| +for an item that is not team-visible, which is the leak §6.1 closed on S3 |
| +and the S1 visibility flag, reopened on S2. *(Review pass, 2026-09-05.)* |
| + |
| §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. |