oficina / home Public

Consumer review: the contract could not deliver a team name

Five findings from docs-e6 reviewing as a declared consumer of the
five-call contract. Two were load-bearing.

Call 2 never carried the team's display name. Section 4 renders team
names on the directory origin, section 10 stores one sealed, and no call
could ever have delivered it — the whole split-origin design existed to
protect a string the contract never sent. Call 2 now carries it, and
three events drive the call rather than one: a grant changes, a team is
renamed (re-announce, so guests stop seeing a stale name), and a team is
deleted (team-scoped revoke). Without the third, every row for a deleted
team points at a host that 404s — a convenience failure under the hint
ruling, which is exactly why it would have survived being missed.

The rekey journal was unbounded for the same reason the jti store was,
with hibernation as the mechanism rather than guest volume: pruning "once
every instance has passed the cursor" never completes when a dormant team
holds the cursor and runs no timers to advance it. It now prunes by time
horizon, with a stale instance doing a full re-upsert on wake instead of
a journal pull.

The third finding is one neither session would have found alone. A
link-share visitor signs in to comment, and signing in is what mints
their guest grant — so they return holding a valid assertion with no
grant, which is correct behaviour and the wrong outcome. Resolved without
touching the assertion: the capability stays instance-side, carried in a
server-built return URL whose nonce keys pending state the instance
holds. The home learns nothing about links or capabilities, so the
identity-never-authorization rule survives rather than needing an
exception — and the nonce travelling in the URL handles the cross-device
case where the magic link completes in a different browser.

Also states which side wins when a theme write-through fails, so a lost
write costs one session instead of silently reverting somebody's choice.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APtNStXDZ5H2fAc1ZuGqtS
Paul Campbell pushed by paul@keymail.dev 4d928a24aa69af0802d3c7b56c49b8ec293aa0c2 parent a26f821
1 file changed, +63 −3
  • docs/superpowers/specs/2026-08-30-home-design.md +63 −3

No CI script — run details

diff --git a/docs/superpowers/specs/2026-08-30-home-design.md b/docs/superpowers/specs/2026-08-30-home-design.md
index 751c7cf..033ee7a 100644
--- a/docs/superpowers/specs/2026-08-30-home-design.md
+++ b/docs/superpowers/specs/2026-08-30-home-design.md
@@ -277,6 +277,25 @@ mints a session — and then **decides everything about capability from its
own `grants` table.** An assertion for a person with no grant on that
instance is a perfectly valid assertion that gets nothing.
+**The link-share ordering problem, and why it does not touch the assertion**
+(docs-e6, 2026-08-30). A link-share visitor who wants to comment signs in —
+and under the attribution rule, signing in is what *mints* their guest
+grant. So when they return from the home they have no grant yet, and "a
+valid assertion that gets nothing" is correct behaviour with the wrong
+outcome.
+
+**Resolution: the capability never rides the assertion. It stays entirely
+instance-side.** The instance begins sign-in with a **server-built return
+URL carrying a nonce** that keys pending state it holds itself — the link
+token and what it grants. On return it verifies the assertion, redeems its
+own pending state to mint the grant, then applies the session. The home
+learns nothing about links, tokens or capabilities, so §5.3's rule survives
+intact rather than needing an exception.
+
+This also handles the cross-device case, where the magic link completes in
+whichever browser opened the email: the nonce travels in the URL rather than
+in a cookie, so the round trip reassembles wherever it lands.
+
Filed as `rastrillo/rastrillo` #1: rastrillo has no helper for this, and two
apps need the identical thing. Until it exists both apps hand-roll it, which
is the duplication the filing exists to end.
@@ -405,6 +424,14 @@ The in-editor toggle writes through to the home and updates the local cookie
optimistically, so the switch is instant and the preference follows the
person into their next team.
+**The home wins at sign-in** (docs-e6, 2026-08-30). If a write-through
+fails, the instance's cookie and the home's `prefs` row disagree — and
+without a stated winner the next sign-in silently reverts somebody's
+choice, which is the kind of bug people report as "it keeps forgetting".
+So: the assertion's value is authoritative at session mint, the instance
+re-writes through on the next toggle, and a lost write costs one session
+rather than being invisible.
+
Theme passes §5.3's boundary test: a hostile value produces an ugly page,
not a breach. **The preference set stays small on purpose** — every addition
is a claim in every assertion, and a preference that fails the boundary test
@@ -417,7 +444,7 @@ is not a preference.
| # | Call | Direction | Notes |
|---|---|---|---|
| 1 | Sign-in and session minting | person → home | The home is the identity authority. Not a hint. |
-| 2 | Directory upsert / revoke | instance → home | Sends the address; **the home hashes** (ruling 4). Via the instance's outbox. |
+| 2 | Directory upsert / revoke | instance → home | Sends the address **and the team's display name**; **the home hashes** (ruling 4). Via the instance's outbox. Also carries team-scoped revoke. |
| 3 | Directory list | person → home | Every team across every suite app, with `kind`. |
| 4 | Provisioning enqueue | person → home | Rate-limited; drained by a human. |
| 5 | Rekey journal pull | instance → home | Cursor-based, carrying `old_hmac → new_hmac`. |
@@ -430,12 +457,45 @@ platform config and its allowlist entry from the operator drain, in the same
step that creates it. An instance with neither cannot call the home and, per
§6, works fine without it.
+**Call 2 carries the team's display name, and it is the only path by which
+the home ever learns one** (docs-e6, 2026-08-30). The first draft had §4
+rendering team names on the directory origin and no call that could ever
+have delivered one — the whole split-origin design existed to protect a
+string the contract never sent. The name is sealed at rest (§3).
+
+Three events therefore drive call 2, not one:
+
+- **A grant changes** — upsert or revoke that person's row.
+- **A team is renamed** — re-announce every row for that team, so guests
+ stop seeing a stale name indefinitely. The outbox already gives this
+ at-least-once delivery.
+- **A team is deleted** — **team-scoped revoke**, dropping every row for
+ that team at once.
+
+Without the third, every row for a deleted team points at a host that 404s.
+Under §6 that is a convenience failure rather than a correctness one, so it
+would survive being missed — which is exactly why it is written here rather
+than left to be inferred.
+
**Email change** is call 5's reason: the person's HMAC changes and their rows
orphan. The journal carries `old_hmac → new_hmac` **and the new readable
address**, because instances hold addresses (ruling 5) and need the new one
to keep notifying. The home learns both transiently and stores neither —
-the journal row's address is sealed and **pruned once every instance has
-passed the cursor.**
+the journal row's address is sealed.
+
+**Pruning is by time horizon, not by the slowest cursor** (docs-e6,
+2026-08-30). "Prune once every instance has passed the cursor" is unbounded
+for the same reason the `jti` store was, with hibernation as the mechanism
+rather than guest volume: a hibernating instance runs no timers and pulls
+only on wake, so a team dormant for a year holds the cursor for a year and
+nothing prunes. A guest-heavy app has many instances a person touched once,
+and some go quiet permanently — where "the slowest cursor" means "never".
+
+So: the journal prunes past a **fixed horizon**, and an instance whose
+cursor is older than the horizon does a **full re-upsert on next wake**
+instead of a journal pull. That trades a rare expensive catch-up for a
+bounded table, and §6's hint ruling is what makes it safe — a stale window
+costs a convenience, never access.
---