Commit Graph
424 Commits
Author SHA1 Message Date
Dario Gabriel LipicarandClaude Opus 5 432f6663c0 fix(plugins): read object-form core dependency entries, refuse the rest
The load gate now understands both forms the LGX spec allows, and reports
what it cannot understand instead of walking past it.

Green — 14/14, was 9/14:

  PASS   : object_entry_yields_its_name()
  PASS   : object_entry_without_constraints_yields_its_name()
  PASS   : json_object_entry_yields_its_name()
  PASS   : hash_object_entry_yields_its_name()
  PASS   : a_number_is_unrecognised()
  Totals: 14 passed, 0 failed

Two behaviour changes at PluginLoader::loadCoreDependencies:

* {"name": "wallet_module", "version": "^2.0.0", "signer": "did:…"} now
  loads wallet_module. Before, QVariant::toString() returned a null
  QString for it and the `if (depName.isEmpty()) continue;` skipped the
  dependency with no error and no log line — the ui plugin then mounted
  on top of a core module that was never loaded, and the first symptom
  was a dead endpoint somewhere else entirely.

  Latent today only because the module ABI still sends bare strings
  (package_manager_impl.cpp's toLogosMap flattens the constraint away).
  It arms itself the moment that is widened. Every other reader in the
  fleet already handles both forms — logos-module's module_metadata.cpp,
  logos-standalone-app's mainwindow.cpp, logos-cpp-sdk's
  metadata_dependencies.h, lgpm's manifest scan. Basecamp was the outlier.

* An entry that is neither a name nor an object with a string "name" is
  now a hard, logged load failure rather than a silent skip. Same reason:
  we cannot honour a dependency we cannot name, and mounting anyway hides
  it. This also stops a stray scalar being stringified into a module
  name — QVariant(42).toString() is "42", and the old loop would have
  gone looking for a module called that.

Unreachable from `lgpm install` today: logos-package's Manifest::fromJson
rejects a non-string/non-object entry outright, and lgpm's own scan warns
and drops what gets past it. The branch exists for the manifests that
bypass both — embedded installs written at build time, and anything
edited in place afterwards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 11:14:46 -03:00
Dario Gabriel LipicarandClaude Opus 5 c96ef04be6 test(plugins): pin what the core-dependency loader actually reads
PluginLoader::loadCoreDependencies read each `dependencies[]` entry as

    QString depName = dep.toString();
    if (depName.isEmpty()) continue;

QVariant::toString() on a QVariantMap returns a NULL QString — no
diagnostic, no exception. So the moment the module ABI is widened to send
the object form {"name": …, "version": "^2.0.0", "signer": "did:…"} that
the LGX spec already allows and lgpm already parses, every CONSTRAINED
core dependency is silently skipped and the ui plugin mounts on top of an
unloaded dependency.

This commit changes no behaviour. It gives that decision a name
(logos::readDependencyEntry) and a test, so the next commit's fix is
visible as a diff in outcomes rather than a diff in expressions.

Red, as expected — 5 of 14 cases fail against the extracted behaviour:

  FAIL!  : object_entry_yields_its_name()                    kind 1, want 0
  FAIL!  : object_entry_without_constraints_yields_its_name() kind 1, want 0
  FAIL!  : json_object_entry_yields_its_name()               kind 1, want 0
  FAIL!  : hash_object_entry_yields_its_name()               kind 1, want 0
  FAIL!  : a_number_is_unrecognised()                        kind 0, want 1

(1 = Unrecognised, i.e. skipped. The last one is the mirror image: a
non-string scalar is currently stringified and loaded as a module name.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 11:03:29 -03:00
Roman ZajicandCopilot Autofix powered by AI ec5579830c Test/pre-seed test_qml_only app for integration-test boot (#343)
* ci(tests): run shutdown-test in the PR gate and enforce the combined suite time budget

* test(harness): add shared fixtures (LGX generator, G-ERR/G-ALIVE/G-EXIT gates, xfail runner) and rea

* test(ui): assert welcome page state on first launch before any interaction (A1)

* fix(tests): retry the A1 greeting assertion so it cannot race the async launcherApps refresh

* ci(tests): tee the app log into BASECAMP_APP_LOG so the ui-tests G-ERR gate scans real output

* fix(tests): typename

* fix(tests): shutdown test to follow behavior from 3a73d33

* fix(tests): reduce comments

* fix: potential flakiness

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: replace full QML tree serialization

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: file URL construction

* fix: use bash explicitly

* test(qml): add spec §4.1 automation objectNames to QML sources

Add the stable automation handles enumerated in spec §4.1, following
the existing sidebar.app.<name> / confirmationDialog.<mode>.<button>
convention: welcomePage.installNow, appManager.{searchField,
category.<name>,emptyView}, appContextMenu root,
addApplicationDialog.{primaryButton,closeButton,errorText,
resolutionBanner}, settings.searchField,
pluginInterface.{call.<method>,result,back}, repositories.{urlField,
addButton,refreshButton,errorBanner,errorDismiss,row.<url>,
removeConfirm.{confirm,cancel}}, and sidebar.buildLabel.

workspace.dock.<name> is deliberately not applied: the dock root's
objectName is pinned to the bare module name by
workspace_area_test.cpp and needs a coordinated rename (recorded as
a finding).

* test(fixtures): pre-seed test_qml_only app for integration-test boot

Add fixture A (test_qml_only v0.1.1, ui_qml) and a seedPlugin(userDir,
fixture) helper to tests/fixtures/lgx.mjs, and seed it into
<user-dir>/plugins/ in nix/integration-test.nix before the app launches
(MCP-TEST-PLAN v2 §0.A/§4.7). package_manager's installed lists are live
directory scans for <name>/manifest.json, so the seeded fixture shows up
in launcherApps at boot without any install IPC, giving sidebar/dock
tests a real launcher app. A1 takes its "Welcome back" branch.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-26 17:49:29 +08:00
Roman ZajicandCopilot Autofix powered by AI f6bca12270 Test/MCP preparation UI tests (#338)
* ci(tests): run shutdown-test in the PR gate and enforce the combined suite time budget

* test(harness): add shared fixtures (LGX generator, G-ERR/G-ALIVE/G-EXIT gates, xfail runner) and rea

* test(ui): assert welcome page state on first launch before any interaction (A1)

* fix(tests): retry the A1 greeting assertion so it cannot race the async launcherApps refresh

* ci(tests): tee the app log into BASECAMP_APP_LOG so the ui-tests G-ERR gate scans real output

* fix(tests): typename

* fix(tests): shutdown test to follow behavior from 3a73d33

* fix(tests): reduce comments

* fix: potential flakiness

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: replace full QML tree serialization

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: file URL construction

* fix: use bash explicitly

* test(qml): add spec §4.1 automation objectNames to QML sources

Add the stable automation handles enumerated in spec §4.1, following
the existing sidebar.app.<name> / confirmationDialog.<mode>.<button>
convention: welcomePage.installNow, appManager.{searchField,
category.<name>,emptyView}, appContextMenu root,
addApplicationDialog.{primaryButton,closeButton,errorText,
resolutionBanner}, settings.searchField,
pluginInterface.{call.<method>,result,back}, repositories.{urlField,
addButton,refreshButton,errorBanner,errorDismiss,row.<url>,
removeConfirm.{confirm,cancel}}, and sidebar.buildLabel.

workspace.dock.<name> is deliberately not applied: the dock root's
objectName is pinned to the bare module name by
workspace_area_test.cpp and needs a coordinated rename (recorded as
a finding).

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-26 16:52:28 +08:00
Dario LipicarandClaude Opus 5 1e07e9887e refactor(plugins): admit consumers through the shared verb (#359)
* refactor(plugins): admit consumers through the shared verb

Replaces the hand-rolled isolate/mint/register sequence with
logos::admitConsumer. Net -16 lines.

This is where the duplication bug lived: the registration used to sit
inside the has-a-backend branch, below an early return, so a pure-QML
plugin registered nothing and called out on the host's ambient ring. That
worked only because the ring already held every token and the handshake was
never reached — and logos-protocol #71 removes the ring, so it would now be
a hard failure rather than a silent elevation.

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

* fix(build): link the view-module-runtime archive before the qt host

The Windows cross-build of this branch failed to link, and it is the only
platform that could have told us:

  liblogos_view_module_runtime.a(LogosQmlBridge.cpp.obj):
    undefined reference to `LogosAPI::forIdentity(QString const&, QObject*)'

The symbol is not missing. It is present and correctly mangled in both halves of
the host package — nm shows T _ZN8LogosAPI11forIdentityERK7QStringP7QObject and
the matching __imp_ thunk in liblogos_qt_host.dll.a — and the archive asks for
exactly that name. What was wrong is the ORDER.

liblogos_qt_host.dll.a is an IMPORT library, so GNU ld treats it like any other
archive: it takes only the members that satisfy references already undefined
when it reaches them. logos_qt_host_shared sat AHEAD of
LOGOS_VIEW_MODULE_RUNTIME_LIB, so when ld walked the host nothing had asked for
forIdentity yet, the member was skipped, and the archive's demand a moment later
had nowhere left to resolve. This is the same rule the comment on logos_core
below already spells out for four other symbols; the host simply had not needed
it yet.

It had not needed it because something else was holding the old order up. While
app/PluginLoader.cpp called LogosAPI::forIdentity itself, basecamp's own objects
demanded the symbol before ld ever reached the host, the member was pulled in
early, and the archive's later reference resolved against it for free. Admitting
consumers through logos::admitConsumer — the commit right before this one —
removed the last first-party call, and with it the accident. So the defect is
latent-made-live, not new: the link line has been wrong for as long as the
archive has referenced a symbol it does not define.

Linux and macOS cannot see any of this. There the host is a real shared object,
and shared libraries satisfy undefined references regardless of position, which
is why the full x86_64-linux matrix — 19 outputs, every check — was green with
the broken order. A green Linux build is not evidence about this line.

Measured on a 24-core x86_64-linux box, cross-building x86_64-windows:

  before  packages.x86_64-windows.symbol-gate            FAILS to link
          symbol-gate-negative / bin-bundle-dir / default  all FAIL, same cause
  after   packages.x86_64-windows.symbol-gate            OK

and basecamp master cross-builds the same output green on the same box, which is
what established this as a regression of this branch rather than a broken
toolchain.

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

* chore(deps): move onto logos-protocol 0.8 and logos-plugin-qt master

logos-plugin-qt#26 made protocol 0.8 a HARD FLOOR for every consumer of
logos-qt-host — cpp/logos_provider_object.cpp and cpp/qt_provider_object.cpp
call TokenManager::saveInboundToken unguarded — so plugin-qt and protocol move
together or not at all.

  logos-protocol   2e3344ac -> 42460e5b  (0.7 -> 0.8, protocol#73)
  logos-plugin-qt  1aa3e31c -> 048152f2  (plugin-qt#26)
  logos-liblogos   eeb5cd32 -> 5c095129  (liblogos#186, NOT MERGED YET)

liblogos is a branch pin because #186 is the liblogos half of this same wave and
is still open; retire it for a plain master URL once that lands.

This PR's CI was last seen failing at logos-co/setup-nix-cache-action with HTTP
502, every build step `skipped`, which made it look like pure infrastructure. It
was not only that. Building the pre-relock tree of THIS branch on a real box
reproduces a second, independent failure — the same one logos-standalone-app#42
had:

  app/PluginLoader.h:12:10: fatal error: logos_consumer.h: No such file or directory

logos_consumer.h arrives with #26, so admitting consumers through the shared verb
could never have compiled against plugin-qt 1aa3e31c. The 502 hid it.

TWO follows added on logos-liblogos, and the honest status of both is that they
are REDUNDANT TODAY. That was measured, not assumed: against the pin above,
liblogos#186's lock already names plugin-qt 048152f2 — this root's rev — so
adding or removing them leaves 6 logos-qt-host derivations and exactly ONE in the
built runtime closure, unchanged. Today the two sides agree by coincidence of two
locks rather than by constraint.

They are added because the next step breaks that coincidence, which was measured
too. nix/app.nix:729 copies ${logosLiblogos}/lib/*.{so,dylib,dll} over this app's
own lib/ while the binary links logos-qt-host directly. Point this input back at
plain master — exactly what retiring the pin does — and liblogos resolves
logos-plugin-qt through its own lock again: liblogos master bfbb1998 pins
1aa3e31c and packages a lib/liblogos_qt_host.so at kdz79ljg… against this root's
ka5vgzb8…, differing byte-wise. The derivation count goes 6 -> 7. The app would
LINK one host runtime and SHIP another in the same directory.

Closure audit on packages.x86_64-linux.default (384 paths):

  logos-qt-host   1  ka5vgzb8…-logos-qt-host-0.1.0
  logos-protocol  1 derivation at 0.8, 3 outputs (lib, headers, join)

and lib/liblogos_qt_host.so compares BYTE-IDENTICAL to the one in that store
path, so the linked host and the shipped host are one image rather than two that
merely agree. liblogos_protocol.so is the sole definer of TokenManager:: —
LogosBasecamp and ui-host define none of it.

logos-view-module-runtime is deliberately NOT given the same follows. It is still
7cbc5a6e, built against plugin-qt ef11c210 and protocol 79894727, so this build
does link a 0.7-era static archive against a 0.8 host. That seam was checked
rather than waved through:

  * LAYOUT is safe. LogosAPI's private members are identical between ef11c210
    and 048152f2 — same five members, same order, no new virtuals — and 0.8
    freezes TokenManager's layout by design.
  * BEHAVIOUR is safe HERE, for a specific reason. 048152f2 changes
    LogosAPI::forIdentity to create the private store EMPTY where it used to be
    bootstrap-seeded, and vmr's include/LogosQmlBridge.h:64 still documents the
    old contract. But nothing reaches it: basecamp constructs the bridge directly
    at app/PluginLoader.cpp:292, `new LogosQmlBridge(consumer.api, this)`, on the
    API logos::admitConsumer already credentialed. vmr's LogosQmlBridge::forIdentity
    factory is dead code in this consumer; it is only in the link line because it
    shares an object file with the constructor that is used.

Give vmr the follows when vmr#27 moves it to 0.8 — not before, since that PR
owns the migration.

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

VERIFIED BY BUILDING, each output individually with --print-out-paths on a
24-core x86_64-linux box. 22 of 23 non-empty:

  x86_64-linux packages  default app main-ui-plugin package-manager-ui-plugin
                         bin-bundle-dir bin-bundle-dir-inspector bin-appimage
                         mcp-server logos-qt-mcp coverage
  x86_64-linux checks    smoke-test symbol-gate symbol-gate-negative unit-tests
                         qml-tests sandbox-test host-services-test
                         integration-test
  x86_64-windows         symbol-gate symbol-gate-negative bin-bundle-dir default

symbol-gate passes on BOTH targets, and Windows is the one that counts: PE has no
symbol interposition, so a duplicate runtime that Linux collapses to one is fatal
there. symbol-gate-negative — the planted-duplicate control — passes on both too,
so the gate is still capable of failing.

The single red is checks.x86_64-linux.shutdown-test, and it is PRE-EXISTING, not
a regression of this branch. basecamp master builds the same check on the same
box and fails it identically — "Linux: Window.close() quits (Alt+F4 / X button
convention) ... FAIL — did not exit within 10000ms", 3 passed / 1 skipped /
1 failed, byte for byte the same shape. It needs a window manager this
environment does not have. Note also that no CI job builds it: build.yml runs
unit-tests, qml-tests, sandbox-test, integration-test, host-services-test and the
symbol gates, and never shutdown-test.

NOT COVERED HERE, and this box cannot cover it — stated rather than implied:

  * aarch64-linux — both build-appimage and test-linux matrix legs
  * macOS — build-macos-app, test-macos, and the *-bundle outputs only they
    build (integration-test-bundle, host-services-test-bundle, smoke-test-bundle)
  * the build-windows job's zip packaging step and its staged-file count
    assertion; the three nix builds it runs are covered, the packaging is not
  * doctests.yml on both ubuntu-latest and macos-latest
  * the three Jenkins packaging jobs (jenkins/prs/package/linux/{x86_64,aarch64}
    and macos/aarch64), which reported nothing at all on the last run

* bump

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 22:29:25 -03:00
Dario Gabriel LipicarandClaude Opus 5 6362789960 chore(deps): relock the package-manager repos, now that they are cut too
The previous relock took this lock to 10,215 nodes and named what was left:
logos-package-manager-ui alone reached ~9,700 of them, and no bump on this
side could touch it, because that repo and its two siblings were still
pinning pre-cut copies of logos-module-builder internally.

Those three have now had the same treatment upstream — one stale pin each,
plus a second pass in the UI once its siblings landed:

    logos-package-manager-module      3,178 nodes ->   379
    logos-package-downloader-module   3,178 nodes ->   379
    logos-package-manager-ui          9,751 nodes -> 1,123

So this side is a three-input bump with nothing clever in it:

    logos-package-manager-ui          6bf15008 -> e3bbf3d5
    logos-package-manager-module      63a6f8ab -> cb1ca3c9
    logos-package-downloader-module   1a47ae68 -> 50dafbf3

    10,215 nodes / 259,003 lines  ->  1,587 / 36,026

Across the whole sequence this lock goes 19,832 -> 1,587 nodes and
507,875 -> 36,026 lines: -92% and -93%.

Only those three of the 21 root inputs move; the other eighteen resolve to
the same revs, and flake.nix is untouched.

Worth recording why it took this shape. The first attempt was deep
`follows` declared HERE, describing three other repos' internal wiring. It
reached -85% and was rejected twice on evidence: once when this branch
moved and the derivation-graph proof stopped holding — the same follows
then broke main_ui's dylib load — and once when a parent overriding
logos-capability-module, which is exactly how logos-workspace consumes this
flake, silently dragged a build-time liblogos eight days older. Fixing it
upstream has neither failure mode: four repos each dropped one stale pin,
and nothing here encodes anyone else's structure.

Verified on the merged master (c8b6189): bin-macos-app builds, and
shutdown-test, smoke-test, symbol-gate, symbol-gate-negative, unit-tests,
qml-tests, sandbox-test, integration-test and host-services-test are all
green. shutdown-test and smoke-test are the two that earn their keep here —
they are what caught the earlier follows breaking main_ui while unit-tests
stayed green throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 08:59:15 -03:00
Dario Gabriel LipicarandClaude Opus 5 e5a4be41a4 feat(ui): let an in-process UI plugin finish before its widget is destroyed
teardownUiPluginWidget destroyed the widget immediately. Core modules and
views (ui-host) both got a grace period when the teardown hook shipped;
in-process `type: ui` plugins did not.

WHY THIS PATH IS NOT THE OTHER TWO. The shipped helper runs a nested
QEventLoop, which is correct where it is used: logos_host calls it after
QtApp::exec() has returned and ui-host after app.exec() has, so in both
cases there is no outer loop left. Here there is. This runs on the LIVE UI
thread, from a user action, in the middle of widget destruction —
pluginWindowRemoveRequested, component->destroyWidget, deleteLater. Spinning
a nested loop there is the re-entrancy hazard that already cost this
codebase a SIGSEGV in the wallet's QtRO read stack. So Asynchronous DEFERS
rather than blocks: connect unloadFinished(), arm a deadline, and run the
existing teardown body from whichever fires first.

WHAT ui_qml CANNOT DO, and why that is not a gap. For ui_qml there is no
in-process plugin object to ask: PluginLoader emits pluginLoaded(...,
nullptr, UiQml, viewHost) — the component is null because the plugin is
QPluginLoader-loaded inside the ui-host CHILD PROCESS. Basecamp holds a
QQuickWidget and a QProcess wrapper. No host-side call from here could ever
reach it, which is exactly why that path's grace period lives in ui-host
instead (logos-view-module-runtime#26). unloadHookTarget() encodes that:
legacy plugins have a target, ui_qml returns nullptr and the teardown
proceeds as before.

Consequences handled rather than hoped for:
  * Idempotence, which this function documents and callers rely on: a
    second call while a deferral is in flight neither starts a second
    teardown nor tears down underneath the first.
  * The widget is held by QPointer across the deferral, not raw — anything
    else may destroy it meanwhile, and a stale raw pointer would be a
    use-after-free rather than a skipped teardown.
  * Both the signal and the deadline are disconnected on whichever arrives
    first, so the body cannot run twice.
  * The synchronous path — no hook, or Synchronous — is byte-for-byte what
    it was. That is the common case and it stays free.

The trade-off is stated on the declaration: a caller that needs the widget
gone before it proceeds cannot get that guarantee from this function any
more. Blocking to restore it is the thing that is not available here.

unit-tests and shutdown-test green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 21:00:41 -03:00
Dario Gabriel LipicarandClaude Opus 5 819a586cab docs: retire references to files, outputs and pins that no longer exist
Sweep of documentation and comments the shell split and the pin retirement
made false. No behaviour change except one dead nix binding, below.

flake.nix
  * Deletes `appImage = import ./nix/appimage.nix {...}`. That file is NOT in
    the tree; the binding survived only because nix is lazy and nothing ever
    forced it. Anyone referencing `appImage` would have hit a file-not-found at
    eval. The shipped AppImage is the `bin-appimage` output, built by
    nix-bundle-appimage.
  * Four stale `Rev-pinned:` comment blocks -- logos-module-loader-qt,
    logos-liblogos, logos-capability-module, logos-package-manager-ui. None of
    those inputs carries a rev or ref any more; 3c21c1f retired the pins and
    left every explanation behind. The logos-package-manager-ui one was a
    DUPLICATED pair of near-identical paragraphs describing two different revs
    of the same dead pin.

    Rewritten rather than deleted, because two carried constraints that outlive
    the pin and would be expensive to rediscover: capability_module fails CLOSED
    without `token_registry` / `token_delivery`, and package-manager-ui is
    loaded IN-PROCESS so it must match the host runtime's generation.

nix/coverage.nix
  * --filter now lists BOTH app/ and src/. The split moved AppsFilterProxy,
    ModulesFilterProxy, InstallEnums, ShortcutBridge and WorkspaceArea into
    src/, and all five are still compiled into unit-test binaries via srcdeps --
    so their .gcno/.gcda were produced and then discarded, and four of the ten
    test binaries contributed nothing to the published numbers. failUnderLine
    defaults to 0, so this cannot break the build.
  * Its scope note still listed MainContainer as an app/ source.

app/CMakeLists.txt
  * The logos_core-last rule is load-bearing on the mingw link and its comment
    justified it by LogosSharedFromDll.cmake emptying the static archives --
    a file deleted in #348, and contradicted by the comment 14 lines below.
    The rule stands; only its stated reason was gone, which is precisely how
    someone deletes it and gets four undefined references.

docs/project.md
  * The app/ tree listed LogosQmlBridge.h/cpp, mdiview.h/cpp, mdichild.h/cpp
    and an app/qml/ subtree of nine QML files. None exists; the QML lives at
    src/Basecamp/. Replaced with the sources actually there.
  * The nix/ listing named appimage.nix, macos-bundle.nix and macos-dmg.nix --
    none of which exists -- and omitted symbol-gate.nix, which :193 relies on
    as the thing enforcing the shell boundary.
  * MainContainer's section still said app/; 9b8cf6e recorded R100 into src/.
    It also said MainContainer creates MainUIBackend, which Window now owns.
  * LogosQmlBridge's section cited app/ paths for an EXTERNAL header that
    arrives from a flake input.
  * MdiView / MdiChild sections describe classes that no longer exist; the role
    is src/WorkspaceArea.
  * Advertised a `.#bin-macos-dmg` output that is not defined anywhere.

tests/shutdown-tests.mjs
  * Pointed at app/main.cpp:224-254 as "the orderly teardown". Those lines are
    startup. The reference was already wrong when written and has since moved
    twice, so it now names the block instead of line-numbering it.

doctests/basecamp-modules-bundle.test.yaml
  * "Basecamp's *own* shell is deliberately NOT here: it is carried as a
    main_ui plugin again" -- a fold-era clause left in front of its own
    replacement, negating the rest of the sentence and the assertion below it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 19:56:02 -03:00
Dario Gabriel LipicarandClaude Opus 5 c8b6189e92 chore(deps): take the cycle cut that already landed upstream
The follows merged in #353 stopped this lock unrolling two private copies
of the package-manager flakes, and left 13,476 nodes — most of them the
logos-module-builder cycle: capability-module -> module-builder ->
standalone-app -> liblogos -> capability-module, which Nix unrolls one
fresh copy of per lap.

That cycle has since been cut UPSTREAM, which is where it always belonged.
The three repos in it are now small on their own masters —
logos-standalone-app 252 nodes, logos-capability-module 130,
logos-liblogos 211 — so this repo does not need to describe three other
repos' internal wiring to get the benefit. It just needs to stop pinning
pre-cut revisions:

    logos-capability-module   c670f7f2 -> 973f71ba
    logos-liblogos            b2a9a0ba -> eeb5cd32
    logos-package-manager     3ceabe25 -> 7c5aad9a

    13,476 nodes / 346,113 lines  ->  10,215 / 259,003   (-24%, -48% from
                                                          the original)

Only these three of the 21 root inputs move; the other eighteen resolve to
the same revs. Worth naming why the win is smaller than the upstream
numbers suggest: logos-package-manager-ui (9,722 nodes),
logos-package-manager-module and logos-package-downloader-module are
ALREADY at their masters here, and their own locks still pin pre-cut
capability/liblogos internally. A bump on this side cannot reach those —
they collapse when those repos relock, not when this one does.

Kept separate from #353 deliberately. That change moved no versions at all,
which is what made it safe to verify once; this one moves three, and
bundling version movement into an unrelated change is how #350 went red
across four jobs earlier — when CI fails you cannot tell which half did it.

Verified on 5caa8fc: bin-macos-app builds, and shutdown-test, smoke-test,
symbol-gate, symbol-gate-negative, unit-tests, qml-tests and sandbox-test
are all green. shutdown-test and smoke-test are the two that matter — they
are what caught a previous lock change breaking main_ui's dylib load while
unit-tests stayed green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 19:28:49 -03:00
Dario LipicarandClaude Opus 5 2c32cfef22 fix(ci): run the symbol gate — on Windows too, where it matters (#354)
* fix(ci): run the symbol gate, and teach it the Windows layout

The gate was exposed in flake.nix `checks` and built by nothing. Both
workflows enumerate individual `nix build .#<output>` steps by hand and no
`nix flake check` runs in this repo, so `git grep -E 'symbol-gate|flake check'
-- .github/` returned nothing: the one control that mechanically enforces the
one-runtime invariant was dead code. A PR relinking the logos runtime into
main_ui would have merged green — and on Linux and macOS it would have RUN
green too, because both interpose a duplicate definition away at load time.

Two things were wrong with the gate itself, both specific to the platform
where a duplicate is actually fatal:

  * the peer-image sweep globbed `$ROOT/lib` for *.dylib/*.so only. On Windows
    every liblogos_* shared image is staged into bin/ as a .dll, so the sweep
    matched nothing, no definer was found, and the exactly-one assertion
    reported "0 definers" for TokenManager, LogosAPI and LogosAPIClient — it
    could not pass on a correct tree. Its three sibling probes (provider,
    negative control, consumer) had each been taught bin/*.dll; this one had
    not.

  * `nm -D` was selected for every non-Darwin target including the mingw
    cross, and a PE has no ELF dynamic symbol table. Measured against a real
    mingw PE (libffi-8.dll, binutils 2.46): `nm -D` reads 0 lines and errors,
    plain `nm` reads 687, `nm --defined-only` 686. valid() would have caught
    that and aborted the gate as vacuous rather than passing it, so this was
    fail-closed — but the gate would never once have run on Windows.

x86_64-windows is not wired into CI here: that attribute cannot be evaluated
as pinned (see flake.nix's binBundleDir note), a pre-existing blocker. The
gate now understands the layout, so wiring it is a one-line follow-up.

Also drops two stale comments that pointed at LogosSharedFromDll.cmake, the
single-provider shim deleted in #348, and the claim that liblogos_core is the
provider — liblogos_protocol and liblogos_qt_host define these types now, which
is exactly why the assertion is exactly-one rather than naming an owner.

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

* fix(symbol-gate): call nm and c++filt by their target-prefixed names

The cross bintools installs ONLY x86_64-w64-mingw32-nm and
x86_64-w64-mingw32-c++filt; there is no bare `nm` or `c++filt` on PATH inside
the Windows derivation. Every measurement therefore produced nothing, and
valid() refused to assert over an empty read -- so the gate got as far as
identifying the right images and then stopped:

  provider  = bin/liblogos_core.dll
  consumers = bin/LogosBasecamp.exe
              plugins/package_manager_ui/package_manager_ui_replica_factory.dll
              plugins/main_ui/main_ui.dll
  == each runtime type is defined by EXACTLY ONE image ==
    liblogos_core.dll   ERROR: nm read 0 symbols — vacuous

That is the vacuity guard working exactly as intended: fail-closed rather than
report a reassuring zero. But it meant the gate could never actually run on
Windows. The tool was missing, not incapable -- the host's own nm reads these
PEs fine (7350 symbols out of liblogos_core.dll).

stdenv.cc.targetPrefix is "" natively, so this is a no-op on Linux and macOS.

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

* fix(symbol-gate): do not count PE import thunks as definitions

With nm reachable, the gate ran on Windows for the first time and reported a
split-brain that is not there:

  TokenManager    4 definers: liblogos_core.dll(3) LogosBasecamp.exe(1)
                              liblogos_protocol.dll(41) liblogos_qt_host.dll(5)

liblogos_core.dll defines ZERO runtime symbols by design, and measures 0 on
macOS. The Windows numbers are an artifact of PE: for every imported function
GNU ld synthesizes a jump stub in .text AND an __imp_<mangled> slot in the
import address table, and `nm --defined-only` reports the stub as `T`. The
pairing is visible directly:

  I __imp__ZN12TokenManager8instanceEv
  T TokenManager::instance()

So the filter is that pairing: a symbol counts as DEFINED only when the image
has no __imp_ slot for it. That is the right discriminator rather than merely a
working one -- a genuine second copy statically linked into an image has no
__imp_ slot and still counts, which is exactly the case this gate exists to
catch.

The PE export table would also have suppressed the phantom (liblogos_core.dll
exports 0 TokenManager symbols, liblogos_protocol.dll exports 45), and it was
the first thing I reached for. It is the wrong tool here: a real private copy
is absent from the export table too, so it would trade this false positive for
a false negative.

Measured on the cross build, the filter reproduces the macOS shape exactly:

  liblogos_core.dll          TokenManager 0    LogosAPI/Client 0
  liblogos_protocol.dll                   41                   109
  liblogos_qt_host.dll                     0                    29
  LogosBasecamp.exe                        0                     0
  main_ui.dll                              0                     0
  package_manager_ui_replica_factory.dll   0                     0

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

* ci(windows): run the symbol gate where it actually matters

The gate now evaluates, builds and passes for x86_64-windows, so wire it into
build-windows and drop the caveat the other two jobs carried saying it could
not be. PE has no symbol interposition, so a duplicate runtime that Linux and
macOS silently collapse to one is fatal only here -- and CI cross-builds this
bundle and never runs it, which makes a build-time assertion the only signal
that exists.

Measured on a real cross build (needs an x86_64-linux builder; logos_build_info.h
is an x86_64-linux derivation, so this cannot run from an aarch64-darwin host):

  TokenManager     1 definer: liblogos_protocol.dll(41)   OK
  LogosAPI         1 definer: liblogos_qt_host.dll(32)    OK
  LogosAPIClient   1 definer: liblogos_protocol.dll(122)  OK
  TIER 1  LogosBasecamp.exe 0 · main_ui.dll 0 · pmui_replica_factory.dll 0
  SYMBOL GATE: PASS

and the negative control rejects a planted duplicate on PE too, catching 41
TokenManager and 122 LogosAPIClient definitions including the one that names
the failure exactly:

  guard variable for TokenManager::instance()::instance

That control is what makes the pass mean something. It also proves the __imp_
thunk filter does not over-reach: a genuine private copy is still detected at
full strength.

Also corrects the binBundleDir note, which was stale in BOTH of its claims --
logos-package-manager-ui not cross-compiling, and the separate EVAL-time
blocker in logos-package-downloader-module. Both were fixed upstream. The
bundle the gate just ran against contains package_manager_ui, so the tree the
note's measurements describe is missing a plugin that is no longer missing.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 19:15:45 -03:00
Dario Gabriel LipicarandClaude Opus 5 da5da842b9 fix(shell): stop reading the package_manager_ui widget after the host frees it
m_pmuiWidget was a raw QWidget* pointing at a widget the HOST owns and frees:
UIPluginManager::unloadUiModuleImpl and ::teardownUiPluginWidget each emit
pluginWindowRemoveRequested and then deleteLater() it. The shell's handler
returned early for exactly that widget without clearing the pointer, so after
an unload it was read at four sites as a freed pointer, and `!m_pmuiWidget`
never became true again -- onSectionIndexChanged(2) therefore never re-issued
loadUiModule("package_manager_ui").

QPointer alone is not the whole fix. The widget had replaced the placeholder at
kModulesStackIndex, so freeing it also takes a page out of a stack that every
section switch indexes into by constant: setCurrentIndex(kModulesStackIndex)
then addresses a two-page stack and the Package Manager section is dead for the
rest of the session. The removal path now puts the placeholder back, which also
restores the exact startup state that onPluginWindowRequested already knows how
to consume -- it looks up widget(kModulesStackIndex) and swaps it out.

Placeholder construction moves into one helper used by both paths, so the
startup page and the restored page cannot drift.

This is the same guard the host applies to its own widget maps
(m_uiModuleWidgets / m_qmlPluginWidgets are QPointer); the shell's copy of the
pointer had not been given it. Reachable from the UI: package_manager_ui is
listed by UIPluginManager::uiModules(), and the Apps Inspector row wires
onAppUnloadRequested to backend.unloadUiModule(name).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 18:29:43 -03:00
Dario Gabriel LipicarandClaude Opus 5 5caa8fc619 chore(deps): stop unrolling two private copies of the package-manager flakes
flake.lock is 19,832 nodes / 507,875 lines. 7,018 of those carry their own
logos-nix edge, and therefore their own nixpkgs. For contrast,
logos-view-module-runtime costs SEVEN nodes — it is the one input here that
already declares `follows`.

logos-package-manager-ui reaches logos-package-manager-module and
logos-package-downloader-module under DIFFERENT INPUT NAMES
(`package_manager`, `package_downloader`) than this root does. Same flakes,
same revs, identical resolved subtrees — Nix just cannot know that, so it
unrolls a private ~3,178-node copy of each. Two root-to-root follows,
retargeted across the name difference:

    19,832 nodes / 507,875 lines  ->  13,476 / 346,113   (-32%)

Nothing about what is built changes: all 21 root inputs resolve to the same
revs before and after, and `nix flake lock` re-runs as a byte-level no-op
with no unmatched-follows warnings.

WHAT IS DELIBERATELY NOT HERE. Four deeper follows take this to 2,992 nodes
(-85%) by sharing logos-module-builder subtrees between consumers and
closing a real cycle (capability-module -> module-builder -> standalone-app
-> liblogos -> capability-module, which Nix unrolls one fresh copy per lap).
They were proven meaning-preserving against an earlier tip — 1361/1361
derivations identical, plus a bisimulation over both lock graphs — and then
that tip moved. On the commit that reworked main_ui into "a plugin that
links no logos runtime", the same follows produce

    Failed to load the main UI plugin from .../main_ui/main_ui.dylib:
      The shared library was not found

with shutdown-test and smoke-test red while the base is green. A
derivation-graph proof is only as durable as the tip it was taken on.

One of the four was disqualified separately, for a reason that would have
outlived any tip: pointing this UI's standalone-app/liblogos at
logos-capability-module's is a no-op HERE, but a parent that overrides
logos-capability-module — exactly how logos-workspace consumes this flake —
pulls them apart, and this UI's build-time liblogos silently follows the
parent's down. Measured at the workspace's own pin: 93207e41 -> 56aa8bb4,
eight days older, no warning.

The two kept are ordinary root-to-root sharing, the idiom already used by
logos-view-module-runtime and nix-bundle-macos-app above. The remaining
10,484 nodes are the module-builder cycle, and the fix for that belongs
upstream in logos-module-builder / logos-capability-module, where closing it
once collapses every consumer at the same time.

Rebased onto master after logos-basecamp#337 merged and its branch was
deleted; re-verified from scratch rather than carried over, because the base
moving is what invalidated the deeper follows. On 9b8cf6e: bin-macos-app
builds, and shutdown-test, smoke-test, symbol-gate, symbol-gate-negative,
unit-tests, qml-tests and sandbox-test are all green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 17:34:50 -03:00
Dario Gabriel Lipicar 9b8cf6e47e feat(shell): ship main_ui as a plugin that links no logos runtime
Measured on the built artefact: main_ui.dylib DEFINES zero and IMPORTS zero of
TokenManager, StoreRegistry, LogosAPI, LogosAPIClient and logos_core_*, out of
3410 symbols read. It links Qt and nothing else from this workspace, which
nix/symbol-gate.nix enforces across the in-process image set.

Getting there needed the last non-Qt types off the boundary: the model
properties cross as QAbstractItemModel*, catalogInstallStageChanged carries an
int rather than InstallStage::Value, and the two prebuilt AppsFilterProxy
instances are declared in QML instead of owned by MainUIBackend. That last one
removes a real inversion -- PackageCoordinator called setRequiredPackages() on
a proxy the host held a pointer to; it now emits requiredPackagesResolved() and
QML binds to the republished property.

Window resolves the plugin, qobject_casts it to IShellView, checks
hostAbiVersion() against IShellHost_abi, and calls createShell(IShellHost*).
No error-label fallback widget: that degraded to something that looked like a
working app with an empty window.

Filter proxies read role constants off host-side models and InstallEnums is
used by nine host files, so app/interfaces/ gains the contract headers both
sides compile against -- the models inherit the role structs, leaving every
AppsModel::NameRole call site unchanged. The plugin's include path is
app/interfaces only, so including a host header does not compile.

Four things only running it finds:
  * Logos::DesignSystem may be linked by exactly ONE image -- both linked it
    and the app aborted with "Cannot add multiple registrations for
    Logos.Icons"; QML module registration is process-global
  * qmltyperegistrar emits no #include for a SOURCES header given as an
    absolute path outside the project
  * each qt_add_qml_module is its own target and inherits no include dirs
  * AUTOMOC pairs header<->cpp by same-basename-same-DIRECTORY, which the
    split breaks

tst_AppManagerView.qml grows five tests for the QML binding, checked with a
negative control: breaking one assertion fails qml-tests, so they run.
2026-08-22 16:42:13 -03:00
Dario Gabriel Lipicar 9c3d023062 feat(shell): put the UI shell behind IShellHost, and order its shutdown
The shell held the host's objects: MainContainer owned MainUIBackend, took a
LogosAPI* and a QtLogosCore*, and connected to backend signals by concrete
type. Nothing stopped it minting identities or reading the token store.

Three headers in app/interfaces/, on the include path both targets share, so
there is exactly one copy and source drift is impossible:

  * IShellHost     -- 8 operations the shell may perform
  * IShellObserver -- 5 notifications the host may deliver
  * IShellView     -- how the host builds and tears down the shell

Only QObject*, QWidget* and Qt value types cross. MainContainer holds one
IShellHost* and nothing else; QML reaches the backend as an opaque QObject*
via backendObject(), resolved through the metaobject, so no host C++ type has
to be nameable by the shell.

Ownership inverts: Window owns MainUIBackend, ShellHostAdapter and
MainShellView; the shell borrows. Teardown stops being a consequence of
construction order and becomes a stated contract:

  1. beginShutdown() unmounts in-process UI plugin widgets WHILE the shell's
     tree is intact -- they are docked inside it
  2. destroyShell() detaches the observer, then deletes the shell
  3. the backend goes, tearing down Package -> UIPlugin -> Core
  4. main() destroys the core facade

Every observer forward is null-guarded: PluginLoader dispatches through
QTimer::singleShot(0, ...) and a 30s ViewModuleHost timeout, so a callback can
land after teardown starts, and QPointer cannot help -- IShellObserver is not
a QObject. UIPluginManager's plugin-widget maps become QPointer for the
mirror-image reason: those widgets are docked inside the shell, so its Qt
parent can destroy them without going through unloadUiModule.

IComponent is untouched and still serves the third-party legacy widget
plugins PluginLoader loads.
2026-08-22 16:42:13 -03:00
Dario Gabriel Lipicar 13665a5e34 feat(host-core): adopt the SDK core facade, and delete both C ABI mirrors
CoreModuleManager and main.cpp each carried a hand-written `extern "C"` mirror
of liblogos' core-management ABI -- two blocks declaring the same symbols in
one image, an ODR hazard with no diagnostic. Both are replaced by
logos::qt::QtLogosCore over logos::host::LogosCore, which owns the
char*/char** marshalling, the `delete[]`-not-`free()` rule, and the pre-start
ordering constraint -- the last as constructor arguments, so the illegal order
stops being expressible.

Four things about the stats path that fail INVISIBLY, all preserved:
  * "cpu"/"memory" stay 1-decimal STRINGS -- QML and ModuleInstanceModel bind
    those names, so the facade's cpuPercent/memoryMb spelling stops here
  * allStats() is called ONCE per tick; each moduleStats(name) repeats the
    full C call and parse, and the snapshot walks every known module
  * the three-way back-compat fallbacks are kept for older runtimes
  * memoryMb (double), never memoryBytes

The cpp-sdk lock moves for that last point: the pin predated the rename, and
that pair does not compile -- qt-sdk's header reads s.memoryMb while the
pinned cpp-sdk struct still called it memoryBytes.

Also drops a QTimer in main.cpp that was started and stopped with no
connect() at all.
2026-08-22 16:42:13 -03:00
Dario Gabriel Lipicar 5a91e65022 chore(deps): pick up ui-host's teardown grace period
Gives an out-of-process UI host time to shut down cleanly instead of being
killed mid-teardown.
2026-08-22 16:42:13 -03:00
Dario Gabriel Lipicar 4877c7b0b7 feat(shared-runtime): link the shared libraries, and delete the shim
The app and every in-process plugin now take TokenManager, StoreRegistry,
LogosAPI and LogosAPIClient from the shared libraries that own them
(liblogos_protocol, liblogos_qt_host) instead of each linking a static copy.
A second copy is a second TokenManager: the host writes a capability token
into one store, another image reads an empty one, and every cross-module call
is refused at runtime with no build diagnostic.

The static archives are emptied on every platform, so the shared library is
the only provider and the LOGOS_SHARED_USE_DLL shim is gone.

nix/symbol-gate.nix enforces the invariant -- across the images sharing one
process, each runtime type is defined by EXACTLY ONE -- with a negative
control that plants a real duplicate and asserts the gate rejects it. Two
measurement traps it had to survive: nm reads zero symbols from nix's shell
wrappers and must be pointed at the real binary, and `find` without -L skips
a symlinked plugin entirely, so the gate silently measured nothing.
2026-08-22 16:42:13 -03:00
Dario Gabriel Lipicar 3c21c1f075 chore(deps): retire the rev pins, now that each upstream has merged
Also pins package-manager-ui at the branch carrying both fixes it needs.
2026-08-22 16:42:13 -03:00
Dario Gabriel Lipicar 7851bf9f46 feat(host): per-plugin identities, an opt-in access policy, and the source split
Each loaded plugin -- including pure-QML ones -- gets its own LogosAPI identity
rather than sharing the host's, so a plugin's calls are attributable and can be
refused independently. The host-services grant is wired through to
capability_module, and its trust root is guarded on an OUTCOME rather than a
log line.

Inter-module access policy stays OFF by default: enforce mode's derived
deny-by-default gates every ui_qml app's calls to its own backend module,
because UI plugins load out-of-process and are not tracked as dependents in the
core ModuleRegistry. Operators opt in per launch with --access-policy enforce
or LOGOS_ACCESS_POLICY.

Takes the Qt host runtime from logos-plugin-qt rather than logos-qt-sdk, which
keeps only the Qt<->lp seam headers, and moves logos-protocol onto the rev that
split host needs. On Windows logos_core must come LAST on the link line: GNU ld
resolves an archive left to right, so the view runtime's references have to be
undefined already when it reaches the import library.

Separates the two source trees -- app/ is the host, src/ is the UI shell -- and
brings the CI onto setup-nix-cache-action. Merges master.
2026-08-22 16:42:13 -03:00
Khushboo Mehta d7c2f6e355 fix: Reinstall doesn't remove files deleted by the new version — install merges into the existing directory
fixes #313
2026-08-19 14:55:29 +02:00
Marko Burčul 9118071484 fix(ci): wrong path to linux arm pipeline in combined jenkinsfile (#335)
Signed-off-by: markoburcul <marko@status.im>
2026-08-19 09:00:46 -03:00
Roman Zajic 3c1a88e9f0 Test/failure paths (#318)
* test: failure paths

* fix: add the connect guard

* fix: add assertion for root.errorMessage
2026-08-19 11:07:11 +08:00
Dario LipicarandClaude Opus 5 f57e1692b9 feat(windows): build and publish a Windows bundle alongside the other platforms (#334)
* feat(windows): build and publish a Windows bundle alongside the other platforms

Windows had CI but no downloadable artifact. A release/* push produced an
AppImage per Linux arch and a macOS .app tarball, and nothing a Windows user
could open.

`build-windows` mirrors those jobs: a CROSS build on ubuntu-latest -- nix does
not run on Windows, so there is no Windows runner here; executing the result on
real Windows is what logos-windows-ci's native-smoke leg does. The release job
needs it, downloads it, and attaches artifacts/*-windows.zip.

bin-bundle-dir rather than `default`: it is the same portable directory the
AppImage and .app jobs package, laid out the way the app expects (bin/ +
lib/qt-6/{plugins,qml} + an explicit qt.conf). A PE records no rpath, so that
layout IS the deployment contract.

.zip rather than .tar.gz, because this is the one artifact here whose users are
on Windows, where Explorer opens a zip and nothing opens a tarball.

THE PACKAGING STEP ASSERTS RATHER THAN TRUSTING ITS EXIT CODE. The bundle's DLLs
are symlinks into the nix store, and an archive of dangling links extracts to a
tree that cannot start. nix-bundle-lgx shipped exactly that: `cp -a` implies
--no-dereference, the payload lost 75% of its files, and the step exited 0. So
this copies with -L and then compares the zip's entry count against the bundle's
file count, failing on a shortfall; it also refuses a bundle with no .exe.

The comparison is `<`, not `!=`: a zip counts directory entries too, so a
healthy bundle legitimately produces MORE entries than files. Rehearsed against
a real bundle on an x86_64-linux builder -- 1677 files -> 1768 zip entries,
3 .exe, 0 dangling links after cp -rL, 117 MB.

Named logos-basecamp-x86_64-windows.zip: the full word `windows`, because
logos-release-set's classifier matches on it and a bare `win` would swallow
every darWINd asset.

* ci(windows): drop the disk reclaim -- the runner has 108 GiB, not 14

The step's own justification was the reason to keep it and it does not hold:
"ubuntu-latest ships ~14 GiB free" came from this project's notes, written
before GitHub upgraded the hosted runners. A real run of this workflow reports

    /dev/root       145G   37G  108G  26% /

so the step was deleting ~10 GiB from a disk with 108 GiB already free, and
paying ~20 s per job for it.

The Windows job substitutes the mingw closure from the Attic cache rather than
building it, so the build-time store requirement is lower still.

Kept available, not deleted: logos-windows-ci's nix-setup still exposes
free-disk-space, defaulting to false, so a genuinely disk-bound job can opt in.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 21:20:53 -03:00
Dario Gabriel Lipicar 536234e343 fix(windows): bump logos-package-manager-ui so the Windows build completes
The x86_64-windows target could not build. It failed 15 files into
package_manager_ui's compile with

    generated_code/include/logos_sdk.h:5:10:
      fatal error: package_manager_api.h: No such file or directory

and basecamp is only the messenger. `package_manager_api.h` is the typed API of
the package_manager MODULE, and this lock carried TWO revisions of it:

    63a6f8ab  publishes  ...and x86_64-windows
    975dcc50  publishes  darwin, linux only

The bad one arrived through this repo's pin of a package_manager_ui that
predated logos-package-manager-ui#68. With no Windows target for that
dependency the header resolved to nothing, no -I reached the compiler, and the
first file including the generated logos_sdk.h failed. Nothing earlier
complained.

`nix flake update logos-package-manager-ui` collapses it to 63a6f8ab alone. It
also brings logos-module-builder 632f601, which includes

    ea3b393 fix(cross): fail loudly when a dep publishes nothing for the
            target system (#199)

so the next dependency in this position fails in seconds naming itself, rather
than as a missing include after an 87-minute build.

VERIFIED on an x86_64-linux builder -- packages.x86_64-windows.default builds
clean, ma16290wbzdnfpglasxn0dy2k0f9j018:

    DLL import closure converged after 5 round(s); staged 9 DLL(s) into bin/
    PE import closure verified: 117 root(s), 7141 import(s) read, 0 unresolved.

Found by the Windows cache-prime job's third stage, which is the first thing to
have cross-built this closure from a store with NO history. package_manager_ui
was verified working on Windows months ago, but hand-built on a machine whose
store already held what a clean runner has to produce -- so the missing pin had
never been exercised.
2026-08-18 18:17:58 -03:00
Dario Gabriel Lipicar adee4734e6 ci: take Nix and the Attic cache from setup-nix-cache-action
build.yml owned its own Nix installer, substituter list, trusted keys, cache
selection and push -- four times over, in two different spellings. All of it now
comes from logos-co/setup-nix-cache-action@v1. 237 lines -> 203, and four
(installer + attic-action) pairs collapse to four one-step uses.

WHAT THIS FIXES, beyond the duplication:

* THE SIGNING KEY. This was the only repo in the fleet trusting
  public:Z1wyVBEx8PHbXujYB52Mysv9Rd8rWIhyQ3bQyef9yy4=; the shared action
  configures public:l4HrXgL4nw246+LBh2SOJyhz64BoGegOYLheT/iIAPU= for the same
  cache name. I could not determine from outside which is current -- a narinfo's
  Sig names the key `public` but not its value, and attempts to discriminate by
  verifying a copy failed on incomplete closures rather than on signatures. Two
  answers to that question was the actual problem; there is now one.

* NIX VERSION. Two jobs ran cachix/install-nix-action@v27, which is Nix 2.22 --
  below the 2.26 floor where `--override-input` keeps the overridden input's own
  lock. The shared action uses @v31 (nix_version=2.35.1). The other two jobs ran
  DeterminateSystems/nix-installer-action@main, i.e. an unpinned ref on a
  release-producing workflow.

* IT NOW READS THE ci CACHE. Every block here configured only
  cache.nix.logos.co/public as a substituter while pushing to `ci` on branches --
  so a branch build could not reuse what any earlier branch build had pushed. The
  shared action configures both.

ONE DELIBERATE REGRESSION, stated rather than buried: the old steps passed
`inputs-from: "."` to attic-action, which also pushes the flake inputs' store
paths. setup-nix-cache-action does not expose that input, so slightly less is
pushed per run. Reading the ci cache should more than compensate on branches, but
it is a real difference and the right fix is an `inputs-from` passthrough in the
shared action rather than keeping a second implementation here.

The `if: env.ATTIC_ENDPOINT != ''` guards are dropped with the env block they
read. They existed to skip the push on fork PRs where secrets are absent; the
shared action needs no guard, because with no token it configures the
substituters and skips the push -- which is what those guards approximated. This
repo's ATTIC_ENDPOINT secret is consequently unused here, and the action's
default endpoint is the same host this file already named.

Verified: actionlint clean, and all five jobs keep their build/test steps
unchanged -- only the preamble moved.

NOT verified: this has not run. It touches the release path, so it wants a real
CI run on a branch before merge.
2026-08-18 17:12:40 -03:00
marko fb64b23144 feat(ci): change s3 storage from DO to Hetzner
Referenced issue:
* https://github.com/status-im/infra-ci/issues/282

Signed-off-by: markoburcul <marko@status.im>
2026-08-18 10:14:13 -03:00
Khushboo-dev-cpp 525b43cd1d feat: support icons available with packages (#321) 2026-08-18 11:52:53 +02:00
Roman Zajic 03e054fb08 test: module unload doctest (#329)
- disable unload/load buttons for core modules
2026-08-18 15:21:26 +08:00
Dario LipicarandClaude Opus 5 72c440fc77 fix(windows): bundle Basecamp with the real bundler, not the identity bypass (#324)
flake.nix carried `winBundler = drv: drv` behind `bundleFor`, placed on an
explicit condition: "the real fix is a PE branch in nix-bundle-dir's bundle.sh
that skips relocation and keeps Qt staging; when that lands, DELETE bundleFor
and go back to dirBundler on every platform."

It landed. No re-pin was needed to pick it up, contrary to expectation: this
flake's root nix-bundle-dir input already resolves to f843b8ec, which IS the
current tip of nix-bundle-dir main (checked against the API, not a local ref).
The unsuffixed `nix-bundle-dir` node in flake.lock is NOT the root's -- root
resolves to nix-bundle-dir_1061 -- and both happen to be f843b8ec anyway.

WHAT THE BYPASS ACTUALLY PRODUCED

Both trees were realised on x86_64-linux from ONE tree, so the only variable is
whether dirBundler is applied; verified by store path, the bundle derivation's
only basecamp input is exactly the un-bundled derivation.

                             bypass(drv:drv)   dirBundler
    entries                              85         1740
    regular files                        59         1657
    symlinks                             12            0
    bytes                            259 MB       479 MB
    *.dll in bin/                        33           88
    bin/qt.conf                     MISSING      present
    .../platforms/qwindows.dll      MISSING      present
    lib/qt-6 (plugins + qml)        MISSING   1533 files
    nix closure refs                     12            1

The 12 symlinks are the part that matters. bin/Qt6Core.dll, Qt6Gui.dll,
Qt6Widgets.dll, Qt6Network.dll, Qt6RemoteObjects.dll, libssl/libcrypto,
libpng16, libzstd, libb2, pcre2 and double-conversion were symlinks into
/nix/store. Copy that tree to a Windows box -- the whole point of a portable
bundle -- and every one dangles: 0xC0000135, no output, before main(). So the
comment claiming a PE "needs no relocation" was only half right; rpath
REWRITING is indeed moot, but Phase 1's `cp -aL` is not, and the bypass skipped
it. The bundled tree has zero symlinks and one remaining nix reference, an
inert /nix string in a PE's data section that imports nothing.

55 DLLs exist only in the bundled bin/: the Qt Quick / Controls / Labs set the
fixpoint sweep pulls in once QML modules are staged, plus libcurl and its
TLS/HTTP2 chain mirrored beside the package_downloader module importing them.

Not a strict superset: README.txt and share/ (a .desktop file and a hicolor
icon, 9 paths) are dropped. Not a Windows regression -- bundle.sh Phase 1
copies bin/, lib/ and extraDirs on every platform, so the shipping Linux and
macOS bundles never had them either; the bypass "kept" them only by doing
nothing. Both are dead weight off-store: README.txt is a build-info file whose
every line is a /nix/store path, and a .desktop file does nothing on Windows.

THREE extraClosurePaths ADDITIONS (nix/app.nix)

Removing the bypass surfaced three real gaps, each a build the bundler FAILED
loudly rather than shipping a tree that dies before main():

  qtdeclarative  staged 17 Qt plugin files, then "QtQuick/QtQml DLLs are in
                 bin/, so this bundle renders QML, but no QML module directory
                 for this target was found in the closure". With it: 1651 QML
                 files staged and 40 more Qt DLLs swept in.
  libjpeg.bin    "libjpeg-62.dll -- imported by .../imageformats/qjpeg.dll"
  sqlite.bin     "libsqlite3-0.dll -- imported by .../sqldrivers/qsqlite.dll"

Same root cause each time: an ELF/Mach-O binary records where its dependencies
live, so Nix scans out a reference and closureInfo gets them for free; a PE
import table carries base names only and embeds no store path, so Nix records
nothing and the bundler cannot stage what is not in the closure. `.bin` rather
than the default output was checked, not assumed -- both DLLs are installed
into the `bin` output.

Windows-gated, and that gating is proven rather than asserted:
packages.aarch64-darwin.app.extraClosurePaths evaluates to the identical two
derivations before and after this commit, while x86_64-windows gains exactly
the three above.

COVERAGE, STATED PLAINLY

Nothing here has been RUN on Windows, by this commit or by CI, which has never
executed a Windows binary. Every claim is build-time and tree-shape only.

The measured trees came from a harness that drops ONE entry from
installedDistributed, packageManagerUIPlugin, because logos-package-manager-ui
does not cross-compile at the rev this flake pins: its generated logos_sdk.h
includes package_manager_api.h, which is not produced for the Windows target.
Pre-existing and independent -- the identical derivation
(b0i9ilm4phszkk0az96z3zhn15nkb9c5) fails when built straight from the pinned
rev 39a5fa5 with no basecamp involved. That plugin takes no part in Qt staging
or the DLL closure, but the numbers above are from a bundle without it.

For the same reason `nix build .#packages.x86_64-windows.*` cannot succeed on
this branch as pinned, and a second unrelated blocker sits in front of it at
EVAL time: logos-package-downloader-module has no x86_64-windows target, so the
attribute does not evaluate. Both blockers predate this change and behave
identically with the bypass in place. The measurement got past the eval blocker
with --override-input onto a local package-downloader-module carrying its
Windows target, and past the build blocker with the one-plugin harness above.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 20:52:02 -03:00
Dario LipicarandClaude Opus 5 aa5884c673 feat(windows): cross-build Basecamp — plus startup and window fixes for every platform (#323)
* feat(windows): wire Basecamp for the x86_64-windows target

First slice of Stage 4. Basecamp now evaluates for the Windows
pseudo-system up to its first genuinely unported dependency
(logos-design-system); native aarch64-darwin and x86_64-linux
evaluation is unchanged.

Four blockers cleared, found by evaluating and fixing in turn:

1. flake.nix used a hardcoded `systems` list, so packages.x86_64-windows
   did not exist. Now routes through logos-nix.lib.forAllTargets, which
   keys the cross target as a pseudo-system — that is what lets the 34
   `dep.packages.${system}.x` interpolations stay untouched. Bundlers
   (nix-bundle-logos-module-install, nix-bundle-dir) are keyed by
   buildSystem instead: they RUN on the builder, so taking them from the
   target set would hand it a PE it cannot execute.

2. meta.platforms was platforms.unix.

3. krb5 does not cross-evaluate: it carries a host-platform bash, so the
   splice fails with "Refusing to evaluate package 'bash-5.3p9'" — an
   error naming bash, several levels from the cause. Guarded in both
   common.buildInputs and app.nix's qtLibPath.

4. qtwebview propagates qtwebengine — a full Chromium — which fails to
   cross-evaluate on cups, three levels from the cause. It is also
   entirely unused: no C++ include, no `import QtWebView` in any QML,
   and app/CMakeLists.txt's find_package COMPONENTS list omits WebView.
   The only surviving mention is a stale line in docs/project.md.

krb5 and qtwebview are GUARDED rather than deleted, so this stays a
Windows-only change. Both look like dead weight on Unix as well, and
dropping them there would shrink every bundle — worth doing separately,
with its own testing.

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

* feat(windows): drop the QML inspector on Windows

logos-qt-mcp is the QML inspector behind the UI test harness. It has no
Windows target and is not needed to run the app -- nix/app.nix already
accepts `logosQtMcp ? null` and gates the inspector on it -- so Windows
builds go without it, and the inspector-dependent outputs are simply
absent from the Windows package set.

With this, logos-basecamp EVALUATES for x86_64-windows end to end; native
aarch64-darwin and x86_64-linux are unchanged.

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

* feat(windows): cross-build LogosBasecamp.exe

Basecamp now produces a real Windows executable. Five blockers, each
found by building and each of a kind already seen in this port.

1. abseil-cpp does not cross-compile (absl/base/internal/
   thread_identity.cc includes <pthread.h>, absent on mingw+mcfgthread).
   It is dead weight -- "absl" appears nowhere in the sources or CMake,
   only in nix files and the README. Guarded here; also removed from
   logos-package-manager-ui's metadata, which pulled it in separately.

2. installedModules cut to [] on Windows. The install path runs each
   module through nix-bundle-logos-module-install -> nix-bundle-lgx ->
   nix-bundle-dir, and nix-bundle-dir is ELF/Mach-O only. This keeps the
   bundler chain, lgpm and four module repos off the critical path.

3. The hand-rolled configurePhase never passed $cmakeFlags, so
   -DCMAKE_SYSTEM_NAME=Windows was dropped. The symptom was nowhere near
   the cause: FindThreads probed for pthreads instead of Win32 threads and
   Qt6Config reported "Qt6 could not be found because dependency Threads
   could not be found".

4. Qt's host-TOOL packages were unreachable. app.nix builds its own cmake
   line and never inherited common.cmakeFlags, so adding the flags there
   was not enough -- they had to go on the invocation too. Symptom again
   misleading: 'Failed to find required Qt component "RemoteObjects"',
   when the TARGET Qt6RemoteObjects is present and it is
   Qt6RemoteObjectsTools, the host package, that is missing.

5. Two unsuffixed-name defects, the class that has now appeared eleven
   times. app/CMakeLists.txt copied `logos_host` without
   CMAKE_EXECUTABLE_SUFFIX, and because it is a POST_BUILD step the
   failure read "FAILED: LogosBasecamp.exe" with no undefined references
   -- the real message was the "Error copying file" line above it. Then
   the installPhase's `if [ -f build/LogosBasecamp ]` had no else-branch,
   so the mingw build installed NOTHING, exited 0, and produced an output
   whose bin/, lib/, modules/ and plugins/ were all empty. It now probes
   both spellings and hard-fails with a directory listing.

Also: ENABLE_QML_INSPECTOR is now gated on logosQtMcp != null rather than
on enableInspector alone -- the inspector cannot be on without it -- and
Windows installs the binary directly instead of behind a /bin/sh wrapper
that could not run there.

NOT YET RUN on Windows; that needs the Qt plugin/QML staging proven in
the design-system milestone.

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

* feat(windows): main_ui configures and compiles under cross

Two fixes, both of kinds already seen in this port.

The code generator must RUN on the builder, so it now comes from the
BUILD system via a new logosSdkBuild binding. This is a SPLIT, not a
swap: the same cpp-sdk output carries both the generator binary AND the
target headers/CMake package, so logosSdk stays for
-DLOGOS_CPP_SDK_ROOT. Getting it backwards succeeds natively and, under
cross, puts a PE on the builder's PATH -- the symptom was
"logos-cpp-generator: command not found".

main-ui.nix has its own hand-rolled configurePhase which, like app.nix's,
never passed $cmakeFlags -- so -DCMAKE_SYSTEM_NAME=Windows was dropped
and FindThreads probed for pthreads, surfacing three levels away as
"Qt6 could not be found because dependency Threads could not be found".
It now passes $cmakeFlags and Qt's host-TOOL package paths.

main_ui now configures and compiles. It does NOT yet link: the qt-sdk
static library is pulled in twice, giving "multiple definition of
LogosAPI::LogosAPI". That is a link-graph question, not a Windows
spelling issue, and is the next thing to solve.

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

* fix(windows): name the main_ui plugin .dll, and link it

src/CMakeLists.txt set the plugin SUFFIX with `if(APPLE) ".dylib"
else() ".so"` and no Windows branch, so the cross build emitted
main_ui.so -- a genuine PE carrying a Unix extension. app/window.cpp
looks for ".dll" under Q_OS_WIN, so it would never have been found and
Basecamp would have gone on reporting "No main UI" with nothing
obviously wrong. Windows is now tested before the else-branch, and
nix/main-ui.nix accepts the .dll spelling too.

With the liblogos_core export fix, main_ui links and installs as
main_ui.dll.

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

* fix: don't block startup on modules that are not loaded

PackageCoordinator guarded its startup IPC with
`if (!client || !client->isConnected()) return;`, which never fired --
see the logos-protocol fix. Past that dead guard it made ~19 blocking
calls (4 directory setters, 8 package_manager subscriptions, 1
package_downloader) against modules that were absent, each waiting 20 s
twice over. All of it runs on the GUI thread inside the Window
constructor (window.cpp invokes createWidget with Qt::DirectConnection),
and main.cpp calls show() only afterwards -- so the window simply did not
appear for ~7 minutes.

Ask the question in-process instead: logos_core_get_loaded_modules, via
the CoreModuleManager this class already holds. No IPC, no timeout. The
same guard already ships in logos-logoscore-cli's daemon, which skips the
identical setEmbeddedModulesDirectory block and reports that package
commands are unavailable. Basecamp already logged "Failed to load
package_manager module by default" roughly 40 s before the first block --
it knew, and ignored it.

The subscriptions are re-armed on CoreModuleManager::coreModulesChanged,
so a module installed later in the session still gets wired up; without
that this would trade a 7-minute stall for a silently non-functional
Modules view, which is the worse bug. Both subscribe paths are
idempotent, and the "not loaded" warning is emitted once rather than on
every stats-timer tick.

Verified on Windows: the guard now fires, the transport warns before the
doomed wait, and Basecamp's real UI renders -- sidebar, Logos mark and
Welcome page -- instead of a blank window.

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

* fix: guard the remaining package_manager startup calls

fetchUiPluginMetadata() and refreshDependencyInfo() checked only
`!m_logosAPI`, so they still blocked ~20 s acquiring a token for an
absent package_manager after the subscribe paths were guarded. That was
the last blocking wait at startup, and the transport's new
"will block up to 20000 ms" warning is what pointed at it.

fetchUiPluginMetadata reuses its existing metadata-unavailable branch
rather than inventing a new one -- it clears m_appsLoading and emits
uiModulesChanged, so the UI settles instead of sitting in a loading state
for the timeout. refreshRepositories needed no change: it guards on
isConnected(), which now tells the truth.

Measured on Windows with no modules installed: "will block up to" 0,
"Timeout waiting for replica" 0, "Requesting object" 0. The UI renders
identically -- sidebar, Logos mark, Welcome page.

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

* fix(windows): resolve UI plugins' private DLLs from their own directory

The logos-module chokepoint fix covers LogosModule::loadFromPath, which
is every process-isolated module. Basecamp constructs its own
QPluginLoaders and was not covered: PluginLoader's background pre-load
and finishCppPluginLoad for UI plugins, and window.cpp for main_ui.

Each lives in its own directory (plugins/<name>/), so anything vendored
beside it is invisible to Windows' loader, which searches the
EXECUTABLE's directory rather than the importing DLL's. This works today
only because every DLL happens to be staged next to the exe; a plugin
with a private dependency would fail with "The specified module could not
be found", naming the plugin rather than the dependency.

The references are deliberately not released: these plugins stay resident
for the process lifetime (QPluginLoader's destructor does not unload).
No-op off Windows, so no platform guards at the call sites.

Cross-builds and native build both green.

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

* chore(windows): key the install bundlers by target

nix-bundle-logos-module-install now does its own host/target split
internally, so keying the whole bundler by buildSystem is no longer
right -- that is what made it derive the .lgx variant name and library
extension from the build platform.

installedModules stays [] on Windows for now: with the labelling fixed,
lgpm correctly refuses to install a windows-x86_64 package on a Linux
builder, and cross-installation needs a platform override that lgpm does
not have yet.

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

* feat(windows): ship the pre-installed modules

installedModules is no longer cut on Windows. The cross-install path now
works end to end: capability_module, package_manager and
package_downloader are laid out under modules/, main_ui and
package_manager_ui under plugins/, each a real PE with its own DLL
closure and a variant file reading windows-x86_64-dev.

It took a host/target split at every layer, and each failed somewhere
unrelated to itself: the .lgx bundler keyed by target (it decides the
variant name and library extension), lgpm from the build system (it runs
there), a --platform override for cross-installation, and a Nix-double
to lgpm-variant translation.

Verified on real Windows: capability_module and package_manager load,
spawn their hosts, and answer IPC over named pipes -- the log shows
ModuleProxy rejecting unauthorized calls, which is the token flow
talking, not a Windows failure.

KNOWN GAP, and it fails loudly: package_downloader does not load --
"Cannot load library ... package_downloader_plugin.dll: The specified
module could not be found". Its external library
libpackage_downloader_lib.dll needs libcurl-4.dll, which is in neither
the module directory nor bin/. LogosModule.cmake copies an external
library's DLL but not that DLL's own dependencies, so the closure must be
walked to a fixpoint there too -- the same lesson as the Qt plugin and
bundle closures. Strictly better than shipping no modules at all, and the
failure names the file.

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

* feat(windows): import the shared runtime instead of linking it

LogosBasecamp.exe and main_ui.dll now compile with LOGOS_SHARED_USE_DLL
and link an emptied logos-qt-sdk archive, so TokenManager, LogosAPI and
LogosAPIClient come from liblogos_core.dll. Before this each image linked
its own copy and therefore its own function-local statics: the host saved
a capability token into its TokenManager, main_ui read its own empty one,
and every cross-module call was refused.

Measured on real Windows across three runs: "rejecting unauthorized call"
29 -> 0, and "No token found" 29 -> 0 replaced by nine "Found token for
module". All modules still load; no missing-symbol or duplicate-symbol
diagnostics.

NOT fixed by this, and not caused by it: the package_manager_ui icon
still does not appear in the sidebar. The pre-change run had the same
zero ui-host spawns. Sidebar entries come from
PackageCoordinator::getInstalledUiPluginsAsync, which reads
package_manager's installed-package registry -- empty in this payload, so
the list is empty regardless of what is on disk in plugins/. That is a
separate unmet precondition.

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

* fix: never open a window larger than the screen

setupUi asked for a fixed 1600x900 and nothing anywhere in this repo consulted
the screen -- the <QScreen> include at the top of window.cpp had been unused.
On a display smaller than that the window opens with its right and bottom edges
off-screen, and on Windows it cannot be dragged back into view, so whatever
hangs off is permanently unreachable: in practice the sidebar's bottom-anchored
system buttons and the version label.

Two halves, because one is not enough:

* setupUi clamps the requested size to availableGeometry, so an oversized
  window is never mapped in the first place.
* showEvent re-checks once. resize() sets the CLIENT size while
  availableGeometry() bounds the FRAME, and the decoration margins are not
  knowable until the platform window exists -- measured, the clamped client
  filled the work area exactly and the frame still hung 72px below it.

Both are skipped under the offscreen platform (QOffscreenScreen hardcodes its
geometry) so headless UI doctests keep their full-size window and qt-mcp's
scene-position clicks still land.

Three things that look like they could be simpler and cannot, each measured:

* The showEvent check compares SIZES, not rectangles. QRect::contains() is the
  obvious formulation and misfires on both platforms: on Windows
  frameGeometry() comes from GetWindowRect, which includes DWM's invisible
  resize border (~13px/side at 192dpi), so a correctly placed window never
  reports as contained and this would run on every launch; on macOS Qt reports
  a frame whose title bar sits above availableGeometry's origin, and acting on
  that moved the window 66px down at launch on a platform where nothing is
  broken.
* The shrink is bounded by the current size. Without boundedTo, a window that
  is merely too TALL is also stretched to the full available WIDTH -- observed
  on this path: 1600 logical wide silently became 1715.
* The clamp is to availableGeometry exactly, not to a fraction of it. Anything
  less is not a no-op on displays that fit the design size, and would undo the
  widening documented at the resize() call (1600 was chosen so the Package
  Manager's Action and Description columns are not clipped).

Measured on Windows (real hardware, DPI-aware capture, visible frame via
DWMWA_EXTENDED_FRAME_BOUNDS against a 3456x1826 work area):

  normal session      bottom overflow 45px -> 2px (rounding), width still 1600
  small desktop       ~550px of width off-screen -> right 14px, bottom 2px
  below app minimum   bottom 126px -> 33px (Basecamp's own 800x600 floor)

Verified a no-op elsewhere by running the identical logic against Qt 6.11.1 on
macOS: constructor clamp no-op, showEvent reports FITS, size and position
unchanged; and under QT_QPA_PLATFORM=offscreen both paths are skipped.

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

* fix: re-fit the window when the screen shrinks under it

The launch clamp (76e0f6d) only runs once. An RDP session with
/dynamic-resolution, or a taskbar appearing, shrinks the desktop under a running
window and nothing reacted -- the window was left overflowing, with the
sidebar's bottom-anchored system buttons under the taskbar and unreachable.

Watches availableGeometryChanged on the screen the window is on, coalesced
through a 150ms settle timer, and re-fits. Measured on real Windows, same
binary pair and run conditions, work area shrinking 1922 -> 1826 physical px:

  shipped   overflow (3,3) -> (4,111)   window did NOT shrink   NO-REFIT
  re-fit    overflow (3,3) -> (3,2)     window shrank           REFIT-WORKS

An 8-agent design pass was REFUTED 3/3 by its own reviewers and this is the
corrected version, not that design. Three of its defects were real:

* THE RATCHET. It fitted to min(previous, available) on every event -- a
  monotone non-increasing map, so the window converges on the smallest work area
  seen all session and never grows back. A taskbar that auto-hides and
  reappears, or an RDP window dragged smaller and back, would have shrunk
  Basecamp permanently. Fixed by bounding against m_desiredSize: the launch
  design size, replaced by any resize the USER performs, so the window grows
  back but never past what was asked for.
* NO isVisible() GATE. closeEvent hides to the tray on EVERY platform and hide()
  does not set Qt::WindowMinimized, so a window-state gate does not cover it.
  The fit would have run and latched on a hidden window whose frame geometry is
  meaningless.
* A QRect LATCH that could not distinguish "the screen shrank under the window"
  from "the window moved to another monitor". Dropped entirely: the
  `target == size()` early return is both the idempotence and the
  non-recursion, so there is no latch to fall out of sync -- and no need for the
  design's re-entrancy flag, which a reviewer showed could never be true on
  entry.

Three things kept from that design because they beat the obvious choice:
screenChanged re-points the watch but never fits (Qt migrates windows onto a
1024x768 lock screen on RDP disconnect, and fitting there would shrink with no
way back); the settle timer, because the fit shrinks and chasing a drag's
intermediates would latch its smallest frame; and connections held by handle
with a QPointer<QScreen>, because a session lock adds and then DELETES a screen.

Every guard lives inside fitFrameToAvailableGeometry() rather than in the
callers, so all three entry points are covered by construction -- a reviewer
caught that the design left the scheduler unguarded while changeEvent called it
on every state change.

Two bugs found in this implementation while writing it: QPointer<QScreen> needs
QScreen COMPLETE (QPointer static_asserts QObject derivation), caught by the
cross build; and resizeEvent recorded the MAXIMIZED size as the user's intent,
so a later un-maximize would try to grow the window to full-screen size. Also
replaced `if (!m_screenChangedConnection)` with an explicit QPointer<QWindow>
comparison: Qt documents Connection::operator bool only as "the connect() call
succeeded" and says nothing about sender lifetime, so a stale-but-truthy
connection would have silently stopped re-pointing the watch.

Verified a no-op elsewhere by running the shipped function body against Qt
6.11.1 on macOS: constructor clamp no-op, fit reports FITS, size and position
unchanged; and under QT_QPA_PLATFORM=offscreen both paths are skipped, so
headless UI doctests keep their full-size window.

NOT verified: whether an RDP resolution change arrives as
availableGeometryChanged on the same screen rather than a screen remove/add,
which is deliberately not fitted on. The work-area path is proven; that one is
inferred.

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

* feat(windows): ship the portable Basecamp with its modules pre-installed

The distributed build cut installedModules to [] on Windows, and skipped
nix-bundle-dir was never the reason -- the comment claimed bundle.sh
would emit an empty payload, which is true, but the conclusion should
have been "don't run bundle.sh on Windows", not "ship no modules".

A PE needs no relocation: its import table carries DLL base names only
and Windows searches the executable's own directory first, so the build
output is already position-independent, and win-dll-link.sh has staged
the closure into bin/. nix-bundle-lgx reached the same conclusion for
module payloads in d539a3f; this applies it to the app bundle.

Without this there is no portable Windows Basecamp with a working
package manager, and therefore no way to install anything from a
catalog: platformVariantsToTry() appends "-dev" to every variant unless
LGPM_PORTABLE_BUILD is set, so the dev build asks for
`windows-x86_64-dev` while a published package is `windows-x86_64` and
every catalog row reports Not Available.

Native builds are unchanged by construction -- bundleFor is dirBundler
off Windows.

* fix(windows): app.nix copied no shared libraries at all

The liblogos shared-library loop globbed *.dylib and *.so only. On Windows
it matched nothing, and because each candidate is guarded by `[ -f "$f" ]`
with a trailing `|| true`, it copied zero files and exited 0.

Thirteen DLLs sit in that same lib/ -- liblogos_core, libpackage_manager_lib,
liblgx, icuuc76, icudt76, libsodium-26 and the mingw runtime -- and
LogosBasecamp.exe imports liblogos_core.dll DIRECTLY. The result is
0xC0000135 (STATUS_DLL_NOT_FOUND) before main(): no Qt error, no stderr, no
output whatsoever. It has only ever run on Windows because those DLLs were
hand-staged into the payload.

Adds *.dll to the glob, sends DLLs to bin/ rather than lib/ (Windows searches
the executable's own directory, has no rpath, and win-dll-link.sh only
processes $out/bin), and makes copying zero libraries a hard error -- this
loop exiting 0 having done nothing is the whole defect.

Also corrects the comment on bundleFor. It claimed skipping nix-bundle-dir on
Windows was right because a PE needs no relocation. That conflates the
bundler's two jobs: relocation (genuinely unnecessary on PE) and Qt plugin /
QML / qt.conf staging (required on every platform, since those DLLs are
LoadLibrary'd and appear in no import table). Skipping it yields a bundle with
no qt.conf and no QPA plugin. The comment now says so and names the real fix.

* fix(windows): stage liblogos' own dependency DLLs, and prove the closure

461ddcd copied ${logosLiblogos}/lib into bin/, which was necessary but not
sufficient. liblogos_core.dll and logos_host.exe each import libspdlog.dll and
libfmt.dll, and those two live in liblogos' BIN, not its lib/ -- nixpkgs'
win-dll-link.sh walked logos_host.exe's imports and staged the closure there.
spdlog is a buildInput of liblogos, not of basecamp, so basecamp's own
win-dll-link pass could not resolve them either.

Measured on the built bundle with a PE-capable objdump, fixpoint over 52 seed
PEs: with lib/ copied and bin/ not, libspdlog.dll and libfmt.dll were the only
non-OS names left unresolved for LogosBasecamp.exe. That is still 0xC0000135
before main() -- the exact symptom copying lib/ was meant to cure. Mirrors what
logos-package-manager/nix/lib.nix already does with liblgx's closure.

This runs in postFixup, not installPhase, and the ordering is load-bearing:
win-dll-link.sh registers _linkDLLs in fixupOutputHooks, which run BEFORE
postFixup. Doing it in installPhase makes the already-present skip vacuous, so
Qt6Core/Qt6Network/Qt6RemoteObjects get copied out of liblogos as real files
while Qt6Gui/Qt6Widgets stay symlinked into basecamp's own qtbase -- a bin/
with a MIXED Qt in it the moment those pins diverge. Verified: in postFixup the
loop stages exactly 2 files and every Qt DLL stays a symlink.

The check asserts the invariant rather than the mechanism: every DLL that
liblogos_core.dll imports AND that liblogos itself ships must resolve next to
the executable. Names liblogos does not ship are OS DLLs, so no hand-maintained
system-DLL list can rot. A zero import count is treated as a measurement
failure (objdump with no PE target), never as a pass. Both failure paths were
observed firing, not assumed.

Native Linux is unchanged: same file set, byte-identical staged .so files.

* fix(windows): drive the bundle's DLL import closure to a fixpoint

$out/modules/<m>/ and $out/plugins/<p>/ were never walked at all.

nixpkgs' win-dll-link.sh registers _linkDLLs in fixupOutputHooks, and
that entry point only ever processes $prefix/bin. modules/ and plugins/
are filled in installPhase from .lgx payloads and UI-plugin outputs -- in
directories the hook does not look at -- so nothing ever read their
import tables. Measured on this bundle before this commit: main_ui.dll
imports Qt6Qml.dll, Qt6QuickControls2.dll and Qt6QuickWidgets.dll and not
one of them was in plugins/main_ui/ or in bin/.

One pass would not have been enough either: you cannot read the imports
of a DLL that is not there yet. The sweep needs five rounds here, and
round 4 is not decoration -- Qt6QmlWorkerScript.dll is reachable only
through Qt6QmlMeta.dll, which round 3 staged.

So: index every .dll in the build closure (closureInfo over the same
roots as buildInputs -- a PE embeds no store paths, so this derivation's
own references are useless for the purpose), then walk bin/, lib/,
modules/* and plugins/* to a fixpoint, staging what is missing into bin/
and FAILING on anything the closure cannot provide. Result: 9 DLLs staged
over 5 rounds, and every non-system import in the bundle now resolves.

This is a hard error and not a warning because the failure it prevents is
totally silent: a missing DLL is 0xC0000135 before main() runs, or
ERROR_MOD_NOT_FOUND blamed on the plugin, with no Qt message and no
stderr. It exits 0 at build time every time.

It is also the only place the windowsHostLibs list in nix-bundle-lgx can
be falsified. That list DELETES DLLs from every .lgx payload on the claim
that the host ships them; nothing there can check the claim, and one
wrong entry (libiconv-2.dll, reached via libcurl-4 -> libidn2-0) shipped
a package_downloader that could not load on real Windows. A/B: with that
entry restored, this build now fails and names the DLL and its three
importers instead of producing a broken bundle.

Two anti-vacuity guards, since a wrong answer here always looks like
success: an empty DLL index is fatal, and reading zero imports across the
whole bundle is treated as a measurement failure (an objdump without a PE
target prints nothing, and then every import "resolves").

Also fixes a silent no-op next door: `if ls "$sdk/lib/"liblogos_sdk.*`
runs with nullglob set, so an unmatched glob left `ls` with NO arguments,
which listed the working directory and succeeded -- after which
`cp -L "$out/lib/"` ran with a single argument and printed
"cp: missing destination file operand", guarded by `|| true`. Test each
candidate instead. This is the only change outside the isWindows guard;
the native output tree is byte-identical apart from self-referential
store paths.

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

* fix(windows): the import gate could not see 12 of its own roots

Three holes, all silent, all in the check that exists to catch silent holes.

`_pe_roots` used `find ... -type f`, which does not match a SYMLINK -- and 12
of the 36 PE entries in $out/bin are relative symlinks created by nixpkgs'
win-dll-link.sh (Qt6Core, Qt6Gui, Qt6Widgets, Qt6Network, Qt6RemoteObjects,
libcrypto, libssl, libzstd, libb2, libpng16, libpcre2, libdouble-conversion).
Their import tables were never read. Four non-system names in this tree are
imported only by those roots, three of them entries on nix-bundle-lgx's
windowsHostLibs -- the list this gate is the only thing capable of falsifying.
`find -L` makes -type f test the target. Measured on the real bundle: root set
105 -> 117, exactly the 12 that were invisible.

Every branch also carried `-maxdepth 1`, so a PE below the first level of lib/,
modules/<m>/ or plugins/<p>/ was skipped with no message. Nothing in the current
bundle sits there, but it was demonstrated by putting one there: the gate stayed
at rc=0 while a full-depth sweep found 8 unresolved imports. Depth limit dropped.

A dangling symlink is invisible to `find -L -type f` and unopenable at runtime,
so it is now a hard error rather than a quiet omission from the root set.

And the success line claimed "Every non-system PE import in bin/, lib/, modules/
and plugins/ resolves" while checking neither the symlinks nor anything deeper
than depth 1. Since it was the only output on the pass path it read as proof of
full coverage. It now states the size of what it checked:

  PE import closure verified: 117 root(s), 7141 import(s) read, 0 unresolved.

* fix: give installing the same IPC budget as downloading

Downloading had a five-minute deadline (kDownloadIpcDeadlineMs) while
installing was left on the 20 s default from logos_mode.h — backwards.
Downloading is network-bound and retryable; installing is disk-bound over a
payload package_manager reads, gunzips and tar-parses three times and
Merkle-hashes twice, then extracts and copies. Measured at ~1 s on a warm dev
box and unbounded on a slow disk, a large package, or a machine whose
antivirus scans each DLL as it lands.

Blowing the deadline cancels none of that work: the files still install and
only the reply is abandoned, so the user is told the install failed when it
did not — which is exactly what a Windows install looked like while a separate
read-only-payload bug was making the reply never arrive at all.

Also names the empty-reply case instead of reporting the generic
"Installation failed". A dropped or timed-out call arrives here as a
default-constructed QVariantMap because installPluginAsync's callback has no
error slot; every real reply carries "path", so an empty map is never a
genuine answer. That inference is INTERIM: logos-cpp-sdk master f3369fa (#132)
already provides installPluginAsyncResult with logos::AsyncResult<T> carrying
the real CallError, and both call sites should switch to it once
feat/windows-cross picks that up — it is 12 commits behind master today. The
comments say so at both sites. f3369fa did not change the async default
deadline, so this Timeout is required either way.

* fix: tell a dropped install reply apart from a failed install

Both call sites used installPluginAsync, whose callback receives a bare
QVariantMap — so a transport failure and a provider that legitimately returned
an empty map are the same thing to the caller. A timed-out call therefore
surfaced as the generic "Installation failed", pointing the user at a package
that was very likely fine, and on Windows that is exactly what a separate
read-only-payload bug produced: every file installed, no reply came back, and
the row went red.

Switches to installPluginAsyncResult, whose callback takes
logos::AsyncResult<QVariantMap> and carries the value and the error together.
Transport failure is now checked FIRST, before `value` is read at all — on a
timeout `value` is default-constructed, and interpreting it as an install
verdict is the precise mistake this channel exists to remove.

The message says the deadline expired and that the package may in fact be
installed, because blowing the deadline cancels nothing already underway.

This replaces an interim heuristic that inferred a dropped reply from an empty
map. The inference was sound — a real reply always carries "path" — but it was
an inference. The real channel arrived on this branch with logos-cpp-sdk
f3369fa (#132, "async callers can see the error, sync callers can set a
deadline"), picked up by the master merge; the comments left at both sites
named it as the thing to switch to.

Builds for x86_64-linux and x86_64-windows in both repos.

* chore(deps): re-pin the chain to its merged revs

Levels 1-8 of the Windows chain are merged.  This branch was locked to
pre-merge revs of every one of them, so it could only evaluate against the
unmerged branches.

Level 9; the workspace re-pin follows once this lands.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 11:47:30 -03:00
vedran cee212ffa8 ci: fix job paths after move under logos folder
Jenkins jobs moved from /logos-basecamp/* to /logos/logos-basecamp/*,
which broke everything referencing the old paths: the combined build
could no longer resolve child package jobs, getPublishDefault never
matched the new JOB_NAME so release builds lost their PUBLISH=true
default, and copyArtifactPermission no longer covered the combined
job's new location.
2026-08-10 15:55:05 +02:00
Roman Zajic 24dfadf3ed Test/persistence portability (#317)
* test: persistence portability doctest

* fix: harden persistence doctest fixture and paths
2026-08-07 15:27:05 +02:00
Roman Zajic 484da77c45 Test/missing dependencies (#312)
* test: missing dependencies

* fix: add on push trigger

* fix: comments timeout

* fix: remove on push trigger
2026-08-05 15:15:52 +02:00
Roman ZajicandCopilot Autofix powered by AI 5be4f4f1c8 Test/basecamp package lifecycle (#298)
* test: basecamp package lifecycle

* fix: accommodate changes from master

* fix: comment - Basecamp fetches its default repository catalog in the background

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: comment - hermetic — no catalog packages are downloaded or installed

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-08-05 10:41:39 +00:00
Roman ZajicandCopilot Autofix powered by AI 6bb899bb9a Test/basecamp package lifecycle (#298)
* test: basecamp package lifecycle

* fix: accommodate changes from master

* fix: comment - Basecamp fetches its default repository catalog in the background

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: comment - hermetic — no catalog packages are downloaded or installed

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
pre-release-aa23776-688 pre-release-87fb0d6-683
2026-07-31 16:34:04 +02:00
Khushboo Mehta 25300f0e90 fix: fix the show/hide behavior for basecamp after its minimized 2026-07-31 13:16:18 +00:00
Khushboo Mehta 67f3244e96 chore: make texts in basecamp selectable pre-release-c7505d9-667 2026-07-31 09:30:18 +00:00
Khushboo Mehta 631494a32f fix: fix the breaking tests 2026-07-31 07:28:47 +00:00
Khushboo Mehta efb48100b9 fix: fix review comments from copilot 2026-07-31 07:28:47 +00:00
Khushboo Mehta 4ed06ed95e feat: update modules settings and move them into their own App Inspector and Module Inspector parts 2026-07-31 07:28:47 +00:00
Khushboo Mehta a55a029f16 fix: fix for appmanager shows an uninstalled module from pmui 2026-07-30 15:42:21 +00:00
Khushboo Mehta 2757b5fee5 chore: remove install lgx from modules settings 2026-07-30 15:42:21 +00:00
Khushboo Mehta f450ac26b4 chore: use LogosScrollView instead of raw ScrollView 2026-07-30 15:42:21 +00:00
Khushboo Mehta a34216abf0 fix: fix the scrollbar position to right of the container with 4px gap and simplify AppManagerView.qml 2026-07-30 15:42:21 +00:00
Khushboo Mehta 3a73d33824 chore: revert x on linux closes app 2026-07-30 14:58:49 +00:00
Roman 601b173899 fix: pin-release-test-modules 2026-07-30 14:03:19 +00:00
Alisher e39cd4060c chore: fix stale text=auto comment — rules are explicit text on .nix/.sh (per @jzaki review) pre-release-d41a72b-629 2026-07-29 15:49:15 +00:00
James Zaki 3e3dc894b9 Apply suggestions from code review
Co-authored-by: James Zaki <james.zaki@proton.me>
2026-07-29 15:49:15 +00:00
AlisherandClaude Opus 4.8 6ce4804e0a build: add .gitattributes enforcing LF to fix CRLF-on-Windows/WSL build breaks
Windows/WSL checkouts (default core.autocrlf=true) rewrite text files to
CRLF, and a CRLF nix/*.nix breaks the configurePhase shell scripts — the
node build then fails on WSL with no obvious cause. A root .gitattributes
with 'text=auto eol=lf' forces LF on checkout for all text files while
leaving binaries untouched, so clones build identically on every platform.

Fixes logos-co/logos-basecamp#288

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-29 15:49:15 +00:00
Roman Zajic e2d60cf019 chore: add coverage report to flake and CI (#293)
Squashed:
  - chore: add coverage report to flake and CI
  - fix: add report upload

Adds a .#coverage nix output that runs unit tests under gcov, drops
the coverage.txt summary into GITHUB_STEP_SUMMARY, and uploads the
full result/ tree as a workflow artifact. Non-blocking
(continue-on-error) for now.
2026-07-29 13:45:33 +02:00