Revision 4: read-only links, and stop overclaiming
Paul took Fable's recommendation on link sharing and set the priority: read/write with team-mates, read-only in public. That resolves the conflict rather than compromising it — read-only public links are exactly what Sheets ruling 12 already said on 2026-08-30. - Ruling 15 is now Paul's and cites Sheets ruling 12. Links are read-only; an "edit link" is a capability to obtain a guest grant at sign-in, so every write still has a named actor and ruling 9 holds. §7.3, the anonymous link editor revision 3 invented, is deleted — it rebuilt the actor Paul had already rejected, without citing the ruling that rejected it, which is the failure §2 diagnoses and §10 exists to prevent. - The provenance envelope splits by path. Revision 3 required an asker on an onward S3 fetch and on a link read, and neither has one; S3 is person-less by its own definition, which made gate 6b unwritable. The person-less paths check custody, the person paths check S2. Withholding is whole-artefact: no envelope can map a synthesis back to the source of a given sentence, so striking "the revoked passage" would assert an alignment nobody computed. - A stale roster stops serving existing sessions, which is what revision 3 claimed to do and did not — on a 30-day sliding session it kept the outage-length revocation tail it said it was rejecting. - A restored home mints a new roster generation. Refusing a lower epoch stopped the regression and not the damage: a restore reissuing the same numbers catches up in two writes. Home §10's "loses credentials, not access" is false for the roster and A.1 now amends it. - §5.5 stops denying what ruling 13 concedes. A compromised credential origin reads and writes as whoever it signs in as and no instance-side check tells the difference. Revision 3 conceded it, then wrote the reassuring clause back two paragraphs later — worse than revision 1's honest-but-wrong single bound. - Ruling 12's rejected column drops "could not have been built" and keeps the three facts actually verified against home §§6 and 7. The status block now lists what revision 4 deliberately leaves open, since revisions 2 and 3 each claimed to answer everything and each was caught doing it. Paul's call: this can be iterated.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2 files changed,
+202
−115
README.md+9 −12docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md+193 −103
Intent
Human
OK, Fable's recommendation is fine. This can all be iterated. The main thing is read/write sharing with team-mates, and being able to share read-only publicly.
Attributed to this commit: 45.3 g CO₂e (5135643 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 6c57aa6..3b5297a 100644 |
| --- a/README.md |
| +++ b/README.md |
| @@ -25,15 +25,12 @@ the conformance suite in CI against the version it pins. |
| ## Status |
| -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. |
| +The doctrine is a design, revision 4. Three review passes have run |
| +against it, by two models, neither of which wrote the draft: Claude Opus 5 |
| +on revision 1, then GPT-6 via Codex on revision 2, then GPT-6 and Fable |
| +independently on revision 3. The last two agreed closely and both asked |
| +for changes. All three reports sit beside the doctrine, unedited. |
| + |
| +Revision 4 fixes what those reviews found most seriously wrong and says, |
| +in its status block, what it has deliberately left for later. It has not |
| +itself been reviewed. §12 step 1 waits. |
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 8c62c3f..fda9dc5 100644 |
| --- a/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md |
| +++ b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md |
| @@ -3,49 +3,58 @@ |
| 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 3**, 2026-09-05. Drafted by the suite |
| +**Status:** design, **revision 4**, 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, 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). |
| +spec named in the row, or in the sessions that produced revisions 3 and 4. |
| + |
| +**Three review passes, by two models, none of which wrote the draft.** |
| +Claude Opus 5 on revision 1: six defects. GPT-6 via Codex on revision 2: |
| +*request changes*, sixteen findings. GPT-6 and Fable 5.1 independently on |
| +revision 3: *request changes* from both, agreeing closely — 6 and 7 of the |
| +sixteen closed respectively, none still fully open, plus twelve and five |
| +new findings. All three reports are beside this file, unedited: |
| +`CODEX-FINDINGS.md`, `CODEX-FINDINGS-R3.md`, `FABLE-FINDINGS-R3.md`. |
| + |
| +**Revision 4 does not claim to answer everything, because revisions 2 and |
| +3 both made that claim and both were caught.** What it fixes: |
| + |
| +- **Ruling 15 is now Paul's and it cites Sheets ruling 12.** Link sharing |
| + is read-only; an "edit link" is a capability to obtain a guest grant at |
| + sign-in, so every write still has a named actor. Revision 3 proposed an |
| + anonymous link editor — the actor Paul had already rejected on |
| + 2026-08-30 — without citing the ruling that rejected it. §7.3 is |
| + deleted. Recorded plainly because it is the failure §2 diagnoses and |
| + §10 exists to prevent, committed by this document. |
| +- **The provenance envelope splits by path** (§7). Revision 3 required an |
| + asker on S3 and on a link read, neither of which has one. Person-less |
| + paths check custody; only person paths check S2. Withholding is |
| + whole-artefact, since no envelope can map a synthesis back to the source |
| + of a sentence. |
| +- **A stale roster stops serving existing sessions** (§5.3), which is what |
| + revision 3 said it was doing and was not. |
| +- **A restored home mints a new roster generation** (§5.3). Refusing a |
| + lower epoch was not enough: a restore that reissues the same numbers |
| + catches up in two writes. |
| +- **§5.5 stops denying what ruling 13 concedes.** A compromised credential |
| + origin reads and writes as whoever it signs in as, and no instance-side |
| + check distinguishes it. Revision 3 conceded that and then wrote the |
| + reassuring clause back two paragraphs later. |
| +- **Ruling 12's rejected column drops its impossibility claim** and keeps |
| + the three facts verified against home §§6 and 7. |
| + |
| +**Openly not answered, and deliberately left for iteration** (Paul, |
| +2026-09-05): call 8's wire encoding and completion marker; result |
| +precedence when a stale roster and a non-custodial item collide; the |
| +activity path's cached authorization after revocation; pinning sibling |
| +baselines, since four memoria citations resolve only on an unmerged |
| +worktree and A.1's ruling number may collide with an existing home ruling |
| +22; and most of the fixtures the gates now describe. These are recorded |
| +here rather than in a passing sentence so the next reader can see the |
| +edge of the work. |
| + |
| +**Revision 4 has not been reviewed.** |
| + |
| **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 |
| @@ -66,7 +75,11 @@ The falsifiable form, in two halves: |
| 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. |
| + **the retention its own spec states** — memoria §7.4 holds that number |
| + today, and a consumer that states none is not conformant. §4.2 carries |
| + the rule and deliberately carries no number, because the number is a |
| + property of the consumer's storage and not of this document. A |
| + compromised consumer 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 |
| @@ -111,10 +124,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) | |
| +| 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. Those three are verified against home §§6 and 7; whether they are together *unbuildable* is a stronger claim than the evidence carries, and revision 3 made it | 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** | |
| +| 15 | **Three rungs and a read-only link.** An item is *private*, *granted* to named people, or *team*. Orthogonally it may carry a **read-only** link capability — view, or comment where the app has that role. **Only the `team` rung is team-visible**, so custody keys on the rung and never on the link. **Every write has a named actor**: a member, or a guest who signed in and took a grant. | Treating a link as team-visible because "anyone can read it", which turns a capability held by whoever has the URL into a listing every team member finds by search. **And anonymous link editing**, which Sheets ruling 12 rejected on 2026-08-30 and which revision 3 rebuilt without citing it. An "edit link" is a capability to *obtain a guest grant at sign-in* — the `granted` rung, and how Docs already builds its commenter link | Paul, 2026-09-05, confirming Sheets ruling 12 | |
| --- |
| @@ -227,7 +240,19 @@ rungs and one capability: |
| | 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** | |
| +| Anyone with the link can read | *a read-only capability on any rung* | **No, by itself** | |
| + |
| +**The product need this serves, in Paul's words: read and write with |
| +team-mates, read-only in public.** Team-mates read and write by team |
| +standing or by a grant. The public reads by link and never writes. An |
| +"anyone with the link can edit" share is the `granted` rung wearing a |
| +convenient front door: the link carries you to sign-in, sign-in mints a |
| +guest grant, and from there you are a named actor like everyone else. |
| +Sheets ruling 12 settled this on 2026-08-30 — *"link sharing is |
| +viewer-or-commenter, never editor"* — and revision 3 rebuilt the actor it |
| +rejected without citing it. Ruling 15 cites it now. That failure is |
| +precisely what §2 diagnoses and §10 exists to stop, committed by the |
| +document that diagnoses it. |
| **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 |
| @@ -427,22 +452,43 @@ 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 TTL with no successful fetch, the roster is **stale**, and team |
| +standing stops being established for anybody — **including people already |
| +in a session**. Revision 3 let existing sessions run on, which on a 30-day |
| +sliding session is the outage-length revocation tail the paragraph claimed |
| +to reject, bought at the price of suite-wide `UNKNOWN`. A stale instance |
| +therefore serves only what an *individual grant* reaches, since a grant is |
| +instance-side and needs no roster, 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, and the cost is real: a team whose |
| +home is unreachable loses team-standing access until it returns. The |
| +alternative is that an unreachable home lets a removed member keep their |
| +access for as long as the outage lasts, which is the thing ruling 4 was |
| +adopted to end. *(GPT-6 finding R3-2; Fable finding 5.)* |
| + |
| +**A restored home gets a new roster generation, because refusing a lower |
| +epoch is not enough.** Revision 3 said an instance never accepts an epoch |
| +below the one it holds. That stops the regression and not the damage: a |
| +restored home simply issues 10, 11, 12 again over the recovered roster, |
| +and two writes later the instance is accepting an epoch it considers fresh |
| +carrying members it had already removed. |
| + |
| +So the roster carries **`(generation, epoch)`**. The generation changes |
| +whenever the home cannot prove continuity with the epochs it last issued — |
| +any restore from backup. An instance seeing an unfamiliar generation holds |
| +its roster stale (above), admits nobody on team standing, and refuses to |
| +publish the new one until an owner confirms it at the directory origin. A |
| +restore is a human event and it should cost a human action; the |
| +alternative is that the recovery path is also the readmission path. |
| + |
| +**A home that loses the `memberships` table entirely has no rebuild path** |
| +— call 2 cannot write it (ruling 12) and no channel reaches a hibernating |
| +instance. Home §10's *"loses credentials, not access"* is therefore false |
| +for the roster, and A.1 amends it. Recovery is the instances' copies plus |
| +an owner's confirmation, or it is re-entry by hand. *(GPT-6 finding R3-3; |
| +Fable finding 4.)* |
| **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 |
| @@ -505,11 +551,18 @@ 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). |
| +**What is left as a real bound, and it is narrower than revision 3 said.** |
| +Against the *directory* tier: an instance's own grants decide what an |
| +admitted member may read, so adding a member does not reach an |
| +individually granted item. Against the *credential* tier: **nothing here |
| +does**. An attacker who can sign in as Ana has Ana's grants, reads what Ana |
| +reads and writes what Ana writes, and no instance-side check distinguishes |
| +them — that is what "signs in as anyone" means, and the bound has to be |
| +read as absent rather than restated in weaker words. |
| + |
| +Revision 3 conceded the credential tier in one paragraph and then wrote |
| +the reassuring clause back in two paragraphs later, which is worse than |
| +revision 1's honest-but-wrong single bound. *(GPT-6 finding R3-1.)* |
| **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 |
| @@ -615,10 +668,29 @@ 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. |
| +it may drop, and re-checks it on every disclosure path. **Which check |
| +depends on whether the path has a person**, because two of them do not: |
| + |
| +| Path | Has an asker? | The check | |
| +|---|---|---| |
| +| An ordinary read, an export, an activity row | Yes | S2 to each app of record, for that person | |
| +| An onward S3 fetch | **No** — S3 is person-less by definition (§6) | **Custody**: every source still team-visible, or S3 answers 404 | |
| +| A read through a read-only link | **No** — a link holder is no roster subject | The item carries no link while any source is not itself link-shared | |
| + |
| +Revision 3 wrote "the sources still authorize the asker" for all of them, |
| +naming an asker on two paths that have none and leaving gate 6b |
| +unwritable. The person-less paths fall back to the predicate that needs no |
| +person, which is the same split decision 3 has always drawn: custody for |
| +what may be held and served without an asker, authorization for what a |
| +person may read. *(GPT-6 finding R3-4; Fable finding 3.)* |
| + |
| +**Withholding is whole-artefact, never per-passage.** If any source stops |
| +authorizing, the entire derived item is withheld and the gap is named. A |
| +synthesis carries no map from its sentences back to its sources, so an |
| +envelope that claimed to strike "the passage from the revoked sheet" would |
| +be asserting an alignment nobody computed. Removing the wrong sentences |
| +leaks; removing the right ones by luck is not a design. *(GPT-6 finding |
| +R3-5.)* |
| Two consequences, both counter-intuitive enough to state: |
| @@ -728,25 +800,10 @@ 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.4 What was rejected, and why it stays rejected |
| +### 7.3 What was rejected, and why it stays rejected |
| **On-behalf-of writes**: a sibling writing "as Ana" under its own token. |
| It would make every consumer token a write capability over every member's |
| @@ -758,6 +815,13 @@ all of it. |
| **Browser-carried only**: forbids unattended writes, which the product |
| wants, and would push implementers toward a person-present pretence. |
| +**A link holder as a writer.** Revision 3 built one, and it was wrong |
| +twice over: Sheets ruling 12 had already rejected it (ruling 15), and it |
| +would have put an actor on the write path that no app can name — which |
| +ruling 9 forbids and §7's provenance envelope cannot carry. A link |
| +reaches sign-in; sign-in mints a grant; the grant names a person. There is |
| +no fourth actor. |
| + |
| --- |
| ## 8. Egress |
| @@ -819,9 +883,10 @@ apps are a suite, and every one is a consequence of ruling 1 or 4. |
| 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 |
| + credential origin. The first two cannot read a private, granted or |
| + link-only item, write as a person, or grant a per-item right. **The |
| + third can do all of it**, as whoever it signs in as. 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 |
| @@ -907,11 +972,13 @@ that cannot be written as stated is a spec bug. |
| 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. |
| + team-visible" and "does not exist". **S2 answers `NOT_VISIBLE` for the |
| + same item id in each state**, byte for byte the answer for the deleted |
| + one — the asker holds a live grant in the `private` and `granted` |
| + states and a *former* grant in the deleted one, since no schema keeps a |
| + live grant against a deleted row. Add **team-plus-link** (team-visible |
| + because of the rung) and **a link added or revoked after ingest** (no |
| + change to custody either way). |
| 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 |
| @@ -927,20 +994,27 @@ that cannot be written as stated is a spec bug. |
| 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. |
| + **failed refresh** past the TTL yields `UNKNOWN`, not `NOT_VISIBLE`, |
| + admits nobody new **and stops serving team standing to an existing |
| + session**. A **home rewound** — both to a lower epoch, and to the same |
| + epoch numbers under a recovered roster — readmits nobody, and an |
| + **unfamiliar generation** holds the roster stale until an owner |
| + confirms. A **failed middle page** leaves the previous roster in place. |
| + 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. 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. |
| +6b. **Provenance survives the hop** (§7), one case per path because the |
| + paths differ. Write an item into app B derived from sources in A and C. |
| + Revoke A's source for a person: B's **ordinary read and export** withhold |
| + the **whole artefact** and name A — never a partial redaction. Make A's |
| + source non-team-visible: B's **S3** answers 404, on custody, with no |
| + asker anywhere in the call. Give B's item a read-only link while A's |
| + source has none: the link is refused. Assert a write is refused when a |
| + source's app of record serves no S2 at all. |
| 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 |
| @@ -1106,6 +1180,12 @@ membership**. **Amend §0 and its gate**: publishing the database now |
| identifies each team's owners, which is a new plaintext class (suite gate |
| 8). |
| +**Amend §10.** *"Loses credentials, not access"* is false for the roster: |
| +a home that loses `memberships` cannot rebuild it — call 2 cannot write it |
| +and no channel reaches a hibernating instance — so recovery is the |
| +instances' copies plus an owner's confirmation. The roster carries |
| +`(generation, epoch)` and a restore mints a new generation (suite §5.3). |
| + |
| 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 |
| @@ -1141,6 +1221,11 @@ 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' link sharing is **read-only** (suite ruling 15). An "anyone with the |
| +link can edit" share is a link to sign-in that mints a guest grant, which |
| +is the shape Docs already uses for its commenter link; the writer is then |
| +a named guest and §7's envelope can carry them. |
| + |
| Docs is the first receiver of the hand-off, so its create endpoint takes |
| suite §7.1 whole: the `Origin` check against the `consumers` allowlist at |
| receipt, pending state, and a commit from Docs' own origin. Its pinned |
| @@ -1163,6 +1248,11 @@ no federation story across teams and there will not be one."* |
| §5) in suite shape, and from `$SHEETS_OWNERS` in lone shape. Per-sheet |
| roles stay here."* |
| +Ruling 12 — *"link sharing is viewer-or-commenter, never editor"* — needs |
| +no amendment. Suite ruling 15 adopts it suite-wide and cites it; Sheets |
| +keeps it as the original. This is the direction §10 asks for, and revision |
| +3 went the other way. |
| + |
| §9: as A.2's §9. |
| AGENTS.md: **keep** *"a contradiction gets fixed in the same commit"* and |