* feat(deps): carry dependency constraints and the mismatch status across the ABI
toLogosMap(const InstalledPackage&) is where the range and the signer died.
lgpm carried {name, version, signer} intact from metadata.json through the
.lgx, `lgx verify` and `lgpm install` onto disk; two readers over the SAME
installed tree then disagreed:
lgpm --json info app -> dependencyConstraints:[{...}]
package_manager.getInstalledPackages -> dependencies:["lib"]
Same library, same files, one serialiser -- this one. Everything past it,
basecamp included, was reading a payload the constraint had been stripped
from.
`dependencyConstraints` is added as a SEPARATE key rather than by widening
`dependencies`, because widening it would fail SILENTLY. basecamp's
PluginLoader::loadCoreDependencies reads that very list (via UIPluginManager's
m_uiPluginMetadata, filled from getInstalledUiPlugins) and does
QString depName = dep.toString(); if (depName.isEmpty()) continue;
-- a QVariantMap stringifies to empty, so an object-form entry would be
skipped and the dependency simply never loaded, with no diagnostic. The graph
edge and the constraint on that edge are different facts; they now travel in
different keys.
toFlatLogosMap gains the same treatment for the tree APIs: version_mismatch
keeps `version` and `installType` because it resolved to a real installed
package and "needs ^2.0.0, have 1.0.0" takes both numbers, and the per-edge
`requiredVersion` / `requiredSigner` are emitted only when the parent declared
them. Nothing here evaluates the signer.
Both keys are absent whenever every entry is a bare name, which is every
package in the fleet today, so this is backward-compatible by construction.
The unit tests build against tests/stubs/package_manager_lib.h, so the stub
and its mock are updated to mirror the real header -- otherwise the impl
compiles in the integration target and not the mocked one.
Tests 100 -> 107. Red on the base, reading only emitted keys so the probe
needs no field the base lacks:
ASSERT failed: list[0].contains("dependencyConstraints")
Note: the five integration test files are NOT built by the `unit-tests` check
-- CMake reports "package_manager_lib not found in ../lib -- skipping
integration tests" because the test derivation never stages the external lib.
The end-to-end tests added to tests/test_scanning.cpp are therefore correct
but unexercised in CI; they were driven by hand against the real library.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(deps): carry the observed signer, and the two signer statuses, across the ABI
Two facts have to reach the far side, and they are different facts:
requiredSigner — a PIN a dependant declared. A demand.
observedSigner — the DID whose signature over the installed package was
VERIFIED at install. An observation.
Everything downstream ("published by a different signer") needs both. The pin
alone does not say what went wrong; the observation alone is an accusation with
no charge attached. `requiredSigner` already crossed and was, in this file's own
words, "carried and compared by nobody" — logos-package-manager now compares it,
so the other half has to arrive too.
New statuses, both about packages that ARE installed:
signer_mismatch — provably not the package the dependant named
signer_unknown — the pin could not be checked; nothing recorded a
publisher. Absence of evidence, its own status.
`observedSigner` is additive and emitted only when something was observed, so
an unsigned, embedded or pre-sidecar install crosses byte-identically to
before, and a reader can tell "nothing recorded" (key absent) from a recorded
value. A key present with an empty string would be neither, and would read as
"observed to be unsigned" — which nothing on disk records.
Also replaces `Installed || VersionMismatch` with
nodeResolvedToAnInstalledPackage at the version/installType gate. That chain
named the two statuses that existed when it was written, so the moment these
two were appended it would have started blanking `version` on packages sitting
right there on disk — in this file and in the library's JSON emitter, quietly,
at two call sites in two repositories. The RED run below shows exactly that:
"expected [1.0.0] but got []" for a signer-mismatched dependency.
And the mock's flatten() is brought back in line with the real library, which
PROMOTES on dedup (a later edge judging a package more harshly wins) rather
than merely deduping. The mock had been left behind by that change, so a module
test could have seen a satisfied row for a package the real resolver rejects. A
test double that disagrees with the thing it doubles is worse than none.
RED (observedSigner dropped from both emitters, gate back to the two-status
chain): 109 passed, 4 failed
getInstalledPackages_carries_the_observed_signer
resolveDependencies_surfaces_signer_mismatch_with_both_dids
resolveFlatDependencies_surfaces_signer_mismatch
resolveDependencies_surfaces_signer_unknown_without_an_observed_did
ASSERT_EQ failed: expected [1.0.0] but got []
GREEN: 113 passed.
Both runs use --override-input logos-package-manager path:<local checkout>:
this branch's flake.lock is still pinned at the parent branch's mid-history
commit and gets relocked when the parent merges, per the stack's merge order.
checks.x86_64-linux.unit-tests
-> /nix/store/rd7lrab96jvi9n3ycjx2h8czhs9j91a3-logos-package_manager-tests-1.0.0
packages.x86_64-linux.lib
-> /nix/store/hlnjk6rrz73q6hjcsn4c3vr14y57liis-logos-package_manager-module-lib-1.0.0
packages.x86_64-linux.default
-> /nix/store/wvlj38yw066icjypvppzncjpc442ympr-logos-package_manager-module
That .so was then staged into a real installed tree and driven through a real
logoscore daemon; the five wire payloads are in the branch summary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(deps): follow the rename of observedSigner to signerDid
logos-package-manager stopped recording who it verified at install and started
carrying the package's own manifest.sig into the install tree, so the value
that crosses this ABI changed meaning and changed name with it.
`observedSigner` was an observation the installer made and wrote down.
`signerDid` is what the installed signature says about itself, reported only
once that signature has been checked against the key its own DID carries. The
difference matters to a reader on the far side: it is not corroborated by
anything outside the document, so it is for display, and the verdict beside it
is NOT these two values compared — it comes from verifying the installed
signature under the PIN's key. A `signer_mismatch` row therefore carries a
`signerDid` that differs from `requiredSigner`, and that is the normal shape of
that row rather than an inconsistency.
Wire key renamed to match, in both the installed-packages map and the
dependency-tree node. Still additive and still absent when there is no usable
signature, so an unsigned or embedded package crosses the ABI byte-identically
to before.
The test stub mirrors the real struct and moves with it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(deps): build against the library this module needs, and pin the wire shape exactly
CI here runs `nix build -L` — the real module, linking the real
logos-package-manager. The lock pinned 7c5aad9a (2026-08-22), whose
DependencyTreeNode has neither `requiredSigner` nor `signerDid`, so that build
does not compile:
error: no member named requiredSigner in const struct DependencyTreeNode
error: no member named signerDid in const struct DependencyTreeNode
That is a red build, not a latent risk. And the one check would never have said
so: `tests.mockCLibs = ["logos_pm"]` builds the unit tests against
tests/stubs/package_manager_lib.h, a hand-maintained MIRROR of the real header.
It compiles and passes against a library it never links, so the check is green
while `nix build` is red and nothing connects the two. flake.nix now pins the
branch carrying those fields, with a REVERT-ON-MERGE note saying exactly this.
Measured: `nix build` and `nix build .#unit-tests` both pass with NO override.
The backward-compatibility test moves from three absences to an EXACT KEY SET.
Naming three keys only rules out the three somebody thought of, and the way an
additive key fails is by turning out not to be conditional. `signerDid` is that
shape: it is a property of the PACKAGE, not of the edge, so it does not become
absent because an edge declared no constraint. A real capture of a satisfied
pin carries `status:"installed"` and a `signerDid` together — so a test that
only looked for requiredVersion/requiredSigner would not have noticed it appear
on an unconstrained node. Counting and naming every key does. Proven by
mutation: one unconditional extra key turns the test red.
Stub and test prose follow the mechanism. What an embedded package is missing
is a manifest.sig, and it can never have one because nothing but
installPluginFile copies one into an install tree — not a record nobody wrote.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* docs(deps): cut this PR's comments back to what a reader needs
217 added comment lines -> 132. Comments only; no code line moved or changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: emit the install event and report a path for QML-only ui_qml packages
installPlugin() gated its corePluginFileInstalled/uiPluginFileInstalled event —
and response["path"] — on installedPluginPath being non-empty. That out-param
is a REPORTING detail, not the success signal: a QML-only ui_qml package has no
backend library, so an older logos-package-manager left it empty on a perfectly
good install. The result was that uiPluginFileInstalled never fired (Basecamp
only discovered the plugin after an app restart) and "path" came back empty,
which logos-package-manager-ui reads as failure and draws as a red RETRY.
Success is now the library's own signal — a non-empty return from
installPluginFile() — and the reported path falls back to that return value
when installedPluginPath is empty, so this stays correct against an
unpatched logos-package-manager.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* Merge origin/master into feat/windows-cross
master landed #58 (manifest version). Merged clean -- no conflicts -- then the
lock was re-pinned to the merged logos-module-builder (L6) and
logos-package-manager (L3).
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add fresh-install gate + thread dep changes through gated flow
Adds a confirmation gate for fresh installs (requestInstall / confirmInstall /
cancelInstall + beforeInstall / installApproved / installCancelled events),
mirroring the existing uninstall/upgrade gate but with no in-module uninstall
step — confirmInstall simply emits installApproved so the initiator runs its
own download+install chain.
Also extends requestUpgrade with an opaque `depChanges` JSON argument that is
echoed into the beforeUpgrade / beforeInstall payload, so a host confirmation
dialog can list the transitive dependency changes an operation will apply. The
module never interprets it (malformed/empty -> empty array).
This lets the host (basecamp) own a single confirmation dialog for install,
upgrade, and downgrade — replacing the double-dialog where PMUI and basecamp
both confirmed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: address review nits — confirmInstall single critical section + depChanges doc
- confirmInstall now validates, captures the echo fields, and clears the gate
in ONE critical section instead of two, so a concurrent cancel/reset/
ack-timeout can't swap the pending action out between the check and the
capture (which would emit installApproved with an empty/wrong payload).
No behavior change under the current single-dispatch model; hardens against
a future concurrency:multi flip.
- Fix requestUpgrade/requestInstall depChanges doc: the module DOES parse the
JSON (to re-embed it as an array via attachDepChanges) but never interprets
or acts on its contents.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* migrate to typed events
* Address PR review: clarify event-related comments
- test_package_manager.cpp: EventCapture now comes from the test
framework, not a local helper "below".
- package_manager_impl.h: drop the misleading "unit-test use" example
for the no-listener case (tests do wire listeners).
- package_manager_impl.cpp: reword the destructor threading note to
reference the typed event methods rather than a stored callback.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor: replace hashesValid with packageValid
Update QVariantMap field names and documentation to match upstream
package_valid rename in SignatureVerificationResult.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: replace TOFU with explicit keyring management API
Remove setTofuEnabled and add addTrustedKey, removeTrustedKey, and
listTrustedKeys methods that call the lgx C API directly. This gives
the UI/app explicit control over trust decisions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review feedback
- Warn on invalid signature policy values instead of silently ignoring
- Distinguish signature verification errors from unsigned packages
- Document optional fields in installPlugin response
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* bump dependencies
* Add tests
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>