Dario LipicarandClaude Opus 5 048152f2a1 fix(glue): route informModuleToken through the INBOUND door (#26)
* fix(tokens): follow logos-protocol's inbound/outbound store split

TokenManager now keeps INBOUND (caller -> what I issued them) and OUTBOUND
(callee -> what I present) in separate maps, with the trust anchor as a
scalar credential rather than a map entry. Call sites here move to the
accessor that names the direction they meant.

The pre-split single map made a grant one way a grant BOTH ways: a token
minted so M could call B was found by B's client when B called M, so
requestModule was skipped and the access policy never ran. See
logos-protocol's companion change.

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

* fix(glue): route informModuleToken through the INBOUND door

The emitted <Provider>::informModuleToken wrote the same value through both
doors: LogosProviderBase::informModuleToken (inbound, correct) and
logos_module_accept_token (which is the OUTBOUND door). The value is a
CALLER's token — capability_module saying "moduleName may call you" — so the
second write filed a caller's inbound token as an outbound credential inside
the cdylib's own protocol copy. That is the one-way-grant bypass, reproduced
one image deeper.

It now calls logos_module_accept_inbound_token (protocol 0.8). onInit's anchor
seeding keeps logos_module_accept_token, because THAT one is genuinely
outbound: it is the module's own credential for calling core and
capability_module. The comment says so on both sides — the two paths look
interchangeable and are not.

Below 0.8 the old write stays in an #else: dropping it would break the
module's outbound calls to that peer, which is a regression, not a fix. Guards
are expanded MAJOR-aware arithmetic, unifdef-resolvable.

Requires logos-protocol fix/token-direction-key-namespace (59b27ef).

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

* chore(deps): relock logos-protocol to 0.8, and raise the consumer bound with it

WHAT MOVED. logos-protocol b37a2e9f -> 42460e5b (0.7 -> 0.8), the single
node in flake.lock; nothing else in the lock changed.

WHY IT HAD TO. This branch emits the informModuleToken glue through
logos_module_accept_inbound_token, which joins the module-impl C ABI at
protocol 0.8 and does not exist before it (21 -> 23 logos_module_* lines in
logos_module_impl.h). The emission sits behind an expanded MINOR >= 8 guard,
so at the old pin the door was simply compiled away: the qt-host-generator
check's `grep -q logos_module_accept_inbound_token inform-0.8` had nothing to
find. The lock was the whole of the failure -- no code defect underneath it.

Note the pin this moves is b37a2e9f and not the 6c24fcb1 this branch forked
from: master merged #27 in between, and its lock had already moved. That merge
is the commit below this one. Without it, the relocked flake.lock conflicts
with master on the same three lines and CI -- which builds the PR MERGE ref --
cannot check the branch out at all.

THE BOUND. #27 shipped cpp/logos_consumer.h with the fleet's only UPPER bound,
`MINOR > 7` spelled as an #error, precisely so that a protocol bump past the
consumer-admission contract stops the build instead of silently emptying every
isolated identity's token store. Relocking to 0.8 fires it by design. The
review it asks for, carried out rather than assumed:

  * bootstrapKeys(), adoptCredential() and adoptCredentialFor() are signature-
    and semantics-identical across b37a2e9f -> 42460e5b. 0.8 moved direction
    into the KEY NAMESPACE -- inbound a reserved-prefix key, outbound the bare
    peer name -- and deliberately left TokenManager's layout byte-identical.
  * credential() became DERIVED from bootstrapKeys() rather than cached, which
    strengthens this path: a cached field read empty on a store another image
    wrote and then refused every push.
  * 0.8's own adoptCredential() contract documents both halves admitConsumer
    depends on -- outbound, capability_module's proxy resolves the presented
    credential from the caller-keyed INBOUND record rather than an anchor key,
    so the caller is named as the identity and not as the host; inbound,
    capability_module pushes with getToken(moduleName), which IS that
    credential, so informModuleToken's trusted-channel gate still passes.

So the bound is raised 7 -> 8, with that reasoning recorded at the guard. The
oracle is the consumer-admission check and not the argument: it runs a real
ModuleProxy in Local mode, and its "NO LOCKOUT: the consumer's own credential
authorizes at capability_module" / "and it is NAMED as itself, not as the host"
assertions are exactly the failure the #error exists to prevent. Both pass at
0.8. It is negative-validated upstream (removing the adopt step fails 5 checks,
swapping the order fails 2), so its green is worth something.

ALL 12 CHECKS BUILT INDIVIDUALLY, x86_64-linux, from source against
cache.nixos.org only (cache.nix.logos.co is returning 502):

  PASS  vanilla-plugin           /nix/store/ab6l4a0czh4nd4h153i8hz82qayb4ah4-logos-plugin-qt-vanilla-test-0.0.1
  PASS  header-generator-guard   /nix/store/1dxrvzk4d1r3babyfx6hfnv9va1im5v6-logos-plugin-qt-header-generator-guard-test
  PASS  headers-emitter-routing  /nix/store/yrjzx24bq010m3z0xm0djvmr3n1wn1fx-logos-plugin-qt-headers-emitter-routing-test
  PASS  consumer-api-style-gate  /nix/store/r5h073ygr1zfy763dmhxssgw9mcl96y8-logos-plugin-qt-consumer-api-style-gate-test
  PASS  qt-host                  /nix/store/mndrdrxcad6kq056jcrxf946iycp5yqg-logos-qt-host-0.1.0
  PASS  shared-runtime-layering  /nix/store/npzah7n5zj7gdlbs1d59ry0idd9vp6si-logos-qt-host-shared-runtime-layering
  PASS  qt-host-generator        /nix/store/xqlhpbz7bnfvz7c17x1aanfassglmrq6-logos-qt-host-generator-test
  PASS  unload-contract          /nix/store/2y64h1im2biyqpbmg0bi591rznl860yx-logos-qt-host-unload-contract-test
  PASS  caller-contract          /nix/store/w1lz78kfy91xcyfd35i277f030jjf0ag-logos-qt-host-caller-contract-test
  PASS  caller-invokable         /nix/store/68b83pvv90xsqszihgshpb5g3fikfmj4-logos-qt-host-caller-invokable-test-0.1.0
  PASS  consumer-admission       /nix/store/hiwnlafxhh5gz0f1pkdi53glw66qm3rq-logos-qt-host-consumer-admission-test-0.1.0
  PASS  glue-compiles            /nix/store/bx9qfp520yazhmmc1in37frsscs6iii8-logos-qt-host-glue-compiles-test-0.1.0

The qt-host closure references logos-protocol-lib-0.8.0, so the relock is in
the artefact and not merely in the lock file. CI itself runs only 4 of these 12.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 14:24:03 -03:00

logos-plugin-qt

Everything specific to running a Logos module as a Qt 6 plugin, in one repo: the build logic logos-module-builder delegates to, and the runtime that build produces plugins against. Keeping both here is what lets the plugin technology be swapped without touching the module builder or individual modules.

Outputs

Output What it is
lib / rawLib The Nix build functions (buildPlugin, generate, buildHeaders, devShellInputs, plus common). rawLib takes its Logos deps as arguments; lib pre-fills them from this flake.
packages.<sys>.logos-qt-host The Qt host runtime a plugin links: LogosAPI, LogosAPIProvider, LogosProviderBase, the legacy QtProviderObject adapter, and core/interface.h. Static library, headers, and a find_package(logos-qt-host) config.
packages.<sys>.logos-qt-host-generator Emits the Qt plugin glue around a cdylib module's C ABI (<name>_cdylib_glue.{h,cpp}) from its LIDL contract.

logos-qt-host is also the default package.

LogosProviderBase's two pure slots — callMethod() and getMethods() — are filled by logos-qt-host-generator --backend cdylib, in the emitted <name>_cdylib_glue.cpp. logos_provider_object.h also still defines the LOGOS_PROVIDER / LOGOS_METHOD macros, but they are vestigial: they were scanned by logos-cpp-generator --provider-header to emit a logos_provider_dispatch.cpp for interface: "provider" modules, and that flag, that interface value and that file are all gone (the flag is now refused with a message naming interface: "universal"). Under universal a module's plain public methods are its API — the contract is derived from the impl header named by codegen.impl_class / codegen.impl_header, and there is no marker to write. Nothing this repo builds expands either macro any more; they are kept only so an older translation unit still compiles.

There is no cmake-module output and no LogosModule.cmake here. LogosModule.cmake lives in logos-module-builder, and only there. This repo shipped a second copy until the builder was made to point LOGOS_MODULE_BUILDER_ROOT at its own copy for every module type: the builder only overrode that variable when a MODULE carried the file (none does), so ui_qml plugins configured with this repo's copy while core modules configured with the builder's, and the two drifted apart in silence. The CMake module reads the builder's own variables (LOGOS_API_STYLE, LOGOS_MODULE_GO_STATIC_LIBS, generated_code/), so the builder is where it belongs.

cmake/ went away with that copy and is gone for good. It briefly came back to hold the four LogosView*.in templates logos_module(REP_FILE ...) instantiates, which had the mirror-image problem — a byte-identical second copy of them lived here with nothing comparing the two. Both copies are now one copy, in logos-view-module, which owns the ui_qml authoring flavour end to end: the templates, LogosViewModule.cmake, the view glue generator, and the .rep-file replica-factory fixture that proves the plugin an authoring build produces still loads and casts. logos-module-builder inputs that repo and hands the directory to every plugin build as LOGOS_VIEW_TEMPLATE_DIR; this backend never names it.

Which is the line this repo now holds to: it handles exclusively what makes a cdylib module loadable by logos-module-loader-qt — the Qt host runtime a plugin links, the generator that wraps a cdylib's C ABI in that plugin, and the Nix functions that compile and package the result. View-plugin authoring is somebody else's repo. (buildPlugin still packages a <name>_replica_factory library when a module's build emits one — that is plugin packaging of a build artifact, the same as the _plugin library beside it, and carries no knowledge of where the templates live.)

lib / rawLib are pure Nix and stay that way: nothing reachable from them mentions the two C++ derivations, so a consumer that only wants the build functions never realises a Qt or protocol build to get them.

By-name contracts

Two things cross the host/plugin boundary as a string rather than a symbol, because a plugin and the host that loads it are separate images: each links its own copy of LogosAPI, ModuleProxy and TokenManager, at distinct addresses and with no undefined reference to the other's (Mach-O is TWOLEVEL, PE has no interposition; only ELF's flat namespace collapses them). A direct C++ call binds to the calling image's copy. QMetaObject::invokeMethod does not — it resolves through metaObject() / qt_metacall, which are virtual, and the vptr was written by the host's constructor.

Contract Emitted by Reached by
aboutToUnload() / unloadFinished() qt-host-generator, on the plugin class cpp/logos_plugin_unload.cpp
currentCallerJson() — who is calling this dispatch cpp/logos_api.h, on LogosAPI cpp/logos_provider_object.cpp, then pushed across the module-impl C ABI by the generated glue

Nothing in either build ties the two ends together: rename one side and every module still compiles, links and loads, and the feature is simply never found — a silent, permanent no-op that looks exactly like a module which legitimately declined. Both halves of both contracts live in this repo, which is what makes tests/test-unload-contract.nix and tests/test-caller-contract.nix possible: they scrape the name out of the emitter and require the consumer to reach for that exact string, in both directions.

Layout

lib/                  the Nix build functions (buildPlugin, generate, buildHeaders)
cpp/                  the Qt host runtime library (logos-qt-host)
core/interface.h      the legacy Qt plugin interface (PluginInterface)
qt-host-generator/    the cdylib -> Qt-plugin glue emitter
nix/                  derivations for the two C++ outputs
tests/                flake checks
S
Description
Qt-specific plugin build logic
Readme
594 KiB
Languages
Nix 49.6%
C++ 45%
CMake 3.3%
Shell 1.4%
C 0.7%