Review pass: seven sentences the amendment made false, and its dependents
Ruling 23's decision is sound and its three reasons check out against main — §6's row really has no owner role, §7 really did say the home never learns who an owner is, and the row really is per (team, app) where a roster is per team. The security claim holds too: any instance writes a directory row under its own consumer token, so reading team standing from one would let a compromised instance announce a member with no owner behind it. What the commit missed is its own blast radius through the document. Seven sentences elsewhere became false the moment the roster landed, each citing a real section — this repo's most-caught defect, arriving seven times in one commit. §2's blast-radius sentence said a compromised home "does not cost a document". The branch's own §13a says a compromised directory origin can add a member, and a member reads everything team-visible in every instance of that team. That sentence is amended now, not annotated from elsewhere. §7 quoted §2.2's old clause — "never holds an authorization model, not never holds a content authorization model" — a clause this same commit deleted, and then said "what §2.2 forbids is a person-level entitlement, and neither path forms one". Ruling 23 forms one deliberately. The line moved rather than held: the home decides about resources it owns, a label and a team's roster, and never about an instance's items. Ruling 19's row is narrowed to match, or it contradicted ruling 23 in the same table. Ruling 15's ground was carried verbatim under a rewritten heading: "a per-team 2FA floor would have breached this" is false of "never holds a per-item grant", because a 2FA floor is not a per-item grant. The objection that survives is that a floor is a policy the home would enforce on an instance's behalf, on the instance's own request path. §3.3's argument was left understated, and it is the material answer to what §0 now costs. Its fingerprint needs several teams plus an outside fact; an owner row needs one team and a fact that is usually public. `(team, person_ref, role=owner)` names a person ref outright and then reveals every other row that ref holds. §3.2's at-rest inventory gains the roster and its (generation, epoch), which is the column class §0's new sentence is about and which gate 8 tests against the schema. roster_epoch was missing from §5.3's claims table, though §5.4 already carried the narrow invalidation-hint exception that exists to admit it. The step-up citation pointed at §14's "step-up for instances" row — a different mechanism, for a different caller, listed there as deliberately not built. The roster write uses the directory origin's own sessions.Fresh, the same check §5.2's show-codes-once page sits behind. And §6's "no call in this table runs home→instance" — a sentence from earlier today — went stale when §9 gained rows 7 and 8. Corrected, and corrected again: call 8 is the instance calling the home, not the reverse, which a first pass at this fix got backwards. AGENTS.md lands in the same commit, per its own rule. "Never federate, data does not cross an instance boundary" becomes "never cross a team", since acme.docs and acme.sheets were always the same team. "Never hold an authorization model" becomes "never hold a per-item grant", with the roster named and the moved line explained. Also: the seed's address-to-person-ref step named, since it needs the pepper and may mint a person stub, so the drain calls a home-side operator verb rather than holding the pepper; call 8's epoch made per-team, or a bump in one team is a cross-team signal to every instance; call 8's wrong-team refusal made 404 not 403; gate 10 naming call 8; §14's row marked as not the roster's step-up; the §1 table reordered and its blank-line break closed; and the Rejected column naming what ruling 23 actually overturns — the status quo where team standing lived only in each instance's grants. Reviewed by Fable 5.1; one pass, CHANGES NEEDED with seven blocking.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PsPrUY5B4wtAZHGZJ3PJyS
2 files changed,
+82
−26
AGENTS.md+14 −4docs/superpowers/specs/2026-08-30-home-design.md+68 −22
CI passed — run details
Intent
Attributed to this commit: 110 g CO₂e (14267882 tokens). This is an attribution, not a measurement — usage counted from this commit's own marker forward to the next one on suite-amendment, 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/AGENTS.md b/AGENTS.md| index 228e9bf..6c6e2e2 100644 |
| --- a/AGENTS.md |
| +++ b/AGENTS.md |
| @@ -80,10 +80,20 @@ improvement. |
| test — spec §12's gates 1 and 9 grew a corpus of known titles, URLs and |
| actor strings that day, so *never stores* has a grep behind it where |
| *never renders* had none. |
| -2. **Never hold an authorization model.** The home knows you can reach a |
| - team in an app. What you may *do* there is the instance's business, |
| - decided from its own grants. |
| -3. **Never federate.** Data does not cross an instance boundary. Sharing is |
| +2. **Never hold a per-item grant.** Which sheets you may edit, which |
| + document a guest may comment on: all instance-side, decided there on |
| + every request. **The home does hold the membership roster** — |
| + `(team, person_ref, role)`, ruling 23 — so it knows who is on a team and |
| + who owns it. It never knows what they may do with any particular thing. |
| + This replaced *"never hold an authorization model"* on 2026-09-05; the |
| + line moved from *facts versus may-this-person* to **resources the home |
| + owns versus an instance's items**, because the roster is a person-level |
| + entitlement the home now holds on purpose. |
| +3. **Never cross a team.** Data does not cross a team label; a team's |
| + instances across apps are one trust domain (spec ruling 23, suite ruling |
| + 1). This replaced *"never federate — data does not cross an instance |
| + boundary"* on 2026-09-05: the boundary was drawn in the wrong place, since |
| + `acme.docs` and `acme.sheets` are the same team and always were. Sharing is |
| a grant on the instance — member or guest — never a copy. §6 keeps the |
| two apart deliberately: "a row implying an org membership someone does |
| not have is a lie the directory tells on the org's behalf". |
diff --git a/docs/superpowers/specs/2026-08-30-home-design.md b/docs/superpowers/specs/2026-08-30-home-design.md| index df4131f..a0fa582 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-22, and five claims |
| +**Status:** design, revised **2026-09-05** (rulings 17-23, and five 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); |
| @@ -125,12 +125,11 @@ 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, sequenced last, and scoped PER TEAM, following `?team=`** — 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; a cross-team merged list (which `design-activity-pull` specified and argued for until its ruling 6 revised it, 2026-09-05); refusing the feature | 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 | |
| +| 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" **about an instance's items** — narrowed by ruling 23, which has the home answer exactly that question for a team's own roster** — §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 | |
| | 21 | **§11's zero-JS rule gains one named carve-out: passkey enrolment and ceremony screens may load script. Everything else stays scriptless** — §11 | Passkeys leaving slice one; a blanket exemption; a progressive-enhancement version, since the ceremony itself cannot happen without script either way | Paul, 2026-09-05 | |
| -| 23 | **The suite sharing doctrine (`oficina/suite`) binds this spec, and membership becomes a record of the home's own.** The team is the boundary; the home owns identity, the team and the membership roster; it never owns a per-item grant. `(team, person, role)` is written only at the directory origin by an owner behind step-up, or seeded by the drain with the team's first owner. A directory row never grants team standing. Call 8, roster pull, is added to §9. | Deriving the roster from call-2 directory rows, which the doctrine's own first revision proposed: the row has no owner role (§6 — `kind` is member or guest), §7 says the home never learns who an owner is, the row is scoped per (team, app) where the roster is per team, and any instance can write one under its consumer token — so deriving from it routes around the step-up entirely | Paul, 2026-09-05 (suite ruling 12). Numbered 23 because the doctrine's appendix said "add ruling 22" and 22 was already taken; a review of the doctrine caught the collision | |
| | 22 | **The team selection is navigation state, not session state: `?team=<sqid>` on every team view, with `/` a generic entry that picks a team and `302`s to the explicit URL** — §11 | A session cookie (history does not restore one, so Back has no correct behaviour; and a shared cookie couples every same-origin team-dependent request across tabs, not just New); a URL fragment read by CSS `:target` (the server never learns the team, so nothing server-rendered can be team-scoped — not the grid, not the switcher's `aria-current`, not gate 4's 404); a `prefs` row (§14) | Paul, 2026-09-05, after a second opinion from **GPT-6 Astra via Codex** (its default; `gpt-5.3-codex` and `gpt-5.3-sol` were both refused by the account, so the model is named from Codex's own rollout record rather than from what was asked for) | |
| - |
| +| 23 | **The suite sharing doctrine (`oficina/suite`) binds this spec, and membership becomes a record of the home's own.** The team is the boundary; the home owns identity, the team and the membership roster; it never owns a per-item grant. `(team, person, role)` is written only at the directory origin by an owner behind step-up, or seeded by the drain with the team's first owner. A directory row never grants team standing. Call 8, roster pull, is added to §9. | **The status quo this overturns: team standing living only in each instance's grants, with the home holding no roster at all** (§5.5, §7 as they stood). And the doctrine's own first revision, which derived the roster from call-2 directory rows, which the doctrine's own first revision proposed: the row has no owner role (§6 — `kind` is member or guest), §7 says the home never learns who an owner is, the row is scoped per (team, app) where the roster is per team, and any instance can write one under its consumer token — so deriving from it routes around the step-up entirely | Paul, 2026-09-05 (suite ruling 12). Numbered 23 because the doctrine's appendix said "add ruling 22" and 22 was already taken; a review of the doctrine caught the collision | |
| --- |
| ## 2. What the home is |
| @@ -167,8 +166,14 @@ aspirational: |
| - **Split origins mean a directory compromise reaches no credentials** (§4), |
| and **every instance re-checks authorization on every request** (§6), so a |
| - compromised directory grants nothing. A compromised home costs the graph |
| - and a convenience outage. It does not cost a document. |
| + compromised directory grants nothing **of an instance's own**. **Ruling 23 |
| + changed the rest of this sentence and it is amended rather than |
| + annotated.** A compromised directory origin can now write the roster, and a |
| + member reads everything team-visible in every instance of that team — so a |
| + compromised home costs the graph, a convenience outage, **and, until an |
| + owner notices, team-visible content across a team's apps.** It still costs |
| + no *per-item* grant, which is the thing the home does not hold. §13a sets |
| + out the full radius; this bullet no longer contradicts it. |
| - **It holds no content *at rest*.** The worst case in a published database |
| is §3's leak list, never anyone's work. **Since the 2026-09-05 amendment a |
| live compromise is larger**: the activity cache holds recent titles in RAM |
| @@ -228,8 +233,18 @@ Each is something somebody will propose in good faith. |
| it was a statement of intent rather than a mechanism. |
| - **Never holds a per-item grant.** Which sheets you may edit, which |
| document a guest may comment on: all instance-side, decided there on every |
| - request (§5.5). Ruling 15 exists because the per-team 2FA floor would have |
| - breached this. |
| + request (§5.5). |
| + |
| + **Ruling 15's ground has to be restated, not carried over.** It read |
| + "ruling 15 exists because the per-team 2FA floor would have breached |
| + this", against the old clause *never holds an authorization model*. A |
| + per-team 2FA floor is not a per-item grant, so under the new clause that |
| + sentence is simply false. The objection that survives is narrower and |
| + still holds: **a 2FA floor is a policy the home would have to enforce on |
| + an instance's behalf, at sign-in, for a team.** The roster says who is on |
| + a team; a floor says what an instance must demand of them, which is the |
| + instance's decision on its own request path. §5.2 and §14's trigger say |
| + "argued past §2.2" and mean this bullet, not the deleted one. |
| **It does hold the membership roster** (suite ruling 4, ruling 23). "Is |
| this person on this team" is a fact about the home's own records. "May |
| @@ -301,6 +316,10 @@ journal's new addresses; **sign-in link state** (§5.1). |
| - Counts of people and teams. |
| - Blind-index values (§3.1's caveat applies). |
| - The bipartite graph: person ref ↔ team ↔ app ↔ `kind`. |
| +- **The membership roster: `(team, person_ref, role)`** (ruling 23), and the |
| + roster's `(generation, epoch)`. **Plaintext**, necessarily — call 8 reads |
| + it — and it is the new column class §0's cost sentence names. `role` is |
| + what makes a published database identify each team's owners. |
| - Team hostnames and sqids — these appear in URLs and were never secret. |
| - Passkey credential ids and **public** keys. |
| - Session rows keyed by **person ref**. Their timestamps are **exact at |
| @@ -333,8 +352,18 @@ fingerprint. One outside fact — "alice consults for Acme and BigCo" — |
| identifies the row and reveals the rest of their memberships, guest rows |
| included. Timestamps correlate with events observable at instances. |
| -So: published timestamps are **coarsened to the day**, and §0 says "some |
| -person" rather than "some unidentified person". **Said plainly rather than |
| +So: published timestamps are **coarsened to the day**, and §0 stopped saying |
| +"some unidentified person" long ago. |
| + |
| +**Ruling 23 makes this argument weaker than it reads, and that is said here |
| +rather than left to §0's one-line cost.** The fingerprint above needs |
| +*several* teams plus an outside fact. An **owner row needs one team and a |
| +fact that is usually public** — who owns Acme is on Acme's website. So |
| +`(team, person_ref, role=owner)` names a person ref outright, with no |
| +multi-team fingerprint required, and naming it then reveals every other |
| +membership and guest row that ref holds. **Naming owners is materially more |
| +identifying than "belongs to a set of teams"**, and it is the class §0's new |
| +sentence is about. **Said plainly rather than |
| counted as a fix: coarsening protects a deliberate publication and nothing |
| else.** §3.1's leak models ship the raw database file, whose session rows |
| carry exact timestamps (§3.2), so the timestamp-correlation fingerprint is |
| @@ -499,6 +528,7 @@ the contract. Two consumers were about to guess. |
| | `iat` / `exp` | See expiry below. | |
| | `jti` | Single use, per verifier. | |
| | `prefs` | Theme only (§8), subject to §5.4's test. | |
| +| `roster_epoch` | The team's roster epoch (ruling 23). **An invalidation hint, not a decision** — §5.4 records the narrow exception it needs, and the instance enforces call 8's answer, never this. | |
| **Signature:** Ed25519. Private key in platform config, never in the |
| database. Verification keys published on the credential origin. |
| @@ -637,7 +667,7 @@ and ruling 23 makes that load-bearing rather than merely true: **a directory |
| row never grants or revokes team standing, whatever its `kind` says.** Call 2 |
| writes these rows under an instance's own consumer token, so if team standing |
| were read from them, any instance could announce a member and walk around |
| -§14's owner step-up. Team standing comes from the membership record and |
| +the owner step-up ruling 23 requires. Team standing comes from the membership record and |
| nowhere else. |
| `kind` is member or guest, because a row implying an org |
| @@ -668,10 +698,11 @@ sign-in. |
| **Pepper rotation uses an epoch, not a sweep.** The first draft called for a |
| "re-announce sweep" as an operator verb — which has no channel to reach |
| -instances: **no call in this table runs home→instance**, and instances |
| -hibernate. (The one call that does run home→instance is call 7, |
| -`design-activity-pull` §4 — approved, not built, and a per-render pull |
| -rather than a channel anything could be pushed down.) A fleet-wide poke |
| +instances: **no call in this table pushes to an instance**, and instances |
| +hibernate. (One call runs home→instance — call 7's activity pull — but it |
| +fires per render, when a person is already looking at their front door, so |
| +it is not a channel the home can reach a sleeping fleet down. Call 8 is the |
| +instance calling the home.) A fleet-wide poke |
| would in any case contradict the platform's zero-infrastructure-access |
| contract. Instead: **every call-2 and call-5 response carries a pepper |
| epoch**, and an instance whose stored epoch differs re-enumerates its grants |
| @@ -724,7 +755,8 @@ a person↔resource entitlement is what §2.2 forbids and ruling 9 refuses |
| ("*where might I have things*, never *may I have this*"). An earlier draft |
| argued this away as "a name, not a document". **That argument is wrong and |
| is recorded here so it is not made again:** §2.2 says *never holds an |
| -authorization model*, not *never holds a content authorization model*, and |
| +authorization model* — the clause as it stood before ruling 23 rewrote it — |
| +not *never holds a content authorization model*, and |
| 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: |
| @@ -759,7 +791,12 @@ handed, not whether there is one. |
| **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 |
| +forbade, before ruling 23, was any *person*-level entitlement, and neither |
| +path formed one. **Ruling 23 forms one deliberately** — the roster is a |
| +person-level entitlement the home holds and enforces — so the line moved |
| +rather than held: the home decides about **resources it owns** (a label, a |
| +team's roster) and never about **an instance's items**. Both paths here are |
| +still on the resources-it-owns side. 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. |
| @@ -848,7 +885,7 @@ every assertion, and one that fails §5.4 is not a preference. |
| | 5 | Rekey journal pull | instance → home | **Scoped to this consumer**, keyed on person refs. | |
| | 6 | Preference write-through | person → home | Redirect POST to the directory origin (§8). | |
| | 7 | Activity pull | home → instance | **An assertion for the person**, `aud` = the instance. The home renders and discards; RAM only, never at rest. `design-activity-pull` §4–5, suite §6. | |
| -| 8 | Roster pull | instance → home | Consumer token, scoped to the caller's own team. Returns `(generation, epoch)` and the team's members with roles, **paged with an epoch-bound cursor**: a page from a superseded epoch is refused, and an instance stages every page and replaces its roster atomically only when a complete set for one epoch has arrived. Suite §5.3. | |
| +| 8 | Roster pull | instance → home | Consumer token, scoped to the caller's own team. Returns `(generation, epoch)` and the team's members with roles, **paged with an epoch-bound cursor**, the **epoch per team** (a suite-wide epoch would make a bump in one team a cross-team activity signal to every instance, which suite ruling 1 forbids), and a request for a team that is not the caller's answered **identically to an unknown team — 404, never 403** (gate 4's pattern): a page from a superseded epoch is refused, and an instance stages every page and replaces its roster atomically only when a complete set for one epoch has arrived. Suite §5.3. | |
| Auth follows Pegamento's service contract: **per-consumer bearer token |
| stored hashed**, consumer identity is its origin, origins allowlisted, |
| @@ -956,11 +993,18 @@ Migrations additive-only. |
| **`memberships`** — `(team, person_ref, role)`, role being `owner` or |
| `member`, added by ruling 23. Written **only** at the directory origin, by a |
| -team owner, behind a fresh-assertion step-up (§14), or seeded by the drain |
| +team owner, behind a fresh-assertion step-up at the directory origin — `sessions.Fresh` (§3.2), the same freshness check §5.2's show-codes-once page sits behind, over the assertion exchange of §4. **Not §14's "step-up for instances" row**, which is a different mechanism for a different caller and is still listed there as not built, or seeded by the drain |
| with the team's first owner from the sealed queue payload §7 already |
| carries. That bootstrap exists because gating a membership write on an owner |
| needs an owner to exist first, and the circularity has to break outside the |
| -roster. Call 8 reads this table and nothing else. Call 2 cannot write it. |
| +roster. **The seed needs one thing said out loud:** the payload holds an |
| +**address**, the roster keys on a **person ref**, and turning one into the |
| +other needs the pepper — which only the home holds (ruling 4) — plus a |
| +person stub if that owner has never signed in (§6). So the drain calls a |
| +**home-side operator verb**, the same surface that marks an instance live; |
| +it never holds the pepper itself. That verb mints an owner, so it is |
| +security-load-bearing and is named rather than left to be improvised. **A |
| +sibling drain (call 4b) seeds nothing** — the team already has owners. Call 8 reads this table and nothing else. Call 2 cannot write it. |
| Guests are **not** on it. A guest grant is minted in the instance and |
| announced to the directory as `kind = guest`, exactly as before; nothing |
| @@ -1230,7 +1274,9 @@ No template gallery, no upsell chrome, no waffle. |
| for addresses, team names, queue payloads **and pulled content — titles, |
| URLs, actors** (§3.4). |
| 10. **No-store** — drive every route on both origins and assert each |
| - authenticated response carries `Cache-Control: no-store`, the |
| + authenticated response carries `Cache-Control: no-store`, **call 8's |
| + roster read included** (it is a bearer-authenticated GET, and whether the |
| + edge refuses on `Authorization` is not something to rely on), the |
| personalised `/` redirect included (ruling 22). It exists for the edge-cache |
| reason §11 sets out. |
| @@ -1334,7 +1380,7 @@ identity authority. That filing belongs at `carlos/pegamento`. |
| | 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 | |
| +| **Step-up for instances** (`RequireFresh`-grade) | An instance flow needs it; requires a fresh-assertion round trip with a `max_age`-style parameter. **Not the roster write's step-up** (ruling 23), which is the *directory origin's own* `sessions.Fresh` check and is built with the roster — different caller, different mechanism | |
| | Any preference beyond theme | It passes §5.4 | |
| | Billing, plans, quotas | Somebody has to pay | |
| | **Mail and Files behind suite SSO** | Keymail or Slopbox verifies assertions per §5.3 in its own repository. Until then they are `integrated: false` registry entries rendered as outbound links (§11, ruling 17) | |