Files
codebase-memory-mcp/src/daemon/runtime.h
T
Martin Vogel 0baddf7887 daemon: permanent lifecycle, daemon-backed CLI/hooks, real-Windows hardening, long-path launcher transactions
Daemon lifecycle and Windows correctness, verified on a real Windows 11
ARM64 VM through the maintained test-infrastructure/vm drivers, plus the
macOS and Linux arm64 suites and the container lint gate.

Daemon lifecycle:

- daemon start/stop/status subcommands. `daemon start` launches a
  PERMANENT daemon (spawn shape is byte-exact argv; survives idle
  periods and session ends) and reports an already-active daemon
  instead of failing. Permanence is honored at every stop latch:
  last-committed-client disconnect, host initial-client window,
  coordinator release, and application final-session close — a
  permanent daemon also keeps admitting new sessions after its last
  one closes.
- daemon stop refuses while sessions are active and lists the blocking
  peers (pid/role) that must finish first; an idle daemon drains
  through the activation-shutdown machinery with the ACK ordered after
  connection interrupts. A second stop is idempotent. The wire ops are
  no-cohort first-frame requests with peer fingerprint authentication,
  so stop/status never conflict with an exact-build admission gate.
- One-shot CLI commands now execute through the daemon (index workers
  keep their local supervised path). A cold CLI run that had to spawn a
  temporary daemon prints a hint that `daemon start` removes the
  per-command startup tax; a warm daemon is recycled silently.
- Hooks are connect-only fail-open: with no daemon present the hook
  emits a visible, rate-limited notice (Claude-dialect systemMessage
  plus stderr for other dialects) and always exits 0 — augmentation is
  never allowed to block the caller's tool use.
- Version skew: a newer-build client automatically drains an
  older-build permanent daemon (strict semantic-version triples only;
  dev builds never auto-drain) and the build-conflict message names
  `cbm daemon stop` as the manual escape hatch.

Windows IPC/runtime (real-VM verified):

- ipc(win): persistent pending overlapped ConnectNamedPipe. The accept
  path used to destroy its listening pipe instance on every 20 ms poll
  timeout; a client attaching in the teardown window was severed or left
  on an orphaned pipe object whose HELLO no server handle could ever
  read, absorbing the connect until the client's own timeout expired.
  The pending connect now survives poll timeouts and nothing is
  destroyed while a client could be attaching.
- ipc(win): drain-before-close for final responses. Closing a named-pipe
  server handle can discard a just-sent response before the peer reads
  it (POSIX stream sockets never lose buffered data on close). A bounded
  cbm_daemon_ipc_connection_drain (read-until-peer-EOF; no-op on POSIX,
  immediate on interrupted connections) now precedes close in
  runtime_worker_finish and runtime_reject_inline, so hello-conflict,
  capacity and disconnect acknowledgements reliably reach the peer.
- runtime: CLOSE_INTENT wire frame. A Windows named-pipe client has no
  transport half-close, so close_begin now announces departure with an
  explicit frame (ordered after APPLICATION_CANCEL, before the local
  interrupt); the server releases the client's admission on receipt
  instead of waiting for the handle to close. Admission-drop timing is
  now identical to POSIX shutdown() semantics on every platform.
- runtime(win): client close cancellation. close_begin serializes with
  request publication under the send lock, best-effort sends the active
  token's APPLICATION_CANCEL frame, then interrupts local I/O; the
  server cancels MCP/subprocess work promptly. Contract tests accept
  both correct outcomes (interrupted transport or decoded CANCELLED).
- runtime: activation acknowledgement ordering. The activation ACK is
  the requester's license to act on "snapshotted and draining", so every
  connection interrupt is now initiated before the ACK is sent; a
  session could previously get one more request serviced after the
  requester observed the ACK.
- service(win): deadline-bounded private-file prepare. The conflict-log
  prepare retry loop (100 x Sleep(2), which rounds up to the ~16 ms
  timer granularity) burned ~1.6 s against permanently obstructed paths,
  stalling hello rejections past the client's timeout. The retry budget
  is now a 250 ms deadline; transient share collisions still retry.
- subprocess(win): cmd.exe /C payload encoder quotes metacharacters
  correctly (root cause of the git-on-Windows failure cluster).
- watcher: SHA-256 buffer sizing (CBM_SZ_64 -> CBM_SZ_128) and a native
  Windows stop/unwatch cancellation test with exact-image verification.
- httpd: send_all writes in bounded 64 KiB slices. A single giant
  nonblocking send() on Windows is absorbed wholesale into AFD kernel
  buffering regardless of SO_SNDBUF, so send deadlines and interrupts
  could never engage against a slow peer (and the full payload was
  pinned in nonpaged pool). Slicing restores a deterministic
  backpressure point; a test hook pins SO_SNDBUF for the deadline and
  interrupt tests.
- ui/http: shutdown lifecycle — interrupt checks, response-wide send
  deadline, explicit connection states, refusal to free a server while
  a listener-owned connection is active.

Windows long-path support:

- Central path-aware wide conversion (canonicalize via GetFullPathNameW
  and prepend the extended-length prefix for absolute paths >=240) at
  the compat chokepoints (cbm_fopen/compat_fs/mkstemp/mkdtemp), sqlite
  store opens, and the daemon build-fingerprint/log paths. Deep managed
  installs (a 64-hex generation directory routinely exceeds MAX_PATH)
  now index, stage and activate correctly.
- activation transaction: its own file APIs and the component-walking
  ancestry validators now operate in the extended-length namespace;
  the launcher path is canonicalized (and prefixed when deep) once at
  entry so every downstream exact-string comparison stays
  form-consistent.
- Executable self-resolution uses the wide APIs (GetModuleFileNameW,
  GetFileAttributesW) so non-ASCII install paths survive argv[0]
  resolution.

Windows launcher install/uninstall transaction:

- FileRenameInfoEx names are NUL-terminated in an over-allocated
  buffer. FileNameLength governs per the contract, but filter drivers
  read FileName as NUL-terminated and appended adjacent heap bytes to
  created names — a flaky, garbage-suffixed rename target. Both the CLI
  and the launcher rename helpers are fixed.
- Uninstall retires state via rename-aside (.cbm ->
  .cbm-retired-v1-<tag>-<pid>) with the retired tag shortened to 16 hex
  chars so the bare rename target stays under the FileRenameInfoEx
  NT-conversion ceiling at guard depths; 64 bits still uniquely
  identify the generation.
- When the running launcher's mapped generation backings pin .cbm
  against rename, the backings are relocated to activation-<pid>-N
  .retired tombstones beside the install (a mapped image may be renamed,
  never deleted; the launcher's liveness-guarded sweep reclaims stale
  tombstones). Every relocation is recorded, and a FAILED uninstall
  reverses the moves after restoring .cbm — via MoveFileExW with
  extended-length paths on both arguments, since the deep generation
  target is beyond the handle-based rename's bare-path reach — so a
  restored install keeps its generation backings and stays runnable.
- After a committed uninstall the retired tree's backings are relocated
  out so the tree is shallow enough for the detached cleanup's rd, and
  the cleanup's working directory strips the extended-length prefix
  (CreateProcessW lpCurrentDirectory silently ignores prefixed paths).
- Files created under Administrators-default-owner directories
  (CopyFileW destinations, CREATE_NEW tombstones, probe directories)
  are explicitly owner-stamped so the exact-owner validators hold on
  runner images; guard fixtures stamp hand-built trees the same way.

Diagnostics, tests and infra:

- diagnostics: discovery is now an always-delivered JSON control record
  (new cbm_log_control) that survives CBM_LOG_LEVEL suppression and
  paths containing spaces; placement honors $TMPDIR with /tmp fallback
  via a diagnostics-local helper; the soak parser reads the JSON record;
  documented in docs/CONFIGURATION.md. Red-first coverage for suppressed
  log levels, TMPDIR-with-spaces, and native Windows output-contract
  assertions.
- tests(win): daemon_ipc/daemon_frontend fixtures now build endpoint
  parents with production-shaped ancestry (LocalAppData on Windows, via
  th_secure_runtime_parent_new) — the runtime ancestry validation
  correctly refuses temp roots whose ancestors grant mutation rights to
  Authenticated Users (C:/msys64/tmp, GitHub-runner work dirs) — and
  drive the documented startup-owner publication flow before reading
  generation-bound endpoint addresses. This turns the 26 Windows
  failures previously visible in CI's full-test job green without
  weakening any validation.
- tests(win): the launcher guard covers the full permanent-launcher
  contract including failed-uninstall restore and immediate reinstall
  after uninstall; new daemon lifecycle and reworked hook-augment
  guards run the start/recycle/stop flow end to end.
- tests: CBM_SKIP_PERF is now actually consumed by the test runner
  (it was set by CI but never read, so perf suites ran everywhere);
  four throughput/bench suites are classified as perf, the heavy
  store_arch suite moved to the slow-timeout tier, and two
  wall-clock-sensitive assertions were rewritten as invariant checks
  with coarse hang-detector backstops.
- build/test infra: build-dir safety contract, UI dev-proxy security
  contract, soak daemon-recovery contract, path-safety helper, the
  Windows VM worktree-sync contract wired into scripts/test.sh, and
  vm/win.sh guards building its clean embedded-UI product in an
  isolated BUILD_DIR so it cannot clobber the incremental test build.
  provision-windows.sh now installs Node.js for the guards UI build.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-21 15:32:15 +02:00

415 lines
23 KiB
C

/*
* runtime.h — Mandatory per-account CBM daemon runtime.
*
* This layer combines the authenticated local IPC transport, exact-build
* HELLO policy, and connection-owned coordinator leases. It deliberately
* does not spawn the daemon process; executable bootstrap is a caller concern.
*/
#ifndef CBM_DAEMON_RUNTIME_H
#define CBM_DAEMON_RUNTIME_H
#include "daemon/daemon.h"
#include "daemon/ipc.h"
#include "daemon/service.h"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
/* Detailed post-admission operation ABI. It is deliberately absent from the
* stable endpoint HELLO: an exact executable fingerprint already selects this
* layout, while conflicting generations must remain able to diagnose each
* other even when this value and every detailed payload have changed. */
#define CBM_DAEMON_RUNTIME_WIRE_ABI 1U
/* Permanent account-wide rendezvous envelope, generation zero. These numeric
* capacities and byte sizes are frozen independently of service/runtime data
* structures. A future generation must preserve both request and response
* layouts exactly at the stable endpoint.
*
* Request: u32 ABI @0, version[64] @4, build[65] @68.
* Response: u32 connect @0, u32 hello @4, u64 client @8, u64 PID @16,
* u32 conflict @24, active version[64] @28, active build[65] @92,
* requested version[64] @157, requested build[65] @221,
* message[512] @286. All integers use network byte order. */
#define CBM_DAEMON_RENDEZVOUS_ABI 1U
#define CBM_DAEMON_RENDEZVOUS_VERSION_TEXT_CAP 64U
#define CBM_DAEMON_RENDEZVOUS_BUILD_FINGERPRINT_CAP 65U
#define CBM_DAEMON_RENDEZVOUS_MESSAGE_CAP 512U
#define CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE 133U
#define CBM_DAEMON_RENDEZVOUS_RESPONSE_SIZE 798U
/* Cross-version activation shutdown is a separate first-frame protocol which
* remains parseable when normal HELLO would report a version/build conflict.
* Request: u32 action @0 followed by the unchanged 133-byte rendezvous
* identity @4. Response: u32 rendezvous ABI @0, u32 accepted @4, u64 active
* client snapshot @8, u64 drained-connection snapshot @16 (the activation
* requester excludes itself). */
#define CBM_DAEMON_ACTIVATION_SHUTDOWN_REQUEST_SIZE 137U
#define CBM_DAEMON_ACTIVATION_SHUTDOWN_RESPONSE_SIZE 24U
/* STATUS/STOP wire sizes. Requests carry only the requester's claimed build
* fingerprint (kernel-peer-verified, cross-build by design). Responses are
* fixed-size with a capped committed-client pid table. */
#define CBM_DAEMON_CONTROL_REQUEST_SIZE 65U
#define CBM_DAEMON_CONTROL_CLIENT_CAP 8U
#define CBM_DAEMON_STATUS_RESPONSE_SIZE 121U
#define CBM_DAEMON_STOP_RESPONSE_SIZE 40U
#define CBM_DAEMON_RUNTIME_PROJECT_KEY_MAX 4096U
#define CBM_DAEMON_RUNTIME_APPLICATION_PAYLOAD_MAX (CBM_DAEMON_MAX_FRAME_SIZE - 16U)
/* Frame flags are operation codes. Every operation has one exact payload
* length (or an explicitly length-prefixed payload); trailing bytes are a
* protocol violation and close the connection without registering a client.
* Multi-byte integers are encoded in network byte order. */
typedef enum {
CBM_DAEMON_RUNTIME_OP_HELLO = 1,
CBM_DAEMON_RUNTIME_OP_HEARTBEAT = 2,
CBM_DAEMON_RUNTIME_OP_JOB_SUBSCRIBE = 3,
CBM_DAEMON_RUNTIME_OP_JOB_UNSUBSCRIBE = 4,
CBM_DAEMON_RUNTIME_OP_DISCONNECT = 5,
CBM_DAEMON_RUNTIME_OP_APPLICATION_REQUEST = 6,
CBM_DAEMON_RUNTIME_OP_APPLICATION_CANCEL = 7,
CBM_DAEMON_RUNTIME_OP_ACTIVATION_SHUTDOWN = 8,
/* Fire-and-forget departure notice sent by close_begin when it interrupts
* an in-flight exchange. POSIX peers additionally signal departure via
* shutdown()/EOF; Windows named pipes have no client half-close, so this
* frame is what releases admission before the handle closes. No response
* is sent: the client's receive path is already interrupted. */
CBM_DAEMON_RUNTIME_OP_CLOSE_INTENT = 9,
/* `daemon status` observability probe. Like ACTIVATION_SHUTDOWN this is a
* one-shot authenticated first frame — no cohort, no HELLO, no admission —
* so it works across build skew, which is exactly when status matters. */
CBM_DAEMON_RUNTIME_OP_STATUS = 10,
/* `daemon stop`. Same authentication shape as STATUS. Refuses while
* committed clients exist (their pids come back in the response) and
* otherwise begins the normal stopping sequence — including for a
* PERMANENT generation, which this op and process kill alone may end. */
CBM_DAEMON_RUNTIME_OP_STOP = 11,
} cbm_daemon_runtime_operation_t;
typedef enum {
CBM_DAEMON_RUNTIME_ACTIVATION_INSTALL = 1,
CBM_DAEMON_RUNTIME_ACTIVATION_UPDATE = 2,
CBM_DAEMON_RUNTIME_ACTIVATION_UNINSTALL = 3,
} cbm_daemon_runtime_activation_action_t;
typedef struct {
bool accepted;
uint64_t active_clients;
uint64_t active_connections;
} cbm_daemon_runtime_activation_result_t;
typedef struct cbm_daemon_runtime_service cbm_daemon_runtime_service_t;
typedef struct cbm_daemon_runtime_client cbm_daemon_runtime_client_t;
typedef enum {
CBM_DAEMON_RUNTIME_SERVICE_STARTING = 1,
CBM_DAEMON_RUNTIME_SERVICE_RUNNING = 2,
CBM_DAEMON_RUNTIME_SERVICE_STOPPING = 3,
CBM_DAEMON_RUNTIME_SERVICE_EXITED = 4,
} cbm_daemon_runtime_service_state_t;
/* Application requests are binary-safe and bounded. TRANSPORT_ERROR is local
* to the client API and is never emitted as a valid wire response. BUSY is
* generated by the runtime when a connection already has an in-flight request;
* CANCELLED is the correlated, request-scoped cancellation result. */
typedef enum {
CBM_DAEMON_RUNTIME_APPLICATION_TRANSPORT_ERROR = 0,
CBM_DAEMON_RUNTIME_APPLICATION_OK = 1,
CBM_DAEMON_RUNTIME_APPLICATION_BUSY = 2,
CBM_DAEMON_RUNTIME_APPLICATION_UNAVAILABLE = 3,
CBM_DAEMON_RUNTIME_APPLICATION_REJECTED = 4,
CBM_DAEMON_RUNTIME_APPLICATION_HANDLER_ERROR = 5,
CBM_DAEMON_RUNTIME_APPLICATION_CANCELLED = 6,
} cbm_daemon_runtime_application_status_t;
typedef uint64_t cbm_daemon_runtime_application_token_t;
#define CBM_DAEMON_RUNTIME_APPLICATION_TOKEN_INVALID UINT64_C(0)
typedef enum {
CBM_DAEMON_RUNTIME_CANCEL_ERROR = -1,
CBM_DAEMON_RUNTIME_CANCEL_STALE = 0,
CBM_DAEMON_RUNTIME_CANCEL_ACCEPTED = 1,
} cbm_daemon_runtime_cancel_result_t;
typedef void cbm_daemon_runtime_application_session_t;
typedef cbm_daemon_runtime_application_session_t *(*cbm_daemon_runtime_application_session_open_fn)(
void *context, cbm_daemon_client_id_t client_id, uint64_t authenticated_process_id);
/* For OK, response_out may receive a malloc-owned binary buffer which the
* runtime frees after sending; NULL is valid only for a zero-length response.
* Non-OK results must leave an empty response. The request buffer is an owned
* runtime copy and remains valid only for the duration of this callback. */
typedef cbm_daemon_runtime_application_status_t (*cbm_daemon_runtime_application_request_fn)(
void *context, cbm_daemon_runtime_application_session_t *session,
cbm_daemon_runtime_application_token_t request_token, const uint8_t *request,
uint32_t request_length, uint8_t **response_out, uint32_t *response_length_out);
/* Request cancellation is non-terminal and may arrive before request() enters.
* The exact token must therefore remain sticky until that request observes it.
* The callback must be nonblocking and safe while request() is running. */
typedef void (*cbm_daemon_runtime_application_request_cancel_fn)(
void *context, cbm_daemon_runtime_application_session_t *session,
cbm_daemon_runtime_application_token_t request_token);
/* cancel is terminal for the session and is invoked after coordinator
* ownership is released. It may run before a newly-created request thread
* enters request(), so the cancellation state must remain sticky. It must be
* nonblocking, safe to invoke while request is running, and arrange for that
* request to return promptly. open and close are synchronous and must also
* return promptly; close is invoked exactly once after any request thread has
* joined. Runtime teardown is necessarily cooperative at this callback
* boundary; it never detaches or frees a still-running callback. */
typedef void (*cbm_daemon_runtime_application_session_cancel_fn)(
void *context, cbm_daemon_runtime_application_session_t *session);
typedef void (*cbm_daemon_runtime_application_session_close_fn)(
void *context, cbm_daemon_runtime_application_session_t *session);
typedef struct {
void *context;
cbm_daemon_runtime_application_session_open_fn session_open;
cbm_daemon_runtime_application_request_fn request;
cbm_daemon_runtime_application_request_cancel_fn request_cancel;
cbm_daemon_runtime_application_session_cancel_fn session_cancel;
cbm_daemon_runtime_application_session_close_fn session_close;
} cbm_daemon_runtime_application_callbacks_t;
typedef struct {
const cbm_daemon_ipc_endpoint_t *endpoint;
cbm_daemon_build_identity_t identity;
const char *conflict_log_path;
size_t conflict_log_cap_bytes;
/* Hard cap on accepted connection threads, including sockets that have not
* completed HELLO. request_timeout_ms bounds every unauthenticated slot
* and therefore must be finite, not CBM_DAEMON_IPC_WAIT_FOREVER. */
uint32_t max_clients;
uint64_t lease_timeout_ms;
uint32_t request_timeout_ms;
/* Bounds cooperative stop and retained participant-guard cleanup. Must be
* finite, not CBM_DAEMON_IPC_WAIT_FOREVER. */
uint32_t shutdown_timeout_ms;
/* Disabled when all handlers are NULL. Otherwise all five handlers are
* required. Function pointers and context are copied; the context's owner
* must keep it alive until service_free returns true. */
cbm_daemon_runtime_application_callbacks_t application;
/* Born via `daemon start`: the service does not begin stopping when its
* last committed client disconnects; only the stop/drain ops or an
* explicit process kill end it. */
bool permanent;
} cbm_daemon_runtime_service_config_t;
typedef enum {
CBM_DAEMON_RUNTIME_CONNECT_ERROR = 0,
CBM_DAEMON_RUNTIME_CONNECT_ACCEPTED = 1,
CBM_DAEMON_RUNTIME_CONNECT_CONFLICT = 2,
CBM_DAEMON_RUNTIME_CONNECT_REJECTED = 3,
} cbm_daemon_runtime_connect_status_t;
typedef struct {
cbm_daemon_runtime_connect_status_t status;
cbm_daemon_hello_status_t hello_status;
cbm_daemon_client_id_t client_id;
/* Kernel-authenticated PID of this client as observed by the daemon. */
uint64_t authenticated_process_id;
cbm_daemon_conflict_t conflict;
char message[CBM_DAEMON_CONFLICT_MESSAGE_SIZE];
} cbm_daemon_runtime_connect_result_t;
/* The fixed HELLO request contains only rendezvous ABI, semantic version, and
* exact executable SHA-256. It has no detailed ABI, client-ID, PID, user, or
* endpoint fields. Strings are NUL-terminated and zero padded. Encoding and
* validation depend only on these stable fields; detailed identity fields may
* be zero or unknown without changing the bytes. */
bool cbm_daemon_runtime_hello_request_encode(uint8_t out[CBM_DAEMON_RENDEZVOUS_REQUEST_SIZE],
const cbm_daemon_build_identity_t *identity);
/* Resolve process_id through the OS kernel's process metadata and hash the
* executable image rather than reopening an unbound pathname. Linux hashes an
* open /proc/<pid>/exe handle anchored to one process instance; macOS accepts
* an opened path only while its vnode is present in that process's executable
* mappings before and after hashing. Windows holds one process instance and a
* stable, non-share-write image-file handle across the hash, with path and
* creation-time snapshots around it (the Windows loader's image-section
* replacement restriction supplies the final path-to-mapping binding).
* Unsupported, inaccessible, replaced, or otherwise raced images fail closed.
* The daemon hashes and retains its stable native image object at startup;
* HELLO first compares a bracketed peer's mapped-image file identity with that
* retained object. A provable identity match avoids re-hashing. Different or
* unverifiable identities fall back to this full process-image fingerprint so
* byte-identical copies remain compatible while changed copies are rejected.
* Stateful HELLO admission requires the proven or computed fingerprint to
* match both the claimed and active build fingerprints. */
bool cbm_daemon_runtime_process_build_fingerprint(uint64_t process_id,
char out[CBM_DAEMON_BUILD_FINGERPRINT_SIZE]);
/* Ask any current daemon generation to drain before install/update/uninstall.
* This is not a normal HELLO and never creates an application session. The
* kernel-authenticated peer image must match identity->build_fingerprint, but
* that build is deliberately allowed to differ from the active daemon. A true
* return means a fixed response was received; inspect result_out->accepted.
* timeout_ms must be finite. */
bool cbm_daemon_runtime_request_activation_shutdown(
const cbm_daemon_ipc_endpoint_t *endpoint, const cbm_daemon_build_identity_t *identity,
cbm_daemon_runtime_activation_action_t action, uint32_t timeout_ms,
cbm_daemon_runtime_activation_result_t *result_out);
typedef struct {
bool permanent;
bool stopping;
uint16_t committed_clients;
uint32_t daemon_pid;
uint8_t client_count; /* entries in client_pids, capped at the wire limit */
uint32_t client_pids[CBM_DAEMON_CONTROL_CLIENT_CAP];
char build_fingerprint[CBM_DAEMON_BUILD_FINGERPRINT_SIZE];
char semantic_version[12];
} cbm_daemon_runtime_status_t;
typedef struct {
bool accepted; /* stopping was initiated */
bool busy; /* refused: committed clients hold the daemon */
uint16_t committed_clients;
uint8_t client_count;
uint32_t client_pids[CBM_DAEMON_CONTROL_CLIENT_CAP];
} cbm_daemon_runtime_stop_result_t;
/* One-shot authenticated status probe (no cohort, no HELLO, no admission).
* Works across build skew. A true return means a valid response arrived. */
bool cbm_daemon_runtime_request_status(const cbm_daemon_ipc_endpoint_t *endpoint,
const cbm_daemon_build_identity_t *identity,
uint32_t timeout_ms,
cbm_daemon_runtime_status_t *status_out);
/* One-shot authenticated stop request. Refuses while committed clients exist
* (result_out lists their pids); otherwise begins the stopping sequence, for
* permanent generations too. */
bool cbm_daemon_runtime_request_stop(const cbm_daemon_ipc_endpoint_t *endpoint,
const cbm_daemon_build_identity_t *identity,
uint32_t timeout_ms,
cbm_daemon_runtime_stop_result_t *result_out);
/* Performs the complete guarded first-participant handoff, starts listening
* synchronously, then owns both that participant claim and its
* accept/connection threads. All config scalar/text data is copied. endpoint
* is borrowed only for this synchronous call and may be freed after a
* successful return. Only one service may listen at an endpoint. */
cbm_daemon_runtime_service_t *cbm_daemon_runtime_service_start(
const cbm_daemon_runtime_service_config_t *config);
/* Host-start variant for preparing inert application resources before the
* listener. The caller must acquire the endpoint lifetime reservation first;
* this lets the host defer watcher/UI/diagnostic threads until the call has
* succeeded. A successful start transfers the reservation into the service
* and clears *reservation_io. On failure it remains owned by the caller unless
* the service had already adopted it and released it during rollback. */
cbm_daemon_runtime_service_t *cbm_daemon_runtime_service_start_reserved(
const cbm_daemon_runtime_service_config_t *config,
cbm_daemon_ipc_lifetime_reservation_t **reservation_io);
cbm_daemon_runtime_service_state_t cbm_daemon_runtime_service_state(
cbm_daemon_runtime_service_t *service);
size_t cbm_daemon_runtime_service_active_clients(cbm_daemon_runtime_service_t *service);
/* Monotonic count of every admission since service start; never decremented.
* Use for "has any client ever connected" decisions — the live count above
* can read zero between two short-lived sessions. */
uint64_t cbm_daemon_runtime_service_clients_admitted_total(cbm_daemon_runtime_service_t *service);
/* Includes accepted connections still waiting for HELLO. Never exceeds the
* configured max_clients; over-cap peers receive REJECTED before close. */
size_t cbm_daemon_runtime_service_active_connections(cbm_daemon_runtime_service_t *service);
size_t cbm_daemon_runtime_service_job_subscribers(cbm_daemon_runtime_service_t *service,
const char *project_key);
uint64_t cbm_daemon_runtime_service_client_process_id(cbm_daemon_runtime_service_t *service,
cbm_daemon_client_id_t client_id);
/* Wait functions use a monotonic deadline and never sleep past timeout_ms. */
bool cbm_daemon_runtime_service_wait_for_clients(cbm_daemon_runtime_service_t *service,
size_t expected, uint32_t timeout_ms);
bool cbm_daemon_runtime_service_wait_for_connections(cbm_daemon_runtime_service_t *service,
size_t expected, uint32_t timeout_ms);
bool cbm_daemon_runtime_service_wait_exited(cbm_daemon_runtime_service_t *service,
uint32_t timeout_ms);
/* Worker supervisors report the end of two-phase cancellation through this
* API. The service cannot exit while a cancelled job remains unreaped. */
bool cbm_daemon_runtime_service_job_reaped(cbm_daemon_runtime_service_t *service,
const char *project_key);
/* Emergency/test teardown only. Normal lifetime is connection-owned: the
* final disconnect makes STOPPING terminal, drains/reaps within the configured
* bound, and exits automatically. stop is itself bounded by timeout_ms. */
bool cbm_daemon_runtime_service_stop(cbm_daemon_runtime_service_t *service, uint32_t timeout_ms);
/* Before wait_exited or a successful stop this returns false without teardown.
* Otherwise it returns true only after every thread is joined, the owned
* participant guard is released, and the service allocation is destroyed. A
* false return retains the service and all retry authority; the caller must
* retry or fail-stop the owning process. */
bool cbm_daemon_runtime_service_free(cbm_daemon_runtime_service_t *service);
/* A successful return is connection-bound. The daemon delivers an explicit
* mismatch result (and persists it) before closing a rejected connection.
* Client exchange timeouts must be finite, not CBM_DAEMON_IPC_WAIT_FOREVER. */
cbm_daemon_runtime_client_t *cbm_daemon_runtime_client_connect(
const cbm_daemon_ipc_endpoint_t *endpoint, const cbm_daemon_build_identity_t *identity,
uint32_t timeout_ms, cbm_daemon_runtime_connect_result_t *result_out);
cbm_daemon_client_id_t cbm_daemon_runtime_client_id(const cbm_daemon_runtime_client_t *client);
uint64_t cbm_daemon_runtime_client_process_id(const cbm_daemon_runtime_client_t *client);
cbm_daemon_subscription_result_t cbm_daemon_runtime_client_job_subscribe(
cbm_daemon_runtime_client_t *client, const char *project_key,
cbm_daemon_subscription_id_t *subscription_id_out, uint32_t timeout_ms);
bool cbm_daemon_runtime_client_job_unsubscribe(cbm_daemon_runtime_client_t *client,
cbm_daemon_subscription_id_t subscription_id,
uint32_t timeout_ms);
bool cbm_daemon_runtime_client_heartbeat(cbm_daemon_runtime_client_t *client, uint32_t timeout_ms);
/* Reserve a monotonically increasing token for the next application request.
* Only one unstarted reservation may exist per client. It must be consumed by
* request_tagged() (or the client must be closed) before another reservation.
* Tokens are never reused within a client connection. */
bool cbm_daemon_runtime_client_application_token_reserve(
cbm_daemon_runtime_client_t *client, cbm_daemon_runtime_application_token_t *token_out);
/* Send a one-way cancellation control for an exact reserved/active request.
* ACCEPTED means the control was queued or written; callback completion is the
* final cancellation linearization point. Stale/wrong tokens are harmless. */
cbm_daemon_runtime_cancel_result_t cbm_daemon_runtime_client_application_cancel(
cbm_daemon_runtime_client_t *client, cbm_daemon_runtime_application_token_t request_token);
/* Executes one binary application request. Calls on one client are serialized.
* response_out is malloc-owned on OK and must be freed by the caller; an empty
* OK response is represented by NULL/0. Invalid/oversized requests are rejected
* locally without poisoning an otherwise usable client. */
cbm_daemon_runtime_application_status_t cbm_daemon_runtime_client_application_request(
cbm_daemon_runtime_client_t *client, const void *request, uint32_t request_length,
uint8_t **response_out, uint32_t *response_length_out, uint32_t timeout_ms);
/* Execute using the sole outstanding token returned by token_reserve(). This
* is the cancellable frontend path; the legacy helper above reserves
* internally. */
cbm_daemon_runtime_application_status_t cbm_daemon_runtime_client_application_request_tagged(
cbm_daemon_runtime_client_t *client, cbm_daemon_runtime_application_token_t request_token,
const void *request, uint32_t request_length, uint8_t **response_out,
uint32_t *response_length_out, uint32_t timeout_ms);
/* Begin a two-phase close without freeing the client. This atomically rejects
* future exchanges and interrupts an exchange already in flight. Exactly one
* caller may begin close. After all threads that could have entered a client
* API are joined, close_finish closes/frees the retained handle. */
bool cbm_daemon_runtime_client_close_begin(cbm_daemon_runtime_client_t *client);
/* Finish a close begun by close_begin. Sends DISCONNECT when the transport was
* not interrupted, always closes/frees the local handle, and returns whether
* the daemon acknowledged DISCONNECT. The client is invalid after this call. */
bool cbm_daemon_runtime_client_close_finish(cbm_daemon_runtime_client_t *client,
uint32_t timeout_ms);
/* One-call convenience wrapper around close_begin + close_finish. Ownership on
* the server is tied to EOF as well as the explicit operation. This remains
* safe with one exchange that was already in flight, but callers with a worker
* that may be immediately about to enter an exchange must use the two-phase API
* and join that worker before close_finish. */
bool cbm_daemon_runtime_client_close(cbm_daemon_runtime_client_t *client, uint32_t timeout_ms);
#endif /* CBM_DAEMON_RUNTIME_H */