oficina / suite Public

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>
Paul Campbell pushed by paul@keymail.dev 18f943ed269032d3174693022cc0de4c7b104dda parent c369e01
2 files changed, +202 −115
  • README.md +9 −12
  • docs/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