Files
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

126 lines
4.9 KiB
C++

#pragma once
#include <QHash>
#include <QObject>
#include <QMutex>
#include <QSet>
#include <QStringList>
#include <QVariantList>
// logos::ConsumerIdentity — what logos::admitConsumer hands back. By value in
// the cache below, so it is included rather than forward-declared.
#include "logos_consumer.h"
class LogosAPI;
class IComponent;
class QWidget;
class QQuickWidget;
class ViewModuleHost;
class LogosQmlBridge;
class CoreModuleManager;
enum class UIPluginType {
Legacy,
UiQml
};
struct PluginLoadRequest {
QString name;
UIPluginType type = UIPluginType::Legacy;
QString pluginPath;
QString iconPath;
QVariantList coreDependencies;
// ui_qml module fields
QString installDir; // Module install directory (import paths root)
QString qmlViewPath; // Resolved QML view entry point
QString mainFilePath; // Backend plugin .so/.dylib path (empty if QML-only)
};
class PluginLoader : public QObject {
Q_OBJECT
public:
// coreModuleManager is the single owner of the logos_core_* C API.
// Used here to load a ui plugin's core dependencies before the ui plugin
// itself mounts. Not owned — the PluginLoader's parent (PluginManager)
// holds a sibling pointer to the same CoreModuleManager.
explicit PluginLoader(LogosAPI* logosAPI,
CoreModuleManager* coreModuleManager,
QObject* parent = nullptr);
void load(const PluginLoadRequest& request);
bool isLoading(const QString& name) const;
QStringList loadingPlugins() const;
signals:
void pluginLoaded(const QString& name, QWidget* widget,
IComponent* component, UIPluginType type,
ViewModuleHost* viewHost);
void pluginLoadFailed(const QString& name, const QString& error);
void loadingChanged();
private:
void startLoad(const PluginLoadRequest& request);
void loadCoreDependencies(const PluginLoadRequest& request);
void continueLoad(const PluginLoadRequest& request);
// legacy ui module loading
void loadCppPluginAsync(const PluginLoadRequest& request);
void finishCppPluginLoad(const PluginLoadRequest& request);
// ui_qml module loading
void loadUiQmlModule(const PluginLoadRequest& request);
void loadQmlView(const PluginLoadRequest& request,
LogosQmlBridge* bridge,
ViewModuleHost* viewHost);
void finishUiQmlLoad(QQuickWidget* qmlWidget,
const PluginLoadRequest& request,
LogosQmlBridge* bridge,
ViewModuleHost* viewHost);
void setLoading(const QString& name, bool loading);
// ── per-plugin identity ─────────────────────────────────────────────
//
// Every plugin basecamp loads into its own process is ADMITTED as a
// consumer: its own isolated token store, its own minted credential, and
// that credential registered with capability_module — in that order, by
// logos::admitConsumer, which is the single owner of the operation.
//
// Handing plugins m_logosAPI — the host's "core" identity — gave each of
// them the host's authority: the host store holds every loaded module's
// root auth token, and the call path reads that store before it ever
// considers minting, so a plugin's call to a module it never declared
// authorised on the first attempt with no capability_module handshake in
// the log at all.
//
// THIS USED TO BE TWO PRIVATE HELPERS, apiForPlugin() and
// registerPluginIdentity(), spelled out here and again — differently — in
// logos-standalone-app. The pure-QML identity bug was one of them getting
// the ORDER wrong: the registration sat inside the has-a-backend branch,
// below an early return, so a pure-QML plugin registered nothing. There is
// now one implementation, in logos-plugin-qt, and no order for a host to
// get wrong.
//
// Returns a falsy ConsumerIdentity when the plugin cannot be admitted.
// That is fatal for the plugin: falling back to m_logosAPI would restore
// exactly the escalation this exists to remove, while looking fixed.
logos::ConsumerIdentity consumerFor(const QString& name);
LogosAPI* m_logosAPI;
CoreModuleManager* m_coreModuleManager; // not owned
// name -> that plugin's admitted identity (its LogosAPI is parented to
// this, so owned here). Cached because a LogosAPI captures its store by
// raw pointer and its clients cache minted tokens; rebuilding one per load
// attempt would re-run the requestModule handshake for every target, every
// time — and, now that a credential is registered rather than discarded,
// would invalidate the credential the previous incarnation still holds.
QHash<QString, logos::ConsumerIdentity> m_consumers;
mutable QMutex m_mutex;
QSet<QString> m_loading;
};