oficina / home Public

The utility carve-out ruling 20 omitted, and both paths end in the drain

Ruling 20 was over-broad in a way that would have shipped an
accessibility bug. Seven utilities — rst-sr-only, rst-mono, rst-m-hide,
rst-grow, rst-nm, rst-danger, rst-cell-mut — have NO attribute twin at
v0.22.0: five .rst-sr-only selectors and zero [rst-sr-only]. tokens.css
says so in the same header this spec already quotes, and rastrillo's
§6-v3 says "class is for utilities and the app's own CSS". An agent
following ruling 20 literally writes <span rst-sr-only>, nothing
matches, and screen-reader-only text renders visible. Kinds, variants
and parts are attributes; those seven stay class. This is the same
failure class as the two dead reasons: a rule that reads right and is
false at the pin.

§7 implied the fast path bypasses the operator. It cannot: §7's own
load-bearing safety fact is that the home holds no provisioning
credential, so nothing but the drain creates an instance, and ruling
7's "a human sees every team before it exists" applies to siblings
exactly as to first teams. Both paths enqueue. What differs is what the
operator is handed — an instance-vouched request arrives with the label
question already settled, an unvouched one does not.

Also recorded, because each was a door left open: consumer-level
authorization is §9's pre-existing business and not what §2.2 forbids,
which is a person-level entitlement; the new instance's first owner and
welcome address arrive as sealed queue payload, carried opaquely and
never judged; the fallback's hint must show kind, since a guest row in
acme.docs is emphatically not "is Acme"; and a compromised acme.docs
can vouch for acme.* siblings, bounded by provisionable, by one row per
team and app, and by the drain still seeing everything.

The §9 rename event could not survive the display-name ruling: if call
2's name never overwrites, a re-announce un-stales nothing. Rename
becomes an operator verb at the home — one row, one write, immediately
true for every instance and guest, which beats a re-announce that
waited on each instance waking. Call 2 now drives two events, not
three, and its sealed team name is advisory: a consistency check the
home may log a divergence on, never a write.

Plus the four the last commit claimed and did not deliver or left
inconsistent: §11 still opened by ruling the class vocabulary thirteen
lines above ruling 20; §15 still listed the spelling as needing Paul
after he had ruled it; §14's auto-drain row still said two decisions
when §7 now names a third; and §9's team-deleted event was still
team-scoped, because the sentence wrapped and the edit missed it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 4e5178ecda66a16ec8666f1b9e12fd4ebea4dc3f parent 634ffef
1 file changed, +70 −28
  • docs/superpowers/specs/2026-08-30-home-design.md +70 −28

CI passed — 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 37b1dff..cbeb7e7 100644
--- a/docs/superpowers/specs/2026-08-30-home-design.md
+++ b/docs/superpowers/specs/2026-08-30-home-design.md
@@ -1,6 +1,6 @@
# 🤖 The Oficina home — design
-**Status:** design, revised **2026-09-05** (rulings 17-19, and four claims
+**Status:** design, revised **2026-09-05** (rulings 17-20, and four claims
corrected). Originally 2026-08-30. Drafted by the Sheets
session with Docs (`oficina/docs`) as a declared consumer.
**Review passes:** consumer review by docs-e6 (5 findings, all applied);
@@ -60,8 +60,8 @@ blind, replaceable*.
| 16 | **`oficina.works` is the primary domain — RP `id.oficina.works`, directory at the apex, instances at `<team>.<app>.oficina.works`** — §4 | Staying on `oncarlos.com` until passkeys forced the move; keeping the `bds` slot | Paul, 2026-09-03 |
| 17 | **The springboard is one page: an app grid, with the team switcher as a dropdown in the header. Which apps exist comes from a registry, not from your grants. A team is a suite-wide entity that instances belong to** — §11, §10 | A team-switcher screen as a first level; deriving tiles from grants alone, which hides an app you have no team in and leaves nowhere to press New; grouping by hostname label alone, with no row that exists before the first instance and outlives any one of them | Paul, 2026-09-05 |
| 18 | **Activity is approved in principle, pulled at render, and sequenced last** — the front door shows a real feed with titles, the home holds none of it at rest, and its slot is a step 10 after step 4 — `design-activity-pull` | Push into the home; a contentless heat map; refusing the feature | Paul, 2026-09-05 |
-| 20 | **Write the attribute spelling — `rst-btn`, not `class="rst-btn"`** — §11 | The class vocabulary, which this section ruled on 2026-08-30 and whose every stated reason has since died; "follow whatever the pin's partials use", which changes this app's markup on a dependency bump | Paul, 2026-09-05 |
-| 19 | **A team name is reserved suite-wide. An existing instance may expand its own team; anyone else falls to the drain. The home answers only facts about its own records, never "may this person"** — §7 | Per-app first-come, which lets a stranger squat `acme.sheets` behind your `acme.docs` on a domain that reads as one suite | Paul, 2026-09-05 |
+| 19 | **A team name is reserved suite-wide. Both paths end in the drain — an existing instance settles the label question for the operator, it does not bypass them. The home answers only facts about its own records, never "may this person"** — §7 | Per-app first-come, which lets a stranger squat `acme.sheets` behind your `acme.docs` on a domain that reads as one suite | Paul, 2026-09-05 |
+| 20 | **Write the attribute spelling — `rst-btn`, not `class="rst-btn"`. The seven unpaired utilities keep `class`** — §11 | The class vocabulary, which this section ruled on 2026-08-30 and whose every stated reason has since died; "follow whatever the pin's partials use", which changes this app's markup on a dependency bump | Paul, 2026-09-05 |
---
@@ -564,19 +564,42 @@ ruling 15 rejected a per-team 2FA floor — a policy with no content in it at
all — on exactly these grounds. So the distinction has to be sharper. Two
paths, and the home answers only facts about its own records in both:
-- **Fast path — an existing instance expands its own team.** "Create
+**Both paths end in the queue**, and this is not a detail. §7's
+load-bearing safety fact is that **the home holds no provisioning
+credential** — so nothing the home can do creates `acme.sheets`, and ruling
+7's "a human sees every team before it exists" holds for siblings exactly as
+for first teams. What the two paths differ in is what the operator is
+handed, not whether there is one.
+
+- **Fast path — an existing instance vouches for its own team.** "Create
`acme.sheets`" is initiated from inside `acme.docs`, by whoever
- `acme.docs` considers an owner, and reaches the home as an
- instance→home call under that instance's consumer credential. The home
- checks one thing: **is the calling origin already in `team_instances` for
- this team?** That is a row the home itself wrote at the drain, not a
- judgement about a person. Who counts as an owner is the instance's
- business, decided from its own grants — §2.2's second never-do, working
- exactly as intended.
-- **Fallback — everyone else goes to the drain.** A request for a reserved
- label from anywhere else is enqueued, and a human decides. The home may
+ `acme.docs` considers an owner, and reaches the home as an instance→home
+ call under that instance's consumer credential. The home checks one
+ thing: **is the calling origin already in `team_instances` for this
+ team?** — joining `consumers.origin` to the instance host the home itself
+ recorded at the drain. That answers the *label* question and enqueues the
+ request pre-settled. Who counts as an owner is the instance's business,
+ decided from its own grants — §2.2's second never-do working exactly as
+ intended, and the home never learns the answer.
+- **Fallback — everyone else arrives unvouched.** The request is enqueued
+ the same way, and the human decides the label question too. The home may
render the requester's own directory rows beside it as a **hint** (ruling
- 9), and that is all they are: the operator adjudicates, the home does not.
+ 9) — with `kind` shown, since a *guest* row in `acme.docs` is emphatically
+ not "is Acme". They are a hint and nothing else: the operator adjudicates.
+
+**Two things this does not smuggle in.** Consumer-level authorization is not
+new and is not §2.2's concern: §9 already scopes every call to its consumer,
+and a label on `oficina.works` is the home's own namespace. What §2.2
+forbids is a *person*-level entitlement, and neither path forms one. And the
+new instance's first owner and welcome address arrive as **sealed queue
+payload**, exactly as a fallback requester's do (§3.2) — the home carries
+that opaquely and never judges it.
+
+**A compromised `acme.docs` can vouch for `acme.*` siblings.** Bounded by
+the registry's `provisionable` set, one row per (team, app), the drain still
+seeing every request, and the sibling getting its own consumer token from
+that drain rather than inheriting one. Recorded rather than left to be
+discovered; §2's blast-radius argument survives it.
So the home's whole vocabulary here is two facts: *is this label free*, and
*is this calling origin already an instance of this team*. It never forms an
@@ -650,7 +673,7 @@ every assertion, and one that fails §5.4 is not a preference.
| # | Call | Direction | Notes |
|---|---|---|---|
| 1 | Sign-in and session minting | person → home | The identity authority. Not a hint. |
-| 2 | Directory upsert / revoke | instance → home | Sends address + sealed team name; home hashes; **returns the person ref** and the pepper epoch. Team-scoped revoke included. Via the outbox. |
+| 2 | Directory upsert / revoke | instance → home | Sends address **+ sealed team name, which is advisory**: the home owns the display name (§10) and does not overwrite it, so this is a consistency check the home may log a divergence on, never a write. Home hashes; **returns the person ref** and the pepper epoch. Instance-scoped revoke included. Via the outbox. |
| 3 | Directory list | person → home | Every team across every app, with `kind`. |
| 4 | Provisioning enqueue | person → home | Rate-limited, depth-capped, human-drained. |
| 5 | Rekey journal pull | instance → home | **Scoped to this consumer**, keyed on person refs. |
@@ -665,11 +688,20 @@ verb exists before launch rather than after the first leak.
**Bootstrap:** a fresh instance gets its token as platform config and its
allowlist entry from the drain, in the step that creates it.
-**Call 2 drives three events**, not one: a grant changes; a team is renamed
-(re-announce, so guests stop seeing a stale name); a team is deleted
-(team-scoped revoke). Without the third, rows point at a host that 404s —
-a convenience failure under §6, which is exactly why it would survive being
-missed.
+**Call 2 drives two events**, not one: a grant changes; and an **instance**
+is deleted (instance-scoped revoke — after §10's split this removes that
+`team_instances` row and its directory rows, and leaves `teams` and its
+label reservation alone while any sibling instance survives). Without the
+second, rows point at a host that 404s — a convenience failure under §6,
+which is exactly why it would survive being missed.
+
+**Renaming is no longer one of them.** An earlier draft had call 2 drive a
+re-announce "so guests stop seeing a stale name". That cannot work now the
+home owns the display name (§10): if call 2's name never overwrites, a
+re-announce un-stales nothing. **Rename is an operator verb at the home** —
+one row, one write, immediately true for every instance and every guest,
+which is strictly better than a re-announce that depended on each instance
+waking up.
### 9.1 Call 5, rewritten
@@ -741,8 +773,20 @@ restored is a hope, not a backup* — the restore drill runs before launch.
## 11. Shape and design
-rastrillo `server` shape, zero-JS baseline, `day` theme, and the **class**
-vocabulary — `class="rst-*"`.
+rastrillo `server` shape, zero-JS baseline, `day` theme, and the
+**attribute** vocabulary — `rst-btn`, not `class="rst-btn"` (ruling 20).
+
+**The seven utilities are the exception, and they are not a loophole.**
+`rst-sr-only`, `rst-mono`, `rst-m-hide`, `rst-grow`, `rst-nm`, `rst-danger`
+and `rst-cell-mut` **keep the class spelling**, because they have **no
+attribute twin at all** — verified at v0.22.0: five `.rst-sr-only`
+selectors and zero `[rst-sr-only]`. tokens.css says why in its own header:
+"Utilities keep the class spelling and are not paired… They are not kinds —
+they are cross-cutting styling, which is what class is for, and they stay
+class after the move", and rastrillo's §6-v3 agrees ("`class` is for
+utilities and the app's own CSS"). Writing `<span rst-sr-only>` matches
+nothing, so screen-reader-only text renders **visible**. Kinds, variants and
+parts are attributes; these seven are classes.
**Corrected 2026-09-05, verified against the pinned `rastrillo v0.22.0`.**
An earlier draft recorded "zero bare `rst-*` attribute selectors against
@@ -897,7 +941,7 @@ ships**, because call 2's person ref is what their grant rows key on.
| **Owner-enforceable per-team 2FA** (ruling 15) | A team asks, *and* either a sixth call carrying team policy is argued past §2.2 or rastrillo grows a per-team notion |
| Pegamento's vault facet — team names the home cannot read | The vault ships with a recovery story |
| Operator reset for a fully locked-out person | The first person loses passkey and codes together |
-| Auto-drain of the queue | The platform ships a machine credential **and** we choose to give up the abuse gate — two decisions |
+| Auto-drain of the queue | The platform ships a machine credential, **and** we choose to give up the abuse gate, **and** ruling 19's fallback adjudication is automated — three decisions. A machine credential supplies no judgement about who may take a reserved name (§7) |
| **Suite-wide sign-out / session revocation** | Named as an exposure now: a stolen laptop's instance sessions live to their TTL with no revocation channel. Trigger: the first report, or before any team holds regulated data |
| **Account deletion / erasure** | Before a person asks, which under most regimes is not optional for long |
| **Step-up for instances** (`RequireFresh`-grade) | An instance flow needs it; requires a fresh-assertion round trip with a `max_age`-style parameter |
@@ -924,11 +968,9 @@ ships**, because call 2's person ref is what their grant rows key on.
it does. Also open there: the per-instance deadline, the fan-out budget,
the RAM cache TTL and the body cap are all named as decisions and none is
chosen.
-- **Class or attribute spelling — §11 needs a re-ruling.** Both reasons §11
- gave for `class="rst-*"` are now false at the pinned version, and
- rastrillo's §6-v3 rules one spelling with the class selectors dropped
- before 1.0. This app has no `rst-` markup yet, so the switch is free
- today and costs a sweep after the first template. Needs Paul.
+- ~~Class or attribute spelling — §11 needs a re-ruling~~ — **closed by
+ ruling 20**, 2026-09-05: the app writes attributes, and the seven
+ unpaired utilities keep `class`.
- ~~A real suite domain — triggers §5.7~~ — closed by ruling 16. §5.7 is
titled for the case it does not apply to: the `oficina.works` move needs
no RP rename drill.