Files
logos-app-poc/app/UninstallPlan.cpp
Dario Gabriel Lipicar 7851bf9f46 feat(host): per-plugin identities, an opt-in access policy, and the source split
Each loaded plugin -- including pure-QML ones -- gets its own LogosAPI identity
rather than sharing the host's, so a plugin's calls are attributable and can be
refused independently. The host-services grant is wired through to
capability_module, and its trust root is guarded on an OUTCOME rather than a
log line.

Inter-module access policy stays OFF by default: enforce mode's derived
deny-by-default gates every ui_qml app's calls to its own backend module,
because UI plugins load out-of-process and are not tracked as dependents in the
core ModuleRegistry. Operators opt in per launch with --access-policy enforce
or LOGOS_ACCESS_POLICY.

Takes the Qt host runtime from logos-plugin-qt rather than logos-qt-sdk, which
keeps only the Qt<->lp seam headers, and moves logos-protocol onto the rev that
split host needs. On Windows logos_core must come LAST on the link line: GNU ld
resolves an archive left to right, so the view runtime's references have to be
undefined already when it reaches the import library.

Separates the two source trees -- app/ is the host, src/ is the UI shell -- and
brings the CI onto setup-nix-cache-action. Merges master.
2026-08-22 16:42:13 -03:00

146 lines
5.3 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "UninstallPlan.h"
namespace uninstallplan {
QString reasonName(KeptReason r)
{
switch (r) {
case KeptReason::Embedded: return QStringLiteral("embedded");
case KeptReason::Protected: return QStringLiteral("protected");
case KeptReason::RequiredBy: return QStringLiteral("requiredBy");
case KeptReason::Unused: return QStringLiteral("unused");
}
return QStringLiteral("unused");
}
namespace {
Row makeRow(const QHash<QString, QString>& displayNames,
const QHash<QString, QString>& versions,
const QSet<QString>& loaded,
const QString& name,
bool isTarget)
{
Row r;
r.name = name;
// Handle both missing key and explicit empty-string binding.
r.displayName = displayNames.value(name, name);
if (r.displayName.isEmpty()) r.displayName = name;
r.version = versions.value(name);
r.isTarget = isTarget;
r.isLoaded = loaded.contains(name);
return r;
}
Plan computeImpl(const Input& in, bool composeMode)
{
const QSet<QString> installedSet(in.installed.cbegin(), in.installed.cend());
// Targets: deduped, order-preserving, installed-only.
QStringList targets;
QSet<QString> targetSet;
for (const QString& t : in.targets) {
if (t.isEmpty() || !installedSet.contains(t)) continue;
if (!targetSet.contains(t)) { targetSet.insert(t); targets << t; }
}
// candidates = targets their installed forward closures.
QStringList candidates = targets;
QSet<QString> candidateSet = targetSet;
for (const QString& t : targets) {
for (const QString& d : in.dependencies.value(t)) {
if (!installedSet.contains(d) || candidateSet.contains(d)) continue;
candidateSet.insert(d);
candidates << d;
}
}
// Survivors: everything a root (installed but not in the closure) needs,
// stays. requiredByOf remembers which roots protected each candidate,
// for the popup's "still required by X" line.
QSet<QString> survivorSet;
QHash<QString, QStringList> requiredByOf;
for (const QString& r : in.installed) {
if (candidateSet.contains(r)) continue;
survivorSet.insert(r);
for (const QString& d : in.dependencies.value(r)) {
if (!installedSet.contains(d)) continue;
survivorSet.insert(d);
if (!candidateSet.contains(d)) continue;
// find/insert instead of `operator[]` so we don't create
// spurious empty entries as a side effect.
auto it = requiredByOf.find(d);
if (it == requiredByOf.end()) {
requiredByOf.insert(d, QStringList{r});
} else if (!it.value().contains(r)) {
it.value() << r;
}
}
}
// Orphans: brought in by a target, wanted by nobody else, and legal to
// remove.
QStringList orphans;
for (const QString& c : candidates) {
if (targetSet.contains(c)) continue;
if (survivorSet.contains(c)) continue;
if (in.embedded.contains(c) || in.protectedNames.contains(c)) continue;
orphans << c;
}
Plan plan;
plan.batch = targets;
if (composeMode) plan.batch += orphans;
const QSet<QString> batchSet(plan.batch.cbegin(), plan.batch.cend());
plan.removable.reserve(plan.batch.size());
for (const QString& n : plan.batch) {
plan.removable << makeRow(in.displayNames, in.versions, in.loaded,
n, targetSet.contains(n));
}
// Kept = closure \ batch, each row tagged with why it survived.
// Priority: Embedded > Protected > RequiredBy > Unused. Unused fires
// only in explain mode; compose mode sweeps orphans into batch.
for (const QString& c : candidates) {
if (batchSet.contains(c)) continue;
KeptRow k;
k.name = c;
k.displayName = in.displayNames.value(c, c);
if (k.displayName.isEmpty()) k.displayName = c;
if (in.embedded.contains(c)) {
k.reason = KeptReason::Embedded;
} else if (in.protectedNames.contains(c)) {
k.reason = KeptReason::Protected;
} else if (auto it = requiredByOf.constFind(c);
it != requiredByOf.cend() && !it.value().isEmpty()) {
k.reason = KeptReason::RequiredBy;
k.requiredBy = it.value();
} else {
k.reason = KeptReason::Unused;
}
plan.kept << k;
}
// Dependents: what breaks. Subtract anything already in the closure.
QSet<QString> seenDependents;
for (const QString& t : targets) {
for (const QString& d : in.dependents.value(t)) {
if (!installedSet.contains(d)) continue;
if (candidateSet.contains(d) || seenDependents.contains(d)) continue;
seenDependents.insert(d);
plan.dependents << makeRow(in.displayNames, in.versions, in.loaded,
d, /*isTarget=*/false);
}
}
return plan;
}
} // anonymous namespace
Plan composeFrom(const Input& in) { return computeImpl(in, /*composeMode=*/true); }
Plan explainOf (const Input& in) { return computeImpl(in, /*composeMode=*/false); }
} // namespace uninstallplan