Summary
ledger has no per-book ownership or authorization of any kind. Any principal
holding a valid token may read, write and post into any book in the
database, including one another principal created. There is no role table, no
owner column, and no action that consults either.
This is not new — it has been true since the rung shipped — but morph#361's
CreateLedger makes it reachable in a new way: a client can now create a book
over the wire, and the book it creates is immediately writable by every other
authenticated client.
What is enforced, and what is not
Enforced (examples/ledger/include/ledger/auth/ledger_authorizer.hpp's own
file comment is accurate about this):
SigningAuthorizer::authorize() verifies a real signed token on every
execute other than AuthModel/Login.
- Each mutating model action refuses an empty principal
(EmptyPrincipalError), and RunReportJob refuses any principal but
kReportRunnerPrincipal.
Not enforced: which principal a book belongs to. LedgerModel::execute(const OpenAccount&) checks only that the ledgers row exists;
execute(const GetLedger&) is a pure read with no principal check at all.
authorizeInstance is left at its permissive default, and the authorizer's
file comment gives the reason — LedgerModel's instances are keyed by
ledgerId and shared across every client that opens the same book, so there
is "no single owning caller for authorizeInstance to compare against".
Verification status
Reproduced over the wire on this branch (scenario-corpus-both-axes, with
morph#361's CreateLedger applied), against ladder_ledger_server on a fresh,
unseeded LEDGER_DB. Real output:
ok step 5 (line 11) [abooks] do CreateLedger name="Alice's private book"
capture book = 1
ok step 6 (line 14) [abooks] do OpenAccount ledgerId=$book name="Alice Cash" kind=0 currency=1
capture acash = 1
ok step 8 (line 19) [auth] do Login username=bob
capture bob = bob
ok step 11 (line 27) [bbooks] do GetLedger ledgerId=$book
ok step 12 (line 31) [bbooks] do OpenAccount ledgerId=$book name="Bob's account in Alice's book" kind=1 currency=1
capture bacct = 2
ok step 14 (line 36) [anon] do CreateLedger name="No token"
Bob reads Alice's book (GetLedger asserts accounts ~ "Alice Cash") and
writes into it (OpenAccount returns id 2 in Alice's book). The last step
confirms the boundary that does hold: a tokenless client is refused with
unauthorized.
Not verified: whether this was a deliberate scope cut. The authorizer's
file comment argues for leaving authorizeInstance permissive, but it argues
from "the model's instances are shared" — which is about instance ownership,
not about book ownership, and says nothing about GetLedger having no
principal check. I could not find a spec statement either way;
docs/superpowers/specs/2026-08-19-ledger-rung5-design.md §11 covers the
empty-principal gate and no more.
Not verified: whether a fix belongs at the authorizer or in each model
action. kanban's answer (a project_roles table plus a requireRole() helper
consulted by every action) is the closest precedent in the ladder, but it is a
schema change plus a gate on every ledger action, not a one-line fix.
Why record it rather than shrug
The rung's whole subject is money and an audit trail. "Any signed-in user may
post entries into any book" is the kind of gap that is invisible from inside
the test suite, because every test uses one principal and one book, and it is
now the first thing a second client of a freshly created book can do.
What would close this
Either a per-book authorization gate (an owner or a role table consulted by
LedgerModel/BudgetModel/RuleModel, kanban-style), or a sentence in
examples/ledger/README.md and the design spec stating plainly that this rung
models one trusted user and shares every book between all of them, so nobody
reads the empty-principal check as an ownership check.
Summary
ledgerhas no per-book ownership or authorization of any kind. Any principalholding a valid token may read, write and post into any book in the
database, including one another principal created. There is no role table, no
owner column, and no action that consults either.
This is not new — it has been true since the rung shipped — but morph#361's
CreateLedgermakes it reachable in a new way: a client can now create a bookover the wire, and the book it creates is immediately writable by every other
authenticated client.
What is enforced, and what is not
Enforced (
examples/ledger/include/ledger/auth/ledger_authorizer.hpp's ownfile comment is accurate about this):
SigningAuthorizer::authorize()verifies a real signed token on everyexecuteother thanAuthModel/Login.(
EmptyPrincipalError), andRunReportJobrefuses any principal butkReportRunnerPrincipal.Not enforced: which principal a book belongs to.
LedgerModel::execute(const OpenAccount&)checks only that theledgersrow exists;execute(const GetLedger&)is a pure read with no principal check at all.authorizeInstanceis left at its permissive default, and the authorizer'sfile comment gives the reason —
LedgerModel's instances are keyed byledgerIdand shared across every client that opens the same book, so thereis "no single owning caller for
authorizeInstanceto compare against".Verification status
Reproduced over the wire on this branch (
scenario-corpus-both-axes, withmorph#361's
CreateLedgerapplied), againstladder_ledger_serveron a fresh,unseeded
LEDGER_DB. Real output:Bob reads Alice's book (
GetLedgerassertsaccounts ~ "Alice Cash") andwrites into it (
OpenAccountreturns id 2 in Alice's book). The last stepconfirms the boundary that does hold: a tokenless client is refused with
unauthorized.Not verified: whether this was a deliberate scope cut. The authorizer's
file comment argues for leaving
authorizeInstancepermissive, but it arguesfrom "the model's instances are shared" — which is about instance ownership,
not about book ownership, and says nothing about
GetLedgerhaving noprincipal check. I could not find a spec statement either way;
docs/superpowers/specs/2026-08-19-ledger-rung5-design.md§11 covers theempty-principal gate and no more.
Not verified: whether a fix belongs at the authorizer or in each model
action. kanban's answer (a
project_rolestable plus arequireRole()helperconsulted by every action) is the closest precedent in the ladder, but it is a
schema change plus a gate on every ledger action, not a one-line fix.
Why record it rather than shrug
The rung's whole subject is money and an audit trail. "Any signed-in user may
post entries into any book" is the kind of gap that is invisible from inside
the test suite, because every test uses one principal and one book, and it is
now the first thing a second client of a freshly created book can do.
What would close this
Either a per-book authorization gate (an owner or a role table consulted by
LedgerModel/BudgetModel/RuleModel, kanban-style), or a sentence inexamples/ledger/README.mdand the design spec stating plainly that this rungmodels one trusted user and shares every book between all of them, so nobody
reads the empty-principal check as an ownership check.