Dario Gabriel LipicarandClaude Opus 5 a1eb99d839 chore(deps): bump onto the module-builder that restores the view hook
logos-module-builder 352df67 -> e45caf1.

This module is `type: core` and emits no ui glue, so nothing here changes
behaviour — the bump exists so consumers stop reaching an older
module-builder through this lock. logos-basecamp relocks onto this repo, and
its ui_qml chain needs e45caf1, which carries logos-view-module d6c8885 and
with it the module teardown hook that generated view plugins had been
missing (module-builder#214).

Only logos-module-builder moves at root; +1 node (rust-sdk gained an
explicit protocol input), so the lock size is effectively unchanged.

Verified on aarch64-darwin and x86_64-linux: every check plus every module
output. The unit-tests check RUNS the suite in-build rather than only
compiling it — 100 passed on both platforms.

One thing worth writing down, since it cost time here: `lm methods` is NOT a
raw QMetaObject dump for provider plugins. logos-module/src/logos_module.cpp:283
takes a LogosProviderPlugin early-return that never reaches the meta-object
walk at :315, so lifecycle methods are omitted and `lm` reports absent for a
plugin that demonstrably has them (`nm -gU | c++filt` finds
aboutToUnload/unloadFinished in the same binary). Anyone verifying a hook
this way needs QPluginLoader + QMetaObject, or nm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-22 23:56:18 -03:00
2025-12-02 15:28:29 -05:00
2025-10-09 09:26:12 -04:00
2026-04-13 13:35:40 -04:00

logos-package-manager-module

Logos module that wraps logos-package-manager as a process-isolated service, exposing local package installation and module scanning via LogosAPI.

This module handles local operations only (installing from .lgx files, scanning installed modules/UI plugins). For online catalog browsing and downloading, see logos-package-downloader-module.

The underlying logos-package-manager library is plain C++17 (no Qt). This module provides the Qt/IPC boundary, converting between std::string/std::vector and QString/QVariantList.

Module API

All methods are accessible via LogosAPI from other modules and UI plugins.

Directory Configuration

Embedded directories (multiple, read-only at runtime):

Method Description
setEmbeddedModulesDirectory(dir) Clear and set a single embedded modules directory
addEmbeddedModulesDirectory(dir) Append an additional embedded modules directory
setEmbeddedUiPluginsDirectory(dir) Clear and set a single embedded UI plugins directory
addEmbeddedUiPluginsDirectory(dir) Append an additional embedded UI plugins directory

User directories (single, writable, where new packages are installed):

Method Description
setUserModulesDirectory(dir) Set directory for user-installed core modules
setUserUiPluginsDirectory(dir) Set directory for user-installed UI plugins

Installation & Inspection

Method Return Description
installPlugin(path, skipIfNotNewer) QVariantMap Install a local .lgx file. Returns {name, path, isCoreModule, signatureStatus, error}. Success is the absence of error; path is the installed main file, or the installed module directory for a package with no main (QML-only ui_qml), and is non-empty on success. When signatureStatus is "signed" or "invalid", also includes signerDid, signerName, signerUrl, trustedAs. When signatureStatus is "error", includes signatureError.
inspectPackage(lgxPath) QVariantMap Inspect an LGX file without installing. Returns metadata + install status: {name, version, type, description, category, rootHash, signatureStatus, signerDid?, signerName?, isAlreadyInstalled, installedVersion?, installedHash?, installedDependents?, variants}. rootHash is the Merkle tree root from manifest.hashes.root — the same identifier the online catalog exposes. When isAlreadyInstalled is true, installedHash is the corresponding value from the on-disk manifest. Used by callers (e.g. Basecamp) to show a confirmation dialog before committing.
uninstallPackage(packageName) QVariantMap Remove a user-installed package immediately (ungated). Refuses embedded packages. Returns {success, error?, removedFiles?}. On success emits corePluginUninstalled or uiPluginUninstalled. Headless callers (lgpm, scripts) should use this. GUI callers should prefer requestUninstall below.

Scanning

Method Return Description
getInstalledPackages() QVariantList All installed packages (modules + UI plugins)
getInstalledModules() QVariantList Installed core modules only
getInstalledUiPlugins() QVariantList Installed UI plugins only
getValidVariants() QStringList Platform variants this build accepts (e.g. ["darwin-arm64-dev"])

Each item in the scan results contains all manifest.json fields plus installDir, mainFilePath, and installType ("embedded" or "user").

Dependency Resolution

Method Return Description
resolveDependencies(packageName, recursive) QVariantMap Forward dependency tree rooted at packageName. Shape: {name, status, version, installType, children: [...]}. recursive=false walks only depth-1 (children have empty children); recursive=true walks the full tree, stopping at NotInstalled/Cycle nodes. Unknown root → {}.
resolveDependents(packageName, recursive) QVariantMap Reverse dependency tree rooted at packageName. Shape: {name, version, type, installType, installDir, children: [...]}. Same depth semantics as resolveDependencies. Unknown root → {}.
resolveFlatDependencies(packageName, recursive) QVariantList Flat projection of the forward walk. Each entry: {name, status, version, installType} (no children). recursive=false → direct children only; recursive=true → every descendant, BFS-ordered, deduped by name.
resolveFlatDependents(packageName, recursive) QVariantList Flat projection of the reverse walk. Each entry: {name, version, type, installType, installDir}. Same recursive semantics as resolveFlatDependencies.

Gated Uninstall / Upgrade Flow

For GUI callers that need to show a confirmation dialog before destructive operations. The protocol ensures destructive work never runs without a live listener driving the dialog.

Protocol:

  1. Caller invokes requestUninstall(name) or requestUpgrade(name, releaseTag, mode). Returns {success, error?} synchronously. On success, sets pending state, emits beforeUninstall / beforeUpgrade, and starts a 3-second ack timer.

  2. A listener receiving the event must immediately call ackPendingAction(name) to cancel the ack timer. This says "I'm driving the dialog — wait indefinitely for the user decision."

  3. If the ack timer fires without an ack (no listener present, or event loop stalled >3s), the module clears pending state and emits uninstallCancelled / upgradeCancelled. No files are removed.

  4. Once acked, the listener shows a confirmation dialog. On user confirm: confirmUninstall(name) / confirmUpgrade(name, releaseTag). On cancel: cancelUninstall(name) / cancelUpgrade(name, releaseTag).

Only one gated flow can be pending globally (across all packages and both operations). A second requestXxx while one is pending returns {success: false, error: "Another <op> is in progress for '<name>'"}.

Method Return Description
requestUninstall(name) QVariantMap Start gated uninstall. Emits beforeUninstall, starts ack timer.
requestUpgrade(name, releaseTag, mode) QVariantMap Start gated upgrade. mode: 0=upgrade, 1=downgrade, 2=sidegrade. Emits beforeUpgrade, starts ack timer.
ackPendingAction(name) QVariantMap Acknowledge receipt of a before* event. Cancels the ack timer. Idempotent.
confirmUninstall(name) QVariantMap Proceed with uninstall. Removes files, emits corePluginUninstalled / uiPluginUninstalled.
cancelUninstall(name) QVariantMap Abort uninstall. Emits uninstallCancelled(name, "user cancelled").
confirmUpgrade(name, releaseTag) QVariantMap Proceed with upgrade. Uninstalls old version, emits upgradeUninstallDone for the caller to drive the download+install of the new version.
cancelUpgrade(name, releaseTag) QVariantMap Abort upgrade. Emits upgradeCancelled(name, releaseTag, "user cancelled").
resetPendingAction() QVariantMap Clear any pending state. Called by Basecamp at startup to recover from a prior crash mid-dialog.

Signature Policy

Method Description
setSignaturePolicy(policy) Set policy: "none", "warn" (default), or "require"
setKeyringDirectory(dir) Override trusted keys directory (default: ~/.config/logos/trusted-keys/)
verifyPackage(lgxPath) Standalone verification. Returns {isSigned, signatureValid, packageValid, signerDid, signerName, signerUrl, trustedAs, error}

Keyring Management

Method Return Description
addTrustedKey(name, did, displayName, url) QVariantMap Add a trusted signing key. Returns {success, error}
removeTrustedKey(name) QVariantMap Remove a trusted key by name. Returns {success, error}
listTrustedKeys() QVariantList List all trusted keys. Each entry: {name, did, displayName, url, addedAt}

Events

Installation events:

Event Data Description
corePluginFileInstalled [path] Emitted after a core module .lgx is installed
uiPluginFileInstalled [path] Emitted after a UI plugin .lgx is installed
corePluginUninstalled [name] Emitted after a core module is uninstalled
uiPluginUninstalled [name] Emitted after a UI plugin is uninstalled

The install events' path is the installed main file when the package ships one, and the installed module directory when it does not — a QML-only ui_qml package has no backend library, so its manifest carries an empty "main". Treat it as an opaque location, not as a file: do not QPluginLoader it or take its fileName(). To load a UI plugin, react to the event by rescanning with getInstalledUiPlugins() and use the mainFilePath / view fields from there (that is what Basecamp does).

Gated flow events (see "Gated Uninstall / Upgrade Flow" above):

Event Data Description
beforeUninstall {name, installedDependents} A gated uninstall was requested. Listener must ack within 3s.
beforeUpgrade {name, releaseTag, mode, installedDependents} A gated upgrade was requested. Listener must ack within 3s.
uninstallCancelled {name, reason} Uninstall was cancelled — either by ack timeout or user cancel.
upgradeCancelled {name, releaseTag, reason} Upgrade was cancelled — either by ack timeout or user cancel.
upgradeUninstallDone {name, releaseTag, mode} Old version uninstalled during upgrade; caller should now download+install the new version.

Usage from another module

LogosModules logos(logosAPI);

// Configure embedded directories (multiple, read-only)
logos.package_manager.setEmbeddedModulesDirectory("/app/modules");
logos.package_manager.addEmbeddedModulesDirectory("/app/extra-modules");
logos.package_manager.setEmbeddedUiPluginsDirectory("/app/ui-plugins");

// Configure user directories (single, writable)
logos.package_manager.setUserModulesDirectory("/home/user/.logos/modules");
logos.package_manager.setUserUiPluginsDirectory("/home/user/.logos/ui-plugins");

// Scan installed packages
QVariantList all = logos.package_manager.getInstalledPackages();
QVariantList modules = logos.package_manager.getInstalledModules();
QVariantList uiPlugins = logos.package_manager.getInstalledUiPlugins();

// Configure signature policy
logos.package_manager.setSignaturePolicy("require");
logos.package_manager.setKeyringDirectory("/home/user/.config/logos/trusted-keys");

// Manage trusted signing keys
logos.package_manager.addTrustedKey("logos-official", "did:jwk:eyJj...", "Logos Foundation", "https://logos.co");
QVariantList keys = logos.package_manager.listTrustedKeys();
// Each entry: {name, did, displayName, url, addedAt}
logos.package_manager.removeTrustedKey("logos-official");

// Verify a package before installing
QVariantMap sigInfo = logos.package_manager.verifyPackage("/path/to/waku_module.lgx");
// sigInfo: {isSigned, signatureValid, packageValid, signerDid, signerName, signerUrl, trustedAs, error}

// Inspect an LGX before installing (shows metadata in a confirmation dialog)
QVariantMap info = logos.package_manager.inspectPackage("/path/to/waku_module.lgx");
// info: {name, version, type, signatureStatus, isAlreadyInstalled, installedVersion?, ...}

// Install a downloaded .lgx file (ungated — for headless/scripted use)
QVariantMap result = logos.package_manager.installPlugin("/path/to/waku_module.lgx", false);
if (result.contains("error")) {
    qWarning() << "Install failed:" << result["error"].toString();
}
// result also includes: signatureStatus ("signed"/"unsigned"/"invalid"), signerDid, signerName, signerUrl, trustedAs

// Flat deduped list of every reverse dependent (BFS over the reverse tree).
QVariantList dependents = logos.package_manager.resolveFlatDependents("my_module", true);
// Or fetch the tree shape directly when you want parent/child structure.
QVariantMap dependentsTree = logos.package_manager.resolveDependents("my_module", true);

// GUI-mode gated uninstall (requires a listener to drive the confirmation dialog)
logos.package_manager.requestUninstallAsync("my_module", [](QVariantMap r) {
    if (!r.value("success").toBool()) qWarning() << r.value("error").toString();
});

// Listen for gated flow events
logos.package_manager.on("beforeUninstall", [&logos](const QVariantList& data) {
    QString name = data[0].toMap().value("name").toString();
    // Ack immediately to cancel the 3s timer
    logos.package_manager.ackPendingActionAsync(name, [](QVariantMap) {});
    // Show dialog... then confirm or cancel:
    // logos.package_manager.confirmUninstallAsync(name, ...);
    // logos.package_manager.cancelUninstallAsync(name, ...);
});

// Listen for installation events
logos.package_manager.on("corePluginFileInstalled", [](const QVariantList& data) {
    QString path = data[0].toString();
    qDebug() << "Core module installed at:" << path;
});

logos.package_manager.on("uiPluginFileInstalled", [](const QVariantList& data) {
    QString path = data[0].toString();
    qDebug() << "UI plugin installed at:" << path;
});

Typical flow with the downloader module

// 1. Download via downloader module
QVariantMap result = logos.package_downloader.downloadPackage("waku_module");

// 2. Install via package manager
if (!result.contains("error")) {
    logos.package_manager.installPlugin(result["path"].toString(), false);
}

Building

Uses logos-module-builder for minimal build configuration.

nix build                # module plugin (lib + include)
nix build .#lib          # plugin .so/.dylib only
nix build .#lgx          # .lgx package
nix build .#lgx-portable # portable .lgx package

Dependencies

  • logos-module-builder — shared Nix/CMake build infrastructure
  • logos-package-manager — package management library (plain C++, linked at build time)
S
Description
No description provided
Readme
801 KiB
Languages
C++ 94%
Shell 3%
CMake 1.6%
C 1%
Nix 0.4%