Design draft: cross-rung reach, and Paul's ruling on the credential
Proposes ruling 16 — an instance reaches a named asker's non-team items at query time, storing nothing, while custody (what it may hold) is unchanged. Paul ruled the credential on 2026-09-19, and ruled none of the three options he was offered: an item reaches memoria only if it carries an explicit "offer to memoria" mark the sibling itself evaluates (memoria §3.5's published-only mode). Naming an asker then adds no read authority, so §6.1's "the naming adds nothing to what it can read" and §7.3's "it adds no reach" are restored verbatim rather than amended — ruling 8 is untouched and §9 items 1-2 stay true as written. Also ruled: granted-only in v1, reached passages excluded from model prompts, and no hand-off of a saved answer built on reached content. Reviewed by Fable (REQUEST CHANGES, adopted in full) and by Astra. Both established that memoria-side controls are theatre and that per-asker rate limits multiply an attacker's allowance by the roster size. The draft records two errors it made: citing revision 3 of a doctrine on revision 8, and describing person-proving credentials as forbidden when ruling 8 rejects only the home-in-every-search shape and §13's row is a trigger.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 file changed,
+391
−0
docs/superpowers/specs/2026-09-19-cross-rung-reach-draft.md+391 −0
CI passed — run details
CI log
=== gate === ok amadan.net/oficina/suite 0.008s ok amadan.net/oficina/suite/conformance 0.004s
diff --git a/docs/superpowers/specs/2026-09-19-cross-rung-reach-draft.md b/docs/superpowers/specs/2026-09-19-cross-rung-reach-draft.md| new file mode 100644 |
| index 0000000..d2c0c7a |
| --- /dev/null |
| +++ b/docs/superpowers/specs/2026-09-19-cross-rung-reach-draft.md |
| @@ -0,0 +1,391 @@ |
| +# Draft amendment — cross-rung reach |
| + |
| +Against `2026-09-05-suite-sharing-doctrine.md` **revision 8** (`1ac4323`, |
| +2026-09-07). **Draft, not a ruling.** §1's table carries "Who, when" and |
| +only Paul fills that column. |
| + |
| +Paul, 2026-09-19: *"Data should not leave its sharing boundary… Memoria |
| +should be able to access all Docs, Sheets etc. that a user has access to… |
| +secure when it needs to be, shareable when it needs to be."* Scoped the |
| +same day to **cross-rung within one team**; cross-team guest reach is |
| +ruling 2's rejected column and is out (§13). |
| + |
| +**Revision 1 of this draft cited revision 3 and was two revisions past |
| +gate 9's tolerance** — AGENTS.md's "fetch before you read", failed in the |
| +ordinary way, by grepping review prose for a number instead of reading the |
| +Status line. Recorded because the next draft will be tempted to do it too. |
| +Ruling 16 is free on every ref as of this date. |
| + |
| +--- |
| + |
| +## 1. What the request already had |
| + |
| +**Ruling 15 is the boundary model.** An item is *private*, *granted* to |
| +named people, or *team*, orthogonally carrying a read-only link. That is |
| +"secure when it needs to be, shareable when it needs to be", settled |
| +2026-09-05 and built. An amendment adding a boundary model would be |
| +rebuilding ruling 15 under a new name. |
| + |
| +**The gap is that memoria reaches exactly one rung.** A member with forty |
| +documents shared to them personally asks memoria and memoria sees none. |
| + |
| +## 2. Ruling 16 (proposed) — reach is a third rule, and it costs ruling 8's argument |
| + |
| +> **Three rules, not two.** Custody bounds what an instance may *hold*: |
| +> team-visible, person-less, sealed (rulings 3, 14). Authorization bounds |
| +> what a person may *read of what is held*: S2, per person, per query, |
| +> never cached, **intersected with custody** (§4.3). **Reach** is the |
| +> third: a query-time call, per named asker, returning content the asker |
| +> may read and the instance does not hold, **unintersected by design**, |
| +> carrying §8.4's invariant — no passage crosses a request. |
| +> |
| +> *Rejected: widening custody to "whatever the asker can see", which makes |
| +> custody per-person and is what ruling 1's rejected column refused when it |
| +> named "memoria holding person-initiated snapshots."* |
| +> |
| +> *Also rejected: calling reach a widening of S2.* S2 is a predicate over |
| +> ids memoria was already told about (memoria §4.1, "memoria can name no id |
| +> it was not told about") and its custody intersection is what stops it |
| +> being an existence oracle — gate 3 requires `NOT_VISIBLE` byte-identical |
| +> across private, granted, guest-only, link-only and deleted. Reach returns |
| +> non-team items by purpose, so it cannot be intersected. A predicate leaks |
| +> one bit about an id the caller held; an enumeration leaks the existence |
| +> and content of ids the caller never held. **Gate 3's guarantee scopes to |
| +> S2 and S3 and does not extend to reach.** |
| + |
| +## 3. What it costs — and revision 1's test was the wrong test |
| + |
| +Revision 1 of this draft argued that rulings 1, 2, 3, 14 and 15 survive |
| +untouched and offered that survival as *evidence the split is real*. The |
| +survival is true and the inference is wrong: **those are not the rulings |
| +reach bears on.** The ruling it bears on is 8, and the doctrine says so in |
| +the sentence that justifies it. |
| + |
| +| Touched | What breaks | |
| +|---|---| |
| +| **Ruling 8** | §6.1: "the sibling is trusted to name the asker because a compromised sibling already reads everything team-visible through S3; **the naming adds nothing to what it can read**." §7.3: the read-side named asker "is acceptable **because it adds no reach**." An amendment *called reach* must pay those two sentences, not route around them | |
| +| **§9 item 1** | "It cannot read an item that was never team-visible — private, granted, or link-only." False under reach: **any consumer-token holder** names each member in turn. Not "a compromised memoria" — S9 answers any holder of a consumer token, so a compromised Docs enumerates every member's private Sheets, and a leaked token goes from a read capability over team-visible content to one over every member's private corpus. **Re-price `origin/worktree-worktree-token-at-rest` accordingly** | |
| +| **§9 item 2** | "The bound holds only because S2 is intersected with custody… without the intersection it would also learn which private items exist." Reach is the unintersected call | |
| +| **§0's inside test** | "The worst anyone learns is what that team could read" — false for a *live* compromised instance (memoria §3.2, "from a live host", the largest entry). Needs a live-instance clause | |
| +| **memoria §4.1, §8.1, §8.4, §8.5; gates 5, 25, 32** | §7 below | |
| + |
| +| Untouched | Why | |
| +|---|---| |
| +| Ruling 1 | One team, one trust domain. Reach stays inside it | |
| +| Ruling 2 | No team label crossed (§13) | |
| +| Ruling 3, custody half | Custody still bounds holding. Reach holds nothing | |
| +| Ruling 14 | Acquisition rule: reach acquires nothing. Render rule: satisfied. **But see §8 — the dodge is real on egress, not on storage** | |
| +| Ruling 15 | Custody keys on the rung. Reach is not custody | |
| +| §6.1's three grounds against push | Reach is memoria→sibling pull; no sibling holds a memoria token; gate 17's route list is unchanged. **The draft is right here and should say so** | |
| + |
| +## 4. RULED 2026-09-19 — source-enforced standing grants |
| + |
| +**Paul ruled none of (a), (b) or (c).** The ruling is §4.2's third path, |
| +which was not on the ballot he was given: |
| + |
| +> **An item reaches memoria only if it carries an explicit "offer to |
| +> memoria" mark that the *sibling* evaluates.** memoria holds |
| +> source-enforced standing read authority over exactly the marked set, |
| +> independent of the named asker. memoria §3.5's published-only mode is the |
| +> precedent and the mechanism. |
| + |
| +**What this buys, and it is the reason to prefer it:** naming an asker adds |
| +no read authority, so doctrine §6.1's *"the naming adds nothing to what it |
| +can read"* and §7.3's *"it adds no reach"* **are restored verbatim rather |
| +than amended**. §3's table collapses — ruling 8 is untouched, §9 items 1–2 |
| +stay true as written, §0's inside test needs no live-instance clause. It is |
| +also Astra's own stated reversal condition, and it needs no new credential, |
| +so it is not blocked on the home's identity layer (§12 step 5). |
| + |
| +**What it costs:** friction. A document nobody marked is invisible to |
| +memoria, and the plausible-empty-list failure is now a product problem |
| +rather than a security one. For fichas the cost is near zero — fichas owns |
| +its content and can mark it by default. |
| + |
| +**Three further rulings, same date:** |
| + |
| +- **`granted` only in v1.** `private` is out of scope until there is a |
| + second team. §11's "product toggle" framing is withdrawn either way — |
| + both reviewers were right that the opt-in belongs at the source, and |
| + under this ruling the mark *is* that opt-in. |
| +- **Reached passages are excluded from model prompts** (§8 entry 2). |
| +- **A saved answer built on reached content cannot be handed off** (§10). |
| + |
| +The three options below are kept because what was rejected is only legible |
| +beside what replaced it, and because revisions 1–2 mislabelled one of them. |
| + |
| +### 4.0 The ballot, as it stood |
| + |
| +- **(a) Consumer token + named asker**, as S2 today. Cheapest to build; |
| + ruling 8 unchanged in *form*. **Price:** *any consumer-token holder* — |
| + not merely a compromised memoria — enumerates every member's private and |
| + granted corpus across every app, and §9 items 1–2 must be rewritten to say |
| + so in writing. **The class that makes this new rather than larger:** |
| + `private` content *about other members of the same team* — an HR note, a |
| + performance review, a salary sheet. Ruling 1's "the team is the boundary" |
| + is true of team-visible content by definition; ruling 15's `private` rung |
| + exists because the team has an internal sub-boundary, and the person who |
| + chose "private" was trusting the word. **A price written into §9 is read by |
| + spec reviewers; that person reads the share UI.** |
| +- **(b) The person's own session.** §7.1's precedent: the browser carries |
| + it to each app, memoria never sees a non-team passage and never merges. |
| + **Price:** no unified ranking, no fact box, no answer over reached |
| + content — much of what the feature is for. |
| +- **(c) A person-proving credential at the sibling** — an issuer that |
| + authenticates the person *independently of memoria's claimed `asker`*; |
| + short-lived, scoped to receiving sibling + team + reach-read; source-side |
| + grants checked every request. |
| + |
| +**Revisions 1 and 2 of this draft described (c) as "ruling 8's rejected |
| +column and §13's never". Both halves are false, and Paul chose against that |
| +label.** Ruling 8's rejected column is *"a token exchange **putting the home |
| +in the loop of every search**; one credential for everything"* — it rejects a |
| +shape, not the category. §13's row is *"A token exchange so siblings can hold |
| +assertions | The home's latency budget can afford being in every search"* — |
| +**a trigger**, in a table that spends the word "Never" freely on four other |
| +rows. (c) needs an explicit amendment to ruling 8's sibling-read rule and |
| +§6.4. It does not need a reversal, and it was never forbidden. |
| + |
| +### 4.1 Two recommendations, and they split |
| + |
| +**Fable: (a), on three conditions enforced at the sibling — else (b).** |
| +**Astra: (c) — keep token+asker for custody-limited S2, whose justification |
| +survives there, because S9 removes exactly the restriction that makes it |
| +safe.** |
| + |
| +**What both say, independently — treat as settled:** |
| + |
| +- Writing the price into §9 is **not** sufficient on its own. |
| +- **Every memoria-side control is theatre.** Rate limits, a live-session |
| + check, an audit row at memoria: a compromised process bypasses its own |
| + client (memoria §6.3), and ruling 13 already refuses to credit checks |
| + performed by the compromised process. |
| +- Per-asker rate limits are not a bound. Astra's sharper form: **per-asker |
| + limits multiply the attacker's allowance by the roster size.** A |
| + source-enforced budget on *successful disclosures per consumer* is the |
| + real shape, and it limits extraction speed, never entitlement. |
| +- **`private` opt-in must be held at the source.** Both reject §11's |
| + "product toggle" framing; Astra's reason is the sharper one — excluding a |
| + corpus from the attacker's readable set is a security benefit, not a |
| + preference. **Amend §11.** |
| +- An audit trail is detection, never prevention, and must live at the |
| + source. Astra's wording rule: record *"memoria requested as Ana"*, never |
| + *"Ana requested"* — under (a) the latter cannot be established. |
| +- **Neither "nothing stored" nor §6's residue gate defends against this |
| + attacker.** They bind an honest implementation; a compromised process |
| + retains every response. |
| + |
| +**Where they actually differ — and it is narrower than it looks.** Fable's |
| +condition 1 is that S9 be a **designated capability**, granted per consumer |
| +by the team owner in the receiving app's share UI and refused when not |
| +designated — S8's shape verbatim (§7.2: "the designation is an instance-side |
| +grant, made in the receiving app's share UI, and it is what makes S8 |
| +refusable per team"), and ruling 6's opt-in test applied inside the team. |
| +Astra holds that designation narrows *who may ask* without ever |
| +authenticating *whom they ask as*, which is the property S9 needs. |
| + |
| +**And they converge on the end state.** Fable's "upgrade path" — the home |
| +minting an assertion at sign-in whose `aud` covers the team's siblings — is |
| +Astra's (c) in precisely the shape ruling 8 does *not* reject, because it is |
| +not an exchange in every search. **Fable says record it and do not block |
| +reach on it; Astra says it is the ruling. The disagreement is sequencing, |
| +not architecture.** |
| + |
| +### 4.2 The third path, from Astra's reversal condition |
| + |
| +Astra reverses to (a) under one condition: **memoria holds explicit, |
| +source-enforced standing read authority over every item S9 can return, |
| +independent of the named asker** — an actual delegation to the service, |
| +granted by those entitled to grant it, not an exception recorded in a leak |
| +list. Then naming an asker adds no read authority and §6.1's original |
| +justification is restored verbatim. |
| + |
| +That is a **different product contract**: not "memoria acts as you" but |
| +"these items have been offered to memoria". memoria §3.5 records |
| +source-enforced *offer-to-memoria* selection as an existing mitigation, so |
| +the precedent exists. **It may also be what a CRM user expects** — and it is |
| +not on the three-option ballot Paul was given. |
| + |
| +### 4.3 Two gates, testing different properties |
| + |
| +Astra's, which §6's residue gate does not cover: **give the attacker a valid |
| +consumer token and the complete roster and no person credential; assert S9 |
| +discloses no non-team content.** Then give it Ana's proof: changing the named |
| +asker to Bo must fail while Ana's own canaries succeed. Expired credentials |
| +and revoked grants must fail. |
| + |
| +**Everything below §5 assumes (a)** and is written so (b) invalidates §5–§9 |
| +rather than reshaping them. Under (c), §5's S9 shape survives and its |
| +credential line changes. |
| + |
| +## 5. The call — S9 |
| + |
| +It *is* a call and it needs a number: S4/S5 went un-numbered because each |
| +resolved into something that exists (a feed row, a TTL); reach resolves |
| +into nothing. Ruling 7: no fixture, no interface. **S4, S5 and S7 are |
| +retired and S8 is taken — do not reuse.** |
| + |
| +`RouteS9 = "/suite/s9"`, POST, `{asker: Subject, query, limit}`. |
| + |
| +**Predicate = the grants join MINUS custody**: readable by the asker AND |
| +not team-visible. The sibling can compute that; it cannot compute "what |
| +memoria does not hold", which is what §2 of revision 1 asked for and is a |
| +property of the caller. Dedupe becomes trivial as a side effect. |
| + |
| +Rows `{item_id, revision, locator, title, text}`, all required, decoded |
| +S1-style — `DisallowUnknownFields`, pointer fields, `rejectAmbiguousJSON`. |
| +Caps with stated reasons: query ≤ 1 KiB (memoria §9.1), results ≤ 50 |
| +(memoria §4.1's page), passage text capped. **Total decoder:** non-2xx or |
| +malformed is a whole-call failure, **distinct from an empty result** (S1's |
| +"an empty feed is not a failed call"); unknown subject kind → `UNKNOWN`; a |
| +stranger's answer is byte-identical to "nothing matched." |
| + |
| +Fixture: extend `custody.json` with per-case reach expectations for asker |
| +and stranger, **and record item ownership, which it does not state today** |
| +(the doc-link case has no owner). |
| + |
| +Note S2's vocabulary is five values on revision 10 (`AHEAD`). |
| + |
| +## 6. The gate — §11 item 10 |
| + |
| +"Stores nothing, at any scope, in any form" is unfalsifiable as written — |
| +RAM and the response body are forms — and it restates memoria §8.4, which |
| +AGENTS.md forbids. **Cite §8.4; gate the residue.** Presence first, per |
| +memoria §10: |
| + |
| +Seed a fixture sibling with a `granted` and a `private` item, each carrying |
| +a canary. Reach as grantee/owner; **assert both canaries in the response |
| +body.** Then assert absence in: SQLite + WAL bytes; bucket objects; **every |
| +sealed blob after `seal.Open`** (gate 2's lesson — a sealed store beats the |
| +grep); logs (gate 4's method); the vector blob (gate 2's sentinel); breaker |
| +counters; auth pending blobs. Then a second asker runs the same query: zero |
| +hits. Then restart with the sibling **down** and re-run as the first asker: |
| +zero hits — no retry buffer, no cache survived a process. |
| + |
| +Failure path: sibling answers 500 with the canary in the body → memoria's |
| +page carries no canary (mirror of `TestHarnessCatchesASecretInARefusalBody`). |
| + |
| +Mutations that must fail it: embed the reached chunk; write a cached |
| +answer; log `q`; hold the result in a retry buffer. |
| + |
| +Server half: the sibling logs no reach query text. |
| + |
| +## 7. memoria's amendments — the ones revision 1 omitted |
| + |
| +**A reached `granted` passage cannot pass S2**: the custody intersection |
| +answers `NOT_VISIBLE`. So reach passages bypass §4.1, and the architecture |
| +that had one authorization gate has two. |
| + |
| +- **§4.1** "Every path that would speak a passage originating in a sibling |
| + app calls S2 first" — amend to name the second path. |
| +- **§8.1** "One gate, both paths" — now one gate per path, two paths. |
| +- **§8.4, §8.5** cite the §4.1 filter; both need the reach clause. |
| +- **Gate 32** becomes "every passage id in a model prompt is one S2 |
| + returned `VISIBLE` for, **or one reach returned in this same request**." |
| +- **Gate 5** likewise. |
| +- **Gate 25's stated mutation** — "add a person ref to memoria's S3 call |
| + and make the fixture honour it" — is a description of reach on the wrong |
| + route. Rewrite it to tell S3 and S9 apart, or it passes the thing it |
| + exists to fail. |
| + |
| +## 8. Three leak entries, and the egress one is the sharp one |
| + |
| +1. **Router transit.** memoria §3.2's "bodies transit the router in |
| + plaintext on every S3 fetch" now includes private bodies. |
| +2. **Provider egress — the real hole in "reach acquires nothing".** With AI |
| + on, every retrieved passage enters a model prompt and leaves to TensorX |
| + (§8.3). So reached private and granted content becomes query-time egress |
| + of exactly the content custody kept out of the provider-visible corpus, |
| + **under an opt-in the team owner gave for team content** (ruling 6, §8). |
| + **Ruling 14's acquisition rule is satisfied and this is not an |
| + acquisition question — which is why revision 1 missed it.** |
| + |
| + **Astra: the credential choice does not touch this, and a per-answer |
| + label does not answer it** — a label discloses only what already |
| + happened. **RULED 2026-09-19: reached passages are excluded from model |
| + prompts.** It is the one option needing no new consent. Revisit only with |
| + an explicit decision taken *before* transmission. |
| +3. **Forbidden stores, named.** §8.1's breaker counters — does a reach |
| + result count `VISIBLE`? — and memoria §12's cached-answers row, which |
| + "expires §8.4's stored nowhere" and would make a cached answer a stored |
| + copy of a private item. |
| + |
| +## 9. Ranking — not "the only unmitigated cost" |
| + |
| +Revision 1 said "only". Given §3 and §8, false. |
| + |
| +And the asymmetry is worse than stated. memoria §2: "Docs ships title and |
| +metadata search; Sheets has a recency list." **Neither sibling can rank |
| +bodies**, so "let the sibling rank its half" is a search engine built twice |
| +more — the largest unbuilt piece here. The alternative, memoria fetching |
| +the asker's non-team bodies per query, is draft 3's per-person S3 minus |
| +storage, at the cost of the asker's whole private corpus per query inside |
| +§6.5's 800 ms deadline. Meanwhile §8.2's router keys on BM25 margin and |
| +top-K spread, undefined for score-less rows, and §8.8's box takes the ruled |
| +top-25. |
| + |
| +**With AI on, the asker's own documents are second-class in the mode the |
| +product is sold on.** Pick a shape and price it. |
| + |
| +## 10. The hand-off — ruled, and revision 1 had the consequence backwards |
| + |
| +Revision 1 said a private draft could ride into a team doc. **The opposite |
| +happens.** §7's envelope carries per-source `(app, item, revision)` and the |
| +receiver re-checks on every read; S2 for a granted or private source |
| +answers `NOT_VISIBLE` by custody intersection; and withholding is |
| +**whole-artefact** (§7). So "save this answer as a Doc" with one reached |
| +passage yields a Doc withheld in full, in Docs, for everyone **including |
| +its author**, on first read. The onward S3 path then 404s it for ingest. |
| + |
| +**RULED 2026-09-19: reached content cannot be handed off.** §7 already has |
| +the shape — "a source the receiver cannot check is a source it must not |
| +accept". The receiver-checks-by-reach-id alternative is rejected. Gate 6b |
| +gains the case. |
| + |
| +## 11. `private` vs `granted` is a product toggle, not a security boundary |
| + |
| +Revision 1 marked this NEEDS PAUL on security grounds. The grounds are |
| +symmetric: from a compromised memoria both rungs are new reach (§3); on the |
| +honest path both are per-asker and nothing-stored; §8.4 holds both equally; |
| +§10 hits both. "Exists for one person" versus two is a product distinction, |
| +and **Paul's own words already include private** — "all Docs, Sheets etc. |
| +that a user has access to". Present it as a toggle under §4's ruling, and |
| +say plainly that offering it asks him to narrow his own direction. |
| + |
| +## 12. Fichas |
| + |
| +**A ficha holds a reference — `(app, instance, item)` — never a copy**, and |
| +under ruling 16 that is the mechanism, not tidiness. Three corrections to |
| +revision 1: |
| + |
| +- **"2 references you cannot open" is forbidden.** It says "this exists and |
| + you may not have it" — the fact §4.2 hides and `s3.go` refuses to fold by |
| + 404ing rather than 403ing. Fichas renders nothing for an unresolvable |
| + reference, **and never a count.** |
| +- **A stored reference to a non-team item, inside a team-visible ficha, is |
| + at-rest knowledge that a private item exists — in a second app.** This is |
| + exactly why S1's visibility flag was removed (memoria §6.1; |
| + `TestHarnessCatchesAFeedRowCarryingVisibility`). |
| +- **No gate can resolve a granted reference by id today:** S2 answers |
| + `NOT_VISIBLE`, S3 takes no person. So **reach needs a by-id form as well |
| + as by-query**, under one credential and one no-store rule. Note what that |
| + is: a per-person S3, the fix memoria §6.1 reverted — now wanted |
| + deliberately, on a separate route, with the reasoning written down. |
| + |
| +The product consequence stands: **the same card shows different things to |
| +different people**, and that is correct. |
| + |
| +## 13. Out of scope |
| + |
| +**Cross-team guest reach** is ruling 2's rejected column — "any federation |
| +story". Reversing a settled ruling is not an amendment. Own decision, own |
| +leak entry, own gate. Paul scoped this away on 2026-09-19. |
| + |
| +## 14. Second-order, from the review |
| + |
| +- **Reach is the first sibling call carrying free text.** memoria §9.1 |
| + promises `q` is never logged; that promise now depends on three other |
| + apps' log discipline, with no gate. §6 adds the server-half check. |
| +- **Every query makes each sibling run a search** under §6.5's deadline. A |
| + cold sibling withholds *the asker's own documents*, and the partial-page |
| + notice must say so. |