mirror of
https://github.com/logos-co/logos-plugin-qt.git
synced 2026-08-27 17:01:11 +00:00
* feat(consumer): logos::admitConsumer — one home for admitting a non-module Two hosts hand-rolled the same three steps independently — isolate an identity, mint a credential, register it with capability_module — and the duplication had already produced a bug: basecamp did the registration inside the has-a-backend branch, below an early return, so pure-QML plugins registered nothing. It survived only because those calls went out on the host's ambient ring, where every token already existed and the handshake was never reached. Remove the ambient ring (logos-protocol #71) and that becomes a hard failure. So the operation gets one home. admitConsumer isolates, mints, registers and installs the credential as a single step, and hands back a ConsumerIdentity. Both hosts lose their private copies: basecamp -16 net lines, standalone-app -14. It lives here rather than in logos-liblogos, which is where it was first proposed. Both hand-rolled sites call LogosAPI, which is this repo's, and the operation needs a capability_module client to register through — going via liblogos's C API would add a hop for hosts already holding the Qt object. logos-liblogos needs no change at all. Named for what it does: a CONSUMER is admitted, not registered as a module. It is never published to the registry, never callable, never in --modules-dir. Two guards, because the sharp edges here are silent ones: * adoptConsumerCredential REFUSES an isolated store. It exists for a co-process adopting its parent's credential into its own process ring, where the store IS that ring. Pointed at an in-process private store it would install whatever it was handed — including the host anchor — straight past adoptCredentialFor's refusal, putting the elevation #71 removes one call away again. * A #error when logos-protocol is newer than the contract this file implements. The wave order is mandatory and was unenforced: bump the protocol alone and every isolated identity gets an empty store, while nothing fails to build (every MINOR guard in the fleet is >=) and the integration tests stay green (the ui-host half keeps working off its stdin credential). The bound fired on its first build — this change ships protocol 0.7 and it was set to 6 — and was proven non-inert by going red at 0.8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(deps): relock logos-protocol onto master, which now carries #71 admitConsumer calls TokenManager::adoptCredentialFor and adoptCredential. Both arrived with logos-protocol#71 ("a private store is created EMPTY"), so the lock had to move past it — this branch still pinned 6c24fcb1, which is 0.6.0 and has neither: logos_consumer.cpp:89: error: 'adoptCredentialFor' is not a member of 'TokenManager' logos_consumer.cpp:155: error: 'class TokenManager' has no member named 'adoptCredential' Now b37a2e9f, protocol master with #71 merged. This is the wave order the header documents, seen from the other side: the empty-store change lands in protocol, and everything that makes it survivable lands here. The guard in logos_consumer.h fires when this repo is BEHIND the protocol; this commit is the ordinary case of catching up to it. Verified on x86_64-linux with the relocked input: consumer-admission PASS qt-host PASS caller-contract PASS glue-compiles PASS Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
157 lines
6.5 KiB
C++
157 lines
6.5 KiB
C++
#include "logos_consumer.h"
|
|
|
|
#include "logos_api.h"
|
|
#include "logos_api_client.h"
|
|
#include "token_manager.h"
|
|
|
|
#include <QDebug>
|
|
#include <QUuid>
|
|
|
|
namespace {
|
|
|
|
// A per-admission secret. UUID rather than anything derived from the identity
|
|
// name: the value must not be guessable from public information, because
|
|
// holding it IS being that consumer.
|
|
QString mintCredential()
|
|
{
|
|
return QUuid::createUuid().toString(QUuid::WithoutBraces);
|
|
}
|
|
|
|
// Make (identity, credential) a known caller AT capability_module.
|
|
//
|
|
// Deliberately over the HOST's client and the HOST's token: informModuleToken
|
|
// is accepted only from a caller presenting the trusted core/capability
|
|
// channel's token (ModuleProxy::informModuleToken), and the host is that
|
|
// channel. The consumer cannot register itself — that is the entire point.
|
|
bool registerAtCapability(LogosAPI* hostApi, const QString& identity,
|
|
const QString& credential)
|
|
{
|
|
if (!hostApi) {
|
|
qWarning() << "logos::admitConsumer: no host LogosAPI - identity" << identity
|
|
<< "cannot be registered, so every call it makes would be refused";
|
|
return false;
|
|
}
|
|
LogosAPIClient* cap = hostApi->getClient(QStringLiteral("capability_module"));
|
|
if (!cap) {
|
|
qWarning() << "logos::admitConsumer: no capability_module client - identity"
|
|
<< identity << "will not be registered";
|
|
return false;
|
|
}
|
|
TokenManager* hostStore = hostApi->getTokenManager();
|
|
const QString hostCapToken =
|
|
hostStore ? hostStore->getToken(QStringLiteral("capability_module")) : QString();
|
|
if (hostCapToken.isEmpty()) {
|
|
qWarning() << "logos::admitConsumer: the host holds no capability_module token,"
|
|
" so it is not the trusted channel; identity" << identity
|
|
<< "will not be registered";
|
|
return false;
|
|
}
|
|
if (!cap->informModuleToken(hostCapToken, identity, credential)) {
|
|
qWarning() << "logos::admitConsumer: capability_module.informModuleToken failed"
|
|
" for identity" << identity;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
logos::ConsumerIdentity logos::admitConsumer(const QString& identity,
|
|
LogosAPI* hostApi,
|
|
QObject* parent)
|
|
{
|
|
if (identity.isEmpty()) {
|
|
qWarning() << "logos::admitConsumer: refusing to admit an unnamed consumer";
|
|
return {};
|
|
}
|
|
|
|
// (1) + (2): isolate, then construct. LogosAPI::forIdentity does both in
|
|
// that order and returns nullptr if the name was already vended on the
|
|
// ambient ring — which must fail the load rather than fall back to the
|
|
// host's own LogosAPI.
|
|
LogosAPI* api = LogosAPI::forIdentity(identity, parent);
|
|
if (!api) {
|
|
qWarning() << "logos::admitConsumer: could not give" << identity
|
|
<< "a token store of its own - refusing to run it with the"
|
|
" host's authority";
|
|
return {};
|
|
}
|
|
|
|
const QString credential = mintCredential();
|
|
|
|
// (3) REGISTER, and only then (4) ADOPT. See the header: this order is what
|
|
// makes the window in which the consumer holds an unknown credential not
|
|
// merely short but nonexistent.
|
|
if (!registerAtCapability(hostApi, identity, credential)) {
|
|
delete api;
|
|
return {};
|
|
}
|
|
if (!TokenManager::adoptCredentialFor(identity, credential)) {
|
|
// Reachable in exactly two ways, and both are bugs here rather than
|
|
// conditions to survive: the identity is not isolated (impossible, step
|
|
// 1 succeeded) or the minted credential collided with the host's own
|
|
// anchor (impossible with a UUID). Fail loudly.
|
|
qWarning() << "logos::admitConsumer: could not install" << identity
|
|
<< "'s own credential in its store - it would be locked out";
|
|
delete api;
|
|
return {};
|
|
}
|
|
|
|
return ConsumerIdentity{api, credential};
|
|
}
|
|
|
|
QString logos::reissueConsumerCredential(LogosAPI* consumerApi, LogosAPI* hostApi)
|
|
{
|
|
if (!consumerApi) return {};
|
|
const QString identity = consumerApi->moduleName();
|
|
if (identity.isEmpty()) return {};
|
|
if (!TokenManager::isIsolated(identity)) {
|
|
qWarning() << "logos::reissueConsumerCredential:" << identity
|
|
<< "was never admitted (its store is the ambient ring);"
|
|
" refusing to rotate a credential it does not have";
|
|
return {};
|
|
}
|
|
|
|
const QString credential = mintCredential();
|
|
if (!registerAtCapability(hostApi, identity, credential)) return {};
|
|
|
|
// Reset AFTER the registration lands: between the two, the identity holds a
|
|
// credential that is already dead at the target, and the reset is what
|
|
// removes it. Doing the reset first would widen that window rather than
|
|
// close it.
|
|
TokenManager::resetIdentity(identity);
|
|
if (!TokenManager::adoptCredentialFor(identity, credential)) {
|
|
qWarning() << "logos::reissueConsumerCredential: could not install the new"
|
|
" credential for" << identity << "- it is now locked out";
|
|
return {};
|
|
}
|
|
return credential;
|
|
}
|
|
|
|
void logos::adoptConsumerCredential(LogosAPI* consumerApi, const QString& credential)
|
|
{
|
|
if (!consumerApi || credential.isEmpty()) return;
|
|
TokenManager* store = consumerApi->getTokenManager();
|
|
if (!store) return;
|
|
|
|
// Refused against an ISOLATED store, and this is the whole reason the
|
|
// function is narrow. adoptCredential checks neither isolation nor
|
|
// anchor-equality — correct for the co-process case it exists for, where
|
|
// the store IS the process ring and the credential came in on stdin. Point
|
|
// it at an in-process private store and it becomes a public verb that will
|
|
// install whatever it is handed, including the host's anchor, straight past
|
|
// adoptCredentialFor's refusal. The elevation this change removed would be
|
|
// one call away again.
|
|
if (store != &TokenManager::instance()) {
|
|
qWarning() << "logos::adoptConsumerCredential: refusing an ISOLATED store."
|
|
<< "This verb is for a co-process adopting its parent's"
|
|
<< "credential into its own process ring. An in-process"
|
|
<< "identity is admitted with logos::admitConsumer, which"
|
|
<< "mints, registers and installs as one operation --"
|
|
<< "TokenManager::adoptCredentialFor is the primitive under"
|
|
<< "it and refuses the host anchor.";
|
|
return;
|
|
}
|
|
store->adoptCredential(credential);
|
|
}
|