oficina / suite Public

Merge branch 'worktree-activity-reconcile' into main

claude agent, operated by paul@keymail.dev pushed by paul@keymail.dev bf0eeb83eab33265f74f630ea77ac5eb72f22fe4 parent 574cc84parent 7334c0c
1 file changed, +51 −8
  • docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md +51 −8
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 304dd32..57b9d81 100644
--- a/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md
+++ b/docs/superpowers/specs/2026-09-05-suite-sharing-doctrine.md
@@ -45,11 +45,15 @@ new findings. All three reports are beside this file, unedited:
**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
+precedence when a stale roster and a non-custodial item collide; pinning
+sibling baselines, since four memoria citations resolve only on an unmerged
+worktree; and most of the fixtures the gates now describe.
+
+Two that were on this list are off it. **The activity path's cached
+authorization** is answered: §4.3 now carves out call 7 explicitly and
+requires a hard maximum age, which the activity-pull session had already
+priced before being asked. **A.1's ruling number** is resolved: home ruling
+22 was indeed taken and the amendment landed as 23. These are recorded
here rather than in a passing sentence so the next reader can see the
edge of the work.
@@ -309,6 +313,29 @@ team standing, not "may this person read this item". An earlier draft of
ruling 3 said "cached no longer than a session", which conflated the two;
memoria's session caught it.
+**One carve-out, and it is call 7.** The rule above governs an authorization
+*answer*: an S2 `VISIBLE` held past its request is a standing capability, and
+it grows — the consumer re-uses it against items and people the app of
+record never spoke about. The home's activity cache is not that. It holds
+*rendered output* for one person, the home holds no grants, and it can only
+ever re-serve the same rows to the same person.
+
+That is a real distinction and it is not a free one. Inside the cache's life
+a person removed from a team still sees titles, and calling that "not cached
+authorization" would be word-play. So it is priced rather than denied: the
+activity cache carries **two ages**, a short refresh TTL saying when to try
+again, and a **hard maximum age after which rows are discarded regardless of
+the pull's outcome, failure included**. The hard maximum is the one that
+bounds the lie. Without it the tail is the hibernating fleet's wake schedule
+rather than a number anybody chose.
+
+This document states that the bound must exist and refuses to carry the
+number, exactly as §4.2 does for retention: the number is a property of the
+consumer, and the consumer states it. `design-activity-pull` §7 holds it.
+*(Reconciled with the activity-pull session, 2026-09-05, which found the
+contradiction between this section and §6's call-7 row and priced it before
+being asked.)*
+
**The app of record for an item is the app whose store it was created
in.** A copy never becomes an authority, and a derived artefact carries the
provenance of every source it was built from, transitively, so two hops
@@ -420,7 +447,17 @@ deserves its own words — **the instance decides nothing from the claim**. A
claim it cannot parse is ignored. A claim that differs from what it holds
buys exactly one call-8 refetch, and call 8's answer is what it enforces.
So a hostile claim costs one request and can neither admit nor exclude
-anybody. A.1 records that exception in the home spec, which owns the test;
+anybody.
+
+**The exception is scoped to the call-8 path and nothing else.** An instance
+answering call 7 sees the same assertion and therefore the same claim, and it
+must not invalidate anything on it — that would be an instance making a
+decision from a home-supplied claim, which is precisely what home §5.4
+forbids and what this exception is narrow enough to avoid. Refetching a
+roster is not a decision; withholding an activity row would be. *(Raised by
+the activity-pull session, 2026-09-05.)*
+
+A.1 records that exception in the home spec, which owns the test;
this document cites it rather than asserting the old test already covered
it. A person whose sign-in reveals a stale roster is admitted against the
fresh roster, never against the claim. *(GPT-6 review, finding 12.)*
@@ -592,7 +629,7 @@ spec, which owns them, and are cited here for completeness of direction.
| Call | Direction | Credential | Proves | Notes |
|---|---|---|---|---|
| 1–6 | person → home, instance → home | session; consumer token | — | Home §9. Unchanged. |
-| 7 activity | home → instance | **assertion** for the person, `aud` = instance | who the front door is for | `design-activity-pull` §4–5. The home renders and discards; RAM cache only. |
+| 7 activity | home → instance | **assertion** for the person, `aud` = instance | who the front door is for | `design-activity-pull` §4–5 owns the wire shape. The home renders and holds rendered output only, under two ages (§4.3). Not in the contract module yet — §12 step 2 is S1, S2 and S3. When it arrives, the fixtures **cite** that spec's gates 10–20 rather than restate them, or this contract rebuilds §2's drift one layer down. |
| 8 roster | instance → home | consumer token | which instance asks | §5.3. Scoped to the caller's team. Paged; the copy has a TTL. |
| S1 change feed | sibling → sibling | consumer token | same-team sibling | `(item_id, revision, gone, touched_at)`. No title, no content, no visibility flag. Per-consumer TTL; past it, full re-announce. |
| S2 authorization | sibling → sibling | consumer token **+ named asker** | same-team sibling, asking for a person | Four values, total decoder, cap 256 pairs. **Never cached**: a passage crosses no request boundary without an S2 `VISIBLE` in the request that carried it (memoria §8.4, gate 26). Asker is a typed subject (§3); the answer is intersected with custody (§4.3). |
@@ -916,7 +953,13 @@ apps are a suite, and every one is a consequence of ruling 1 or 4.
calls to instances as a matter of course, its outbound traffic stops
being a compromise signal (activity design §2.2(b)). Both were
disclosed there and belong here too.
-11. **A link capability is bearer-shaped** (ruling 15). Whoever holds the
+11. **A cacheable call-7 response is a cross-person leak** at the platform
+ edge, not merely a slow one. The shared edge would serve one person's
+ rows to the home's next pull for another, so a cacheable response is a
+ *failed* pull rather than a fast one. `design-activity-pull` gate 16
+ tests it; recorded here because the failure is in the platform between
+ two apps and neither app's own tests would see it.
+12. **A link capability is bearer-shaped** (ruling 15). Whoever holds the
URL reads or writes the item, the app cannot name them, and a
forwarded link is a grant nobody recorded. This is what "anyone with
the link" means and there is no version of it that is not this.