14 Commits
Author SHA1 Message Date
Khushboo Mehta be25f5f95b feat(intents): add the frozen app-to-app intent surface
Three QML symbols, meant never to change:

  logos.request(intent, params, callback)
  logos.intentRequested(requestId, intent, params, requesterName)
  logos.respond(requestId, ok, data, error)

request() returns void deliberately. The requester never receives a
requestId, so it cannot forge a respond() for its own request and cannot
hold a handle to whoever answered — the single property that lets the
router underneath be replaced without any app noticing.

LogosIntent.h carries the shared vocabulary (five error codes, the name
grammar, payload rules, the {ok,data,error} envelope) as a header-only,
non-QObject namespace, so a host's broker and this bridge can never
disagree about what a legal name or a legal error is.

LogosIntentRouter.h is the seam. The bridge does NO policy: no `uses`
check, no grammar check, no payload check, no provider lookup. All of
that belongs to the router implementation, which is expected to be
DELETED when the core runtime takes over provider selection. Policy
placed in the bridge would survive that deletion and turn removing it
into a migration for every installed app.
2026-08-27 10:15:07 +02:00
Dario LipicarandClaude Opus 5 ba56a70b04 feat(ui-host): adopt the parent's credential through the shared verb (#27)
* feat(ui-host): adopt the parent's credential through the shared verb

ui-host is handed its parent's per-spawn credential on stdin and installs
it under both bootstrap keys. That was already the right shape — it is the
one place in the system that seeded a store with its OWN identity's
credential rather than the host's — but it spelled the bootstrap key set
out itself, making it the fifth such site.

It now goes through logos::adoptConsumerCredential, so
TokenManager::bootstrapKeys() owns the set. No behaviour change here: this
image's store IS the process ring, which is exactly the case that verb is
narrowed to.

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

* chore(lock): logos-plugin-qt -> master for logos_consumer.h, protocol to the rev master implements

WHAT MOVED

  logos-plugin-qt  ef11c210 -> 7cad5ec3  (master, #27 merged)
  logos-protocol   79894727 -> b37a2e9f  (0.5.0 -> 0.7.0)

WHY plugin-qt HAD TO MOVE

This branch's ui-host calls logos::adoptConsumerCredential and the bridge
takes a logos::ConsumerIdentity, both declared in cpp/logos_consumer.h. That
header did not exist at ef11c210, so every build here died at

  LogosQmlBridge.h:17:10: fatal error: logos_consumer.h: No such file or directory

logos-plugin-qt#27 ("logos::admitConsumer -- one home for admitting a
non-module") merged as 7cad5ec3 and ships logos_consumer.h / .cpp. Relocking
that input is the whole fix for the reported red.

WHY protocol MOVED TO 0.7.0 AND NOT TO MASTER

logos-plugin-qt's logos-protocol input `follows` OURS, so this repo -- not
plugin-qt's own lock -- decides which protocol logos-qt-host is compiled
against. The first attempt took protocol to master (42460e5b, 0.8.0). It does
not build, and it does not fail in this repo:

  logos-plugin-qt/cpp/logos_consumer.h:68:4: error: #error "logos-protocol is
  newer than the consumer-admission contract this file implements. A private
  token store is created empty; if the protocol changed how a consumer is
  seeded, this file and the hosts calling logos::admitConsumer must move in the
  SAME wave. Review adoptCredentialFor / bootstrapKeys, then raise this bound."

That is a deliberate upper bound in plugin-qt master:

  #if defined(LOGOS_PROTOCOL_VERSION_MINOR) \
      && (LOGOS_PROTOCOL_VERSION_MAJOR > 0 \
          || (LOGOS_PROTOCOL_VERSION_MAJOR == 0 && LOGOS_PROTOCOL_VERSION_MINOR > 7))

Every other version guard in the fleet is a `>=` floor; this one is a ceiling,
written precisely so a protocol bump cannot outrun the consumer-admission
contract silently. protocol 42460e5b (0.8.0, the INBOUND/OUTBOUND direction
split) merged at 14:16:33Z and logos-plugin-qt#27 merged at 14:16:41Z, eight
seconds later, on top of 0.7 -- plugin-qt master's own flake.lock pins
logos-protocol b37a2e9f. So plugin-qt master implements the <= 0.7 contract and
no branch anywhere raises the bound yet.

b37a2e9f is therefore not a compromise, it is the rev plugin-qt master itself
locks: this closure is the one plugin-qt's own CI validates, and there is no
protocol split. Verified in the built closure:

  logos-view-module-runtime  compiles against  logos-protocol 0.7.0
  logos-qt-host-0.1.0        links             logos-protocol-lib-0.7.0
  logos-cpp-sdk              contributes no protocol path at all
                             (header-only Qt-free types; its own 0.2.0
                              logos-protocol lock node never reaches the build)

Nothing in this repo references a 0.8-only symbol (no saveInboundToken, no
adoptCredential/adoptCredentialFor, no LOGOS_PROTOCOL_VERSION guard), so 0.8
buys this branch nothing today. flake.nix is untouched and the lock's
`original` for logos-protocol stays master-tracking -- once plugin-qt raises
the bound, a plain `nix flake lock --update-input logos-protocol` carries this
repo to 0.8 with no edit.

CHECKS -- every target the flake exposes for this builder, built individually
with --print-out-paths, upstream cache only (cache.nix.logos.co is 502), so
these are real from-source builds:

  packages.x86_64-linux.default    OK
    /nix/store/gajjq3050m46sprbiqqz0c32mhi95vb7-logos-view-module-runtime-1.0.0
  checks.x86_64-linux.default      OK
    /nix/store/9a4wvnp6b226s2lam54dgxmzm86r9j58-logos-view-module-runtime-check-1.0.0
  packages.x86_64-linux.tests      OK  (same derivation as the check)
    /nix/store/9a4wvnp6b226s2lam54dgxmzm86r9j58-logos-view-module-runtime-check-1.0.0
  checks.x86_64-windows.default    OK  (cross to mingw; compile only, nix
                                        forces doCheck off for a cross build,
                                        so no ctest ran there)
    /nix/store/csp4n8hn0xrf2j2lxflv6k8affbn180v-logos-view-module-runtime-check-x86_64-w64-mingw32-1.0.0
  packages.x86_64-windows.default  OK  (bin/ui-host.exe is a real PE32+)
    /nix/store/s8y4s43nfl8932hnffg645bzn9dqy5fs-logos-view-module-runtime-x86_64-w64-mingw32-1.0.0

checks.x86_64-linux.default runs the suite in its checkPhase:

  Start  1: LogosQmlBridgeUnitTests        Start  6: BridgeIdentityTests
  Start  2: LogosQmlBridgeE2ETests         Start  7: LogosQmlBridgeDeferredTests
  Start  3: LogosQmlBridgeGuiTests         Start  8: LogosQmlBridgeCallsTests
  Start  4: LogosQmlBridgeResultTests      Start  9: LogosQmlBridgeReplayTests
  Start  5: LogosQmlBridgeHandshakeTests   Start 10: UiHostUnloadTests
  100% tests passed, 0 tests failed out of 10
  Total Test time (real) =  39.36 sec

BridgeIdentityTests is the suite this branch adds, so admitConsumer /
adoptConsumerCredential are exercised, not merely linked.

NOT VERIFIED: the aarch64-* and *-darwin outputs. This builder is x86_64-linux
and there is no darwin remote; CI's macos-latest leg covers them.

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

* chore(lock): protocol 0.8 + plugin-qt master, and un-collapse the fixture's two token stores

WHAT MOVED

  logos-protocol   b37a2e9f (0.7.0) -> 42460e5b (0.8.0, "separate INBOUND from OUTBOUND")
  logos-plugin-qt  7cad5ec3          -> 048152f2 (plugin-qt#26)

Six lines of flake.lock; the lock stays at 14 nodes. logos-plugin-qt's
logos-protocol input `follows` OURS, so this repo decides which protocol
logos-qt-host is compiled against, and both halves have to move together.

THIS SUPERSEDES e370ce5's REASONING, WHICH WAS CORRECT WHEN WRITTEN

e370ce5 took protocol to 0.7 rather than master and gave the reason: plugin-qt
master carried a CEILING, not a floor, in cpp/logos_consumer.h --

  #if ... || (LOGOS_PROTOCOL_VERSION_MAJOR == 0 && LOGOS_PROTOCOL_VERSION_MINOR > 7))
  #error "logos-protocol is newer than the consumer-admission contract this
          file implements ... Review adoptCredentialFor / bootstrapKeys, then
          raise this bound."

-- and no branch anywhere had raised it. logos-plugin-qt#26 has since merged as
048152f2 and performed exactly the review that text demands. Read from GitHub at
both revs rather than inferred:

  7cad5ec3  cpp/logos_consumer.h:67   ... VERSION_MINOR > 7))
  048152f2  cpp/logos_consumer.h:90   ... VERSION_MINOR > 8))

#26 also makes 0.8 mandatory rather than merely permitted: cpp/logos_provider_
object.cpp and cpp/qt_provider_object.cpp call TokenManager::saveInboundToken
unguarded, which does not exist before 0.8. plugin-qt master and protocol 0.8
are now one step, and 0.7 is no longer reachable from this branch.

Verified in the built closure rather than in the lock -- the version literal in
the header that actually reached the compiler, on both targets:

  LOGOS_PROTOCOL_VERSION_MAJOR 0 / LOGOS_PROTOCOL_VERSION_MINOR 8

and exactly ONE logos-qt-host and ONE logos-protocol derivation in the .drv
graph of every output (the runtime closure cannot see either -- logos_qt_host is
linked statically -- so this was read from `nix-store -qR` over the DERIVATION,
which is the instrument that finds a split host):

  x86_64-linux      logos-qt-host-0.1.0.drv                  x1
                    logos-protocol{,-lib,-headers}-0.8.0.drv  one derivation
  x86_64-windows    logos-qt-host-x86_64-w64-mingw32-0.1.0.drv x1
                    logos-protocol-*-x86_64-w64-mingw32-0.8.0.drv one derivation

── THE ONE TEST THAT MOVED, AND WHY IT IS THE FIXTURE AND NOT THE PRODUCT ─────

At 0.8 this branch built clean and ran 10/11, with case 1 red:

  FAIL!  : TestBridgeIdentity::aHostIdentityBridgeReachesAnUndeclaredBackend
           WithNoHandshake() '!payloadIsError(payload)' returned FALSE.
  ({"error":"Module source unavailable",
    "message":"call to 'backend_module' rejected: token not recognized
               (re-exchange failed)"})

Case 1 is the deliberately-GREEN ambient-ring escalation control, and its own
comment says that if it goes red the ambient ring changed shape and every
conclusion in the file needs re-deriving. So this had to be decided, not
silenced. Two readings were live:

  (A) the fixture seeds the ring with saveToken -- the OUTBOUND door -- and 0.8
      authorizes against the INBOUND view, so the fixture stopped modelling
      production and the TEST should change;
  (B) any host that seeds a peer's token with saveToken loses in-process
      authorization at 0.8, and the PRODUCT should change.

It is (A), but NOT for the reason (A) states, and the difference decides what
the replacement asserts. The fixture's ambient seed is still a faithful model of
logos-liblogos/src/logos_core/module_manager.cpp:336. What the fixture got wrong
is that it COLLAPSED TWO STORES INTO ONE: both ModuleProxys took the defaulted
store, so the "module's" credential was whatever the host's ambient ring held --
the capability bootstrap kCapToken -- instead of the module's own root token.

In production those are two objects in two images, holding ONE value:

  logos-liblogos/src/logos_core/module_manager.cpp:329-336
      the HOST mints authToken, sendToken()s it to the module, and caches it
      OUTBOUND under the module's name.
  logos-module-loader-qt/src/host/module_initializer.cpp:169-170
      the MODULE's image writes that same value under both bootstrapKeys(),
      which is what TokenManager::credential() is derived from.

0.8 closed the OUTBOUND-MAP route into a provider. It left the ANCHOR route
open, deliberately, and logos-protocol pins that at master in
tests/protocol/test_token_direction.cpp:221
(TokenDirection.TheOwnCredentialStillAuthorizesAndStillGatesPushes): a module
authorizes anyone presenting ITS OWN credential. Since the ambient ring's entry
for a module IS that module's credential, the escalation case 1 records is STILL
LIVE at 0.8 -- which is why the fix is to give the backend the store a real
module has, and emphatically NOT to make the call fail.

THE REMEDY NOT TAKEN. Routing the seed through informModuleToken /
saveInboundToken -- reading (A) as literally stated -- files a caller GRANT.
Case 1 would then assert "a caller that was granted may call": trivially true,
green forever, and blind to the escalation the file exists to record. That is
worse than red, and it is the trap this fleet keeps hitting.

WHAT CHANGED IN tests/test_bridge_identity.cpp

  * backendImageStore() -- the backend module gets its OWN TokenManager
    (isolateIdentity + forIdentity + adoptCredential(kBackendRootToken)), the
    same three steps logos-protocol's own ModuleImage helper uses, and
    backendProxy is constructed with the 3-arg ModuleProxy ctor against it.
  * the ambient-ring seed is UNCHANGED -- it was never the thing that was wrong.
  * capability_module DELIBERATELY stays on the ambient ring: its credential is
    the host's kCapToken, which is what lets logos::admitConsumer's
    informModuleToken push clear the trusted-channel gate. Isolating it too
    breaks cases 3-8 for reasons unrelated to any of them.
  * case 1 now pins the MECHANISM, not only the outcome:
        QCOMPARE(TokenManager::instance().getToken("backend_module"),
                 fx.backendStore->credential());     // one secret, two stores
        QVERIFY(fx.backendStore->inbound().keys().isEmpty());  // nothing granted
    plus a null/identity check on the store, because a null would have silently
    defaulted the proxy back onto the ambient ring and made the whole case
    theatre.

── THE CONTROLS. A CONTROL THAT CANNOT FAIL IS NOT A CONTROL ─────────────────

Case 1 asserts that an undeclared backend IS reachable, so "reachable" is its
GREEN state and it cannot be made red by making the backend reachable. What CAN
be shown -- and was, by mutating one thing at a time in the built tree and
re-running -- is that it goes red on every way the ambient ring's shape can
change, and that the tautology substitution is blocked. Five mutations, five
reds, each in a different assertion:

  NULL  unmutated                                   11 passed, 0 failed
  A     ring caches a value no image holds          9/2  -- QCOMPARE (mechanism 1)
        (module_manager.cpp:336 changes shape)              + case 5's ring assert
  B     the two stores collapsed back into one       10/1 -- '!payloadIsError'
        (literally today's fixture)                         with the exact 0.8
                                                            "token not recognized
                                                            (re-exchange failed)"
  C     target INFORMED of the caller instead        10/1 -- inbound().keys()
        (the tautology substitution; the call                .isEmpty()
        still succeeds, and the test refuses
        to call that a pass)
  D     module image never adopts a credential       10/1 -- QCOMPARE (mechanism 1)
        (module_initializer.cpp:169-170 changes)
  E     case 5's admitted consumer is HANDED the     10/1 -- 'payloadIsError'
        ambient entry, so an undeclared backend             ("undeclared backend
        IS reachable with no handshake                       was REACHED: hello")

E is the literal "make it reachable, show RED", answered by the assertion that
owns that claim: case 5 is the polarity case 1 is not. The file carries both, so
it cannot pass vacuously in either direction.

── PRODUCT: NOTHING TO CHANGE HERE ───────────────────────────────────────────

This repo has no saveToken call in production source at all. ui-host/main.cpp:221
adopts its OWN per-spawn credential through logos::adoptConsumerCredential -- the
value the parent minted FOR IT, not a peer's token through the outbound door --
which is the correct direction and is what this branch already did. The one
shipped (B) in the fleet is logos-logoscore-cli's daemon
(src/daemon/daemon.cpp:629, saveToken("cli_client", ...)), fixed separately in
logoscore-cli#104.

── CHECKS, EVERY OUTPUT INDIVIDUALLY, --print-out-paths ──────────────────────

  packages.x86_64-linux.default      34vfy1yshm3hqk06ngsl3dycfwqlqssj-…-1.0.0
  packages.x86_64-linux.tests        38md8bjl93ga42z7d94nxrqjqq8qldx6-…-check-1.0.0
  checks.x86_64-linux.default        38md8bjl93ga42z7d94nxrqjqq8qldx6-…-check-1.0.0
  packages.x86_64-windows.default    kyncplgbrryb40sa8p8dg5037k3qrslq-…-mingw32-1.0.0
  packages.x86_64-windows.tests      jdpgmgi4w7qaxhxzhl5s0kpixbncj7j6-…-check-mingw32
  checks.x86_64-windows.default      jdpgmgi4w7qaxhxzhl5s0kpixbncj7j6-…-check-mingw32

checks.x86_64-linux.default runs the suite in its checkPhase:

  1/10 LogosQmlBridgeUnitTests        6/10 BridgeIdentityTests      Passed
  2/10 LogosQmlBridgeResultTests      7/10 LogosQmlBridgeGuiTests
  3/10 LogosQmlBridgeE2ETests         8/10 LogosQmlBridgeDeferredTests
  4/10 LogosQmlBridgeReplayTests      9/10 UiHostUnloadTests
  5/10 LogosQmlBridgeHandshakeTests  10/10 LogosQmlBridgeCallsTests
  100% tests passed, 0 tests failed out of 10   (38.96 sec)

BridgeIdentityTests itself: 11 passed, 0 failed, 0 skipped. The cross target
compiles tests/test_bridge_identity.exe but nix forces doCheck off for a cross
build, so no ctest ran there.

NOT BUILT: aarch64-linux and both darwin. This builder is x86_64-linux and there
is no darwin remote; all nine of those outputs EVALUATE (drvPath resolved for
packages.default, packages.tests and checks.default on each), which is the part
that catches the pseudo-system eval traps. CI's macos-latest leg covers the rest.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 18:05:23 -03:00
Dario LipicarandClaude Opus 5 b9a6778fff docs: the module-side templates live in logos-view-module now (#25)
* feat(bridge): a QML view's bridge carries ITS identity, not the host's

The QML a view module ships runs inside the host process, and the bridge it
was handed was the host's own LogosAPI — which carries the host's ambient
token ring, holding every loaded module's root auth token. A view could reach
any module in the system with no requestModule in the log at all.

LogosQmlBridge::forIdentity(name) builds the bridge on a LogosAPI bound to
that identity's ISOLATED store, and returns nullptr rather than falling back
to the host's authority. tokenStore() exposes the store a bridge presents
from, because that — not the name — is the identity that decides anything.

tests/test_bridge_identity.cpp is the acceptance test: two real published
providers, an ambient ring seeded the way module_manager seeds it, and a
capability_module that mints only for declared (origin, target) pairs. A
host-identity bridge reaches an undeclared backend with requestModuleCalls
== 0; an identity bridge is refused.

* feat(b4): link the Qt host runtime from logos-plugin-qt, not logos-qt-sdk

The B1 split moved the Qt host runtime — LogosAPI and the provider objects —
out of logos-qt-sdk into logos-plugin-qt, which publishes it as
packages.<sys>.logos-qt-host with the CMake target
logos-qt-host::logos_qt_host. This repo now consumes that target directly, so
B2b can delete the forwarding shim in logos-qt-sdk.

logos-qt-sdk is dropped as an input entirely rather than kept alongside: the
host runtime was the only thing this repo ever took from it. logos_api.h is
the single qt-sdk-provided header anything here includes; every other non-Qt
header on the include lines (token_manager.h, logos_api_client.h,
module_proxy.h, remote_transport.h, logos_instance.h, logos_mode.h,
logos_types.h, logos_json_convert.h, logos_call_error.h, logos_object.h,
logos_provider_interface.h) comes from logos-protocol, which logos-qt-host
links PUBLIC. Nothing here touches the surface that stays behind in qt-sdk —
no logos_ui_plugin_context.h (that is for ui_qml module backends, not for the
host that loads them), no logos_qt_lp_bridge.h / logos_qt_wire.h, no
logos-qt-generator.

LOGOS_QT_SDK_ROOT becomes LOGOS_QT_HOST_ROOT, matching what
logos-standalone-app already passes. Because a find_package that resolves to
the wrong prefix would leave the target undefined and every LogosAPI symbol
unresolved at link time — an error a long way from its cause — the import is
followed by an explicit TARGET check that is a FATAL_ERROR, never a skip.

Evidence the swap is a no-op for the produced artifacts: with the same
inputs, lib/liblogos_view_module_runtime.a is byte-identical (md5
2e205e1668f57f59fd5ccfc005a26600) before and after, bin/.ui-host-wrapped is
byte-identical, the installed headers are identical, and the 317 LogosAPI
symbols in the ui-host binary are unchanged. The only difference in the
derivation is that logos-qt-sdk.drv left the input closure and
logos-qt-host-0.1.0.drv took its place. checks.default runs the same six
ctest cases (LogosQmlBridgeUnitTests, LogosQmlBridgeE2ETests,
LogosQmlBridgeGuiTests, LogosQmlBridgeResultTests,
LogosQmlBridgeHandshakeTests, BridgeIdentityTests), 6/6 passing on both sides.

* docs: name the check that holds these headers to the module side

LogosViewPlugin and LogosViewReplicaFactory are the HOST half of a pair. The
module half is declared separately, by logos-plugin-qt's LogosView*.in
templates, and the two cannot share a header: a module plugin must compile
against Qt alone, and this repo depends on logos-plugin-qt, so the include
could only ever point the wrong way. They bind at runtime through the IID
string, where a mismatch is silent — qobject_cast returns nullptr and the view
never appears.

The module-side copy used to carry a comment asking whoever edited it to keep
these files in sync. It pointed at src/; the files are in include/. That is
what a sync comment is worth.

logos-module-builder — the one repo that depends on both sides — now runs a
`view-interface-abi` check in CI comparing the IID and the pure-virtual list.
These headers say so, so an editor of this half knows what will catch them.

No code change.

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

* docs: say what the ABI check reads, and that nothing here runs it

The note pointed at logos-module-builder's `view-interface-abi` and described
it as comparing "the IID and the pure-virtual list". That undersold it and,
more importantly, it did not say that the argument of Q_DECLARE_INTERFACE
below is itself compared after resolution — that macro argument is the string
qobject_cast matches on, it does not have to be the #define above it, and
changing just that one token is a real divergence.

Also states the thing that is easy to miss from inside this repo: there is no
.github here, so nothing checks these headers at the moment they are edited.
The check first sees a change when logos-module-builder bumps its pin. Editing
this header is a two-repo change.

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

* chore(deps): rev-pin logos-plugin-qt and logos-protocol at the pushed tips

Both inputs were master-tracking, and neither master can build this repo
after the qt-host retarget:

  * logos-plugin-qt's master (8846fc5) has no `logos-qt-host` package at
    all — evaluation died with "attribute 'logos-qt-host' missing" at
    flake.nix:48. cc24fa1 is the tip of that repo's
    feat/b4-qt-host-windows-target, already rebased onto its master, and
    is the SUPERSET of the two branches carrying the qt-host work: the
    sibling feat/b4-qt-host-windows-target-8ccb1fc (989f6ae) drops the
    commits logos-module-builder pins. Pinning the superset is what keeps
    ONE logos-qt-host in the downstream closure rather than two.

  * logos-protocol had to move with it. logos-qt-host calls
    TokenManager::forIdentity / isolateIdentity, which are on
    feat/per-client-token-store (c8bab12) and not on protocol master —
    and because logos-plugin-qt's logos-protocol `follows` THIS input,
    leaving it master-tracking would have built the Qt host runtime
    against a protocol lacking those symbols. c8bab12 is a fast-forward
    from master, so nothing is given up.

Both revs are pushed branch tips, not local-only commits. Drop the revs
once the branches merge.

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

* fix(tests): retarget the two tests master added onto logos-qt-host

Rebasing this branch onto master replayed the qt-host retarget over a
master that had meanwhile added test_logos_qml_bridge_deferred (#20) and
test_logos_qml_bridge_calls (#21). Both link
logos-qt-sdk::logos_qt_sdk, and the retarget commit never saw them, so
the textual auto-merge left them pointing at a target this repo no
longer imports. CMake failed the generate step outright:

  Target "test_logos_qml_bridge_calls" links to:
    logos-qt-sdk::logos_qt_sdk
  but the target was not found.

Retargeted to logos-qt-host::logos_qt_host, the same rename the other
eight executables already carry. Restoring the logos-qt-sdk input was
the wrong fix in the other direction: it would put a second copy of the
Qt host runtime back in the closure, which is what the retarget exists
to prevent. Both tests only include logos_api.h (now from qt-host) and
the protocol headers qt-host links PUBLIC, so nothing qt-sdk-only is
lost.

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

* docs: the module-side templates live in logos-view-module now

Both host-side interface headers named logos-plugin-qt/cmake/LogosView*.h.in,
a path that no longer exists — the view-authoring concerns moved to
logos-view-module.

LogosViewReplicaFactory.h also justified the split with "that repo is upstream
of this one, so the two cannot share a header". That was true of
logos-plugin-qt, which this repo really does depend on. It is false of
logos-view-module: there is no edge between the two in either direction, which
is the actual reason they cannot share a header, and is what
logos-module-builder's view-interface-abi check now says.

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

* chore(deps): track protocol and plugin-qt master, and fix the check that was red

logos-protocol#59 and logos-plugin-qt#19 merged, so both rev pins are retired.

Also repairs this repo's ONLY check, which was already red before the pin work
and would have kept the PR red regardless: tests/CMakeLists.txt linked
logos-qt-sdk::logos_qt_sdk, a target that cannot exist since the split removed
logos-qt-sdk as an input here. The other ten test targets already linked
logos-qt-host::logos_qt_host — 5510acd retargeted the two tests master had added,
then the master merge in 3ef779c brought a third the retarget had not covered.

Confirmed pre-existing rather than pin fallout with a control: a pristine
`git archive HEAD` carrying the OLD pins fails with the byte-identical CMake
error.

checks.default builds green.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:54:10 -03:00
Khushboo Mehta 0d9c677bd6 fix: fix the hot relaods 2026-08-18 11:08:40 +02:00
471dd56640 feat(windows): cross target, and quit ui-host with WM_QUIT instead of hard-killing it (#23)
* feat(windows): wire the view-module-runtime for x86_64-windows

Routes through logos-nix.lib.forAllTargets, widens meta.platforms, and
picks up Qt's host-tool cmake flags (empty natively).

wrapQtAppsHook is GATED rather than removed: it fails to EVALUATE for a
mingw host, and wrap-qt-apps-hook.sh would skip a PE anyway
(`isELF || isMachO || continue`) -- but removing it outright would change
native behaviour. dontWrapQtApps = true is the mandatory other half, or
qtbase's own setup hook hard-errors with "depends on qtbase, but no
wrapping behavior was specified".

Windows and native (aarch64-darwin) both evaluate.

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

* fix(windows): quit ui-host with WM_QUIT instead of hard-killing it

QProcess::terminate() cannot stop ui-host on Windows, so every UI-plugin
teardown paid the full 3s grace period and then TerminateProcess.

MEASURED, not inferred. terminate() on Windows is EnumWindows(WM_CLOSE) plus
PostThreadMessage(tid, WM_CLOSE) (qprocess_win.cpp:648-653). ui-host owns ZERO
windows -- it is a bare QCoreApplication linking no Qt6::Gui at all
(ui-host/main.cpp:70, CMakeLists.txt:78-86); the QQuickWidget lives in the
PARENT and ui-host only remotes the plugin object over QtRO. So the enumeration
finds nothing. A controlled zero, not a broken command: the same enumeration in
the same run found 7 windows for Basecamp, including the two never-shown QPA
observer windows a GUI process must own. The thread message IS delivered, but
Qt's dispatcher only branches on WM_QUIT (qeventdispatcher_win.cpp:545-548) --
WM_CLOSE falls through to a no-op for a thread message. terminate() returns
void, so it "succeeds" having done nothing.

A/B against the shipped ui-host.exe: replicating terminate() byte-exactly left
it ALIVE past the full 3000ms; PostThreadMessage(WM_QUIT) exited it in 8ms with
code 0. End to end through the real teardown path (graceful Basecamp quit with
package_manager_ui loaded), same harness and trigger, only the payload differing:

  shipped   ui-host exit 3905ms  code 62097 (0xF291 Qt kill)  "did not exit gracefully" present
  fixed     ui-host exit  690ms  code 0                       warning absent
  (Basecamp total 4395ms -> 1186ms)

REPLACES terminate() on Windows rather than preceding it. An earlier draft
inserted WM_QUIT above the existing terminate()+3000+kill+1000, which would have
made the worst case 6000ms -- worse than today while claiming to improve it.
Since terminate() was measured to be dead time here, keeping it buys nothing:
the ceiling stays 4000ms and the typical case is now sub-second. This also
matches logos-container-subprocess, which REPLACES its equally no-op
request_exit() rather than supplementing it.

The POSIX path is byte-identical -- the original block is preserved verbatim
inside #else; git diff shows zero removed lines.

Getting the child's main THREAD id is the one subtle part. QProcess does not
expose it, but CreateProcessArguments carries the PROCESS_INFORMATION pointer it
hands to CreateProcess. The modifier runs BEFORE CreateProcess, so dwThreadId is
not populated yet: capture the pointer there, read through it once started() has
fired, and null it immediately -- QProcess owns that allocation and frees it in
cleanup(). No CreateProcessW reimplementation and no Toolhelp32 snapshot needed.

NOT fixed here, deliberately: orphan reaping is absent on Windows. ui-host's
setsid + PR_SET_PDEATHSIG + getppid watchdog are wholly inside #ifndef _WIN32
(ui-host/main.cpp:40-68) and ViewModuleHost assigns no Job Object, so a Basecamp
crash leaks ui-host.exe. That is a real second defect, but it is an untested
process-lifetime change and does not belong bundled into a latency fix.

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

* chore(deps): re-pin the L1-L4 inputs to their merged revs

logos-nix (L1), logos-protocol (L2), logos-cpp-sdk (L3) and logos-qt-sdk (L4)
are all on their default branches now, so the lock can name the merged revs
instead of the pre-merge branch tips it was resolving against while those PRs
were open.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-11 14:49:08 -03:00
Dario LipicarandClaude Opus 5 d26550f157 fix(qml-bridge): stop callModule/callModuleAsync failing on a module that is merely starting (#21)
* fix(qml-bridge): stop callModule/callModuleAsync failing on a module that is merely starting

Both carried the same `if (!client->isConnected())` guard the event path did,
and QML issues calls from the same place it subscribes — Component.onCompleted,
the one moment the dependency's host has been spawned but has not called
listen(). The guard was dead code for years (isConnected() returned a latch that
was always true), so nothing exercised what it does when it goes live: it
returns {"error":"Module not connected"} to a view that will never retry, for
the life of the process.

Deleting the guard alone is not the fix either. Without it the call falls
through to the transport's default acquire budget — 20 s, paid twice because the
token handshake tries capability_module first — on the GUI thread. That is the
~417 s Basecamp stall the isConnected() fix exists to prevent.

The two forms owe their callers different things, so they get different answers:

* callModuleAsync() owes a CALLBACK, so it can wait. A module that is still
  starting is no longer an error: the call is held via the protocol's new
  whenObjectAvailable() and dispatched when the module appears, including one
  installed mid-session. Nothing blocks. The deadline the caller already passes
  (timeoutMs) now bounds the wait for the module as well as the call, so a
  module that never appears still gets an answer instead of hanging politely.

* callModule() owes a RETURN VALUE now, so it cannot wait for a module at all.
  It waits a short bounded time (1500 ms — QtRO retries its endpoint every
  250 ms and a starting module arms in roughly 50-150 ms, so this covers the
  real race several times over) and then answers. Its error now says "Module
  not reachable yet" and names callModuleAsync, because for a view whose first
  paint depends on the answer the async form is the correct tool, not a longer
  timeout here.

The reachability question is asked once, ahead of both failure shapes: an absent
module surfaces as either an acquire CallError or an invalid result depending on
transport and timing, and inferring "still starting" from which shape came back
is what made a startup race look like a broken module.

tests/test_logos_qml_bridge_calls.cpp is new, and is the first coverage this
path has had in either direction. It pins the two contracts separately, drives
the async form through a real QJSEngine (a non-callable QJSValue is silently
ignored, so a C++ lambda would not exercise what QML does), and asserts the
SHAPE of each failure rather than just that one occurred — "some error payload"
is what the broken version returned too. With the guards restored, three cases
go red, two of them reporting the old {"error":"Module not connected"} verbatim.

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

* test(qml-bridge): scope the publish timer so it cannot fire on dead locals

syncCall_modulePublishedDuringTheWait used QTimer::singleShot with by-reference
captures of two function locals. That is only safe while the call under test
waits long enough for the timer to fire inside it — which is exactly the
property the test exists to check, so it does not hold on the un-fixed tree the
test is supposed to go red against. There the call returns immediately, the
function exits, and the detached timer later constructs a Publisher through
dangling references.

Not hypothetical: it segfaulted the red run (`enableRemoting() Error: Unable to
Replicate an object that does not have objectName() set`, empty module name,
then SIGSEGV), and took the four cases after it down with it — so the red run
could not report on the very case that matters most.

The timer is now a scoped QTimer declared after `pub`, so it is destroyed first
and cancels any pending fire while its captures are still alive.

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

* fix(qml-bridge): three defects in the call-path change, and the tests that were missing

A compatibility audit found three bugs in the previous commit. All three shipped
green, because that commit touched one source file and no test file — so the 7/7
it reported said nothing about any of them. Each fix below now has a test that
was shown to fail against the un-fixed tree and pass with it.

1. A TIMED-OUT CALL WAS STILL SENT. The whenObjectAvailable callback ran
   dispatch() unconditionally; the `fired` flag suppressed only the CALLBACK. So
   an install / send / transfer could execute minutes after the view had been
   told it timed out. Holding a call is only at-most-once if the hold is
   abandoned when the caller stops waiting — otherwise it is a delayed re-send
   with extra steps. The new test asserts on the PROVIDER's call count, not the
   callback, because "suppressed" and "never sent" are indistinguishable from
   the callback and only the second is the property claimed.

2. timeoutMs <= 0 COULD STRAND THE CALLER. Documented as "pass 0 to disable".
   With no deadline and an unbounded wait for the module, the callback never
   fired at all. Dispatching immediately instead — the first attempt — was worse:
   it reaches the synchronous acquire inside invokeRemoteMethodAsync and blocks
   the calling thread for the full default budget, which is the GUI-thread stall
   this whole area exists to remove, smuggled back in through the one path that
   opted out of deadlines. "No deadline on the call" now means exactly that: the
   WAIT still carries one, used only to guarantee a callback, never to cap the
   method. The test deliberately never publishes the module, because the earlier
   version of it published one and therefore passed against the broken code too.

3. THE STARTUP BUDGET CAPPED EXECUTION. One Timeout feeds both
   acquireCachedObject and callMethod, so Timeout(1500) capped every method, not
   just the acquire — a network fetch, a package install, a chain RPC all began
   failing at 1500 ms, and old QML shipped inside a .lgx cannot opt out because
   callModule takes no timeout argument. The short budget now applies only when
   the module is NOT reachable, which is the case it was written for; a reachable
   module keeps the budget it always had.

7/7 green, and 3/3 of the new cases red against the previous commit.

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

* chore(deps): re-pin logos-protocol to 0183e8c (#53 + #55)

This branch calls whenObjectAvailable(), which landed in logos-protocol#53. The
lock inherited 07b0fb1 from #20's re-pin, and that revision does not have the
symbol -- the build fails to compile, not to link.

Pinned to 0183e8c rather than to #53's merge commit on purpose. This PR's
callModule advertises a 1500ms bound, and until logos-protocol#55 that bound was
not real: the capability handshake runs FIRST on an un-tokened target and
hardcoded 20000 twice, so a first call could block on the order of 40s before the
part this PR bounds even began. #55 threads the caller's budget through, and
0183e8c is the first master revision that has it. Pinning below it would ship a
guarantee its own dependency does not provide.

VERIFIED FROM THE LOCK, driving this repo's own flake with no override of any
kind -- the workspace flake supplies logos-protocol through `follows` and would
answer for a pin this repo does not use:

    nix build 'path:./#checks.aarch64-darwin.default'
    100% tests passed, 0 tests failed out of 7

Seven now: LogosQmlBridgeCallsTests joins the six that #20 runs.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 15:05:51 -03:00
Dario LipicarandClaude Opus 5 31bbfad97b fix(qml-bridge): defer onModuleEvent instead of refusing it once (#20)
* fix(qml-bridge): defer onModuleEvent instead of refusing it once

A QML plugin subscribes in Component.onCompleted, which runs while the
view is being built -- in Basecamp that is immediately after
PluginLoader spawned the core dependency's host process and well before
that process called listen(). onModuleEvent asked `isConnected()`
anyway, warned once, returned false, and never tried again for the life
of the process. Method calls kept working (they reach the replica by a
path that never asks), so this read as "QML events are broken".

The measured shape, on Windows but platform-independent:

    RemoteTransportConnection: Registry connect attempt started
    LogosQmlBridge::onModuleEvent: "hello_module" not connected   <- same ms
    ... 45 s later ...
    RemoteTransportConnection: Requesting object: "hello_module"  <- the CALL works

Now: no isConnected() probe, no requestObject(), nothing on this path
that can block the GUI thread. The subscription goes to
LogosAPIClient::onEventWhenAvailable and arms when the module becomes
reachable, including a module installed mid-session by the package
manager.

Return-value contract: true now means ACCEPTED, not live. false is kept
only for errors no retry can fix -- no LogosAPI (the existing null-API
test still asserts that), an empty name, or a VIEW module (whose signals
come off its typed replica). Every caller in the workspace was checked:
nothing in production reads the value; the only site that did was
test_2proc_bridge_driver, whose "first subscription proves the
connection is up" probe is replaced by pendingEventSubscriptions()
draining.

De-duplication lives here rather than in the transport, because
lp_subscribe legitimately allows two subscriptions to one event while
QML re-running Component.onCompleted must not double-deliver.

tests/test_logos_qml_bridge_deferred.cpp is the regression guard:
subscribe-before-publish (the bug), publish-before-subscribe (control,
green either way), duplicate-subscribe (was delivering twice), and a
non-blocking budget so a future "just call requestObject() from the
retry" cannot creep back in.

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

* fix(qml-bridge): verify the de-dupe record instead of trusting it

onModuleEvent kept a set of (module, event) pairs it had already subscribed and
short-circuited on it. The set was only ever cleared on the abandon path, so any
other way a subscription could stop being tracked left the bridge believing it
was live: a QML view re-calling logos.onModuleEvent got `true` back and nothing
armed — a permanently silent success, which is the original bug wearing the
fix's clothes.

It now records the subscription id and checks it against
LogosAPIClient::eventSubscriptionState() before short-circuiting. Unknown means
the registry is not tracking it, so the call falls through and re-arms.

Also states on the API what it does not promise: arming is not retroactive and
no transport buffers, so a module that emits a one-shot event synchronously
inside its own init() can still be missed by a view subscribing in
Component.onCompleted.

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

* test(qml-bridge): make the stale-record case actually reproduce the defect

The first version cancelled an ARMED subscription, which by design leaves its
callback attached to the shared handle -- so events kept arriving and the test
passed against the trusting-the-record version too. It now makes the record
stale while the subscription is still PENDING, where nothing is attached, so a
re-subscribe that gets swallowed as a duplicate delivers nothing and the test
goes red. Asserts the cancel hit the right id rather than assuming it.

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

* docs(qml-bridge): answer the three review comments on onModuleEvent

All three were right.

The VIEW-module warning was streamed across << operands with embedded quotes,
and QDebug quotes every QString it is given -- so the one part of the message a
reader is meant to copy came out as logos.module(" "chat_module" "). One
formatted string with noquote(), and the reason recorded at the call site,
because the streamed form looks correct in the source. Deliberately not applied
to the sibling warnings, where << moduleName << eventName WANTS the quoting: it
is what tells an empty name from a missing one.

The "roughly 50-150 ms" arming window was measured on one machine and read as a
contract. Now described as brief and load-dependent, which keeps the two facts a
caller can act on and drops the one they cannot rely on. The advice underneath
is unchanged and is what matters: a module whose one-shot startup event matters
must also expose a method the view can call after subscribing.

The return contract listed three cases; the implementation has five. Both
missing ones are documented rather than tightened away -- "no client for the
module" is LogosAPI failing to build one at all, not the module being down, and
the id == 0 refusal is a guard whose only job is keeping two contracts in
agreement. Deleting it would remove the thing that notices when they stop
agreeing. The comment now also states what was implicit and matters more than
the list: a module that is merely unreachable is still an ACCEPTANCE.

Comment-only apart from the warning's formatting.

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

* chore(deps): re-pin logos-protocol to 07b0fb1 (#47)

This branch calls onEventWhenAvailable, cancelEventSubscription,
eventSubscriptionState and pendingEventSubscriptions. The lock pinned 0f26ffd,
which has none of them, so every green run of this branch until now was produced
with --override-input and the lock itself had never resolved.

07b0fb1 is #47's merge commit. Its narHash is identical to the branch tip the
bridge was verified against, so this pin is byte-for-byte the tree those runs
used, not merely a compatible one.

VERIFIED FROM THE LOCK, which is the part that was missing:

    nix build 'path:./#checks.aarch64-darwin.default'    # no override of any kind
    100% tests passed, 0 tests failed out of 6

Driving the repo's OWN flake matters here. Building the workspace flake's
logos-view-module-runtime target does not test this lock at all: the workspace
supplies logos-protocol to every consumer through `follows`, so it answers a
different question and will happily go green (or red) on a pin this repo does not
use. The first attempt at this verification made exactly that mistake and failed
with "no member named 'onEventWhenAvailable'" while the lock was already correct.

Note the closure still holds two logos-protocol revisions: root and logos-qt-sdk
resolve to 07b0fb1, while logos-cpp-sdk keeps its own, because only qt-sdk
declares `inputs.logos-protocol.follows`. That is the configuration verified
above and it builds clean. Adding the missing follows to logos-cpp-sdk would
collapse it to one revision and is worth doing -- separately, since it changes a
configuration nothing has tested yet.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:06:21 -03:00
Khushboo Mehta 8085c22a18 feat: add pre fetch to model 2026-07-23 09:34:58 +02:00
Khushboo Mehta 96735cb9c8 fix: Pass per-spawn auth token to ui-host via private socket 2026-06-10 21:00:03 +02:00
Khushboo Mehta d611d697bf Revert "fix: handle long module names"
This reverts commit 1b35afb543.
2026-04-17 15:09:42 +02:00
Khushboo Mehta 1b35afb543 fix: handle long module names 2026-04-17 13:56:59 +02:00
Khushboo Mehta 5fcf427b49 feat: let qml only apps subscribe and listen to events from core modules 2026-04-14 15:04:35 +02:00
Khushboo Mehta b4d5cb70f0 move watch as function in bridge so qml doesnt need to make new imports 2026-04-10 16:02:21 +02:00
Khushboo Mehta d9fa197f87 feat: add src headers and flake files needed 2026-04-08 22:27:19 +02:00