Files
logos-protocol/cpp/logos_protocol.h
Dario Gabriel LipicarandClaude Opus 5 e514c53ef5 fix(tokens): separate the INBOUND and OUTBOUND stores — a grant one way was a grant both ways
TokenManager was one flat map with no direction tag, written from both
sides of every relationship: the client stored the token it PRESENTS to a
callee under the CALLEE's name, and a token RECEIVED from a caller was
stored under the CALLER's name. Same namespace, last write wins.

This was documented as safe by topology — separate images keep the two
directions apart — and pending only for the shared-runtime migration. That
was wrong. It is live in the DEFAULT out-of-process configuration, and the
chain needs no attacker:

  1. capability_module mints one token so M may call B.
  2. It pushes it to B  -> B's store gets ["M"] = T   (inbound)
  3. It returns it to M -> M's store gets ["B"] = T   (outbound)
  4. B then calls M. B's client looks in its OWN store for something to
     present to M, finds T because step 2 put it there, and therefore SKIPS
     requestModule entirely.
  5. M authorizes T, having cached the identical value in step 3.

One grant M->B silently produces B->M, with no handshake, nothing logged,
and capability_module's access policy never consulted. Reproduced on the
pre-split tree, both halves red:

    B's outbound lookup for M returned the token M was issued for calling B
       Actual: false  Expected: true
    B authorized at M using the token minted for M -> B
       Actual: true   Expected: false

A second defect from the same cause: an inbound push CLOBBERS the outbound
cache under the same key, so M's next call to B goes out carrying B's own
inbound token, is rejected, and burns the single re-exchange recovering
from a collision it caused itself.

THREE ROLES, TWO MAPS AND A SCALAR:

  m_outbound   callee -> what I present when calling out
  m_inbound    caller -> what I issued to that caller
  m_credential MY OWN host-issued credential

The anchor is what made a two-way split look impossible: it is genuinely
both directions, presented outbound to capability_module and compared
against inbound. But it is not a MAP. It is one value under two role labels
— "core" and "capability_module" — that collide with module names by
accident, and adoptCredential already wrote a single credential under every
bootstrap key. As a scalar it has no key namespace, so no reverse lookup
can produce a name from it. A key living in both maps would have been a
rename, not a split.

The hoped-for cheap route did not exist: logos_module_accept_token forwards
to lp_token_save in BOTH backends, and the Qt glue calls that one export
for an inbound caller token in one place and the module's own anchor in
another. The doors were never divided, so this could not be done by routing
two existing C ABI doors to two maps — but it needs no new door either.

Four detector tests red before, green after. Constant time survives at the
instruction level and the comparison count stays input-independent.

KNOWN GAP, deliberately not fixed here: the generated glue writes the same
inbound token through BOTH doors (lidl_gen_cdylib_glue.cpp:371-380). Safe
only while module cdylibs are separate images — which is exactly what the
shared-runtime migration collapses. Closing it means a dedicated inbound
export on the module-impl C ABI: a MINOR bump plus a definition in both
backends in the same wave. Price it before that migration lands, not after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-23 12:07:42 -03:00

654 lines
35 KiB
C

#ifndef LOGOS_PROTOCOL_H
#define LOGOS_PROTOCOL_H
/* ===========================================================================
* logos_protocol.h — the public, language-neutral C ABI of logos-protocol.
*
* This is the ONE seam every Logos SDK builds on. The data model is
* JSON-in-strings: method arguments are a JSON array, results are a JSON
* value, event payloads are a JSON array — all UTF-8 `const char*`.
*
* Ownership:
* - Every `char*` RETURNED by this library is heap-allocated and owned by
* the caller; free it with lp_string_free() (safe on NULL).
* - Every `const char*` PASSED IN is borrowed for the duration of the call.
*
* Bytes encoding: binary data crossing this ABI is encoded inside JSON as
* {"_bytes": "<base64url>"}
* (a single-key object). This is lossless for arbitrary bytes, including
* embedded NUL. It matches the plain-wire encoding (json_mapping.cpp) and is
* the canonical representation at this boundary.
*
* Error shape: structural failures report one canonical JSON object through
* `out_error_json` / error callbacks:
* {"code": "<machine_code>", "message": "<human text>", "origin": "<module>"}
*
* Threading / event-loop contract:
* - Callbacks may arrive on an internal protocol thread — never assume
* they run on your own thread.
* - Handles are thread-safe per-handle: calls on one handle may be made
* from any thread; the library marshals to the handle's owner thread
* internally where required.
* - Qt-free transports (plain tcp/tcp_ssl, mock) are serviced by the
* library's own workers — no caller event loop is needed.
* - The Qt Remote Objects transport (the current default inside module
* processes) ADDITIONALLY requires a running Qt event loop in the
* process. Every Logos module process has one (logos_host runs it).
* Standalone non-Qt consumers must use the plain transport.
* A client on that transport is created on — and owned by — the Qt main
* thread no matter which thread calls lp_client_create(), because its
* node and socket are only serviced by that thread's loop. Calls from
* other threads marshal onto it and block until it answers.
* - lp_invoke() blocks the calling thread until the result arrives or the
* timeout elapses (timeout_ms <= 0 selects the default, currently 20s).
*
* Cancellation / lifetime:
* - After lp_client_destroy() / lp_unsubscribe() RETURNS, no further
* callbacks fire for that handle; pending async results are dropped.
* `user_data` may be freed only after that point, never before.
* - Both are safe to call from ANY thread, including a worker that happens
* to drop the last reference to a client. lp_client_destroy() defers the
* underlying teardown to the client's owner thread when called elsewhere,
* so the handle may outlive the call by an event-loop turn — the
* no-callbacks guarantee above holds regardless.
*
* Versioning: this library carries the logos-protocol semantic version —
* the single number that governs Logos load/call compatibility. Two
* participants interoperate iff they share the same MAJOR. MINOR is
* additive/back-compatible; PATCH never affects compatibility.
* =========================================================================== */
// HOW TO GUARD A CONDITIONAL SURFACE, and it is not what it looks like.
//
// Every entry below is additive at a MINOR, so codegen guards the surface on
// the version it appeared at. The obvious spelling is wrong:
//
// #if LOGOS_PROTOCOL_VERSION_MINOR >= 5 // WRONG
//
// because at 1.0.0 the MINOR resets to 0 and every such guard silently goes
// false. Nothing fails to build and nothing fails to load — the definitions and
// the calls disappear together — so the symptom is modules quietly losing
// teardown and grantability, with no diagnostic anywhere. Compare the pair:
//
// #if defined(LOGOS_PROTOCOL_VERSION_MINOR) && // RIGHT
// (LOGOS_PROTOCOL_VERSION_MAJOR > 0 ||
// (LOGOS_PROTOCOL_VERSION_MAJOR == 0 &&
// LOGOS_PROTOCOL_VERSION_MINOR >= 5))
//
// Emit the arithmetic expanded rather than behind a function-like macro: the
// generated sources are resolved by `unifdef` in the backends' ABI checks, and
// unifdef handles nested integer arithmetic but silently no-ops on what it
// cannot evaluate. logos-rust-sdk already gets this right by comparing the
// tuple (major, minor).
#define LOGOS_PROTOCOL_VERSION_MAJOR 0
// 0.6: the caller of a dispatch — logos_module_set_call_caller()
// (logos_module_impl.h), which carries WHO is calling into the module image for
// the duration of one dispatch, plus the host half that produces the document
// (logos::CallerScope / logos::currentInboundCallerJson, logos_caller_scope.h,
// resolved by ModuleProxy::authorize as a by-product of the authorization scan
// it was already performing).
//
// Additive at the ABI level and at the INTERFACE level, which are two separate
// claims and both matter here. At the ABI: a cdylib generated below 0.6 exports
// no such symbol and the glue generated alongside it emits no call. At the
// interface: the caller is NOT a declared parameter and never appears in a
// .lidl — it is an ambient accessor — so no module's signature changes, nothing
// opts in per method, and a module that never asks is unaffected.
//
// WHY A MINOR AND NOT A PATCH, since the surface is only reachable through a
// generated call: a new REQUIRED module-impl export is exactly the thing the
// two prior ABI breaks were. Both (grant_host_services at 0.3, the teardown
// pair at 0.5) shipped with caller and module in perfect agreement about the
// version and still failed at dlopen on Linux only, because version agreement
// says nothing about which SYMBOLS a backend's emitter writes. The MINOR is
// what the guards are keyed to — the generator guard that emits the call, the
// backend guard that emits the definition, and the exports.txt every backend
// diffs itself against — so a surface with no MINOR of its own has no way to be
// guarded and no way to be checked.
// 0.5: the module teardown pair — logos_module_about_to_unload() and
// logos_module_set_unload_done_callback() (logos_module_impl.h), which let a
// module finish work before it is torn down. Additive at the ABI level: a
// cdylib generated below 0.5 exports neither, and the glue generated alongside
// it emits no calls, so an older module keeps the teardown it always had.
//
// An earlier version of this note went further and called that arrangement
// safe. It is not, and the ABI has been broken twice on the strength of it —
// grant_host_services at 0.3 and this pair at 0.5. Being generated in the same
// build makes the glue and the module agree on the VERSION; it says nothing
// about which SYMBOLS a given language backend's emitter writes for that
// version, because each backend implements this ABI independently. Both
// breakages happened at perfect version agreement, and both were invisible on
// macOS and fatal on Linux. See the note above logos_module_about_to_unload in
// logos_module_impl.h, and nix/module-impl-abi.nix, which publishes this
// header's export list so every backend can check itself against it.
// 0.4: per-identity token stores — lp_token_isolate_identity() and the four
// functions around it (lp_token_identity_is_isolated, lp_token_get_for,
// lp_token_save_for, lp_token_reset_identity), plus lp_client_create resolving
// its store through the origin instead of the image singleton. Additive: with
// nothing isolated, forIdentity() returns the same object instance() does, so
// every pre-0.4 host and binding runs on exactly the store it ran on before —
// the new symbols are the only way to get any other behaviour.
// 0.3: the trust-root surface — lp_grant_host_services() plus the two functions
// it gates (lp_token_keys, lp_inform_module_token_to), and the module-impl
// export that carries the grant across the cdylib boundary
// (logos_module_grant_host_services, logos_module_impl.h). Additive: no
// existing symbol changes behaviour, and an image that is never granted sees
// exactly the pre-0.3 surface — the gates are closed by default — so a host
// that knows nothing about the grant keeps working unmodified.
// 0.2: per-module concurrent dispatch ("multi"). Additive/back-compatible — a
// multi module returns a deferred-completion sentinel from callMethod and pushes
// the result as a __logos_call_complete__ event (see logos_async_dispatch.h);
// the provider/host ABI is UNCHANGED, so same-MAJOR hosts (incl. 0.1 daemons)
// load and forward multi modules without modification. A pre-0.2 *consumer*
// would see the raw sentinel rather than awaiting it — graceful, not a crash.
// 0.7: an isolated identity's OWN credential — lp_token_adopt_credential(),
// and the behaviour change that makes it necessary: a private token store is
// created EMPTY instead of inheriting this image's "core"/"capability_module"
// tokens. That inheritance handed every isolated identity the HOST's credential,
// which authorized as the host at any callee (the caller document came back
// {"kind":"host"}) and satisfied ModuleProxy::informModuleToken's
// trusted-channel gate — a write into another module's token map, reachable
// with three public calls and no generated glue.
//
// ADDITIVE AT THE ABI, BREAKING FOR ISOLATED IDENTITIES, and the two halves have
// to be said separately. No symbol changes signature, no module-impl export is
// added, and a host that never calls lp_token_isolate_identity /
// TokenManager::isolateIdentity is bit-for-bit unaffected: forIdentity() still
// returns instance() pointer-identically for every un-isolated name. A host that
// DOES isolate and does not adopt is broken loudly and immediately — its first
// outbound call dies at ModuleProxy::authorize's empty-token check with
// "auth token not recognized" — which is the intended failure mode for a change
// that removes a credential nobody was entitled to. Ship protocol, then
// logos-plugin-qt, then the hosts, with matching flake.locks.
#define LOGOS_PROTOCOL_VERSION_MINOR 7
#define LOGOS_PROTOCOL_VERSION_PATCH 0
#define LOGOS_PROTOCOL_VERSION_STRING "0.7.0"
/* ---------------------------------------------------------------------------
* Export marking.
*
* These lp_* functions are the stable C ABI the JS and Rust SDKs bind to, so
* they must appear in the export table of the shared build (liblogos_protocol
* .dll / .so). On Windows that is not automatic: CMake builds shared libraries
* with symbol export disabled unless symbols are marked explicitly or
* WINDOWS_EXPORT_ALL_SYMBOLS is set -- the cross-built DLL was measured with
* ZERO exports before this macro existed, so every FFI consumer would have
* failed to bind at load time.
*
* Marked explicitly rather than via WINDOWS_EXPORT_ALL_SYMBOLS so the ABI
* surface is the one we declare, not whatever happens to have external
* linkage. Mirrors logos_module_impl.h's LOGOS_MODULE_IMPL_EXPORT.
* ------------------------------------------------------------------------- */
#if defined(_WIN32)
#if defined(LOGOS_PROTOCOL_BUILDING_SHARED)
#define LP_API __declspec(dllexport)
#else
/* Consumers get plain declarations: dllimport would force them to link the
* import library even when they use the static archive. */
#define LP_API
#endif
#else
#define LP_API __attribute__((visibility("default")))
#endif
#ifdef __cplusplus
extern "C" {
#endif
/* ---------------------------------------------------------------------------
* Return codes (negative = failure). Functions returning int use these.
* ------------------------------------------------------------------------- */
#define LP_OK 0
#define LP_ERR_INVALID_ARG (-1)
#define LP_ERR_UNSUPPORTED (-2) /* provider surface: exercised in a later phase */
#define LP_ERR_INTERNAL (-3)
#define LP_ERR_UNAVAILABLE (-4) /* target module/object could not be acquired */
/* ---------------------------------------------------------------------------
* Version
* ------------------------------------------------------------------------- */
/** Version string "MAJOR.MINOR.PATCH" of the linked logos-protocol.
* Returns a static string — do NOT free. */
LP_API const char* lp_protocol_version(void);
/** MAJOR component of the linked logos-protocol version. Equal majors are
* compatible; unequal majors are not. */
LP_API int lp_protocol_abi_major(void);
/* ---------------------------------------------------------------------------
* Memory
* ------------------------------------------------------------------------- */
/** Free a string returned by this library. Safe to call with NULL. */
LP_API void lp_string_free(char* s);
/* ---------------------------------------------------------------------------
* Process-global mode / transport defaults
* ------------------------------------------------------------------------- */
/** Set the process-wide communication mode: "remote" (IPC, default),
* "local" (in-process registry) or "mock" (in-memory, for tests).
* Returns LP_OK or LP_ERR_INVALID_ARG. */
LP_API int lp_set_mode(const char* mode);
/** Current mode as "remote" | "local" | "mock". Static string — do not free. */
LP_API const char* lp_get_mode(void);
/** Set the process-global default transport from a JSON object, e.g.
* {"protocol":"local"}
* {"protocol":"tcp","host":"127.0.0.1","port":6001,"codec":"json"}
* {"protocol":"tcp_ssl","host":"...","port":6443,"codec":"cbor",
* "ca_file":"...","cert_file":"...","key_file":"...","verify_peer":true}
* Returns LP_OK or LP_ERR_INVALID_ARG on parse failure. */
LP_API int lp_set_default_transport(const char* transport_json);
/* ---------------------------------------------------------------------------
* Consumer: clients, invoke, subscribe
* ------------------------------------------------------------------------- */
typedef struct lp_client lp_client;
typedef struct lp_subscription lp_subscription;
/** Result callback for lp_invoke_async.
* ok != 0 → `json` is the result JSON value; ok == 0 → `json` is the
* canonical error object. `json` is only valid for the duration of the
* callback — copy it if you need it longer. */
typedef void (*lp_result_cb)(int ok, const char* json, void* user_data);
/** Event callback for lp_subscribe. `data_json` is a JSON array (the event
* payload), valid only for the duration of the callback. */
typedef void (*lp_event_cb)(const char* event_name, const char* data_json,
void* user_data);
/**
* Create a client for calling `target_module` on behalf of `origin_module`.
*
* `target_transport_json` / `capability_transport_json`: JSON object as for
* lp_set_default_transport(), or NULL to use the process default. The
* capability transport is used by the automatic `requestModule` token-fetch
* flow (this library dials `capability_module` transparently the first time
* a target requires a token — every language gets that flow for free).
*
* Owner thread: for a Qt-affine transport (Qt Remote Objects / local mode) the
* client is constructed on the Qt main thread — blocking this call until that
* thread runs it — because its node and socket are only serviced there. Any
* thread may call this. For the Qt-free transports (tcp / tcp_ssl / mock) the
* calling thread becomes the owner thread, as before.
*
* Returns NULL on invalid arguments.
*/
LP_API lp_client* lp_client_create(const char* target_module,
const char* origin_module,
const char* target_transport_json,
const char* capability_transport_json);
/** Destroy a client. After this returns, no further callbacks fire for the
* client or its subscriptions. */
LP_API void lp_client_destroy(lp_client* client);
/**
* Call `method` on the client's target module, blocking until the result
* arrives or the timeout elapses.
*
* `args_json`: JSON array of arguments (NULL means "[]").
* `timeout_ms <= 0` selects the default timeout.
*
* On LP_OK: *out_result_json (if non-NULL) receives the result JSON value
* (may be "null" — today's protocol does not distinguish "no result" from
* a failed call at this level; that matches existing behavior).
* On failure: *out_error_json (if non-NULL) receives the canonical error
* object. Both out-strings are owned by the caller (lp_string_free).
*/
LP_API int lp_invoke(lp_client* client,
const char* method,
const char* args_json,
int timeout_ms,
char** out_result_json,
char** out_error_json);
/**
* Asynchronous variant of lp_invoke. Returns LP_OK if the call was
* dispatched; `cb` then fires exactly once with the result (from the
* client's owner thread). Safe to call from any thread.
*
* `cb` carries the same outcome the sync twin splits across its return code
* and out-params: ok != 0 → `json` is the result JSON value; ok == 0 → `json`
* is the canonical error object lp_invoke would have written to
* out_error_json. A LP_OK return therefore means "dispatched", never
* "succeeded" — the outcome is only known in the callback.
*
* WHAT ok == 0 COVERS, precisely, because "the same outcome as the sync twin"
* is a statement about PARITY and not about completeness. Reported: failure to
* acquire the target ("object_unavailable"), a call that exceeds its deadline,
* a rejected auth token, and MODULE_NOT_LOADED from a host that is up. Both
* twins report all four; neither did before.
*
* NOT reported, and it is not an oversight: an unknown method name. Every
* provider flavour answers one with a bare null, byte-identical to a method
* that legitimately returns null, so the distinction does not exist on the
* wire to be reported. Closing it needs a provider-contract change across the
* SDKs, not a transport change here. A provider's own rejection of well-formed
* arguments ("dispatch_failed") is likewise NOT folded in by either twin — it
* arrives as a result, and the generated wrappers fold it.
*
* Argument/handle validation still fails synchronously with
* LP_ERR_INVALID_ARG and `cb` is NOT called in that case.
*/
LP_API int lp_invoke_async(lp_client* client,
const char* method,
const char* args_json,
int timeout_ms,
lp_result_cb cb,
void* user_data);
/**
* Subscribe to `event_name` emitted by the client's target module.
* `cb` fires once per event with the payload as a JSON array.
*
* The target module does NOT have to be reachable yet. This is the normal
* case, not an edge case: a module subscribes to its dependency during init(),
* and a ui_qml backend during onContextReady(), both of which run while the
* dependency's host has been spawned but has not called listen(). The
* subscription is held and armed when the module appears — including a
* mid-session package install — so a NULL return means the ARGUMENTS were
* refused, never "not there yet".
*
* What it does not promise: arming is not retroactive and no transport buffers,
* so an event the module emits in the window before the subscription arms
* reaches nobody. A module that fires a one-shot "ready" event synchronously
* inside its own init() can still be missed; if that event matters, expose a
* method the subscriber can call after subscribing.
*
* Returns NULL only for a null/empty client, event name or callback.
*/
LP_API lp_subscription* lp_subscribe(lp_client* client,
const char* event_name,
lp_event_cb cb,
void* user_data);
/** Cancel a subscription. After this returns the callback will not fire again
* (already-running invocations are allowed to finish first) — that part is
* synchronous and unconditional.
*
* The client also stops TRACKING it, so a subscription cancelled while still
* waiting for its module leaves the retry machinery instead of being warned
* about forever. That half is EVENTUAL, not immediate: it is posted to the
* client's owner thread and takes effect on a later turn of that thread's
* event loop. Doing it synchronously would mean blocking on the owner thread
* while holding a lock that thread's delivery callback also takes — a
* deadlock, and an outright hang once that event loop has stopped, which is
* exactly when a language binding's subscription handle is dropped.
*
* Consequence for callers: lp_pending_subscriptions() may still list a
* just-cancelled subscription until the owner thread runs. If the client is
* destroyed first the cancellation simply never runs, which is correct — the
* registry died with it. */
LP_API void lp_unsubscribe(lp_subscription* sub);
/** Diagnostics: a JSON array of "<module>::<event>" for every subscription on
* this client that has been accepted but has not armed yet — i.e. is waiting
* for its module to appear. `[]` when everything is live.
*
* Exists because the Qt consumer has had this visibility all along and the C
* ABI had none, which is precisely why a subscription that silently never
* armed was undetectable from Rust, Nim or a universal C++ module. Caller
* frees via lp_string_free; NULL only for a null client. */
LP_API char* lp_pending_subscriptions(lp_client* client);
/** Introspect the target module's methods/events as a JSON array (the
* same shape `lm` prints). Caller frees via lp_string_free. NULL on
* failure. */
LP_API char* lp_get_methods(lp_client* client);
/* ---------------------------------------------------------------------------
* Tokens
*
* THE WHOLE lp_token_* FAMILY IS THE **OUTBOUND** FAMILY, and this paragraph is
* the contract, not a description of today's callers. `module_name` everywhere
* below is the module being CALLED, and the value is the token this image will
* PRESENT to it. None of these writes authorizes anybody to call US.
*
* WHY THERE IS NO INBOUND DOOR HERE, and why that is safe rather than an
* oversight. Authorization happens in exactly one function — ModuleProxy::
* authorize — which exists only where a ModuleProxy is constructed, and across
* the fleet that is one production file (logos-plugin-qt's LogosAPIProvider).
* A cdylib image has no LogosAPI, no ModuleProxy, and never authorizes
* anything; every read of a cdylib's store is an outbound presentation. So an
* inbound write from a cdylib is not merely discouraged, it is
* UNREPRESENTABLE — which is a stronger guarantee than a doc comment on a door
* that exists.
*
* That includes `logos_module_accept_token` (logos_module_impl.h), whose
* generated body in both backends forwards straight to lp_token_save. The Qt
* glue calls it from two places with two meanings — forwarding an inbound
* caller token, and seeding the module's own anchor — and after the split the
* first of those writes a value the cdylib can only ever use outbound, which
* the peer then correctly refuses, forcing a real requestModule. That is a
* behaviour improvement, not a regression.
*
* WHAT AN INBOUND DOOR WOULD COST, priced now so nobody discovers it late. A
* Qt-free host that authorizes in-image would need
* `logos_module_accept_inbound_token` (or lp_token_save_inbound). That is a
* MINOR bump here, a declaration that nix/module-impl-abi/extract-exports.sh
* picks up automatically, AND a definition in BOTH cdylib backends in the same
* wave (logos-cpp-sdk's lidl_gen_cdylib.cpp and logos-rust-sdk's
* rustgen_provider.rs). Miss one and it links clean and dies at dlopen with an
* undefined symbol — on Linux only, invisible on macOS. logos_module_impl.h
* records that this has shipped twice at perfect version agreement. Note also
* that the module-impl-abi checks in both SDKs only go red AFTER each bumps its
* logos-protocol lock: declaring alone turns nothing red, and that lag is the
* real risk.
* ------------------------------------------------------------------------- */
/** Get the OUTBOUND token for `module_name` — what this image presents when it
* CALLS `module_name`. Returns NULL when absent; caller frees via
* lp_string_free.
*
* Reads this IMAGE's store — the one lp_client_create uses for every origin
* that has not been isolated. For an isolated origin, see lp_token_get_for.
*
* Does NOT see tokens this image issued to its own callers: those live in the
* inbound half, which has no lp_* reader by design (see above). */
LP_API char* lp_token_get(const char* module_name);
/** Store the OUTBOUND token for `module_name` — what this image will present
* when it CALLS `module_name`.
*
* Storing a token here does not let `module_name` call US. When `module_name`
* is "core" or "capability_module" this also installs the value as this
* store's identity credential, which is how the generated glue's
* logos_module_accept_token("core") seeding keeps working unchanged. */
LP_API int lp_token_save(const char* module_name, const char* token);
/* --- per-identity token stores ---------------------------------------------
*
* A host that loads several modules IN ONE IMAGE gives all of them the same
* token store, and that store holds every loaded module's own auth token. Since
* a client presents a cached token before it ever mints one, each module in such
* a host can reach every other one with authority it was never granted, and no
* `requestModule` is logged. These four functions let a host give a named
* identity a store of its own, so origin SELECTS the tokens a caller can present
* instead of merely labelling it.
*
* Additive and inert by default: with nothing isolated, every function above
* and every lp_client_create behaves exactly as before, on the same store.
* ------------------------------------------------------------------------- */
/** Give `identity` a private token store. The store is created EMPTY — it does
* NOT inherit this image's "core" / "capability_module" tokens, which are the
* HOST's credential and would let the identity authorize as the host. The host
* must give the identity its OWN credential with lp_token_adopt_credential
* before it can call anything; until then every call it makes is refused.
*
*
* Idempotent. Returns LP_ERR_UNSUPPORTED — changing nothing — if a client for
* this identity was already created against the shared store; isolating then
* would leave some callers on the shared store and some on the private one.
* Call this BEFORE creating any client for the identity, and treat the refusal
* as fatal for that identity rather than continuing. */
LP_API int lp_token_isolate_identity(const char* identity);
/** 1 if `identity` has a private store, 0 if it shares this image's store,
* LP_ERR_INVALID_ARG for NULL. */
LP_API int lp_token_identity_is_isolated(const char* identity);
/** Get the token `identity` would present to `module_name`. NULL when absent;
* caller frees via lp_string_free. Reads this image's shared store for an
* identity that has not been isolated — and, like every use of an identity's
* store, that counts as vending the shared store: isolate FIRST, then read. */
LP_API char* lp_token_get_for(const char* identity, const char* module_name);
/** Store a token in `identity`'s store — how a host seeds an isolated identity
* with the tokens it is actually entitled to. For the identity's OWN
* credential, use lp_token_adopt_credential instead: it owns the bootstrap key
* set, so a binding never has to spell "core"/"capability_module" itself.
* Writes this image's shared store
* for an identity that has not been isolated, which is almost certainly not
* what the caller meant: the token becomes visible to every other non-isolated
* caller, and lp_token_isolate_identity then refuses the name rather than
* stranding this write outside the private store. Isolate first. */
LP_API int lp_token_save_for(const char* identity, const char* module_name,
const char* token);
/** Clear an isolated identity's store — the unload hook, so a reloaded module
* does not present tokens minted for its previous incarnation. The identity's
* CREDENTIAL goes with it: a reload re-mints and re-registers, which
* invalidates the old credential at the target, so the caller must follow this
* with lp_token_adopt_credential for the new one. Returns LP_ERR_UNSUPPORTED
* for a non-isolated identity, whose store is shared and must not be cleared
* out from under everyone else. */
LP_API int lp_token_reset_identity(const char* identity);
/** Install `credential` as `identity`'s OWN credential in its private store:
* its value under every bootstrap key ("core", "capability_module"). This is
* what makes an isolated identity able to speak at all — it is the token
* presented to `capability_module.requestModule`, and the token
* capability_module pushes back with.
*
* The host mints `credential`, registers it with capability_module
* (lp_inform_module_token / informModuleToken over the trusted channel) and
* only THEN calls this. Register-before-adopt, so at no instant does the
* identity hold a credential capability_module has not yet accepted.
*
* Returns LP_ERR_INVALID_ARG for a NULL/empty argument, and LP_ERR_UNSUPPORTED
* — writing nothing — when `identity` is not isolated (the store would be the
* shared one, handing the credential to every un-isolated caller) or when
* `credential` is this image's own host anchor (adopting the host's credential
* as your own is the elevation this whole surface exists to prevent). */
LP_API int lp_token_adopt_credential(const char* identity, const char* credential);
/** The module names this image's OUTBOUND token store holds, as a JSON array.
* Caller frees via lp_string_free.
*
* Requires the "token_registry" host service (lp_grant_host_services): an
* ungranted image gets NULL. NULL is therefore "refused", never "empty" — a
* granted call with nothing stored returns "[]". Order is unspecified.
*
* This is the known-caller gate a trust-root module needs: it answers "have I
* ever been handed a token for this module?" without exposing any token
* value. */
LP_API char* lp_token_keys(void);
/** Deliver a module token to the client's target (the consumer-side
* `informModuleToken`). Returns LP_OK when the target accepted it.
*
* Note the fixed destination: this reaches `capability_module`, whatever the
* client's target is. It is the CONSUMER half of the exchange — "core, here is
* a token" — not a way to push a token at an arbitrary module. For that, see
* lp_inform_module_token_to below. */
LP_API int lp_inform_module_token(lp_client* client,
const char* auth_token,
const char* module_name,
const char* token);
/** Deliver the token for `module_name` TO `origin_module` — the provider half
* of the exchange, the direction capability_module pushes.
*
* Prefers `origin_module`'s handshake surface so a target still running its
* initializer is reachable, and falls back to its business object for modules
* built before that surface existed. `timeout_ms <= 0` selects the default
* (20s), which bounds the fallback acquire and the call together.
*
* Requires the "token_delivery" host service (lp_grant_host_services): an
* ungranted image gets LP_ERR_UNSUPPORTED. Returns LP_ERR_INTERNAL when the
* target refused or could not be reached. */
LP_API int lp_inform_module_token_to(lp_client* client,
const char* auth_token,
const char* origin_module,
const char* module_name,
const char* token,
int timeout_ms);
/* ---------------------------------------------------------------------------
* Host services — the privileged surface a trust-root module is granted
*
* Two functions above are closed by default and opened only by an explicit
* grant: lp_token_keys ("token_registry") and lp_inform_module_token_to
* ("token_delivery"). They are what a capability/trust-root module needs and
* what an ordinary module must not have.
*
* The grant is per-IMAGE, and that is the whole point rather than an
* implementation detail. A host binary and a module cdylib each link their own
* copy of this library, so each has its own process-global state — a grant
* recorded in the host is invisible to a cdylib calling lp_token_keys. The
* grant therefore has to cross the module-impl C ABI exactly as the auth token
* already does (logos_module_grant_host_services in logos_module_impl.h), and
* a gate "simplified" into the host would silently never fire.
* ------------------------------------------------------------------------- */
/** Grant this image the named host services. `services_json` is a JSON array
* drawn from the closed set {"token_registry", "token_delivery"}.
*
* REPLACES the current grant rather than adding to it. NULL, the empty string
* or `[]` clears it, closing both gates again — clearing is the fail-closed
* direction, so the lenient input handling costs nothing.
*
* Returns LP_ERR_INVALID_ARG for malformed JSON, a non-array, a non-string
* element, or ANY unrecognised service name; the existing grant is left
* untouched in that case. An unknown name is a caller that believes it holds a
* privilege it does not, which must not be absorbed silently. */
LP_API int lp_grant_host_services(const char* services_json);
/* ---------------------------------------------------------------------------
* Provider (GROUNDWORK — defined and compiled in this version, fully
* exercised when module authoring lands on the common cdylib module-impl
* C ABI. Until then lp_provider_register/emit return LP_ERR_UNSUPPORTED.)
* ------------------------------------------------------------------------- */
typedef struct lp_provider lp_provider;
/** Dispatch a method call. Return a heap string (result JSON value) that the
* library frees with lp_string_free; return NULL to signal failure. */
typedef char* (*lp_dispatch_cb)(const char* method, const char* args_json,
void* user_data);
/** Return the module's method/event metadata as a JSON array (heap string,
* freed by the library via lp_string_free). */
typedef char* (*lp_getmethods_cb)(void* user_data);
/** Accept a token delivered by another module. Return LP_OK to accept. */
typedef int (*lp_token_cb)(const char* module_name, const char* token,
void* user_data);
LP_API lp_provider* lp_provider_create(const char* module_name,
const char* transport_set_json);
LP_API void lp_provider_destroy(lp_provider* provider);
LP_API int lp_provider_register(lp_provider* provider,
lp_dispatch_cb dispatch,
lp_getmethods_cb get_methods,
lp_token_cb on_token,
void* user_data);
LP_API int lp_provider_emit_event(lp_provider* provider,
const char* event_name,
const char* data_json);
LP_API int lp_provider_save_token(lp_provider* provider,
const char* module_name,
const char* token);
#ifdef __cplusplus
}
#endif
#endif /* LOGOS_PROTOCOL_H */