oficina / sheets Public

Assertions carry identity, never authorization

docs-e6's argument, and it is a layering rule rather than a detail, so it
belongs in section 8.1 and not only in the filing.

A guest's assertion is the same shape as an owner's. The assertion says
who you are; the instance decides what you may do from its own grants.
Narrowing a comment-only guest's assertion is tempting and belongs on the
wrong side of the boundary: encoding capability requires the home to hold
an authorization model, which cuts against section 9's whole design. The
security case does not survive either, since a leaked assertion buys
exactly what a stolen session buys — so narrowing reduces no blast radius
while adding a second assertion type and a second audience-binding
surface. Stated positively because a reviewer will otherwise propose the
opposite as an improvement.

Also records the sharper slip criterion: "its absence leaves a coherent
product" rather than "it has no dependents". Charts passes on the
stronger reading — a spreadsheet without charts is still a spreadsheet —
so the original answer was right for a better reason than it was given.
Version history fails it, which is why it is not on the list at all.

Appendix A carries the assembled filing with both consumer paragraphs,
ready to post to github.com/rastrilloorg/rastrillo once Paul says so.
Docs contributed the self-pruning replay store requirement, which its
guest population surfaces and ours does not.

Adds .gitignore for the paths the harness materialises as character
devices, which make `git add -A` fail outright.
Reviewed-By: Fable (dispatched; see spec status line)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01APtNStXDZ5H2fAc1ZuGqtS
Paul Campbell 29de2f8ec129c6ad6573877d9549ec06720853dc parent 282b882
2 files changed, +122 −1
  • .gitignore +12 −0
  • docs/superpowers/specs/2026-08-30-sheets-design.md +110 −1
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..20ced2c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+.claude/
+.bash_profile
+.bashrc
+.gitconfig
+.gitmodules
+.idea
+.vscode
+.profile
+.zprofile
+.zshrc
+.ripgreprc
+.mcp.json
diff --git a/docs/superpowers/specs/2026-08-30-sheets-design.md b/docs/superpowers/specs/2026-08-30-sheets-design.md
index 58c868a..7e1ab5b 100644
--- a/docs/superpowers/specs/2026-08-30-sheets-design.md
+++ b/docs/superpowers/specs/2026-08-30-sheets-design.md
@@ -389,6 +389,27 @@ enroll passkeys and never mount `Gate`.
- **`§9.1` call 1 is therefore unambiguous:** the home mints the session,
the instance trusts a signed assertion. The first draft left both plausible.
+**Assertions carry identity, never authorization** (docs-e6, 2026-08-30 —
+stated positively because a reviewer will otherwise propose the opposite as
+an improvement).
+
+A guest's assertion is the same shape as an owner's. The assertion says *who
+you are*; the instance decides *what you may do* from its own `grants`
+table. Narrowing a comment-only guest's assertion is tempting and belongs on
+the wrong side of the boundary: encoding capability requires the home to
+know capability, which puts an authorization model inside the identity
+origin and cuts against §9's design, where the home knows you have access to
+a team in an app and nothing else.
+
+The security argument does not survive either — a leaked assertion buys
+exactly the grants of the person it names, which is what a stolen session
+buys anyway. Narrowing reduces no blast radius while adding a second
+assertion type, its key handling, and a second audience-binding surface.
+
+**Assertion expiry is seconds, not minutes.** It is exchanged for a session
+immediately; the long-lived thing is the session, which is `sessions`'
+problem and already solved.
+
**Forward, and recorded so this is a staging post rather than a ceiling
(Paul, 2026-08-30):** CARLOS is expected to grow support for per-instance
auth. One RP at the home is the right answer for Sheets now, and it is
@@ -825,7 +846,16 @@ formatting (a second expression evaluator plus a rules UI). Naming charts as
the sole slip candidate understated the risk.
So the slip order is written down rather than discovered: **charts first,
-conditional formatting second.** Version history is not a slip candidate
+conditional formatting second.**
+
+**The criterion is "its absence leaves a coherent product", not "it has no
+dependents"** (docs-e6, 2026-08-30). The two usually agree, and where they
+disagree the first is the one that matters. Charts passes on the stronger
+reading: a spreadsheet without charts is still a spreadsheet. Conditional
+formatting is second because a spreadsheet without it is also still a
+spreadsheet, merely a duller one — whereas a spreadsheet without version
+history is one nobody trusts with real data, which is why history is not on
+this list at all. Version history is not a slip candidate
because §4.3's log design is load-bearing for live sync — but **its minimal
form is named now**: view history and restore a *cell* or a *range*.
Restore-a-whole-sheet is the extension, not the baseline.
@@ -872,3 +902,82 @@ it is a fallback rather than a scramble.
- **Language neutrality upstream.** A discussion proposing CARLOS picks the
language per app rather than assuming Go. Nothing here depends on the
outcome; Go stays right for this app either way.
+
+---
+
+## Appendix A — assertion verification, filing draft (awaiting Paul's nod)
+
+**Destination: `github.com/rastrilloorg/rastrillo`.** Rastrillo has no
+amadan remote, so this is a GitHub issue rather than an `amadan discuss`,
+and it needs Paul's go-ahead. Drafted jointly with docs-e6; both consumer
+paragraphs are below.
+
+**Title: An instance needs to verify a signed identity assertion**
+
+Instance-per-account makes single sign-in a day-one problem — `decisions.md`
+says it directly: *"three instances must not mean three credentials."* Two
+apps have now independently arrived at the same answer, which is why this is
+a framework ask rather than an app one.
+
+The shape both reached: **one identity origin holds the credentials and runs
+the ceremony; instances receive identity as a signed assertion and verify a
+signature.** Instances never run WebAuthn, never hold a passkey RP, never
+see a password.
+
+This is Tito's recorded pattern: *identity lives beside the router, outside
+the per-account blast radius, and reaches instances only as **signed**
+assertions — headers are stripped-and-re-minted, and signing (not stripping
+alone) is what stops a peer socket from forging them.*
+
+**What rastrillo has:** `sessions`, `auth`, `password`, `passkey` — every
+one authenticating *at* the app. **What is missing:** anything letting an
+app say "I trust assertions minted by this identity origin; verify this one
+and mint a session from it."
+
+**Why the framework rather than two apps.** The parenthetical in Tito's rule
+is the point — *signing, not stripping alone* — and the failure modes do not
+announce themselves:
+
+- **Audience binding.** An assertion must name the instance it is for, or
+ one minted for team A replays against team B. Both apps are multi-tenant
+ by construction, so every deployment has thousands of sibling instances to
+ replay against. This is the one most likely to differ subtly between two
+ independent implementations.
+- **Replay.** Single-use identifier plus short expiry — seconds, not
+ minutes; the long-lived thing is the session minted afterwards.
+- **Key distribution and rotation**, without a flag day.
+- **Algorithm confusion.**
+
+That is squarely "supplies what is hard to get right twice", which is how
+rastrillo describes its own scope — and it is literally twice: two apps,
+same week, same pattern, neither having seen the other's code.
+
+**Rough shape**, not a design: verify against pinned or fetched public keys,
+check audience against this instance's own identity, enforce single-use and
+expiry, return a Subject plus claims, hand off to `sessions.SignIn` — which
+is the whole contract the existing identity plugins have with the core. A
+fourth plugin, not a new seam.
+
+**One thing to state positively, because a reviewer will otherwise propose
+it as an improvement: assertions carry identity, never authorization.** A
+guest's assertion is the same shape as an owner's; the instance decides
+capability from its own grants. Encoding capability would require the
+identity origin to hold an authorization model, and a leaked assertion buys
+exactly what a stolen session buys anyway — so narrowing reduces no blast
+radius while adding a second assertion type and a second audience-binding
+surface.
+
+**Named consumers:**
+
+- **Sheets (`oficina/sheets`)** — the home's credential origin is the sole
+ RP, confirmed by Paul 2026-08-30; instances verify assertions and never
+ enroll passkeys.
+- **Docs (`oficina/docs`)** — same shape, same week, independently reached.
+ Docs adds one operational requirement Sheets does not surface: its guest
+ population is dominated by single-purpose visitors who signed in once to
+ comment and never returned, so the single-use replay store churns against
+ a population far larger than the membership list and unbounded in time.
+ **The store must be self-pruning, sized by assertion lifetime rather than
+ by user count** — a naive implementation is correct on day one and a
+ growing table for the life of the deployment. That belongs in the helper,
+ because "single-use" is the helper's promise to keep.