# 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](https://github.com/logos-co/logos-cpp-sdk)** | SDK root: LogosAPI, code generator, IPC layer. Pins nixpkgs/Qt. | | **[logos-liblogos](https://github.com/logos-co/logos-liblogos)** | Core library: `liblogos_core` C API, `logos_host` subprocess host | | **[logos-module](https://github.com/logos-co/logos-module)** | Module introspection library and `lm` CLI | | **[logos-package-manager-module](https://github.com/logos-co/logos-package-manager-module)** | Package management module (auto-loaded at startup) | | **[logos-package-downloader-module](https://github.com/logos-co/logos-package-downloader-module)** | Online catalog browsing and package download | | **[logos-capability-module](https://github.com/logos-co/logos-capability-module)** | Inter-module authorization and token management | | **[logos-package-manager-ui](https://github.com/logos-co/logos-package-manager-ui)** | Package manager UI module (embedded at build time) | | **[logos-design-system](https://github.com/logos-co/logos-design-system)** | Centralized color/theme definitions (LogosText, LogosButton) | | **[logos-qt-mcp](https://github.com/logos-co/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:** ```cpp LogosAPIClient* client = m_logosAPI->getClient("package_manager"); if (client && client->isConnected()) { QVariant result = client->invokeRemoteMethod("package_manager", "methodName", arg1); } ``` **Using the generated wrapper:** ```cpp 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 `DenyAllNAMFactory` blocks all outgoing HTTP/HTTPS. Apps that need network do so indirectly through Logos Modules via the QML bridge. - **Filesystem:** a `RestrictedUrlInterceptor` resolves only `qrc:` 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 `/[.ver]/qmldir` candidates 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, a `ui_qml` app could ship a `qmldir` with a `plugin` directive plus a matching Qt plugin `.so` and have Qt `dlopen()` 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(/../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(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):** - `/../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):** - `/../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:** ```bash nix build ``` The result includes the application binary at `result/bin/LogosBasecamp` with all embedded modules. **Build individual outputs:** ```bash 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:** ```bash # 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:** ```bash nix develop ``` **Override local dependencies:** ```bash 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: ```bash 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:** ```bash 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 `-dev` variant 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 `qCritical` output - 5-second timeout - Logs saved to `result/smoke-test.log` ```bash 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 ```bash 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`](https://github.com/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 1. **No workspace persistence** — The set of loaded modules and tab layout is not saved across application restarts. 2. **No module updates** — There is no mechanism to detect or install module updates automatically. 4. **QML inspector in release** — The inspector is disabled in release builds and cannot be enabled at runtime.