Files
logos-plugin-qt/cpp/logos_provider_object.cpp
T
Dario LipicarandClaude Opus 5 048152f2a1 fix(glue): route informModuleToken through the INBOUND door (#26)
* fix(tokens): follow logos-protocol's inbound/outbound store split

TokenManager now keeps INBOUND (caller -> what I issued them) and OUTBOUND
(callee -> what I present) in separate maps, with the trust anchor as a
scalar credential rather than a map entry. Call sites here move to the
accessor that names the direction they meant.

The pre-split single map made a grant one way a grant BOTH ways: a token
minted so M could call B was found by B's client when B called M, so
requestModule was skipped and the access policy never ran. See
logos-protocol's companion change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(glue): route informModuleToken through the INBOUND door

The emitted <Provider>::informModuleToken wrote the same value through both
doors: LogosProviderBase::informModuleToken (inbound, correct) and
logos_module_accept_token (which is the OUTBOUND door). The value is a
CALLER's token — capability_module saying "moduleName may call you" — so the
second write filed a caller's inbound token as an outbound credential inside
the cdylib's own protocol copy. That is the one-way-grant bypass, reproduced
one image deeper.

It now calls logos_module_accept_inbound_token (protocol 0.8). onInit's anchor
seeding keeps logos_module_accept_token, because THAT one is genuinely
outbound: it is the module's own credential for calling core and
capability_module. The comment says so on both sides — the two paths look
interchangeable and are not.

Below 0.8 the old write stays in an #else: dropping it would break the
module's outbound calls to that peer, which is a regression, not a fix. Guards
are expanded MAJOR-aware arithmetic, unifdef-resolvable.

Requires logos-protocol fix/token-direction-key-namespace (59b27ef).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore(deps): relock logos-protocol to 0.8, and raise the consumer bound with it

WHAT MOVED. logos-protocol b37a2e9f -> 42460e5b (0.7 -> 0.8), the single
node in flake.lock; nothing else in the lock changed.

WHY IT HAD TO. This branch emits the informModuleToken glue through
logos_module_accept_inbound_token, which joins the module-impl C ABI at
protocol 0.8 and does not exist before it (21 -> 23 logos_module_* lines in
logos_module_impl.h). The emission sits behind an expanded MINOR >= 8 guard,
so at the old pin the door was simply compiled away: the qt-host-generator
check's `grep -q logos_module_accept_inbound_token inform-0.8` had nothing to
find. The lock was the whole of the failure -- no code defect underneath it.

Note the pin this moves is b37a2e9f and not the 6c24fcb1 this branch forked
from: master merged #27 in between, and its lock had already moved. That merge
is the commit below this one. Without it, the relocked flake.lock conflicts
with master on the same three lines and CI -- which builds the PR MERGE ref --
cannot check the branch out at all.

THE BOUND. #27 shipped cpp/logos_consumer.h with the fleet's only UPPER bound,
`MINOR > 7` spelled as an #error, precisely so that a protocol bump past the
consumer-admission contract stops the build instead of silently emptying every
isolated identity's token store. Relocking to 0.8 fires it by design. The
review it asks for, carried out rather than assumed:

  * bootstrapKeys(), adoptCredential() and adoptCredentialFor() are signature-
    and semantics-identical across b37a2e9f -> 42460e5b. 0.8 moved direction
    into the KEY NAMESPACE -- inbound a reserved-prefix key, outbound the bare
    peer name -- and deliberately left TokenManager's layout byte-identical.
  * credential() became DERIVED from bootstrapKeys() rather than cached, which
    strengthens this path: a cached field read empty on a store another image
    wrote and then refused every push.
  * 0.8's own adoptCredential() contract documents both halves admitConsumer
    depends on -- outbound, capability_module's proxy resolves the presented
    credential from the caller-keyed INBOUND record rather than an anchor key,
    so the caller is named as the identity and not as the host; inbound,
    capability_module pushes with getToken(moduleName), which IS that
    credential, so informModuleToken's trusted-channel gate still passes.

So the bound is raised 7 -> 8, with that reasoning recorded at the guard. The
oracle is the consumer-admission check and not the argument: it runs a real
ModuleProxy in Local mode, and its "NO LOCKOUT: the consumer's own credential
authorizes at capability_module" / "and it is NAMED as itself, not as the host"
assertions are exactly the failure the #error exists to prevent. Both pass at
0.8. It is negative-validated upstream (removing the adopt step fails 5 checks,
swapping the order fails 2), so its green is worth something.

ALL 12 CHECKS BUILT INDIVIDUALLY, x86_64-linux, from source against
cache.nixos.org only (cache.nix.logos.co is returning 502):

  PASS  vanilla-plugin           /nix/store/ab6l4a0czh4nd4h153i8hz82qayb4ah4-logos-plugin-qt-vanilla-test-0.0.1
  PASS  header-generator-guard   /nix/store/1dxrvzk4d1r3babyfx6hfnv9va1im5v6-logos-plugin-qt-header-generator-guard-test
  PASS  headers-emitter-routing  /nix/store/yrjzx24bq010m3z0xm0djvmr3n1wn1fx-logos-plugin-qt-headers-emitter-routing-test
  PASS  consumer-api-style-gate  /nix/store/r5h073ygr1zfy763dmhxssgw9mcl96y8-logos-plugin-qt-consumer-api-style-gate-test
  PASS  qt-host                  /nix/store/mndrdrxcad6kq056jcrxf946iycp5yqg-logos-qt-host-0.1.0
  PASS  shared-runtime-layering  /nix/store/npzah7n5zj7gdlbs1d59ry0idd9vp6si-logos-qt-host-shared-runtime-layering
  PASS  qt-host-generator        /nix/store/xqlhpbz7bnfvz7c17x1aanfassglmrq6-logos-qt-host-generator-test
  PASS  unload-contract          /nix/store/2y64h1im2biyqpbmg0bi591rznl860yx-logos-qt-host-unload-contract-test
  PASS  caller-contract          /nix/store/w1lz78kfy91xcyfd35i277f030jjf0ag-logos-qt-host-caller-contract-test
  PASS  caller-invokable         /nix/store/68b83pvv90xsqszihgshpb5g3fikfmj4-logos-qt-host-caller-invokable-test-0.1.0
  PASS  consumer-admission       /nix/store/hiwnlafxhh5gz0f1pkdi53glw66qm3rq-logos-qt-host-consumer-admission-test-0.1.0
  PASS  glue-compiles            /nix/store/bx9qfp520yazhmmc1in37frsscs6iii8-logos-qt-host-glue-compiles-test-0.1.0

The qt-host closure references logos-protocol-lib-0.8.0, so the relock is in
the artefact and not merely in the lock file. CI itself runs only 4 of these 12.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 14:24:03 -03:00

126 lines
5.9 KiB
C++

#include "logos_provider_object.h"
#include "logos_api.h"
#include "token_manager.h"
// LOGOS_PROTOCOL_VERSION_{MAJOR,MINOR} for the guard below — by name, so an
// undefined macro cannot quietly turn the guard FALSE and leave every module
// with a permanently unknown caller.
#include "logos_protocol.h"
#include <QDebug>
#include <QMetaObject>
#include <QString>
#if defined(LOGOS_PROTOCOL_VERSION_MINOR) && (LOGOS_PROTOCOL_VERSION_MAJOR > 0 || \
(LOGOS_PROTOCOL_VERSION_MAJOR == 0 && LOGOS_PROTOCOL_VERSION_MINOR >= 6))
// Only for logos::callerUnknownJson() — the fail-closed document. NOT for
// logos::currentInboundCallerJson(): calling that here would read THIS image's
// thread-local, which is the bug this whole file exists to avoid. See below.
#include "logos_caller_scope.h"
#endif
// The LogosProviderObject universal-interface defaults and Std bridges moved
// to logos-protocol (logos_provider_interface.cpp) together with the abstract
// interface. What remains here is LogosProviderBase — the base the generated
// Qt plugin glue (<name>_cdylib_glue.cpp) derives from — because it talks to
// LogosAPI, which layers above the protocol. It used to be hand-derived by
// `interface: "provider"` module code; that interface is gone, and module code
// now derives from logos-cpp-sdk's Qt-free LogosModuleContext instead.
// ---------------------------------------------------------------------------
// LogosProviderBase
// ---------------------------------------------------------------------------
void LogosProviderBase::init(void* apiInstance)
{
m_logosAPI = static_cast<LogosAPI*>(apiInstance);
qDebug() << "[LogosProviderObject] LogosProviderBase::init called";
onInit(m_logosAPI);
}
bool LogosProviderBase::informModuleToken(const QString& moduleName, const QString& token)
{
if (!m_logosAPI) {
qWarning() << "[LogosProviderObject] informModuleToken: LogosAPI not available";
return false;
}
TokenManager* tokenManager = m_logosAPI->getTokenManager();
if (!tokenManager) {
qWarning() << "[LogosProviderObject] informModuleToken: TokenManager not available";
return false;
}
// THE INBOUND DOOR, and the direction is the whole of the choice.
//
// `moduleName` here is the module that will CALL US; `token` is what it
// will present. saveToken() is the OUTBOUND half — "what I present when I
// call moduleName" — and writing here landed both directions in one map
// under one key, which is what let a grant <m -> b> silently also authorize
// <b -> m> and let this push clobber our own per-target cache for the same
// peer. See the DIRECTION note in logos-protocol cpp/token_manager.h and
// the detector at tests/protocol/test_token_direction.cpp.
qDebug() << "[LogosProviderObject] Saving inbound token for caller:" << moduleName;
tokenManager->saveInboundToken(moduleName, token);
return true;
}
// THE PULL. Everything about the four lines below is deliberate; see the
// declaration in logos_provider_object.h and logos_caller_scope.h in
// logos-protocol for the measurement behind it.
//
// invokeMethod, NOT m_logosAPI->currentCallerJson(). This translation unit is
// compiled into the MODULE image, which links its own copy of LogosAPI —
// meta-object included — with its own function-local statics at its own
// addresses and no undefined reference to the host's. A direct call binds to
// THIS image's copy and reads THIS image's caller thread-local, which no
// CallerScope ever wrote. It is silently empty forever on macOS and Windows,
// and correct on Linux, so a green Linux run proves nothing about it.
// invokeMethod resolves through metaObject()/qt_metacall — virtual, vptr
// written by the HOST's constructor — and therefore lands in host code.
//
// BY NAME, so this string and the Q_INVOKABLE in logos_api.h are one
// contract with no compiler between them. tests/test-caller-contract.nix is
// what holds them together; without it a rename is a silent, permanent
// Unknown across the whole fleet.
//
// DirectConnection, because the answer is per-THREAD. The scope is open on
// the thread the dispatch was delivered on, and this call must read that
// thread's slot rather than hop to LogosAPI's owner thread. (Qt also refuses
// Q_RETURN_ARG on a queued connection, so the alternative is a runtime
// warning and a permanent Unknown, not a build failure.)
std::string LogosProviderBase::currentCallerJson() const
{
#if defined(LOGOS_PROTOCOL_VERSION_MINOR) && (LOGOS_PROTOCOL_VERSION_MAJOR > 0 || \
(LOGOS_PROTOCOL_VERSION_MAJOR == 0 && LOGOS_PROTOCOL_VERSION_MINOR >= 6))
QString caller;
if (m_logosAPI
&& QMetaObject::invokeMethod(m_logosAPI, "currentCallerJson",
Qt::DirectConnection,
Q_RETURN_ARG(QString, caller))
&& !caller.isEmpty()) {
return caller.toStdString();
}
// No LogosAPI yet, a host too old to carry the invokable, or an empty
// answer. Fail closed to the ONE document logos-protocol produces for
// "could not name the caller" — never to an empty string, which on the
// module-impl C ABI is not a weaker identity but a different operation.
return logos::callerUnknownJson();
#else
// Below protocol 0.6 there is no caller surface at either end: no
// logos_module_set_call_caller for the glue to push into, and no
// callerUnknownJson() to name. Empty is the absence of the whole mechanism,
// and the glue's push is guarded on this same expression, so nothing reads
// it.
return std::string();
#endif
}
void LogosProviderBase::emitEvent(const QString& eventName, const QVariantList& data)
{
if (m_eventCallback) {
qDebug() << "[LogosProviderObject] emitEvent:" << eventName;
m_eventCallback(eventName, data);
} else {
qWarning() << "[LogosProviderObject] emitEvent: no listener set for" << eventName;
}
}