* fix(windows): point the shared-runtime .def at logos-qt-host, and fail loudly
The Windows single-provider scheme makes liblogos_core.dll the one provider of
the shared C++ runtime, and it names its inputs by CMake TARGET. logos-qt-sdk no
longer carries an archive — the Qt host runtime moved to logos-qt-host — so
$<TARGET_FILE:logos-qt-sdk::logos_qt_sdk> no longer resolves and the whole
mechanism had to be repointed.
More importantly, the old shape failed OPEN. The guard was
if(WIN32 AND TARGET logos-protocol::... AND TARGET logos-qt-sdk::logos_qt_sdk)
so a missing target did not error — the condition simply went false and the
--whole-archive link, the nm scan and the generated .def were all skipped
SILENTLY. The result is the split-brain this file exists to prevent: main_ui and
ui-host each end up with their own TokenManager and every cross-module call is
refused. The code already fails loudly when `nm` is missing, for exactly this
reason; it had no equivalent guard for the target being absent.
Now the target tests are a foreach + FATAL_ERROR inside if(WIN32), so a missing
provider stops the configure instead of quietly restoring the split-brain.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(access-policy): say ON/OFF out loud, and pin the flag both directions
Deny-by-default enforcement already existed here: `mode: "enforce"` is the
switch, and under it computeDerivedAllowedCallersLocked derives each target's
allowed callers from the declared dependency graph. Nothing about that
changes — this makes the switch legible and pins its contract.
setAccessPolicy now states which side it landed on for every input (no
policy / unparseable / non-enforce mode / enforce). Enforcement that silently
failed to arm is the dangerous outcome: it looks identical to enforcement
that is working and simply has nothing to deny, so an operator who mistyped
`"mode":"enforced"` previously got a wide-open runtime and a clean log.
DenyByDefaultFlagTest drives one scenario through the flip: `declared`
declares `target`, `undeclared` declares nothing. Flag off, the target has no
restriction at all (today's behaviour). Flag on, `declared` is on the list and
`undeclared` is not. The declared half carries the weight — an implementation
that refused everything would pass the denial half on its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(b4): link the Qt host runtime from logos-qt-host, not logos-qt-sdk
B1 moved LogosAPI / LogosAPIProvider / LogosProviderBase / PluginInterface
out of logos-qt-sdk into logos-plugin-qt, published as the CMake target
logos-qt-host::logos_qt_host. This repoints liblogos at that target so B2b
can delete logos-qt-sdk's forwarders.
The host runtime is found DIRECTLY: add logos-plugin-qt as an input (pinned
to the B1 rev, since logos-qt-host is not on its master yet) with
logos-protocol / logos-nix / nixpkgs following, and find_package it against
a new LOGOS_QT_HOST_ROOT. It is deliberately not inherited through
find_package(logos-qt-sdk) -- qt-sdk does not carry a dependency on the host
runtime today, and will carry even less of one after B2b.
That also repairs the Windows single-provider block. It already named
logos-qt-host::logos_qt_host, but its comment claimed the target arrived via
find_package(logos-qt-sdk), which was false -- so its FATAL_ERROR guard would
have fired on the first real Windows build. The guard is unchanged (still a
hard error, never a silent skip); its premise is now true.
logos-qt-sdk stays an input, for the developer headers nix/include.nix
re-exports (logos_ui_plugin_context.h and friends) -- not for the host
runtime. Its now-unused -DLOGOS_QT_SDK_ROOT flag is dropped so CMake does not
warn about an unused variable; the env entry stays.
Two silent skips converted to hard errors along the way:
- tests/CMakeLists.txt guarded its SDK include dirs with `if(EXISTS ...)`,
so a bad root compiled the tests against a different copy of LogosAPI
than they link. Now FATAL.
- nix/include.nix copies the host headers over the qt-sdk ones so consumers
of this prefix see the declaration liblogos_core actually links (qt-host's
carries LOGOS_SHARED_API, the dllimport that keeps Windows on one
TokenManager). Everything copied before it is mode 0444 out of the store,
so a plain `cp -r` fails with EACCES and the existing `|| true` would have
swallowed it -- hence chmod + `cp -rf`, plus an assertion that the
installed logos_api.h really is qt-host's.
Verified on aarch64-darwin with local overrides for cpp-sdk, qt-sdk, protocol
and plugin-qt: logos-liblogos-lib, -include, -tests, default and portable all
build; the `tests` check runs 185 tests, 0 failures. Pointing
LOGOS_QT_HOST_ROOT at a nonexistent path fails the build with the intended
FATAL_ERROR rather than falling back. The header prefix is a strict superset
of the previous one (58 -> 63 files, none removed).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(deps): raise logos-protocol to the rev logos-qt-host actually needs
b3b2e50 repointed this repo from logos-qt-sdk's archive to logos-plugin-qt's
logos-qt-host, but left logos-protocol on master (03842db). That made the
repoint INERT: the tree did not build at all.
cpp/logos_api.cpp:38:52: error: no member named 'forIdentity' in 'TokenManager'
cpp/logos_api.cpp:48:24: error: no member named 'isolateIdentity' in 'TokenManager'
cpp/logos_api.cpp:57:50: error: no member named 'forIdentity' in 'TokenManager'
logos-qt-host's LogosAPI is built on the per-identity token store, and protocol
master has none of it -- 03842db's TokenManager carries only instance(),
m_tokens and m_mutex. The identity API arrives in c8bab12
(feat/per-client-token-store), so that is the floor for consuming qt-host at
all. The rev is pinned in the url, not just the lock, for the same reason
logos-plugin-qt already is: it is not on master, so a bare url would let
`nix flake update` silently walk this back to three compile errors.
The rev is also not merely "new enough". It is the SAME rev logos-basecamp and
logos-standalone-app pin, and that identity is the point. liblogos_core, the
app image and every in-process UI plugin share one TokenManager; two protocol
generations across that boundary give two token stores, which is the
"ModuleProxy: rejecting unauthorized call ... auth token not recognized"
failure the Windows .def block in src/CMakeLists.txt exists to prevent. On PE
that shows up as duplicate definitions; on Mach-O the second store is simply
linked into whichever image referenced a symbol liblogos_core failed to export.
Which is exactly what was happening here: with no forIdentity to import, a
consumer drags logos_api.cpp.o out of the static archive and token_manager.cpp.o
comes with it.
Only logos-protocol moves. logos-cpp-sdk, logos-qt-sdk, logos-plugin-qt and
default-module-loader already `follows` it, so all four now compile against
c8bab12 and the lock diff is one node.
Verified on aarch64-darwin, every package and check built by name, all EXIT=0:
default, logos-liblogos, -bin, -include, -lib, -modules, -tests, portable, and
the `tests` check -- 185 tests, 0 failures. The acceptance measurement on the
built library:
nm -gU lib/liblogos_core.dylib | grep -c LogosAPI11forIdentity -> 1 (was: no build)
nm -gU lib/liblogos_core.dylib | grep -c TokenManager8instanceEv -> 1 (still the provider)
liblogos_core now exports the whole identity surface -- LogosAPI::forIdentity,
TokenManager::forIdentity / isolateIdentity / isIsolated / seedBootstrapTokens
-- so consumers import them instead of re-linking a second copy.
NOT fixed here, and blocking on other repos:
- packages.x86_64-windows does not evaluate on this branch:
`attribute 'x86_64-windows' missing` at logos-plugin-qt.packages.<system>.
logos-qt-host. logos-qt-sdk exposes a windows pseudo-system via
forAllTargets/mkWindowsPkgs; logos-plugin-qt has only forAllSystems over the
four real systems. So b3b2e50 traded a Windows-capable provider for one that
is not, and the Windows single-provider machinery this repo owns cannot be
evaluated, let alone measured, until logos-plugin-qt grows that target.
liblogos master (5035877) still evaluates it.
- the consumer half of the single-provider scheme still fails open:
logos-basecamp/cmake/LogosSharedFromDll.cmake's
logos_use_shared_runtime_from_dll() skips names that are not targets with no
else, so a rename there is a silent no-op rather than an error. Its callers
pass the right names today, so the hazard is latent rather than active.
logos-qt-sdk's comment claims that file is duplicated into
logos-logoscore-cli; at that repo's current HEAD (df31c82) it is not --
there are no .cmake files there at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): raise logos-plugin-qt to the pushed qt-host branch tip
8ccb1fc -> cc24fa1c, the tip of logos-plugin-qt's
feat/b4-qt-host-windows-target, which is now on origin.
This is not a routine refresh. 8ccb1fc keyed `packages` off forAllSystems and
so had no x86_64-windows attribute at all, while this flake reads
logos-plugin-qt.packages.${system}.logos-qt-host from forAllTargets. Against
the old rev, packages.x86_64-windows.default did not merely fail to build, it
failed to EVALUATE:
error: attribute 'x86_64-windows' missing
at flake.nix:145:25
logosQtHost = logos-plugin-qt.packages.${system}.logos-qt-host;
Against cc24fa1c it evaluates to a derivation. Both directions were confirmed
with --override-input rather than assumed.
cc24fa1c rather than the sibling feat/b4-qt-host-windows-target-8ccb1fc
(989f6ae): the two branches carry the same work and their nix/qt-host.nix is
byte-identical, so they build the same runtime. The tiebreak is that
logos-qt-sdk pins cc24fa1c. This flake deliberately does not make
logos-qt-sdk's logos-plugin-qt follow this one, so pinning the other tip would
put two logos-qt-host builds in a single closure -- two LogosAPI/TokenManager
copies in one process, which is exactly the split-brain the .def block in
src/CMakeLists.txt exists to prevent.
Nothing else in the lock moved, deliberately. logos-protocol was already at
c8bab12; logos-cpp-sdk, logos-qt-sdk and logos-capability-module stay on
master, which is what this branch was written against -- include.nix already
copies qt-host's headers OVER qt-sdk's forwarders and asserts on
LOGOS_SHARED_API, so the surviving forwarders are handled rather than merely
tolerated. That assertion passes.
Verified on aarch64-darwin: all 8 packages plus checks.tests build, exit 0
each; the test suite reports 185 tests, 0 failures, 0 errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(windows): export the lp_* C ABI from liblogos_core.dll
The generated .def kept Itanium-mangled C++ only:
if (name !~ /^_Z/) next
Every lp_* symbol is `extern "C"`, hence unmangled, so the filter dropped the
entire logos-protocol C ABI from the export table by construction. Measured on
the built PE before this change: 3050 exports, of which lp_* = 0 — not
exported, and not even defined in the image.
It stayed invisible because until B5 only C++ callers reached the shared
runtime. B5 re-emits every Qt-typed dependency wrapper as a VENEER over the lp
path, so a consumer that compiles such a wrapper into its own image now calls
lp_invoke / lp_client_create / lp_token_save directly. logos-basecamp compiles
package_manager_api.cpp into LogosBasecamp.exe, and the link failed with plain
`undefined reference to 'lp_invoke'` — no diagnostic pointing at the .def.
Exporting is the correct fix rather than letting the consumer link
liblogos_protocol.a itself: lp_token_save and friends operate on the
TokenManager singleton, so a static copy in the exe would reinstate exactly the
split-brain token store this whole .def scheme exists to prevent. Module plugins
are separate processes and keep their own per-image copy by design.
The prefix is deliberately tight — `lp_` only, not "anything unmangled" — so the
toolchain bookkeeping the ^_Z test was there to exclude (qt_version_tag_*, which
every image legitimately defines) stays excluded.
Delta is fully attributed: 3050 -> 3080 exports, exactly the 30 lp_* symbols,
with TokenManager (45) and LogosAPI (123) unchanged. LogosBasecamp.exe then
links, and still defines 0 TokenManager::instance while importing it from
liblogos_core.dll.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* chore(deps): track protocol and plugin-qt master
logos-protocol#59 and logos-plugin-qt#19 merged, so both rev pins are retired and
their rationales rewritten to name the PRs that closed the gaps.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
11 KiB
logos-liblogos
The core runtime library for the Logos modular application platform. Provides liblogos_core (a C-API shared library) and logos_host (the module subprocess host binary).
logos-liblogos is a library. It is consumed by two frontends:
- logos-basecamp — the desktop GUI application shell
- logos-logoscore-cli — the headless CLI runtime (
logoscore)
Composed from
The runtime pulls its module-loading behaviour from a few separate repos so each piece can be swapped independently:
- logos-capability-module — issues the auth tokens that gate inter-module calls.
- logos-container — the container interface (where/how a module runs); current default is the subprocess implementation: logos-container-subprocess (one OS process per module).
- logos-module-loader — the format-loader interface (what kind of module); current default is : logos-module-loader-qt.
How to Build
The project uses a Nix flake for reproducible builds with a modular structure:
Build Complete Library (Binaries + Libraries + Headers)
# Build everything (default)
nix build
# Or explicitly
nix build '.#logos-liblogos'
nix build '.#default'
The result will include:
/bin/- Host binary (logos_host)/lib/- Core library (liblogos_core)/include/- Headers (logos_core.h, interface.h)
Build Individual Components
# Build only the binaries (outputs to /bin)
nix build '.#logos-liblogos-bin'
# Build only the libraries (outputs to /lib)
nix build '.#logos-liblogos-lib'
# Build only the headers (outputs to /include)
nix build '.#logos-liblogos-include'
# Build and run tests
nix build '.#logos-liblogos-tests'
# Build portable variant (selects portable LGX variants instead of dev)
nix build '.#portable'
Running Tests
# Build and run tests (tests run automatically during build)
nix build '.#logos-liblogos-tests'
# To run tests manually after building:
./result/bin/logos_core_tests
# Run specific tests
./result/bin/logos_core_tests --gtest_filter=AppLifecycleTest.*
# List all available tests
./result/bin/logos_core_tests --gtest_list_tests
Development Shell
# Enter development shell with all dependencies
nix develop
Note: In zsh, you need to quote targets with # to prevent glob expansion.
If you don't have flakes enabled globally, add experimental flags:
nix build '.#logos-liblogos' --extra-experimental-features 'nix-command flakes'
The compiled artifacts can be found at result/
Modular Architecture
The nix build system is organized into modular files in the /nix directory:
nix/default.nix- Common configuration (dependencies, flags, metadata)nix/build.nix- Shared build that compiles everything oncenix/bin.nix- Extracts binaries (logos_host, includes libraries for runtime linking)nix/lib.nix- Extracts libraries onlynix/include.nix- Header installationnix/tests.nix- Test suite build and execution
Note: The logos-liblogos-bin package includes both the logos_host binary and its required libraries to ensure proper runtime linking.
Local Development
To build against a local checkout of a dependency, override its input. For example the SDK:
nix build --override-input logos-cpp-sdk path:../logos-cpp-sdk
The container and format-loader abstractions live in their own repos, consumed
as inputs the same way process-stats is. To build against local checkouts:
nix build \
--override-input logos-container path:../logos-container \
--override-input logos-module-loader path:../logos-module-loader \
--override-input default-container path:../logos-container-subprocess \
--override-input default-module-loader path:../logos-module-loader-qt
(default-container / default-module-loader are the input slots for the
built-in default implementations; they point at the subprocess / qt-plugin repos.)
Library API
logos-liblogos exposes a C API via logos_core.h:
// Lifecycle
void logos_core_init(int argc, char *argv[]);
void logos_core_start();
void logos_core_cleanup();
// Module directory management
void logos_core_add_modules_dir(const char* dir);
// Instance persistence
void logos_core_set_persistence_base_path(const char* path);
// Per-module transport configuration (forwarded to the module's
// child subprocess so its LogosAPIProvider binds every listener
// instead of only the global default LocalSocket). Must be called
// before the module is loaded.
void logos_core_set_module_transports(const char* name, const char* transport_set_json);
// Inter-module access policy (per-target allowed-caller allowlists).
// Core parses it and registers the per-target restrictions with
// capability_module, which then denies token issuance for disallowed
// (caller, target) pairs. Call before logos_core_start(); NULL/"" clears.
void logos_core_set_access_policy(const char* policy_json);
// Module management
int logos_core_load_module(const char* name, bool with_dependencies);
int logos_core_unload_module(const char* name, bool with_dependents);
char* logos_core_process_module(const char* path);
void logos_core_refresh_modules();
// Dependency graph queries (forward + reverse edges; recursive walks BFS)
char** logos_core_get_module_dependencies(const char* name, bool recursive);
char** logos_core_get_module_dependents(const char* name, bool recursive);
// Module queries
char** logos_core_get_loaded_modules();
char** logos_core_get_known_modules();
// Module stats and tokens
char* logos_core_get_module_stats();
char* logos_core_get_token(const char* key);
See src/logos_core/logos_core.h for the full API.
Inter-module access enforcement (off by default)
By default a loaded module may call any other loaded module. Enforcement is
opt-in, and mode in the access policy is the switch:
{"version": 1, "mode": "enforce"}
Installing that document (via logos_core_set_access_policy, before
logos_core_start()) turns on deny-by-default: for every loaded target,
core derives the allowed callers from the declared dependency graph — the
target's loaded dependents, plus the trusted core / core_service — and
registers them with capability_module. A module that never declared the target
as a dependency is refused a token, so its call can never proceed, and
capability_module logs the refusal with both names:
[capability_module] access policy denies 'caller_module' -> 'target_module'
Anything other than mode: "enforce" — no policy, NULL, "", unparseable
JSON, a different mode — leaves enforcement off, which is the pre-existing
behaviour. Core says which side it landed on at startup, so a mistyped mode is
visible rather than silently permissive:
Inter-module access enforcement is ON (mode=enforce): deny-by-default — ...
Inter-module access enforcement is OFF (no access policy set): ...
A restrictions entry overrides the derived list for that target verbatim,
which is the escape hatch for callers that legitimately cannot declare their
target (out-of-process ui_qml plugins are not tracked as dependents, so they
need an explicit entry):
{"version": 1, "mode": "enforce",
"restrictions": {"accounts_module": {"allowedCallers": ["accounts_ui"]}}}
capability_module, core and core_service are never restricted as targets.
Hosts expose this as --access-policy — see the logoscore CLI and Basecamp
READMEs.
Thread safety
Module load/unload operations (logos_core_load_module, logos_core_unload_module) are serialised internally by a single mutex. It is safe to call them concurrently from multiple threads, including rapid and repeated load/unload cycles on the same module — each call waits for its turn and the process management layer handles teardown cleanly before the next launch. logos_core_unload_module with with_dependents=true in particular holds the lock for its entire leaves-first teardown so a late-arriving load can't interleave between tearing down a dependent and its parent.
logos_core_refresh_modules is synchronised through the module registry's reader-writer lock — it is safe to call concurrently with other registry accesses, but it is not serialised against load/unload by the same mutex as above.
Read-only accessors (logos_core_get_known_modules, logos_core_get_loaded_modules) use that shared reader-writer lock and are safe to call concurrently with each other and with logos_core_refresh_modules.
Dev vs Portable Builds
The library supports two build modes controlled by the LOGOS_PORTABLE_BUILD CMake flag:
- Dev build (default): Module loading looks for LGX variants with
-devsuffix (e.g.,linux-amd64-dev). Used in Nix/development environments. - Portable build (
-DLOGOS_PORTABLE_BUILD=ON): Looks for portable variants without suffix (e.g.,linux-amd64). Used in self-contained distributed applications.
Build the portable variant with nix build '.#portable'.
Supported Platforms
- macOS (aarch64-darwin, x86_64-darwin)
- Linux (aarch64-linux, x86_64-linux)
Building with a different container or module loader
The container and format-loader are selected by which package liblogos links — not by its C++ or CMake. To use your own, build a package that:
- implements the contract interface (
LogosCore::ModuleContainerfromlogos-container, orLogosCore::ModuleFormatLoaderfromlogos-module-loader), - defines the factory symbol (
LogosCore::makeContainer()/LogosCore::makeFormatLoader()), and - ships the generic CMake config (
LogosContainerImpl/LogosFormatLoaderImpl, exposing the…::impltarget).
(Copy the structure from logos-container-subprocess / logos-module-loader-qt.)
Then point liblogos at it by overriding the input slot:
# different container
nix build '.#logos-liblogos' --override-input default-container <flake-ref>
# different format loader
nix build '.#logos-liblogos' --override-input default-module-loader <flake-ref>
<flake-ref> is e.g. github:you/your-impl or path:../your-impl; it must expose
packages.<system>.default carrying that config + factory symbol. For a permanent
default, add it as an input and set containerImpl / formatLoaderImpl in
flake.nix. Container and loader are independent — swap either or both.
Note: the format-loader package also provides the logos_host binary that
bin.nix re-exports, so a replacement loader must ship its own host binary.
Disclaimer
This repository is part of an experimental development environment. Components are under active development and may be incomplete, unstable, modified or discontinued at any time.
The software is provided for development and testing purposes only and is not intended for production use.
The code and related materials are made available on an open-source, “as-is” basis without warranties or guarantees of any kind, express or implied, including warranties of correctness, security, performance or fitness for a particular purpose. Use at your own risk.