What I found
Rung 5 (examples/ledger) has no authentication or authorization story at all, and the consequence is that every mutating LedgerModel/BudgetModel/RuleModel action fails in the shipped desktop client, in both deployment modes.
Every mutating action in the rung opens with the same gate:
const auto* ctx = morph::session::current();
if (ctx == nullptr || ctx->principal.empty()) {
throw EmptyPrincipalError{};
}
There are two independent ways rung 5 fails to supply that principal.
1. Local mode: nobody ever installs one
examples/ledger/gui/main.cpp builds an AppContext and four QML bridges, and never calls ctx.login(...). Nothing in examples/ledger/gui_lib/ calls Bridge::setDefaultSession either:
$ grep -rn "login\|setDefaultSession\|principal" examples/ledger/gui_lib/ examples/ledger/gui/main.cpp
examples/ledger/gui/main.cpp:65: // Nothing is torn down and rebuilt around login: a login only installs
AppContext's default session is empty, so LocalBackend installs an empty session::Context and OpenAccount — the first thing a user does — throws EmptyPrincipalError.
Contrast bookmarks/kanban, whose gui/main.cpp files each mint a Local-mode token and log in explicitly.
2. Remote mode: RemoteServer clears the claim, and there is no server anyway
RemoteServer::dispatchExecute (include/morph/core/remote.hpp) does:
if (auto verified = _authorizer->authenticate(env.session)) {
env.session.principal = std::move(*verified);
} else {
env.session.principal.clear();
}
which is correct and deliberate (docs/spec/security.md: never pass an unverified claim through). But IAuthorizer::authenticate's default — and therefore AllowAllAuthorizer's — returns nullopt, so with the allow-all default any principal is cleared. Rung 5 ships no src/auth/, no AuthModel, no Login action and no token secret, so allow-all is the only authorizer it can install.
It also ships no src/server/, so there is no ladder_ledger_server for --server <url> to connect to.
Verification status
Inferred from reading the code, not reproduced by running the GUI. What I did measure, on fix/160-ledger-app-layer (branched from d6690c5): an App dispatching RunReportJob over a RemoteServer with the allow-all default failed with the model's own principal check, which is the same mechanism:
[ERROR] [ledger::App] RunReportJob dispatch failed for job 1: RunReportJob: only the report runner may run a report job
Switching that dispatch to a LocalBackend (which installs the session directly, with no authenticate step) made it pass. That is direct evidence for (2). For (1) I have only the greps above and the gate's source; I did not launch ladder_ledger_gui.
Note that examples/ledger/tests/test_multiclient.cpp uses BackendRig in Mode::Local, so rung 5's own suite has never exercised a RemoteServer carrying a principal — which is why this has stayed invisible.
What would change the verdict
Close this if either turns out to be wrong:
- launching
ladder_ledger_gui and successfully creating an account (would falsify (1));
- a session path I missed that installs a principal for the rung's bridges.
Otherwise this closes when rung 5 gains a login + authorizer (and, if it is to have a Remote mode at all, a src/server/main.cpp), the way rungs 1-4 each did.
Not folded into #160
Found while implementing #160 (moving the report job out of LedgerModel into an App layer). Per AGENTS.md it is filed rather than fixed there: that PR works around (2) by dispatching over a LocalBackend and says so in examples/ledger/include/ledger/app/app.hpp, and does not touch (1) at all.
What I found
Rung 5 (
examples/ledger) has no authentication or authorization story at all, and the consequence is that every mutatingLedgerModel/BudgetModel/RuleModelaction fails in the shipped desktop client, in both deployment modes.Every mutating action in the rung opens with the same gate:
There are two independent ways rung 5 fails to supply that principal.
1. Local mode: nobody ever installs one
examples/ledger/gui/main.cppbuilds anAppContextand four QML bridges, and never callsctx.login(...). Nothing inexamples/ledger/gui_lib/callsBridge::setDefaultSessioneither:AppContext's default session is empty, soLocalBackendinstalls an emptysession::ContextandOpenAccount— the first thing a user does — throwsEmptyPrincipalError.Contrast
bookmarks/kanban, whosegui/main.cppfiles each mint a Local-mode token and log in explicitly.2. Remote mode:
RemoteServerclears the claim, and there is no server anywayRemoteServer::dispatchExecute(include/morph/core/remote.hpp) does:which is correct and deliberate (
docs/spec/security.md: never pass an unverified claim through). ButIAuthorizer::authenticate's default — and thereforeAllowAllAuthorizer's — returnsnullopt, so with the allow-all default any principal is cleared. Rung 5 ships nosrc/auth/, noAuthModel, noLoginaction and no token secret, so allow-all is the only authorizer it can install.It also ships no
src/server/, so there is noladder_ledger_serverfor--server <url>to connect to.Verification status
Inferred from reading the code, not reproduced by running the GUI. What I did measure, on
fix/160-ledger-app-layer(branched fromd6690c5): anAppdispatchingRunReportJobover aRemoteServerwith the allow-all default failed with the model's own principal check, which is the same mechanism:Switching that dispatch to a
LocalBackend(which installs the session directly, with no authenticate step) made it pass. That is direct evidence for (2). For (1) I have only the greps above and the gate's source; I did not launchladder_ledger_gui.Note that
examples/ledger/tests/test_multiclient.cppusesBackendRiginMode::Local, so rung 5's own suite has never exercised aRemoteServercarrying a principal — which is why this has stayed invisible.What would change the verdict
Close this if either turns out to be wrong:
ladder_ledger_guiand successfully creating an account (would falsify (1));Otherwise this closes when rung 5 gains a login + authorizer (and, if it is to have a Remote mode at all, a
src/server/main.cpp), the way rungs 1-4 each did.Not folded into #160
Found while implementing #160 (moving the report job out of
LedgerModelinto an App layer). PerAGENTS.mdit is filed rather than fixed there: that PR works around (2) by dispatching over aLocalBackendand says so inexamples/ledger/include/ledger/app/app.hpp, and does not touch (1) at all.