hecate_om_identity (hecate_om v0.23.0)

View Source

Loads the service-principal cert at boot; identity + seed/opts resolution for the mesh pool hecate_om_sup supervises alongside this gen_server (piece A, PLAN_HECATE_OM_MESH_WRAPPERS.md).

Each hecate-service has its OWN realm-signed credential (NOT a user's). The credential lives at /etc/hecate/secrets/service-cert.pem inside the container; the host mounts the per-service directory from /etc/hecate/secrets/<service-name>/ onto that path.

v1: long-lived realm-signed cert provisioned out-of-band by a realm-admin script. v2: short-lived UCAN auto-rotated from a realm HTTP endpoint. The v2 swap-in lands here without touching consumers.

Connect-degradation: with no seeds configured, hecate_om_sup never starts a mesh pool child at all, and macula_client/0 returns {error, no_client} forever -- consumers fall back to no-op behaviour, same contract as before this piece. The service stays up either way; it just doesn't talk to the mesh.

**The pool itself is no longer this gen_server's state.** It used to be: a hand-rolled self() ! connect / 5s-retry / erlang: monitor + DOWN dance defending against a pool crash and an early-boot race against the macula OTP application not being up yet. Both turned out to be things macula_client and OTP already give for free once the pool is an ordinary supervised sibling (hecate_om_sup, restart => permanent): each seed link dials and retries forever on its own timer without ever crashing the pool process for an unreachable seed (confirmed by reading macula_client.erl directly), so there is nothing for a hand- rolled monitor to catch that OTP's own restart doesn't already cover; and hecate_om.app.src already lists macula in applications, so standard OTP boot ordering means macula has already finished starting before hecate_om_app:start/2 -- and so this module's own init/1 -- is ever called. start_mesh_pool/0 below is that sibling child's start function: it runs strictly after this gen_server (an earlier sibling in hecate_om_sup's children list) has already loaded the keypair, so it reads it back via keypair/0 rather than duplicating the loading logic. macula_client/0 now simply checks whether that sibling is registered and alive -- no gen_server round trip, no state to keep in sync with reality.

**Piece H** (PLAN_HECATE_OM_MESH_WRAPPERS.md): every OTHER accessor here (service_cert/0, realm/0, keypair/0, org/0, cert_chain/0, realm_ca/0) used to be a bare gen_server:call, which raises {noproc, _} if called before this gen_server has started. Three independent repos (hecate-biotope, hecate- society, hecate-dronex) hand-rolled a try/catch around exactly this -- hecate_om_identity:realm() specifically, confirmed by reading biotope_mesh.erl/society_mesh.erl/dronex_mesh.erl directly, not assumed. safe_call/1 converts that one specific failure mode (not a genuine timeout -- a hung gen_server is a real bug worth crashing loudly over, not silently degrading) into {error, not_booted}, obsoleting all three call sites' defenses at once rather than leaving each caller to reinvent it.

Summary

Functions

The cert chain to embed in advertisements: this service's leaf cert followed by its org CA (PEM). {error, no_cert_chain} when either half is missing — the service then advertises without a chain and is reachable only by open-mode consumers (Slice 7c Direction B). {error, not_booted} (piece H) when called before this gen_server has started.

The service's stable signing keypair, {error, no_keypair} when running on an ephemeral identity (callers that sign DHT records degrade to no-op on that), or {error, not_booted} (piece H) when called before this gen_server has started.

The mesh pool handle, or {error, no_client} when no seeds are configured (so hecate_om_sup never started the pool child at all) or the pool hasn't registered itself yet. A direct whereis/1 check on the pool's own registered name -- no gen_server round trip to this module, and nothing here to fall out of sync with reality.

This service's org name (the <org> segment of its procedure URIs). Always a binary -- <<"_">> both when unconfigured and (piece H) when called before this gen_server has started, since "we don't have a real org value yet" is the same situation to every caller either way, and this accessor's whole contract is never raising and never asking the caller to unwrap a tuple.

The 32-byte realm tag, {error, no_realm} when unset, or {error, not_booted} (piece H) when called before this gen_server has started -- this is the specific accessor hecate-biotope, hecate-society, and hecate-dronex each wrap in a hand-rolled try/catch today, confirmed by reading their *_mesh.erl directly.

The realm CA a verifying consumer trusts as the direct-dial trust anchor (PEM). {error, no_realm_ca} when unconfigured — a verify => true call then cannot verify and drops every provider. {error, not_booted} (piece H) when called before this gen_server has started.

The realm-signed service-principal cert, {error, no_cert} when unset, or {error, not_booted} (piece H) when called before this gen_server has started.

Start function for the mesh-pool child hecate_om_sup includes in its children list whenever seeds are configured (piece A). Runs as a sibling started strictly after this gen_server, so keypair/0 is already resolved -- no duplicated loading logic here, just a read-back. Registers the pool under ?MESH_POOL_NAME so macula_client/0 can find it without a round trip through this module, then hands {ok, Pid} (or a genuine {error, _}) back to the supervisor exactly like any other child start function.

Functions

cert_chain()

-spec cert_chain() -> {ok, binary()} | {error, no_cert_chain | not_booted}.

The cert chain to embed in advertisements: this service's leaf cert followed by its org CA (PEM). {error, no_cert_chain} when either half is missing — the service then advertises without a chain and is reachable only by open-mode consumers (Slice 7c Direction B). {error, not_booted} (piece H) when called before this gen_server has started.

configured_seeds()

handle_call(Msg, From, State)

handle_cast(Msg, S)

handle_info(Msg, S)

init(_)

keypair()

-spec keypair() -> {ok, macula_identity:key_pair()} | {error, no_keypair | not_booted}.

The service's stable signing keypair, {error, no_keypair} when running on an ephemeral identity (callers that sign DHT records degrade to no-op on that), or {error, not_booted} (piece H) when called before this gen_server has started.

keypair_from(_)

macula_client()

-spec macula_client() -> {ok, pid()} | {error, no_client}.

The mesh pool handle, or {error, no_client} when no seeds are configured (so hecate_om_sup never started the pool child at all) or the pool hasn't registered itself yet. A direct whereis/1 check on the pool's own registered name -- no gen_server round trip to this module, and nothing here to fall out of sync with reality.

org()

-spec org() -> binary().

This service's org name (the <org> segment of its procedure URIs). Always a binary -- <<"_">> both when unconfigured and (piece H) when called before this gen_server has started, since "we don't have a real org value yet" is the same situation to every caller either way, and this accessor's whole contract is never raising and never asking the caller to unwrap a tuple.

realm()

-spec realm() -> {ok, <<_:256>>} | {error, no_realm | not_booted}.

The 32-byte realm tag, {error, no_realm} when unset, or {error, not_booted} (piece H) when called before this gen_server has started -- this is the specific accessor hecate-biotope, hecate-society, and hecate-dronex each wrap in a hand-rolled try/catch today, confirmed by reading their *_mesh.erl directly.

realm_ca()

-spec realm_ca() -> {ok, binary()} | {error, no_realm_ca | not_booted}.

The realm CA a verifying consumer trusts as the direct-dial trust anchor (PEM). {error, no_realm_ca} when unconfigured — a verify => true call then cannot verify and drops every provider. {error, not_booted} (piece H) when called before this gen_server has started.

service_cert()

-spec service_cert() -> {ok, binary()} | {error, no_cert | not_booted}.

The realm-signed service-principal cert, {error, no_cert} when unset, or {error, not_booted} (piece H) when called before this gen_server has started.

start_link()

start_mesh_pool()

-spec start_mesh_pool() -> {ok, pid()} | {error, term()}.

Start function for the mesh-pool child hecate_om_sup includes in its children list whenever seeds are configured (piece A). Runs as a sibling started strictly after this gen_server, so keypair/0 is already resolved -- no duplicated loading logic here, just a read-back. Registers the pool under ?MESH_POOL_NAME so macula_client/0 can find it without a round trip through this module, then hands {ok, Pid} (or a genuine {error, _}) back to the supervisor exactly like any other child start function.

NOTE: connection no longer depends on the realm-signed cert. The macula identity opt wants a raw Ed25519 keypair, not a cert, and the mesh does not yet verify realm membership at connect/publish — so requiring a cert to connect was spurious (it kept every service dark). The cert is still loaded + held (service_cert/0) for the v2 swap-in, when the SDK enforces realm-signed identity and this is where it gets passed.

terminate(Reason, State)