mirror of
https://github.com/logos-co/logos-protocol.git
synced 2026-08-30 21:41:10 +00:00
* feat: the host-services C ABI a trust-root module needs
capability_module is the last legacy Qt Q_INVOKABLE provider, and it cannot
become an ordinary `interface: universal` module while the two things it does
have no C entry point: reading the token store, and pushing a token to an
ARBITRARY target. This adds both, plus the grant that gates them. Purely
additive — no existing symbol changes behaviour.
lp_token_keys() the module names THIS image's TokenManager
holds. NULL means REFUSED, never "empty" — a
granted call with no tokens answers "[]", and a
known-caller gate needs to tell those apart.
lp_inform_module_token_to() routes to LogosAPIClient::informModuleToken_module,
the 5-arg form. Note the existing
lp_inform_module_token is the WRONG DIRECTION
for this: it reaches a consumer path that
hardcodes requestObject("capability_module"),
i.e. core -> capability, not capability ->
target. That 5-arg method had no C entry point.
lp_grant_host_services() sets the in-image grant over the closed set
{token_registry, token_delivery}. Replaces
rather than merges; NULL/""/"[]" clears. An
unknown name is rejected wholesale and leaves
the existing grant untouched, so a typo can
never silently drop a service.
Why the gate is per-IMAGE, which looks like an odd choice until it doesn't:
the host binary and a module's cdylib each link their own copy of this library,
so they have separate process-global state. A gate "simplified" into the host
would be checked against state the calling image can never set, and would read
as ungranted forever. The grant therefore crosses the module-impl C ABI the
same way the auth token already does — hence the logos_module_grant_host_services
declaration added to logos_module_impl.h, whose generated body and host-side
call land in logos-cpp-sdk and logos-module-loader-qt respectively.
MINOR 2 -> 3; MAJOR unchanged, so the equal-MAJOR compatibility rule is
unaffected. 387/387 tests pass, including 6 new ones covering both gates
closed, both opened, clearing re-closing them, and the unknown-name rejection.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(tokens): a per-CLIENT token store, selected by origin
TokenManager::instance() is the IMAGE's store, and in a host that loads
plugins in-process it is also an ambient ring: the host writes
`name -> that module's root auth token` for EVERY module it loads. On the
hot path a client asserts no identity at all — invokeRemoteMethod reads
the store first and only mints on a miss — so a plugin asking for target X
finds X's own root token sitting there and presents it. The provider
accepts any token in its image's store, so the call authorizes and no
requestModule is ever logged. Every plugin in that image holds every other
module's authority, and giving a plugin its own ORIGIN STRING changes none
of it, because origin was never consulted on the path taken.
This makes origin SELECT THE STORE rather than merely label the caller.
TokenManager::forIdentity(x) the store to present tokens from when I am x
TokenManager::isolateIdentity(x) give x a private store (host opt-in)
isIsolated / isolatedIdentities / bootstrapKeys / seedBootstrapTokens /
resetIdentity
ADDITIVE BY CONSTRUCTION, not by promise: forIdentity() returns the SAME
OBJECT instance() returns — pointer-identical — for every name until
someone isolates that exact name, so a host that knows nothing about this
is byte-for-byte unchanged. All seven are static member FUNCTIONS: no data
member, no virtual, nothing moc sees. Measured, not asserted: the exported
symbol table of liblogos_protocol.dylib gains exactly 12 names (7 statics +
5 lp_*) and LOSES NONE (736 -> 748). Neither ABI-sensitive private layout
(LogosAPIClient, LogosAPIConsumer) was touched at all.
Construction paths in this repo:
* LogosAPIClient / LogosAPIConsumer: an explicit store still wins; a NULL
store now resolves to forIdentity(origin) instead of being a guaranteed
crash on the first getToken().
* lp_client_create: &TokenManager::forIdentity(origin), not instance().
This is the whole answer to that function's frozen signature — the store
cannot be handed to it, so the origin it already takes must select it.
Bootstrap (constraint 4) survives because a private store is created seeded
with "core" and "capability_module" copied from instance(), and with
NOTHING else — the two keys the first requestModule authenticates with, not
a copy of the ring. resetIdentity() is the unload hook: it clears the
contents and re-seeds, while the store OBJECT stays immortal because a
client holds it by raw pointer from continuations that outlive their caller.
The trust root (constraint 3) is unaffected, and it is checked rather than
argued: lp_token_keys() still reads instance(), isolation only ADDS stores,
and the one thing that moves — an isolated identity's consumer-side CACHE
write — is keyed by TARGET while the known-caller gate consults ORIGIN
names, which the HOST writes and this change never touches.
C ABI grows five additive symbols, each carrying LP_API:
lp_token_isolate_identity, lp_token_identity_is_isolated, lp_token_get_for,
lp_token_save_for, lp_token_reset_identity. Protocol version 0.3.0 -> 0.4.0
(MINOR: additive).
Tests: 439/439 before, 469/469 after. The 30 new cases were validated as
DETECTORS the way this suite requires — against a throwaway build with
forIdentity()'s isolation branch neutered to `if (true)`, i.e. origin as a
label again. 15 go RED there (the walled identity holds the target's root
token; the handshake count is 0 instead of 2; lp_token_keys() lists the
identity's private mint), and the other 15 are pins of behaviour that must
be identical either way. Every escalation case carries an ambient CONTROL
asserting the token IS reachable without isolation.
Hosts are deliberately NOT changed here.
* feat(codec): shape-check the untyped containers
`[any]` and `{tstr:any}` both spell `nlohmann::json` in C++ — LogosList and
LogosMap are aliases of it — so no Codec<T> specialization can tell them apart
and fromJson<T> has nothing to dispatch on. Their SHAPE is still declared,
though, and array-ness / object-ness is the whole of the declared type at that
layer.
jsonRequireArray / jsonRequireObject check exactly that and hand the value on
UNCHANGED, throwing through the codec's own detail::typeError so the message is
the one every other surface already produces ("expected array at arg0, got
string"). The value is not rebuilt from JSON: that would retype nested elements
for no validation gain, which is the same reasoning logos_qt_arg_decode.h gives
for the Qt surface.
This is what logos_codec.h:36 already promised and these two types quietly did
not honour — "shape mismatches throw CodecError … rather than silently
substituting a default, silent defaults are how a mangled value reaches business
logic."
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
500 lines
25 KiB
C
500 lines
25 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.
|
|
* =========================================================================== */
|
|
|
|
#define LOGOS_PROTOCOL_VERSION_MAJOR 0
|
|
// 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.
|
|
#define LOGOS_PROTOCOL_VERSION_MINOR 4
|
|
#define LOGOS_PROTOCOL_VERSION_PATCH 0
|
|
#define LOGOS_PROTOCOL_VERSION_STRING "0.4.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
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
/** Get the stored token for `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. */
|
|
LP_API char* lp_token_get(const char* module_name);
|
|
|
|
/** Store a token for `module_name` in this image's store. */
|
|
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, seeded with the trust-root bootstrap
|
|
* ("core" and "capability_module", copied from this image's store) so its first
|
|
* call can still run the `requestModule` handshake.
|
|
*
|
|
* 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. 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 and re-seed the bootstrap — the unload
|
|
* hook, so a reloaded module does not present tokens minted for its previous
|
|
* incarnation. 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);
|
|
|
|
/** The module names this image's 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 */
|