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>
34 KiB
Project Description
Project Structure
logos-basecamp/
├── CMakeLists.txt # Root CMake configuration
├── README.md # Project overview and build instructions
├── CLAUDE.md # Developer notes
├── metadata.json # Package metadata
├── flake.nix # Nix flake configuration
├── flake.lock # Nix flake lock file
├── docs/
│ ├── index.md # Documentation index
│ ├── spec.md # High-level specification
│ └── project.md # This document
├── app/ # Main application executable (the host)
│ ├── CMakeLists.txt # App build configuration
│ ├── main.cpp # Entry point
│ ├── window.h/cpp # Main window (QMainWindow)
│ ├── interfaces/ # IShellHost/IShellObserver, IShellView, IComponent
│ ├── utils/ # Utility classes (paths, file helpers)
│ ├── macos/ # macOS-specific code (titlebar styling)
│ ├── icons/ # Application icons
│ ├── MainUIBackend.h/cpp # Core logic (module state, stats, navigation)
│ ├── ShellHostAdapter.h/cpp # IShellHost over MainUIBackend
│ ├── CoreModuleManager.h/cpp # Core-module lifecycle over the SDK facade
│ ├── UIPluginManager.h/cpp # UI-plugin load/unload, widget ownership
│ ├── PluginLoader.h/cpp # Per-plugin identities, ui-host spawning
│ ├── PackageCoordinator.h/cpp # Install/uninstall flows
│ ├── AppsModel.h/cpp # App list model
│ ├── ModuleInstanceModel.h/cpp # Module list model
│ └── restricted/ # ui_qml sandbox (network + filesystem + native-plugin)
│ │ ├── QmlSandbox.h/cpp # applies the sandbox policy to a QML engine
│ │ ├── DenyAllNetworkAccessManager.h/cpp
│ │ ├── DenyAllNAMFactory.h/cpp
│ │ ├── DenyAllReply.h/cpp
│ └── RestrictedUrlInterceptor.h/cpp
├── tests/ # Integration tests
│ ├── ui-tests.mjs # Node.js test suite (logos-qt-mcp)
│ ├── host-services-tests.mjs # Capability trust-root guard (spec)
│ └── host-services-assert.mjs # ...its assertion, shared with ui-tests
├── src/ # The main_ui UI shell plugin
│ ├── CMakeLists.txt # Plugin build (Qt only, no logos runtime)
│ ├── MainShellView.h/cpp # IShellView entry point
│ ├── MainContainer.h/cpp # UI coordinator (sidebar + content)
│ ├── WorkspaceArea.h/cpp # Dock-based app workspace
│ ├── Basecamp/ # QML UI files, by feature
├── nix/ # Nix build modules
│ ├── default.nix # Common build settings
│ ├── app.nix # Application package
│ ├── main-ui.nix # main_ui UI shell plugin
│ ├── smoke-test.nix # Smoke test derivation
│ ├── integration-test.nix # UI integration test harness
│ ├── host-services-test.nix # Host-services grant guard
│ ├── symbol-gate.nix # One-runtime gate + its negative control
│ ├── unit-tests.nix # C++ unit tests
│ ├── qml-tests.nix # QML tests
│ ├── sandbox-test.nix # ui_qml sandbox-escape regression test
│ ├── shutdown-test.nix # Quit-gesture teardown tests
│ ├── coverage.nix # gcovr report over app/ and src/
│ └── build-info.nix # Version/build metadata
├── qt-ios/ # iOS build configuration (experimental)
│ ├── CMakeLists.txt
│ ├── Main.qml
│ └── metadata.json
├── scripts/ # Build and utility scripts
├── assets/ # Application assets
└── LICENSE-* # MIT and Apache 2.0 licenses
Stack, Frameworks & Dependencies
| Component | Purpose |
|---|---|
| C++17 | Implementation language |
| CMake 3.16+ | Build system |
| Qt 6 (Widgets, RemoteObjects, Quick, Qml, QuickWidgets, QuickControls2, WebView) | GUI framework, plugin system, IPC, declarative UI |
| QML | Declarative UI layer for sidebar, views, and module controls |
| Nix | Package management and reproducible builds |
| Ninja | Build backend (via Nix) |
External Logos Dependencies
| Dependency | Purpose |
|---|---|
| logos-cpp-sdk | SDK root: LogosAPI, code generator, IPC layer. Pins nixpkgs/Qt. |
| logos-liblogos | Core library: liblogos_core C API, logos_host subprocess host |
| logos-module | Module introspection library and lm CLI |
| logos-package-manager-module | Package management module (auto-loaded at startup) |
| logos-package-downloader-module | Online catalog browsing and package download |
| logos-capability-module | Inter-module authorization and token management |
| logos-package-manager-ui | Package manager UI module (embedded at build time) |
| logos-design-system | Centralized color/theme definitions (LogosText, LogosButton) |
| logos-qt-mcp | QML inspector plugin for testing and development |
Embedded Components (bundled at build time)
Logos Modules (managed by liblogos, run in isolated logos_host processes):
| Module | Purpose |
|---|---|
package_manager |
Package management (auto-loaded at startup) |
package_downloader |
Online catalog browsing and download |
capability_module |
Inter-module authorization and token management |
UI Apps (Qt plugins loaded directly by Basecamp, displayed as MDI tabs):
| App | Type | Purpose |
|---|---|---|
package_manager_ui |
QML | Package management UI |
How liblogos Is Used
Basecamp is a frontend for liblogos_core, the C shared library that provides the Logos module runtime. The application binary links against liblogos_core and uses its C API to manage the lifecycle of Logos Modules — the process-isolated backend services. Basecamp does not use liblogos for managing UI Apps (Qt plugins) — those are loaded and managed by Basecamp itself using Qt's QPluginLoader and QQuickWidget.
The SDK wrapper (LogosAPI from logos-cpp-sdk) is used on top of the C API to provide ergonomic inter-component communication — it is how UI Apps call methods on Logos Modules via the Logos API.
C API Call Sites
All logos_core_* calls are made from two locations: app/main.cpp (startup/shutdown) and app/MainUIBackend.cpp (runtime module management).
Initialization and startup (app/main.cpp):
| Call | Purpose |
|---|---|
logos_core_add_modules_dir(embeddedDir) |
Add the embedded modules directory (read-only, pre-installed at build time) |
logos_core_add_modules_dir(userDir) |
Add the user-writable modules directory for runtime installs |
logos_core_start() |
Scan module directories, initialize the capability module, start the remote object registry |
logos_core_load_module("package_manager", true) |
Auto-load the package manager module (with dependencies) at startup |
logos_core_get_loaded_modules() |
Query loaded module names for initial status display |
Runtime Logos Module management (app/MainUIBackend.cpp):
| Call | Purpose |
|---|---|
logos_core_load_module(name, true) |
Load a Logos Module and all its declared dependencies (topological sort). Also called when loading a UI App that depends on Logos Modules. |
logos_core_unload_module(name, false) |
Terminate a Logos Module's host process and clean up |
logos_core_refresh_modules() |
Re-scan module directories after a package install |
logos_core_get_loaded_modules() |
Query which Logos Modules are currently running (for Modules view status) |
logos_core_get_known_modules() |
Query all discovered Logos Modules (for Modules view listing) |
logos_core_get_module_stats() |
Get JSON-formatted CPU/memory stats for all loaded Logos Modules (polled every 2s) |
Shutdown (app/main.cpp):
| Call | Purpose |
|---|---|
logos_core_cleanup() |
Terminate all module host processes and release resources |
LogosAPI Usage
A single LogosAPI instance is created in main() with the module name "core" and passed through the component hierarchy: main() → Window → MainContainer → MainUIBackend.
Getting module clients:
LogosAPIClient* client = m_logosAPI->getClient("package_manager");
if (client && client->isConnected()) {
QVariant result = client->invokeRemoteMethod("package_manager", "methodName", arg1);
}
Using the generated wrapper:
LogosModules logos(m_logosAPI);
logos.package_manager.installPluginAsync(filePath, false, callback);
logos.package_manager.on("corePluginFileInstalled", [](const QVariantList& data) { ... });
QML bridge: When loading a QML-based UI App, a LogosQmlBridge is created and injected into the QML context as logos. QML code calls logos.callModule("module", "method", [args]) which dispatches through LogosAPIClient::invokeRemoteMethod() to call Logos Modules via the Logos API, returning a JSON-serialized result.
Core Modules
Application Entry Point
Files: app/main.cpp
Purpose: Initializes the Qt application, configures plugin directories (embedded + user-writable), calls logos_core_start() to boot the runtime, auto-loads the package_manager module, creates the LogosAPI instance, creates the main window, starts a stats polling timer (2s interval), starts the QML inspector (if enabled), and runs the event loop. On exit, calls logos_core_cleanup().
Window
Files: app/window.h, app/window.cpp
Purpose: Main QMainWindow derivative. Loads the UI shell from the main_ui Qt plugin with QPluginLoader, casts it to IShellView, checks hostAbiVersion() against IShellHost_abi, and calls createShell(IShellHost*).
The shell's entire contract is IShellHost: a QWidget* out, eight named operations in. It holds no LogosAPI*, no QtLogosCore* and no TokenManager access, and links no logos runtime — nix/symbol-gate.nix enforces that across the in-process image set rather than trusting it.
Window also owns MainUIBackend and ShellHostAdapter (the shell only borrows them) and drives the ordered teardown described in ~Window. It manages system tray integration (minimize/restore) and applies platform-specific window styling (macOS native titlebar).
MainUIBackend
Files: app/MainUIBackend.h, app/MainUIBackend.cpp
Purpose: Core logic layer exposed to QML. Central coordinator for both Logos Modules and UI Apps — calls liblogos_core to manage Logos Modules, uses QPluginLoader/QQuickWidget to manage UI Apps, polls stats, handles package install events, and manages navigation. This is where most logos_core_* calls and LogosAPI interactions happen.
Properties (exposed to QML):
| Property | Type | Description |
|---|---|---|
currentActiveSectionIndex |
int | Currently selected navigation section |
sections |
QVariantList | Navigation entries (Dashboard, Modules, Settings, apps) |
uiModules |
QVariantList | Discovered UI Apps with load status |
coreModules |
QVariantList | Discovered Logos Modules with load status and CPU/memory stats |
launcherApps |
QVariantList | UI Apps available for launching |
currentVisibleApp |
QString | Currently focused UI App name |
Key Methods:
| Method | Description |
|---|---|
loadUiModule(name) |
Load a UI App (QML or C++ plugin) — resolve Logos Module dependencies via logos_core_load_module(name, true), then load the Qt plugin and create a tab in MDI |
unloadUiModule(name) |
Remove tab from MDI, destroy widget, clean up tracking state. Logos Module dependencies are left running. |
loadCoreModule(name) |
Load a Logos Module via logos_core_load_module(name, true), spawning a logos_host process |
unloadCoreModule(name) |
Unload a Logos Module via logos_core_unload_module(name, false), terminating its host process |
refreshCoreModules() |
Call logos_core_refresh_modules() then logos_core_get_known_modules() to refresh the Logos Module list |
updateModuleStats() |
Call logos_core_get_module_stats(), parse JSON, update m_moduleStats map for Logos Modules |
subscribeToPackageInstallationEvents() |
Register event listeners on package_manager Logos Module for corePluginFileInstalled and uiPluginFileInstalled events |
fetchUiPluginMetadata() |
Async call to package_manager.getInstalledUiPluginsAsync() to populate UI App metadata cache |
confirmInstallGate(name) / cancelInstallGate(name) |
Forward the user's install-gate decision back to package_manager so the initiator (package_manager_ui) proceeds or aborts |
MainContainer
Files: src/MainContainer.h, src/MainContainer.cpp — plugin side, Qt only
Purpose: UI coordinator that assembles the sidebar (QML SidebarPanel) and content area (stacked widget with WorkspaceArea + QML system views), and routes navigation between them. It does not create MainUIBackend any more — Window owns that and the shell borrows it through IShellHost, reaching it from QML as an opaque QObject* via backendObject().
LogosQmlBridge
Files: none in this repo — LogosQmlBridge is an external header, included by app/PluginLoader.cpp from a flake input's include path (logos-view-module-runtime).
Purpose: Bridge between QML-based UI Apps and Logos Modules. Injected into each QML UI App's context as logos, enabling UI Apps to call Logos Module methods via the Logos API.
API:
| Method | Description |
|---|---|
callModule(module, method, args) → QString |
Get a LogosAPIClient for the target Logos Module, call invokeRemoteMethod(), serialize the result to JSON |
The bridge validates that the LogosAPI is available and the target Logos Module is connected before dispatching. Results are serialized to JSON (objects, arrays, primitives) for consumption by QML.
WorkspaceArea
Files: src/WorkspaceArea.h, src/WorkspaceArea.cpp — plugin side, Qt only
Purpose: Dock-based app workspace, one dock per loaded UI App: addPluginDock / removePluginDock / activatePluginDock. It replaced the earlier MdiView / MdiChild QMdiArea tab pair, which no longer exist.
QML Sandbox
Files: app/restricted/QmlSandbox.h/cpp, app/restricted/DenyAllNetworkAccessManager.h/cpp, app/restricted/DenyAllNAMFactory.h/cpp, app/restricted/DenyAllReply.h/cpp, app/restricted/RestrictedUrlInterceptor.h/cpp
Purpose: Security layer for QML-based UI Apps (ui_qml modules), applied by QmlSandbox::configure() (the single setup PluginLoader::loadQmlView runs on each app's QQmlEngine). A ui_qml app is meant to be QML/JS only, confined to its own install directory; the sandbox enforces that on three fronts:
- Network: a
DenyAllNAMFactoryblocks all outgoing HTTP/HTTPS. Apps that need network do so indirectly through Logos Modules via the QML bridge. - Filesystem: a
RestrictedUrlInterceptorresolves onlyqrc:URLs and local files under an allow-list of roots (the app's own dir, the vetted app lib dir's shared Logos QML modules, and Qt's own module dirs). Other schemes and existing paths outside the roots are blocked. A non-existent path is passed through untouched — Qt's module resolution probes many non-existent<importPath>/<Module>[.ver]/qmldircandidates before finding the real one, and a path that doesn't exist can load nothing; if it later resolves to a real file, that file is re-intercepted (now with a non-empty canonical path) and vetted against the roots then. The only fail-closed case is the genuinely anomalous one — a path that exists but still won't canonicalise (e.g. a symlink loop) — which is blocked because it can't be vetted yet could back a real resource. - Native code: the app's install dir is not added to the engine's native-plugin search path, and a qmldir living under the app's own (untrusted) dir may not declare a native
plugin. Without this, aui_qmlapp could ship aqmldirwith aplugindirective plus a matching Qt plugin.soand have Qtdlopen()it straight into the host process — full native code execution, defeating the network/filesystem guarantees (formerly tracked as finding F-008). Native plugin loading bypasses URL interception entirely, so the qmldir that declares the plugin is the choke point: rejecting that qmldir makes the malicious module simply "not installed". Vetted roots (the app lib dir, Qt's module dirs — which legitimately ship native plugins like QtQuick) are exempt.
The escape and its fix are covered by the sandbox-test check (tests/sandbox/, nix build .#sandbox-test), which builds a real malicious QML plugin and asserts it is never loaded while a legitimate pure-QML module still is. The same check also regresses the rest of the ui_qml sandbox policy — network deny (HTTP and file://), URL-interceptor blocking of remote-scheme loads and out-of-root file reads, and the matching positive cases (files under the module's own dir and qrc: resources still resolve) — i.e. the guarantees the counter_qml probe app exercises by hand, now driven against the real QmlSandbox::configure. On top of those mechanism-level slots, tests/sandbox/evil_app/ is an end-to-end adversarial fixture — the evil twin of counter_qml — a real ui_qml view whose Main.qml automatically fires every escape vector on load and tallies an escapes count; the check loads it through the real sandbox and asserts escapes == 0 (plus a QML-only F-008 probe: an EvilModule/qmldir declaring a native plugin must be rejected at import).
| Class | Description |
|---|---|
QmlSandbox (namespace) |
configure(engine, installDir, qmlViewPath, appLibDir) — applies the whole ui_qml sandbox policy to a QML engine. Factored out of PluginLoader so it is unit-testable against a bare QQmlEngine. |
DenyAllNetworkAccessManager |
Qt network access manager that rejects all requests |
DenyAllNAMFactory |
Factory that creates deny-all NAM instances for QML engines |
DenyAllReply |
Network reply that immediately signals error |
RestrictedUrlInterceptor |
URL interceptor: gates file/qmldir resolution to allowed roots (non-existent probe candidates pass through so Qt's module resolution still works; an existing path that can't be canonicalised fails closed), and rejects a qmldir under an untrusted root that declares a native plugin |
QML UI Layer
SidebarPanel
File: src/Basecamp/Sidebar/SidebarPanel.qml
Purpose: Left-hand navigation panel. Sections are filtered by type — "workspace" entries appear at the top, "view" entries at the bottom. Loaded apps appear in the middle with close/activate interactions.
ContentViews
File: src/Basecamp/Shell/ContentViews.qml
Purpose: Content area using StackLayout with four indices: MDI area (index 0), Dashboard (1), Modules (2), Settings (3). The active index is controlled by the sidebar selection.
ModulesView
File: src/Basecamp/Settings/ModulesView.qml
Purpose: Component management screen with two tabs: UI Apps (Qt plugins managed by Basecamp) and Logos Modules (process-isolated modules managed by liblogos). Lists available/loaded components with load/unload buttons, icons, and status indicators. The Logos Modules tab also shows CPU/memory stats for running modules. Includes "Install LGX Package" action.
DashboardView / SettingsView
Files: src/Basecamp/Settings/DashboardView.qml, src/Basecamp/Settings/SettingsView.qml
Purpose: System views for overview information and application configuration.
Sequence Flows
Application Startup
main()
├─ QApplication(argc, argv)
├─ logos_core_add_modules_dir(<app>/../modules) # Embedded modules (read-only)
├─ logos_core_add_modules_dir(~/.local/share/.../modules) # User modules (writable)
├─ logos_core_start() # Scan dirs, init capability module, start registry
├─ logos_core_load_module("package_manager", true) # Auto-load package manager
├─ logos_core_get_loaded_modules() # Log loaded modules
├─ LogosAPI("core", nullptr) # Create SDK instance
├─ Window(&logosAPI)
│ └─ setCentralWidget(new MainContainer(&logosAPI))
│ ├─ MainUIBackend(logosAPI)
│ │ ├─ initializeSections() # Dashboard, Modules, Settings + app sections
│ │ ├─ m_statsTimer.start(2000) # Poll module stats every 2s
│ │ ├─ refreshCoreModules()
│ │ │ └─ logos_core_refresh_modules()
│ │ ├─ subscribeToPackageInstallationEvents()
│ │ │ ├─ logos.package_manager.setUserModulesDirectory(...)
│ │ │ ├─ logos.package_manager.on("corePluginFileInstalled", ...)
│ │ │ └─ logos.package_manager.on("uiPluginFileInstalled", ...)
│ │ └─ fetchUiPluginMetadata()
│ │ └─ logos.package_manager.getInstalledUiPluginsAsync(callback)
│ └─ setupUi() # Create sidebar + content area + MDI
├─ statsTimer.start(2000) # Console stats logging
├─ QML Inspector start (if enabled)
├─ app.exec() # Qt event loop
└─ logos_core_cleanup() # Terminate all modules on exit
Loading a UI App (QML)
User clicks "Load" in UI Apps tab (or clicks app icon in sidebar)
└─ MainUIBackend::loadUiModule(name)
├─ Look up app metadata from m_uiPluginMetadata cache
├─ Load Logos Module dependencies (if any)
│ └─ logos_core_load_module(dep, true) for each dependency
├─ Create QQuickWidget (loaded in Basecamp process, NOT via liblogos)
├─ Configure QML engine:
│ ├─ Set import/plugin paths
│ ├─ Install DenyAllNAMFactory (block network)
│ ├─ Install RestrictedUrlInterceptor (whitelist app dir only)
│ └─ Set base URL to app directory
├─ Create LogosQmlBridge(logosAPI) → inject as "logos" context property
├─ Load QML source file
├─ Store widget in m_qmlPluginWidgets and m_uiModuleWidgets
├─ emit pluginWindowRequested(widget, name) → MdiView adds tab
└─ emit navigateToApps() → sidebar switches to Apps view
Loading a UI App (C++ Qt Plugin)
User clicks "Load" in UI Apps tab
└─ MainUIBackend::loadUiModule(name)
├─ QPluginLoader(pluginPath).load() (loaded in Basecamp process, NOT via liblogos)
├─ qobject_cast<IComponent*>(plugin)
├─ component->createWidget(logosAPI) → get QWidget
├─ Store in m_loadedUiModules and m_uiModuleWidgets
├─ emit pluginWindowRequested(widget, name) → MdiView adds tab
└─ emit navigateToApps()
Unloading a UI App
User clicks close on tab or "Unload" in UI Apps tab
└─ MainUIBackend::unloadUiModule(name)
├─ emit pluginWindowRemoveRequested(widget) → MdiView removes tab
├─ For C++ apps: component->destroyWidget(widget)
├─ For QML apps: widget->deleteLater()
├─ Remove from all tracking maps
├─ emit uiModulesChanged(), launcherAppsChanged()
└─ (Logos Module dependencies remain running)
Loading/Unloading a Logos Module
User clicks "Load" in Logos Modules tab
└─ MainUIBackend::loadCoreModule(name)
├─ logos_core_load_module(name, true) # liblogos spawns logos_host process
└─ emit coreModulesChanged()
User clicks "Unload" in Logos Modules tab
└─ MainUIBackend::unloadCoreModule(name)
├─ logos_core_unload_module(name, false) # liblogos terminates logos_host process
└─ emit coreModulesChanged()
Package Installation
Installs are initiated by package_manager_ui — from the catalog, or from a
local .lgx the user picks in PMU's file dialog. Basecamp's only role is the
confirmation gate.
package_manager_ui → package_manager.requestInstall(name, version, repoUrl, depChanges)
└─ Event emitted: "beforeInstall" { name, releaseTag, repositoryUrl, depChanges }
└─ PackageCoordinator::onBeforeInstall → ack within 3s
└─ emit installGateConfirmationRequested → ConfirmationDialog "installGate"
├─ Cancel → cancelInstallGate(name) → module emits "installCancelled"
└─ Install → confirmInstallGate(name) → module emits "installApproved"
└─ PMU performs the install (download first for a catalog
package; a local .lgx is already on disk)
└─ package_manager.installPluginAsync(path, false)
├─ Extracts the platform variant from the LGX archive
├─ Files copied to user modules/plugins directory
└─ Event emitted: "corePluginFileInstalled" or
"uiPluginFileInstalled"
└─ PackageCoordinator event handler:
├─ refreshCoreModules()
│ └─ logos_core_refresh_modules()
└─ fetchUiPluginMetadata()
Stats Polling (Logos Modules only)
Every 2 seconds (m_statsTimer):
└─ MainUIBackend::updateModuleStats()
├─ logos_core_get_module_stats() → JSON string (Logos Module processes only)
├─ Parse JSON array: [{name, cpu_percent, memory_mb}, ...]
├─ Store in m_moduleStats map
└─ emit coreModulesChanged() → QML updates Logos Modules tab
Note: Stats are only available for Logos Modules because they run in separate logos_host processes that liblogos can monitor. UI Apps run in the Basecamp process itself and are not separately monitored.
UI App Calling a Logos Module
QML UI App code: logos.callModule("storage_module", "getFiles", ["/data"])
└─ LogosQmlBridge::callModule("storage_module", "getFiles", ["/data"])
├─ m_logosAPI->getClient("storage_module") → LogosAPIClient*
├─ Check client != null && client->isConnected()
├─ client->invokeRemoteMethod("storage_module", "getFiles", "/data")
│ └─ LogosAPI → Remote Object Registry → storage_module logos_host process → method call → result
└─ Serialize QVariant result to JSON string → return to QML
Component Directory Resolution
Logos Modules and UI Apps are discovered from separate directories, reflecting their different management layers.
Logos Module Directories (managed by liblogos)
Configured via logos_core_add_modules_dir() in main.cpp (embedded and user-writable directories). liblogos scans these directories for .so/.dylib/.dll files and extracts module metadata.
Embedded (read-only):
<app-dir>/../modules/
User-installed (writable):
- macOS:
~/Library/Application Support/Logos/LogosBasecampDev/modules/ - Linux:
~/.local/share/Logos/LogosBasecampDev/modules/
UI App Directories (managed by Basecamp)
Basecamp discovers UI Apps by querying the package_manager Logos Module and resolving paths via LogosBasecampPaths.
Embedded (read-only):
<app-dir>/../plugins/
User-installed (writable):
- macOS:
~/Library/Application Support/Logos/LogosBasecampDev/plugins/ - Linux:
~/.local/share/Logos/LogosBasecampDev/plugins/
All directory paths are managed via the LogosBasecampPaths utility class.
Build Artifacts
| Artifact | Description |
|---|---|
bin/LogosBasecamp |
Main application executable |
lib/liblogos_core.{so,dylib} |
Core library (from logos-liblogos) |
bin/logos_host |
Module subprocess host (from logos-liblogos) |
modules/ |
Embedded Logos Module bundles |
plugins/*/ |
Embedded UI App bundles |
Distribution Artifacts
| Artifact | Platform | Description |
|---|---|---|
| AppImage | Linux | Single-file self-contained executable |
| .app bundle | macOS | Ad-hoc signed application bundle |
| DMG | macOS | Disk image for distribution |
Operational
Nix (Recommended)
Nix provides reproducible builds with all dependencies managed automatically.
Build the application:
nix build
The result includes the application binary at result/bin/LogosBasecamp with all embedded modules.
Build individual outputs:
nix build '.#app' # Standard development build
nix build '.#bin-bundle-dir' # Self-contained portable build
nix build '.#bin-appimage' # Linux AppImage
nix build '.#bin-macos-app' # macOS .app bundle
nix build '.#logos-qt-mcp' # QML inspector for testing
Run tests:
# Smoke test (validates app starts without errors)
nix build '.#smoke-test' -L
# Integration tests
nix build '.#logos-qt-mcp' -o result-mcp
node tests/ui-tests.mjs --ci ./result/bin/LogosBasecamp
Development shell:
nix develop
Override local dependencies:
nix build --override-input logos-liblogos path:../logos-liblogos
nix build --override-input logos-cpp-sdk path:../logos-cpp-sdk
Workspace CLI
From the logos-workspace root:
ws build logos-basecamp # Build
ws build logos-basecamp --auto-local # Build with local overrides for dirty deps
ws test logos-basecamp # Run tests
ws run logos-basecamp # Build and run
ws develop logos-basecamp # Enter dev shell
CMake
Prerequisites:
- CMake 3.16+
- C++17 compatible compiler
- Qt 6 with Widgets, RemoteObjects, Quick, Qml, QuickWidgets, QuickControls2 modules
Build:
nix develop # Get all dependencies
mkdir -p build && cd build
cmake ..
cmake --build . -j$(nproc)
CMake Options:
| Option | Default | Description |
|---|---|---|
LOGOS_PORTABLE_BUILD |
OFF | Self-contained build for distribution |
LOGOS_DISTRIBUTED_BUILD |
OFF | For AppImage/DMG packaging |
ENABLE_QML_INSPECTOR |
ON (Debug), OFF (Release) | Enable QML inspector server |
Dev vs Portable Builds
- Dev build (default): Modules loaded with
-devvariant suffix. Dependencies reference the Nix store. - Portable build (
LOGOS_PORTABLE_BUILD=ON): Modules loaded without suffix. All dependencies bundled. No Nix store references at runtime.
Environment Variables
| Variable | Purpose |
|---|---|
LOGOS_USER_DIR |
Override application base directory as-is (also settable via --user-dir) |
QML_INSPECTOR_PORT |
QML inspector server port (default: 3768) |
Testing
Smoke Test
File: nix/smoke-test.nix
Validates the application starts correctly:
- Runs with
-platform offscreen(headless) - Checks for QML errors, crashes, and
qCriticaloutput - 5-second timeout
- Logs saved to
result/smoke-test.log
nix build '.#smoke-test' -L
Integration Tests
File: tests/ui-tests.mjs
End-to-end UI tests using the logos-qt-mcp framework:
- Clicks buttons, verifies visible text
- Tests package manager and core modules
- Supports CI mode (headless) and interactive mode
- Skips GPU-dependent tests in offscreen mode
nix build '.#logos-qt-mcp' -o result-mcp
node tests/ui-tests.mjs --ci ./result/bin/LogosBasecamp
QML Inspector
Development tool for inspecting the running QML tree over TCP:
- Default port: 3768 (localhost)
- Tools:
qml_find_and_click,qml_screenshot,qml_get_tree,qml_list_interactive - Used by integration tests and AI agents for UI automation
Continuous Integration
Two GitHub Actions workflows run on every push/PR to master:
.github/workflows/build.yml("Build & Release") — builds and smoke-tests the distributable artifacts: an AppImage per Linux architecture and a macOS app bundle. A separate job runs the unit tests, the QML tests, the sandbox test, the integration (UI) tests, the host-services grant guard, and a coverage report..github/workflows/doctests.yml— runs the repo's doc-tests across an OS matrix and publishes the execution reports.
Both get Nix and the shared binary cache from
logos-co/setup-nix-cache-action,
which installs Nix with flakes enabled and configures the Attic cache in one
step. (This replaced a hand-rolled install-Nix + cachix pair.)
Supported Platforms
- Linux (x86_64, aarch64) — AppImage distribution
- macOS (x86_64, aarch64) — DMG distribution
Known Limitations
- No workspace persistence — The set of loaded modules and tab layout is not saved across application restarts.
- No module updates — There is no mechanism to detect or install module updates automatically.
- QML inspector in release — The inspector is disabled in release builds and cannot be enabled at runtime.