Skip to content

core+qt+net: fix listInstances session-stamping and model-level attachActionLog forwarding - #122

Merged
Yaraslaut merged 3 commits into
masterfrom
framework/listinstances-session-and-actionlog-forwarding
Aug 17, 2026
Merged

core+qt+net: fix listInstances session-stamping and model-level attachActionLog forwarding#122
Yaraslaut merged 3 commits into
masterfrom
framework/listinstances-session-and-actionlog-forwarding

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Summary

Two small, independently-scoped framework bugs, discovered and fixed while implementing the kanban rung (rung 4 of the application ladder — see the companion PR), pulled out here as their own PR since they're morph library fixes, not rung-specific work.

Both are minimal, targeted diffs against master — no ladder/example code touched.

Fix 1 — QtWebSocketBackend/SocketBackend::listInstances never stamped the session

listInstances() in both classes built the wire envelope via morph::wire::makeInstances(typeId) and sent it directly, unlike every other envelope-building call site in the same classes — register, attach, assign, execute, and deregister all set env.session before sending; listInstances was the one exception.

RemoteServer's instances handler authorizes with IAuthorizer::authorize(env.session, typeId, {}). With no session on the envelope, a SigningAuthorizer-derived authorizer (or any authorizer that actually inspects the session) rejects the call as unauthorized — even on a connection that already completed a correctly-authenticated execute().

Every existing rung's instances() coverage used an AllowAllAuthorizer-derived authorizer (or ran Local/in-process, where authorizeInstance never runs at all per docs/spec/security.md), so authorize() was always permissive regardless of session, and this path went unexercised until kanban became the first rung to combine a SigningAuthorizer with a Socket-mode instances() call.

Fix: stamp env.session from currentSession() (SocketBackend) / _session (QtWebSocketBackend) before sending, matching every sibling call site. One line added per file.

Filed as morph#113.

Fix 2 — IModelHolder::attachActionLog never forwarded to a model-level attachActionLog

RemoteServer::LogProvider's attach path (attachLogIfConfigured) only ever populated the type-erased holder's own _actionLog/_contextKey, used by recordIfAttached's auto-append — never a model instance's own state. A model that keeps its own model-level IActionLog reference to read its history back later (e.g. an activity-stream view over entries(entityKey)) had no way to receive the same log a registry-constructed, remote/keyed attach populates the holder with, since such an instance is always default-constructed and never otherwise touched.

Fix: adds IModelHolder::onActionLogAttached, a protected virtual hook (no-op default) that attachActionLog now calls before storing its own state. ModelHolder<Model> overrides it to forward to Model::attachActionLog(log, contextKey) when Model structurally satisfies a new ModelLevelActionLogAttachable concept — the same "detect the hook structurally, forward only if present" shape onBackendChanged()/BackendChangedMixin already use, so a model with no attachActionLog of its own is entirely unaffected.

Updates docs/spec/core/registry.md and docs/spec/journal/journal.md to document the new hook.

Filed as morph#114.

Verification

Both bugs were caught and fixed while building kanban's BoardModel/KanbanAuthorizer (kanban is the first rung to combine a SigningAuthorizer with a Socket-mode instances() call, and the first rung to need a model-level activity-stream view over its own journal). See the companion kanban PR for the integration tests that surfaced each bug.

For this standalone PR: verified the ModelLevelActionLogAttachable concept is structurally satisfied by exactly one type in the current tree (kanban's BoardModel, added in the companion PR — not yet present on master), so on master alone the new if constexpr branch is dead code and the onActionLogAttached hook's default no-op body runs unconditionally for every existing model — a pure no-op addition until a model opts in.

Built and ran the full morph_tests suite against this branch (based on master, no other changes): 10,062 assertions in 1,054 test cases, all passing.

cmake -S . -B build/cl-debug -G Ninja -DCMAKE_BUILD_TYPE=Debug -DMORPH_BUILD_QT=ON -DMORPH_BUILD_TESTS=ON
cmake --build build/cl-debug --target morph_tests
./build/cl-debug/tests/morph_tests.exe
# All tests passed (10062 assertions in 1054 test cases)

Scope note

MORPH_BUILD_NET (which builds morph::net::SocketBackend) is a no-op on Windows per the project's own CMake warning — morph::net is POSIX-only today (Winsock2 support is documented future work) — so socket_backend.hpp's half of Fix 1 wasn't exercised by this build. It's a one-line, source-identical mirror of the QtWebSocketBackend fix in the same file's sibling listInstances implementation, reviewed by inspection.

Yaraslau Tamashevich and others added 2 commits August 17, 2026 12:26
…ctionLog

RemoteServer::LogProvider's attach path (attachLogIfConfigured) only ever
populated the type-erased holder's own _actionLog/_contextKey, used by
recordIfAttached's auto-append -- never a model instance's own state. A
model that keeps its own model-level IActionLog reference to read its
history back later (e.g. an activity-stream view over entries(entityKey))
had no way to receive the same log a registry-constructed, remote/keyed
attach populates the holder with, since such an instance is always
default-constructed and never otherwise touched.

Adds IModelHolder::onActionLogAttached, a protected virtual hook (no-op
default) that attachActionLog now calls before storing its own state.
ModelHolder<Model> overrides it to forward to Model::attachActionLog(log,
contextKey) when Model structurally satisfies the new
ModelLevelActionLogAttachable concept -- the same "detect the hook
structurally, forward only if present" shape onBackendChanged()/
BackendChangedMixin already use, so a model with no attachActionLog of its
own is entirely unaffected.

Updates docs/spec/core/registry.md and docs/spec/journal/journal.md to
document the new hook.

Verified with a standalone probe program exercising ModelHolder<Model> in
isolation, and end-to-end in the kanban App bootstrap (next commit) where a
registry-constructed BoardModel's GetActivity now sees entries the same
dispatch produced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
… the session

Both classes' listInstances() built the wire envelope via
morph::wire::makeInstances(typeId) and sent it directly, unlike every
other envelope-building call site in the same classes (register, attach,
assign, execute, and deregister all set env.session before sending).

RemoteServer's instances handler authorizes with
IAuthorizer::authorize(env.session, typeId, {}) -- with no session on the
envelope, a SigningAuthorizer-derived authorizer (or any authorizer that
actually inspects the session) rejects the call as unauthorized, even on
a connection that already completed a correctly-authenticated execute().

Every existing rung's instances() coverage used an AllowAllAuthorizer-
derived authorizer (or ran Local/in-process, where authorizeInstance
never runs at all per docs/spec/security.md), so authorize() was always
permissive regardless of session and this path went unexercised. Fixed
by stamping env.session from currentSession() (SocketBackend) / _session
(QtWebSocketBackend) before sending, matching every sibling call site.

Found while adding a rung's first Socket-mode instances() test against a
SigningAuthorizer-derived authorizer.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 57.14286% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
include/morph/core/model.hpp 57.14% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

Every existing attachActionLog test used ALModel, which has no model-level
attachActionLog of its own -- so all of them exercised only the
ModelLevelActionLogAttachable<Model> == false arm of ModelHolder<Model>::
onActionLogAttached's `if constexpr`. The == true arm (the actual
forwarding mechanism this hook exists for) had no coverage at all on this
branch, since the only model in the tree that declares a matching
attachActionLog is kanban::BoardModel, which lives on a separate branch.

Adds ALLoggingModel, a minimal model declaring attachActionLog matching
ModelLevelActionLogAttachable's exact shape, plus two tests:

- The forwarding case: attachActionLog on the holder reaches the wrapped
  model's own attachActionLog with the same log/contextKey, exactly once,
  without disturbing the holder's own hasActionLog/recordIfAttached state.
- The no-op case (ALModel): attachActionLog still succeeds and populates
  the holder's own state when the wrapped model has no attachActionLog of
  its own -- confirming the hook's default body runs harmlessly for the
  overwhelming majority of models that don't opt in.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant