6 Commits
Author SHA1 Message Date
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
Dario LipicarandClaude Opus 5 7cad5ec381 feat(consumer): logos::admitConsumer — one home for admitting a non-module (#27)
* feat(consumer): logos::admitConsumer — one home for admitting a non-module

Two hosts hand-rolled the same three steps independently — isolate an
identity, mint a credential, register it with capability_module — and the
duplication had already produced a bug: basecamp did the registration
inside the has-a-backend branch, below an early return, so pure-QML plugins
registered nothing. It survived only because those calls went out on the
host's ambient ring, where every token already existed and the handshake
was never reached. Remove the ambient ring (logos-protocol #71) and that
becomes a hard failure.

So the operation gets one home. admitConsumer isolates, mints, registers
and installs the credential as a single step, and hands back a
ConsumerIdentity. Both hosts lose their private copies: basecamp -16 net
lines, standalone-app -14.

It lives here rather than in logos-liblogos, which is where it was first
proposed. Both hand-rolled sites call LogosAPI, which is this repo's, and
the operation needs a capability_module client to register through — going
via liblogos's C API would add a hop for hosts already holding the Qt
object. logos-liblogos needs no change at all.

Named for what it does: a CONSUMER is admitted, not registered as a module.
It is never published to the registry, never callable, never in
--modules-dir.

Two guards, because the sharp edges here are silent ones:

  * adoptConsumerCredential REFUSES an isolated store. It exists for a
    co-process adopting its parent's credential into its own process ring,
    where the store IS that ring. Pointed at an in-process private store it
    would install whatever it was handed — including the host anchor —
    straight past adoptCredentialFor's refusal, putting the elevation #71
    removes one call away again.

  * A #error when logos-protocol is newer than the contract this file
    implements. The wave order is mandatory and was unenforced: bump the
    protocol alone and every isolated identity gets an empty store, while
    nothing fails to build (every MINOR guard in the fleet is >=) and the
    integration tests stay green (the ui-host half keeps working off its
    stdin credential). The bound fired on its first build — this change
    ships protocol 0.7 and it was set to 6 — and was proven non-inert by
    going red at 0.8.

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

* chore(deps): relock logos-protocol onto master, which now carries #71

admitConsumer calls TokenManager::adoptCredentialFor and adoptCredential.
Both arrived with logos-protocol#71 ("a private store is created EMPTY"), so
the lock had to move past it — this branch still pinned 6c24fcb1, which is
0.6.0 and has neither:

  logos_consumer.cpp:89:  error: 'adoptCredentialFor' is not a member of 'TokenManager'
  logos_consumer.cpp:155: error: 'class TokenManager' has no member named 'adoptCredential'

Now b37a2e9f, protocol master with #71 merged.

This is the wave order the header documents, seen from the other side: the
empty-store change lands in protocol, and everything that makes it survivable
lands here. The guard in logos_consumer.h fires when this repo is BEHIND the
protocol; this commit is the ordinary case of catching up to it.

Verified on x86_64-linux with the relocked input:
  consumer-admission  PASS
  qt-host             PASS
  caller-contract     PASS
  glue-compiles       PASS

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 11:16:41 -03:00
Dario Gabriel LipicarandClaude Opus 5 c459e47787 feat(glue): pull the caller from the host image and push it across the C ABI
Everything under this has merged — the protocol resolves the caller and
opens a scope (#70), both backends define logos_module_set_call_caller
(cpp-sdk #147, rust-sdk #47) — and nothing connected the two, so
currentCaller() returned Unknown everywhere. This is the wire.

Per dispatch, all on the dispatch thread: pull the caller document back out
of the HOST image, push it into the module image, dispatch, pop.

WHY THE PULL IS AN invokeMethod AND NOT A CALL
The host binary and the module plugin EACH define LogosAPI — meta-object
included — with their own statics at distinct addresses and no undefined
reference to the other's. Mach-O is TWOLEVEL, PE has no interposition. A
direct logosAPI()->currentCallerJson() binds to the PLUGIN copy and reads
the PLUGIN thread-local: empty, forever, silently, on macOS and Windows.
invokeMethod resolves through metaObject()/qt_metacall, which are virtual
and whose vptr the HOST constructor wrote, so it lands in host code on the
calling thread. Same channel initLogos and aboutToUnload already use.

IN MULTI THE PULL IS BEFORE THE CAPTURE
callMethod is entered on the dispatch thread and captures by value into
QThread::create, so the pull happens there and the JSON rides along; the
push/pop triple moves verbatim into the worker. Pulling inside the worker
also compiles, and every multi call would read Unknown because that thread
never had a scope. A test asserts the source ORDER, and it was driven red
by making exactly that mistake.

The invokable's name was not chosen here: logos-protocol master already
names currentCallerJson in three places, including a CMakeLists comment
saying the header ships so this file can answer it.

Two checks beyond the wire, because the by-name test alone left gaps:

  * test-glue-compiles — NOTHING in this repo compiled the emitted glue.
    test-qt-host-generator.cpp documents a bug that escaped through exactly
    that hole. Both branches now build as a real Qt plugin with the C ABI
    stubbed and -Wl,--no-undefined, so a missing symbol fails at link
    rather than at a user's dlopen.
  * test-caller-invokable — six runtime assertions, including that a scope
    open on one thread is INVISIBLE on another, which is the actual
    justification for the multi placement.

The contract test strips comments before its negative assertions (both
files legitimately NAME the forbidden calls in prose) and guards that with
a positive control, so the negatives cannot pass vacuously.

Guard is MAJOR-aware expanded arithmetic. Dropping only the MAJOR > 0 arm
still passes a grep, so the test resolves at 1.0 and asserts the call
survives; that mutation was driven red too.

11 checks green on x86_64-linux, the three new ones green on
aarch64-darwin, and the Windows mingw cross builds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 18:46:35 -03:00
Dario Gabriel LipicarandClaude Opus 5 ef11c2108a feat(qt-host): share the teardown helper, and guard the glue on MAJOR too
Two changes to the same surface, both about the by-name teardown hook this
repo's qt-host-generator emits.

1. logos::runPluginAboutToUnload(), ported verbatim from
   logos-module-loader-qt's logos_host.cpp, where it was a file-local
   helper. ui-host (logos-view-module-runtime) needs the identical algorithm
   before `delete pluginObject`, and a second hand-written copy of a nested
   event loop plus deadline is not a thing to have twice.

   Both load-bearing properties are intact: the hook is invoked BY NAME
   rather than through the vtable (PluginInterface is compiled separately
   into every plugin, so a new virtual would shift the vtable under every
   plugin already built), and `deadline.isActive()` remains the only bit
   distinguishing "finished" from "gave up".

   The grace CONSTANT deliberately does not move with it. It stays a
   parameter because the two hosts have different budgets: the container
   gives logos_host 5000ms and it carves out 3000, while ViewModuleHost::stop()
   gives ui-host 3000ms total. Porting the constant would have consumed
   ui-host's entire budget and hard-killed every async view mid-teardown —
   exactly what the hook exists to prevent.

   It lives HERE because this repo emits the surface it reaches. Both halves
   under one roof is what makes checks.unload-contract possible: it generates
   real glue, scrapes the symbol names out of it, and greps the consumer for
   those same names. Nothing in either build tied them together before.

2. The two version guards this generator emits tested
   LOGOS_PROTOCOL_VERSION_MINOR and ignored LOGOS_PROTOCOL_VERSION_MAJOR. At
   protocol 1.0.0 the MINOR resets to 0, both go false, and the glue stops
   emitting the grant_host_services call (0.3) and the teardown pair (0.5).

   Not a link error and not a dlopen failure: logos-cpp-sdk guards the
   matching DEFINITIONS the same way, so the calls and the definitions
   disappear together and everything still builds and loads. Modules just
   quietly stop having teardown. logos-rust-sdk would keep emitting all ten
   exports, because it compares the (major, minor) tuple — so the two sides
   would silently disagree.

   The arithmetic is emitted expanded rather than behind a function-like
   macro, because the generated sources are resolved by unifdef in
   logos-cpp-sdk's ABI check and unifdef silently no-ops on what it cannot
   parse. Matching logos-cpp-sdk#145 character for character.

unload-contract, qt-host-generator, qt-host and vanilla-plugin all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 18:08:50 -03:00
Dario LipicarandClaude Opus 5 1aa3e31c02 feat(qt-host): add logos_qt_host_shared, and assert its layering (#22)
* feat(qt-host): add logos_qt_host_shared, and assert its layering

PR-3 of the shared-runtime migration. Requires logos-protocol#65, which exports
logos_protocol_shared from the CMake package; without it this build hard-fails
by design rather than falling back.

WHY. LogosAPI (here) and TokenManager / LogosAPIClient / StoreRegistry (in
logos-protocol) must exist EXACTLY ONCE per process. Every image linking a
static archive gets its own copy of every function-local static inside it, so
the host writes a capability token into one store and another in-process image
reads an empty one -- with no build diagnostic. Linking ONE shared library is
what replaces the whole-archive + generated-.def scheme in which liblogos_core
absorbed both archives and re-exported them.

THE STATIC ARCHIVE STAYS, and not transitionally. Module plugins and ui_qml
backends run in their OWN processes, so their own copy is the CORRECT
per-process singleton; staying static also keeps a .lgx self-contained, since a
.lgx records an empty nix closure and a shared library would not travel with it.
In-process images link the shared target; out-of-process images link the archive.

THE ONE LINE THAT MATTERS is that logos_qt_host_shared PUBLIC-links
${LP_SHARED_TARGET}, not ${LP_TARGET}. Linking the static archive there would
embed a second TokenManager INSIDE liblogos_qt_host itself -- the same bug one
layer down, and invisible to the consumer-side symbol gate, which would treat
this library as a provider and pass. It is a one-word mistake that builds,
links, installs and loads, and surfaces only as refused calls at runtime in a
different repo. So this PR carries its own check:

  checks.<system>.shared-runtime-layering

asserting liblogos_qt_host.{dylib,so} DEFINES LogosAPI, DEFINES NEITHER
TokenManager NOR LogosAPIClient, IMPORTS TokenManager, and has
liblogos_protocol on its link line. The positive assertion doubles as the
validity control: if nm or c++filt were broken it reports 0 for LogosAPI and
fails, rather than reporting a reassuring zero for the other two.

LogosAPI switches from LOGOS_SHARED_API to LOGOS_QT_HOST_API (logos-protocol#64)
because the two are not the same choice in one translation unit: building this
library, LogosAPI must be EXPORTED while TokenManager must be IMPORTED. On
Windows the shared target also compiles with LOGOS_SHARED_USE_DLL so the
protocol-owned types become dllimport; LOGOS_QT_HOST_BUILDING_SHARED is tested
FIRST in logos_shared_api.h, so LogosAPI still resolves to dllexport there.

ARCHIVE DESTINATION on the install is load-bearing on Windows and inert
elsewhere: a shared library's import library (.dll.a) is the ARCHIVE artifact
while the DLL is RUNTIME, so omitting it installs no import library and a
consumer gets an imported target whose IMPORTED_IMPLIB does not exist.

VERIFIED, aarch64-darwin:

  liblogos_qt_host.dylib   defines LogosAPI:: 23
                           defines TokenManager:: 0, LogosAPIClient:: 0
                           imports TokenManager:: 5, LogosAPIClient:: 1
                           links @rpath/liblogos_protocol.dylib
  static archive           symbol tables IDENTICAL to master (1494 lines), one
                           byte differing -- ar metadata, not content. The macro
                           switch is behaviour-preserving for every existing
                           consumer.
  all 7 checks             PASS

The layering check was confirmed discriminating rather than vacuous by running
its logic against liblogos_core.dylib, which defines TokenManager 32 times and
is correctly rejected.

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

* chore(deps): take logos-protocol master, which now exports the shared library

logos-protocol#65 merged as 2e3344a. The lock pinned 0d2a3c0, the commit BEFORE
it, so this branch would have configured against a protocol with no exported
logos_protocol_shared and tripped its own guard:

    logos-protocol::logos_protocol_shared is not a target, so
    logos_qt_host_shared cannot be built.

That guard is deliberate -- the alternative is silently falling back to the
static archive and embedding a second TokenManager -- but it means the lock bump
is a hard prerequisite rather than housekeeping.

Also moves protocol 0.4.0 -> 0.5.0 (#63, the module teardown surface), which is
why the Windows export table grows 360 -> 839: more symbols in the archive, all
of them generated rather than curated.

All 7 checks PASS against the merged protocol, including shared-runtime-layering.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 22:29:02 -03:00
Dario LipicarandClaude Opus 5 9b2c64e5a4 feat: the Qt host runtime and cdylib-glue generator (#19)
* feat: the Qt host runtime and the cdylib-glue generator

This repo was the Qt plugin BUILD backend — pure Nix functions plus a CMake
module, with no C++ target at all. But the two things a Qt host actually needs
at runtime lived in logos-qt-sdk, where they are neither a language wrapper over
the protocol C API nor codegen:

  LogosAPI / LogosAPIProvider   owns a LogosTransportHost per transport,
                                constructs ModuleProxy / ModuleHandshakeProxy,
                                publishes the handshake surface, seeds trust
                                anchors, injects the token validator
  LogosProviderBase             the base every generated provider derives
  PluginInterface               the Qt plugin loading contract
  the cdylib->Qt glue generator the emitter that wraps a language-neutral
                                cdylib in a Qt plugin

They move here, so "swap the plugin technology" is a one-repo change.

ADDITIVE: the sources are COPIED and logos-qt-sdk is untouched. Removing them
there now would turn nine downstream masters red at once; that comes later,
after consumers are repointed.

qt_provider_object (and logos_qt_arg_decode, which its QMetaObject dispatch
needs) is carried deliberately even though it is legacy: logos_api_provider
falls back to wrapping a plain QObject in it, and the modules that rely on that
have not been migrated yet. It goes once they are.

The generator links Qt Core and logos-lidl only — never logos-cpp-sdk. It needed
exactly one helper from that SDK's shared frontend, lidlToPascalCase (~12
lines), which is inlined instead, the same way logos-view-module does it. It
also REFUSES `--backend <anything but cdylib>` rather than ignoring the flag:
callers are migrating from a tool where --backend was required and dispatched
on, so silently treating `--backend qt` as cdylib would emit confidently wrong
artifacts with a zero exit.

`rawLib`, `lib` and `cmake-module` deliberately do not reference the new
derivations, so a consumer that only wants the Nix build functions never
realises a Qt/protocol build. Proven, not assumed: with both new inputs
overridden to a bogus flake, cmake-module and rawLib still evaluate while
logos-qt-host fails — so the override bites and the cheap outputs really never
touch it.

Behaviour preservation is the whole claim of a relocation, so it is measured:
the emitted glue is BYTE-IDENTICAL to logos-qt-generator --backend cdylib over
every one of the 20 .lidl contracts in the workspace, in both single and
concurrency:multi mode (40 pairs, exit codes included), and single vs multi do
differ from each other, so both code paths were really exercised. The built
liblogos_qt_host.a is byte-identical to liblogos_qt_sdk.a, exporting the same
390 symbols.

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

* feat: qt-style consumers take their wrappers from logos-qt-generator

The qt api-style path emitted its per-dependency wrappers from
logos-cpp-generator's legacy ApiStyle::Qt emitter — a second implementation of
the consumer surface, with its own conversion table and its own transport. The
qt-generator `--backend consumer` veneer exists to replace it so there is ONE
transport, one codec and one Qt type mapper under both consumer surfaces. It was
golden-tested and wired into nothing.

The umbrella is what made this non-obvious. `--general-only` emits both the
per-dep wrappers AND logos_sdk.{h,cpp} (the LogosModules aggregate);
`--backend consumer` emits no umbrella at all, and the umbrella is live —
storage-ui and wallet-ui both use LogosModules. Nor is the umbrella
style-agnostic: makeUmbrellaHeaderFromDeps branches hard on apiStyle, emitting
`LogosModules(LogosAPI*)` with `dep(api)` members for qt and a
default-constructible struct with `dep("<origin>")` for lp.

What it IS agnostic to is WHICH generator produced the wrappers for a given
style — the qt branch's two constructor shapes are exactly what the consumer
backend emits for Static and Bound, and both spell the member's type with the
same PascalCase routine, shared through cpp-sdk's share/lidl-frontend. So
cpp-generator still emits the umbrella into a scratch dir, from which only
logos_sdk.{h,cpp} is copied, and qt-generator emits the wrappers. No legacy
wrapper text reaches the build tree; the shipped wrapper has exactly one author.

--dep is dropped from the umbrella call (those flags only drive wrapper
emission; the umbrella's members come from metadata.json dependencies), while
--interface is kept, because the bind_<name> factories come from the interface
names and a cross-repo interface can only reach the generator through a flag.

The lp path is emitted verbatim as before.

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

* feat: drop PROVIDER_HEADER from LogosModule.cmake

Follows the removal of logos-cpp-generator --provider-header. Nothing in the
workspace passed PROVIDER_HEADER to logos_module().

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

* feat(glue): forward the host-services grant across the C ABI (C3)

The cdylib glue now reads a `hostServices` property off the LogosAPI object and
calls logos_module_grant_host_services with it, on the SAME property-stamping
channel the host already uses for authToken/modulePath/instanceId — no new
host<->plugin API.

It has to cross the C ABI rather than being recorded host-side: the host binary
and the cdylib each link their own copy of logos-protocol, so each has its own
process-global grant state, exactly as each has its own TokenManager (which is
why the auth token is re-seeded here too). Measured, not assumed: a built
plugin DEFINES 25 lp_* symbols and imports zero.

Ordered before the context forward, so a privileged impl may already use the
granted services from its context-ready hook.

An ordinary module has no such property and stays fail-closed with no call at
all — lp_grant_host_services REPLACES the current grant, so pushing an empty
string would be a pointless clear. A refused grant (malformed JSON, or a name
outside the closed set) is reported rather than swallowed: the module keeps
running UNPRIVILEGED, and the symptom otherwise surfaces far away as an
unexplained empty token registry.

Tests assert the forwarding, the empty-property guard, the refusal warning, and
the ORDER (by comparing emitted line numbers, since that is the kind of thing a
refactor silently reverses and which then breaks a trust-root module at
startup).

Note ws test ran only 1 of this repo's 4 checks, so the new test was run
directly by name — see the ws check-discovery gap.

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

* test: document the two-copy drift this generator test now guards

logos-qt-sdk ships the same cdylib-glue emitter. Both compile and both emit
loadable glue, so calling the wrong one is not an error — it silently emits
OLDER glue, which is how the host-services grant went undelivered for a whole
phase while every build stayed green.

The grant assertions added with the forwarding are what make that visible: if
the builder is pointed back at qt-sdk's generator, these fail instead of a
module quietly coming up unprivileged.

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

* fix(glue): guard the grant forwarding, and include the header that defines it

Same protocol-MINOR guard as the C-ABI export, so glue for a module built
against logos-protocol < 0.3 still compiles.

The guard needs logos_protocol.h, which the glue did NOT include — and an
undefined macro in `#if defined(X) && X >= 3` is simply false, so the first
version of this change compiled everywhere and silently stopped forwarding the
grant: logos-test-modules ipc-tests went PASS -> FAIL again, with capability
refusing every requestModule exactly as it had before.

That is the second time this seam has failed by going quiet rather than loud.
Verified in BOTH directions this time: template_module builds against the old
protocol (guard off), and ipc-tests passes against the new one (guard on).

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

* feat(tokens): a LogosAPI can be built on its own token store

Origin was never consulted on the hot path — LogosAPIClient reads its store
first and mints only on a miss — so a per-plugin `LogosAPI(origin=name)` was
a no-op while this ctor hard-wired `&TokenManager::instance()`. What has to
differ is the STORE.

The existing ctors now resolve their store through
`TokenManager::forIdentity(module_name)`, which returns instance() itself —
pointer-identical — for every name nobody has isolated, so no existing caller
changes by one byte. New: a ctor taking an explicit store, and
`LogosAPI::forIdentity(name)`, which isolates first and then constructs (the
required order: a client captures its store by raw pointer) and returns
nullptr rather than a half-isolated identity.

* feat: a module's own qt wrapper comes from its contract, not its plugin

buildHeaders was the last place the legacy Qt consumer emitter was reached on
purpose, and BOTH of its paths reached it: the native one introspected the
compiled plugin (`<plugin.so> --module-only --api-style qt`), the cross one ran
`--general-only --dep` and then deleted the umbrella it incidentally emitted.
B5 moved buildPlugin's consume-time wrappers to `logos-qt-generator --backend
consumer`; the wrapper a module PUBLISHES for its own consumers kept coming
from the other emitter, so the same file name had two authors depending on
which derivation produced it.

Contract-first now. A module that publishes a LIDL contract — every universal
and cdylib module does, and a handcrafted one can commit src/<name>.lidl — has
its qt wrapper generated from that contract, natively and under cross alike.
Introspection survives exactly where nothing else describes the module: a
legacy Qt plugin whose QMetaObject is the only account of its API that exists.
That residue dies with the last such module, not here.

The public declaration set does not move. Six real providers, emitted both ways
and diffed over the surface a consumer can name: capability_module 12,
chat_module 84, storage_module 102, package_manager 131, test_fullapi_cpp 113,
test_fullapi_rust 113 — identical in every case. The entire diff is private:
`ensureReplica` / `packVariantList` / `m_client` / `m_eventReplica` collapse to
one `logos::qt::LpBridge* m_bridge`. That is the transport, which is the point
of the exercise.

Two consequences worth naming. The qt variant no longer references the compiled
plugin at all, so capability_module's headers-qt went from depending on the SDK
plus the module .dylib to depending on logos-qt-generator plus a .lidl — a
contract module's headers now cost a parse instead of a Qt build. And the cross
path stops being a DIFFERENT emitter from the native one: `--api-style qt` at
host=x86_64-w64-mingw32 takes the same branch darwin takes, which retires the
`--general-only` + `rm logos_sdk.*` shape that only ever existed because
introspection is impossible there.

The failure mode this file has to defend against is silence, not breakage: a
routing bug leaves every build green while the wrapper quietly comes from the
old emitter again. So the selection is announced with its reason
(`buildHeaders: emitter=…`), the contract path asserts its own output names
LpBridge before installing it, and the one branch where a contract EXISTS but
the caller passed no generator is reported as a REGRESSION on stderr rather
than silently demoted. tests/test-headers-emitter-routing.nix pins all four
selections against the real routing code.

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

* feat(b2b): repoint this repo's LogosModule.cmake at logos-qt-host

This file is not a spare copy. logos-module-builder's buildCppPlugin sets
LOGOS_MODULE_BUILDER_ROOT only when the MODULE'S OWN repo ships a
cmake/LogosModule.cmake, and none do -- so for every ui_qml / view plugin, and
for the counter fixture, the CMake module that actually runs is this one, taken
from the backend root. logos-module-builder's copy is what mkLogosModule's core
path uses. Two live copies, selected by a pathExists probe.

That was invisible while logos-qt-sdk forwarded the host runtime's headers: this
copy probed ${LOGOS_QT_SDK_ROOT}/include/cpp/logos_api.h, put
${LOGOS_QT_SDK_ROOT}/include{,/cpp,/core} on the include path, and linked
logos-qt-sdk::logos_qt_sdk -- all of which resolved through the forwarders and
the INTERFACE hop. With the forwarders gone it configures straight into
"logos-qt-sdk not found" while looking at a perfectly good prefix. I found this
by deleting the file on the theory that it was dead and watching eight module
builds fail; the theory was wrong and the build said so.

Repointed the same way logos-module-builder's copy already was:

  - LOGOS_QT_HOST_ROOT selects the host runtime, defaulting to this repo (the
    CMake module sits next to the cpp/ it needs) and hard-erroring otherwise.
    There is no logos-qt-sdk fallback -- it would configure and then fail in the
    compile.
  - the source layout compiles ${LOGOS_QT_HOST_ROOT}/cpp/*.cpp, now including
    logos_qt_arg_decode.cpp, which qt_provider_object.cpp's dispatch needs and
    the qt-sdk-era list omitted.
  - the installed layout links logos-qt-host::logos_qt_host, refusing a package
    that resolves without defining its target.
  - LOGOS_QT_SDK_ROOT stays, probed by logos_qt_wire.h (a name this SDK owns;
    logos_api.h is in neither of its layouts any more) and contributing only the
    Qt-typed consumer headers.

Every plugin built through this file is byte-identical before and after.

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

* feat: stop shipping a second LogosModule.cmake

This repo's cmake/LogosModule.cmake was a duplicate of
logos-module-builder's, and the build chose between them by module type:
logos-module-builder set LOGOS_MODULE_BUILDER_ROOT only when a MODULE carried
that file (none does), so every ui_qml plugin configured with THIS copy while
every core module configured with the builder's. Both compiled. That is how a
stale generator, a stale host-runtime repoint and a missing source file each
shipped in a green tree.

The CMake module is the builder's build-system contract -- it reads
LOGOS_API_STYLE, LOGOS_MODULE_GO_STATIC_LIBS and generated_code/, none of
which this backend knows about -- and it cannot live here anyway: the
dependency runs builder -> backend, so this repo could never re-export the
builder's file. It is deleted, not forwarded. logos-module-builder now passes
LOGOS_MODULE_BUILDER_ROOT for both module types (its 0ddac92), so the deletion
lands after the routing, not before it.

Consequently:
  - lib/default.nix no longer defaults LOGOS_MODULE_BUILDER_ROOT to backendRoot.
    A caller that forgets it now gets a FATAL_ERROR from the module's own
    CMakeLists instead of a build against whatever this repo contains. That was
    backendRoot's only use, so the parameter goes too.
  - packages.cmake-module is gone; `default` is logos-qt-host, the output this
    repo now produces that a consumer can build.
  - cmake/ is gone. LogosViewPluginBase.{h,cpp}.in were read only by the deleted
    file. The two LogosViewReplicaFactory templates are read only by
    tests/test-rep-file-plugin.nix, so they move into that fixture's own cmake/
    and are what they are: a test fixture, not a shipped template.

Checks: header-generator-guard, headers-emitter-routing, qt-host-generator,
rep-file-plugin, vanilla-plugin OK. qt-host fails identically at the pre-change
baseline -- this repo's lock pins a logos-protocol whose TokenManager has no
forIdentity/isolateIdentity, so cpp/logos_api.cpp will not compile from it.
It builds green in the workspace closure.

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

* feat: the view templates exist once, and this repo is where

The four LogosView*.in templates had two byte-identical copies:

  logos-module-builder/cmake/                         (LIVE — LogosModule.cmake)
  logos-plugin-qt/tests/rep-file-plugin/cmake/        (fixture — rep-file-plugin)

Nothing compared them. This is the fifth defect of that exact shape in this
refactor, and it was created by the fix for the fourth: when this repo's
duplicate LogosModule.cmake was deleted, the templates it read were MOVED into
the fixture rather than removed, because the fixture still needed them.

It needed them because it cannot reach where they went. The edge runs
logos-module-builder -> logos-plugin-qt, one way, so a fixture inside this repo
can never consume logos-module-builder's copy, and this repo cannot re-export
it either. "Own them in the builder" is not reachable; a copy in the fixture is
what "own them in the builder" degrades into.

So ownership follows the direction that works. The templates are this repo's:

  * cmake/ holds the one copy, with cmake/README.md stating the rule and the
    argument for it.
  * packages.<sys>.logos-view-templates publishes them as a nameable output.
  * lib.buildPlugin / lib.generate set LOGOS_VIEW_TEMPLATE_DIR (cmake flag and
    env var) on every plugin build, so logos-module-builder's LogosModule.cmake
    receives the directory without either repo growing a new input.
  * devShellInputs exports it too, so a hand-run cmake in a module dev shell
    resolves it the same way.

LogosModule.cmake itself does NOT come back here. That file is the builder's
build-system contract and stays there; only the Qt-specific templates it
instantiates are published from this side of the edge.

The fixture now takes the directory from the harness and has nothing to fall
back to — a missing LOGOS_VIEW_TEMPLATE_DIR is a FATAL_ERROR, not a quiet
second copy. Proof it reads the owned file and not a leftover: changing the IID
in cmake/LogosViewReplicaFactory.h.in makes rep-file-plugin fail its
"IID found in binary" assertion.

rep-file-plugin is also added to CI. It is the check that instantiates these
templates and it was never listed in the workflow, so the templates had no CI
coverage at all — part of why a duplicate could sit in the fixture unnoticed.

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

* test(view): assert the exact IID, and load the plugin the way the host does

`rep-file-plugin` asserted `strings | grep -q "logos.view.replica_factory"`.
That is a SUBSTRING match, and it is satisfied by every way the plugin can be
broken. Two mutations were run through it and it stayed green on both:

  * `Q_PLUGIN_METADATA(IID LogosViewReplicaFactory_iid)` changed to the literal
    "logos.view.replica_factory/2.0" — the built dylib then carries BOTH
    strings, because Q_DECLARE_INTERFACE still emits /1.0, and the substring
    matched either one.
  * `Q_INTERFACES(LogosViewReplicaFactory)` deleted — no string in the binary
    changes at all; only moc's qt_metacast entry disappears, so the host's
    qobject_cast returns nullptr and the view is blank.

They are two independent bindings, so they need two assertions:

  * the IID is now compared EXACTLY. Every logos.view.replica_factory/<v>
    string in the binary is collected and the set must be exactly the one
    expected IID, which is what makes the /2.0 case fail: a binary carrying
    both is proof that Q_PLUGIN_METADATA and Q_DECLARE_INTERFACE disagree.
  * the plugin is then actually loaded. load_factory_check.cpp does what
    LogosQmlBridge::loadFactory does — QPluginLoader::instance() followed by
    qobject_cast<LogosViewReplicaFactory*> — and also checks the factory is
    wired to the repc-generated RepTestReplica. Deleting Q_INTERFACES fails
    here, at the same call the host would have failed at.

The checker deliberately has AUTOMOC OFF: it includes the generated header
only for the interface declaration and its Q_DECLARE_INTERFACE, and must not
moc the concrete plugin class that header also declares. It carries no copy of
the interface — that duplication is the thing being guarded against.

Comments in cmake/ updated to say what the two checks actually cover now, and
that the module side is checked from the binary end here while agreement with
the HOST side is logos-module-builder's `view-interface-abi`.

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

* feat(windows): logos-qt-host builds for the x86_64-windows target

The Qt host runtime moved here from logos-qt-sdk, but its Windows target did
not come with it. logos-qt-sdk keys `packages` by `forAllTargets`; this repo
keyed them by `forAllSystems` over four unix systems, so
`packages.x86_64-windows.logos-qt-host` did not exist and every consumer that
threads the pseudo-system through — logos-liblogos, and through it
logos-basecamp — failed at EVALUATION with

    error: attribute 'x86_64-windows' missing

before a single derivation was built. The split traded a Windows-capable
provider for one that is not.

`packages` now uses `logos-nix.lib.forAllTargets`, the same helper
logos-protocol and logos-lidl already build their Windows legs with. `checks`
and `devShells` deliberately stay on `forAllSystems`: every check either runs
what it built or loads a plugin, and a PE does not run on the Linux builder.

Two things the cross target needs, both gated on the HOST platform so the
native derivation does not move:

  * `$cmakeFlags` is forwarded to the hand-rolled cmake line. mkDerivation puts
    -DCMAKE_SYSTEM_NAME / -DCMAKE_SYSTEM_PROCESSOR / the host-system triple
    there for a cross build and leaves it empty natively, so a hand-rolled
    configure that omits it is precisely the shape that silently drops them.
  * the Qt cross flags (QT_HOST_PATH, QT_ADDITIONAL_HOST_PACKAGES_PREFIX_PATH)
    plus dontWrapQtApps. CMAKE_AUTOMOC is on and RemoteObjects is required, so
    moc and repc must come from the BUILD platform's Qt; and the Qt wrapper
    hooks cannot evaluate for a mingw host, while qtbase's setup hook aborts in
    qtPreHook unless one of them ran or dontWrapQtApps is set. There is nothing
    to wrap in any case — this package installs an archive and headers.

NATIVE DERIVATION UNCHANGED, measured rather than argued: `nix derivation show`
for aarch64-darwin before and after differs in the `src` snapshot alone (`src =
./.`, so any commit moves it). Every other env var is byte-identical and
inputDrvs compares equal, so nothing downstream rebuilds for a reason other
than this repo's source having changed.

`logos-qt-host-generator` is deliberately NOT published for the Windows target.
It is a HOST TOOL, executed during a consumer's build, which is why every
caller in logos-module-builder already reaches for it as
`packages.${buildSystemFor system}.logos-qt-host-generator` — resolving to
x86_64-linux there. A PE under `packages.x86_64-windows` would only offer the
Linux builder a binary it cannot run.

LAZINESS PROVEN BY CONTROL, not by grep. With BOTH inputs the host runtime
needs overridden to a flake that provides no packages at all:

    lib                                         -> evaluates (7 attrs)
    rawLib                                      -> evaluates (7 attrs)
    packages.aarch64-darwin.logos-view-templates -> evaluates
    packages.x86_64-windows.logos-view-templates -> evaluates
    packages.aarch64-darwin.logos-qt-host        -> FAILS at logos-protocol
    packages.x86_64-windows.logos-qt-host        -> FAILS at logos-protocol
    packages.aarch64-darwin.logos-qt-host-generator -> FAILS at logos-lidl

So the override really bites, the cheap outputs a consumer of the Nix build
functions asks for still never realise a Qt + protocol build, and the new
Windows branch does not drag one in either.

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

* chore(deps): rev-pin logos-protocol at c8bab12 (per-client token store)

LogosAPI is now constructible on its own token store, which needs
TokenManager::forIdentity / isolateIdentity. Those landed on
logos-protocol's feat/per-client-token-store branch and are NOT on its
master — master is still LOGOS_PROTOCOL_VERSION_MINOR 2, so the
previously locked e6d5b57 (protocol master's tip) cannot compile
cpp/logos_api.cpp, and the cdylib glue's `MINOR >= 3` guard would
silently drop the host-services grant.

Rev-pinned in the URL rather than left master-tracking, because
`nix flake update` cannot reach a commit that is not on the tracked
branch. Re-point at master once that branch merges.

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

* refactor(buildPlugin): drop the transitional header-copy fallback

For a dependency that published no LIDL contract, the plugin build copied that
dependency's PRE-BUILT `headers-<apiStyle>` output into generated_code/ --
which also forced the dependency's whole plugin to be compiled just to obtain
headers. Every dependency now arrives as `--dep <name>=<name>.lidl` and its
wrapper is generated from the contract instead.

The path is dead, measured rather than assumed. `moduleDepIncludes` was
interpolated into the plugin derivation's `preConfigure`, so its liveness is
readable at EVAL time: I probed `.preConfigure` on every package reachable
within two hops of the workspace flake (303 derivations) plus every variant of
every module in logos-test-modules (40 more) and grepped for the block's own
"(legacy header-copy)" echo. 343 non-empty preConfigure strings, ZERO hits --
including test_ipc_module and test_ipc_new_api_module, the two modules with
concrete `dependencies` and no `interface` declaration.

That probe has a control, because a zero-hit grep is worth nothing without one:
patching mkLogosModule's `depIsLidl` to `false` makes the same eval print
"Copying qt-typed include files from test_basic_module (legacy header-copy)..."
for both of test_ipc_module's deps. The probe sees the block when it is there.

`moduleDeps` is still ACCEPTED, and asserted EMPTY. The caller fills it from
`legacyHeaderDepNames` -- deps for which `depIsLidl` is false -- and a caller
that predates this change keeps passing them. Silently dropping those would
leave the module with neither a `--dep` wrapper nor a copied header, and the
build would die inside a generated translation unit on a missing
`<dep>_api.h`, pointing at the generator instead of at the stale dependency.
So it throws, naming the deps. Verified with the same forced-`depIsLidl` patch:
the error names test_basic_module and test_extlib_module.

Verified:
  * all six checks built by name, each exit 0 -- header-generator-guard,
    headers-emitter-routing, qt-host, qt-host-generator, rep-file-plugin,
    vanilla-plugin
  * test_ipc_module, test_ipc_new_api_module, test_fullapi_cpp and the
    workspace's logos-accounts-ui all build against this tree (exit 0), with
    reach proven by store-path diff: test_ipc_module is
    867kcfxr... with the override and pnj60rsc... without it
  * test_qml_backend fails, IDENTICALLY with and without the override
    ("no member named 'add' in 'TestBasicModule'") -- pre-existing at the
    pinned revs, not caused by this change

NOT removed, and it is worth saying why: the Windows cross-compile branch in
buildHeaders.nix (`crossNoIntrospect` -> `crossBuildPhase`) does NOT serve this
fallback. It is the emitter for `--api-style lp` headers under cross, selected
because a Linux builder cannot dlopen a PE, and its `!contractIsLidl` half is
the hard error that stops a contract-less module shipping an empty API under
cross. Both halves are still reachable.

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

* feat(buildPlugin): honour the consumer surface, and gate origin binding

Backend half of codegen.consumer_api_style (logos-module-builder 08ae7ac).
`apiStyle` now honours `config.consumer_api_style`, which also selects which
pre-built `dep.headers-${apiStyle}` variant a consumer takes.

Adds `originBound = isQt && packagedAsCdylib`, which is what actually passes
`--binding origin` to the generators. Deriving it here rather than reading a
key means the hazardous combination cannot be requested: only a cdylib-packaged
image reaches it, and that is the image whose TokenManager is filled by
logos_module_accept_token rather than by LpBridge::syncTokens. A Qt plugin given
an origin-bound wrapper would be silently unauthenticated.

`assertConsumerApiStyle` restates the gate here as well as in parseMetadata,
because callers can reach rawLib.generate directly and bypass metadata parsing
entirely.

The flag is appended as a suffix on an existing argument
(`--api-style qt${bindingFlag}`) so the emitted shell is byte-identical when
empty — verified by A/B'ing the generated tree for a Qt plugin, not just the
built output.

New check `consumer-api-style-gate` drives the real `generate` and asserts on
the emitted generator command line across 16 configurations. Its mutation
control is the important one: setting `originBound = isQt` — dropping the
packagedAsCdylib conjunct — makes it fail by name on legacy and ui_qml, so the
suite pins the conjunct itself rather than only the lp refusal.

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

* fix(qt-host-generator): capture isVoidMethod in the multi worker lambda

`concurrency: "multi"` runs the handler on a QThread, and the lambda it hands
to QThread::create takes no capture-default on purpose — it outlives the frame,
so every capture has to be by value deliberately. The capture list was written
out by hand in two variants selected on `resultMethods`, but the BODY reads two
flags, `isVoidMethod` and `isResultMethod`, each emitted under its own
condition. Two lists cannot cover four combinations:

  void + result   -> captured isResultMethod only; body still read isVoidMethod
  void, no result -> captured neither; body still read isVoidMethod
  result, no void -> correct
  neither         -> correct

So any module with a void method failed to compile under multi, at the
generated glue rather than anywhere the author could see. Building the list
from the same two conditions that decide whether the locals exist removes the
possibility of them disagreeing.

Found by giving test_ipc_new_api_module `concurrency: "multi"` — it has a void
method (triggerBasicEvent) and no result method, i.e. the doubly-broken case.

The existing multi test did not catch this because it only ever GREPPED the
emitted text and never compiled it, and its fixture happens to declare both a
void and a result method — so it was reading output that would not build. The
assertions added here close that specific gap rather than the general one: for
each flag the body reads, the capture list must name it. A second contract
covers void-without-result, the combination neither hand-written list could
express. Both were confirmed to FAIL against the pre-fix generator (the first
reports the actual emitted list) and pass after.

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

* ci: use logos-co/setup-nix-cache-action for Nix setup and caching

Replaces the per-repo installer + cachix pair with the shared action, which
installs Nix with the Logos Attic cache (cache.nix.logos.co) preconfigured and
publishes what the job builds — master to the public cache, every other ref to
ci.

Each converted job also gains

    environment: ${{ github.ref == 'refs/heads/master' && 'public-cache' || '' }}

because ATTIC_TOKEN_PUBLIC only exists inside that environment. Without it the
secret resolves empty on master and publishing is silently skipped — the job
still passes, so the omission would not show up as a failure.

The action installs Nix itself on every runner, macOS included. That is a
deliberate reversal of the workaround these files carried: the comments here
said cachix/install-nix-action collides with the runner's pre-existing _nixbld
users (eDSRecordAlreadyExists), so DeterminateSystems' installer was used
instead. It no longer reproduces — logos-delivery-module has already been
converted the plain way and its `build-and-test (macos-latest)` leg passes.
Keeping the workaround would have meant a second installer plus a duplicated
substituter/key block in ten files, guarding against something two green runs
say does not happen. If it ever recurs it fails loudly at install, which is
recoverable; the silent-skip above is the failure mode worth engineering
against.

One property is deliberately NOT carried over: the old cachix step ran with
`continue-on-error: true` so a failed cache push could not fail a job whose
tests passed. The action exposes no equivalent, and adding one here would also
swallow genuine setup failures now that the same step installs Nix rather than
only publishing at the end.

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

* chore(deps): track logos-protocol master again

The rev pin on feat/per-client-token-store existed because the trust-root
surface it needed lived only on that branch while protocol master was still
LOGOS_PROTOCOL_VERSION_MINOR 2. Both comments here said to re-point once it
merged; it has (logos-protocol#59), and master is 0.4.0 — MINOR 4, carrying
lp_token_keys, lp_inform_module_token_to, lp_grant_host_services and
TokenManager::forIdentity / isolateIdentity.

That matters beyond compiling: the cdylib glue's grant forwarding is guarded on
MINOR >= 3, so a master pin taken too early would not have failed loudly — it
would have dropped the grant silently. The guard now opens.

Verified against master rather than assumed: #default builds and the checks pass
(cpp-sdk `tests`, plugin-qt `qt-host-generator`).

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

* docs: mark the provider macros vestigial and fix the glue banner

logos_provider_object.h still presented LOGOS_PROVIDER / LOGOS_METHOD as "the
developer-facing API" and said logos-cpp-generator --provider-header produces
the dispatch from them. That scanner was removed, so nothing reads LOGOS_METHOD
any more; the macros are kept only so an older TU still compiles. Module code
no longer derives LogosProviderBase either — it derives logos-cpp-sdk's Qt-free
LogosModuleContext, and LogosProviderBase is now the base the *generated*
<name>_cdylib_glue.cpp derives from.

The drift guard in tests/ was written when logos-qt-sdk shipped a rival copy of
this emitter; that copy is gone and the flag now refuses, so the comment is
recast as history. The assertion stays — it pins the property that made the
divergence detectable.

Also fixes the banner this generator stamps into its own output, which
credited "logos-cpp-generator --cdylib". Nothing pins the old string.

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

* refactor: shed the view-plugin templates, keeping this repo to cdylib loading

This repo's job is making a cdylib module loadable by logos-module-loader-qt.
The LogosView*.in templates, the logos-view-templates output, the
LOGOS_VIEW_TEMPLATE_DIR plumbing and the .rep fixture are ui_qml AUTHORING, a
different concern; they now live in logos-view-module, which already owned that
flavour and inputs only logos-nix so nothing can cycle through it.

Removed: cmake/ (the four templates + README), the logos-view-templates
package, common.nix's viewTemplateDir and its -DLOGOS_VIEW_TEMPLATE_DIR entry,
the LOGOS_VIEW_TEMPLATE_DIR env entries, and tests/rep-file-plugin with its
check.

logos-module-builder supplies LOGOS_VIEW_TEMPLATE_DIR from its own
logos-view-module input through the extraCmakeFlags/extraEnv seams it already
uses for the other five roots, so nothing here needed a new parameter — the
change is pure deletion.

Three sites carried the variable, not two: buildPlugin's env, generate's env
(dead — mkBuildPlugin.generate sets dontConfigure, so cmake never runs and
nothing reads it), and devShellInputs' shellHook, which logos-module-builder
splices verbatim into every module dev shell. Dropping the third without the
builder taking it over would have left `nix develop` broken for every ui_qml
module, visible only when someone ran cmake by hand.

buildPlugin.nix keeps its <name>_replica_factory packaging: the LIBRARY is a
build artifact of the module tree this backend compiles, exactly like the
_plugin library beside it, and packaging what a plugin build emits needs the
output filename convention, not view knowledge.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 15:50:49 -03:00