hecate_om_capabilities (hecate_om v0.16.0)
View SourceAdvertises a service's capabilities on the mesh, and resolves other services' capabilities from the DHT.
A capability carrying handler => {HandlerModule, Args} is advertised TWICE via macula_response:advertise_direct/7 — the SDK's own supervised wrapper, which registers a handler with the pool AND publishes the signed procedure_advertisement DHT record naming this pool's connected station, in one call:
1. under the bare capability name (Name) — the any-provider, backward-compatible registration every caller could always reach; 2. under org_procedure(Org, Name) (<<Org/binary, "/", Name/binary>>) — a SEPARATE wire-level ADVERTISE and DHT record. macula_remote_advertise_registry (station-side) keys purely on the opaque procedure string, so this lands as a genuinely distinct registry entry, not a second write to the same slot. Two orgs serving the same capability name from the SAME relay station therefore each hold their own entry, rather than one shared bare-name slot where whichever republish landed last wins — the exact bug a live test (test_live/hecate_om_capabilities_live_station_tests.erl's org_scoped_call_reaches_only_the_targeted_org_test_) caught 2026-08-29: both an acme- and a contoso-targeted call were answered by whichever org's registration was most recent.
macula_direct_dial:discovery_uri/2 (RealmHex/Procedure) and this module's procedure_uri/3 (RealmHex/Org/Name) produce the IDENTICAL string when Procedure = org_procedure(Org, Name) — so advertise_direct's own internal DHT publish already lands the org-qualified record at exactly the key discovery_key_org/3 resolves on read. No separate record-only write is needed for the handler-bearing case.
A capability with no handler key gets the legacy record-only path (build_advertisement/5,6 + put_record): discoverable, never callable via call_capability, kept for a capability another mechanism serves.
call_capability/5,7 resolves CapName under Org first (discovery_key_org/3), falling back to the bare (any-provider) key only when Org has published nothing there yet. resolve_full/4 tags each resolved provider with which wire-level procedure string actually matched, and the CALL uses that string, not the raw CapName — an org-scoped resolution CALLs org_procedure(Org, CapName), never the bare name, so a targeted call can only ever be answered by that org's own registration, all the way to the wire.
reuse_sup/0's pid is round-tripped through this worker's state — one slot per DISTINCT procedure string, so the bare and org-qualified registrations each keep their own supervisor — and passed back in as advertise_directs own reuse_sup option on every 30s republish tick — a station's wire-level registration for a procedure is tied to the connection that sent it and does not survive that connection being replaced (see macula_response:advertise_direct/7 and hecate-tube's tube_mesh_providers.erl, which hit this bug live before this option existed); periodic re-advertise without reuse_sup would also leak one factory supervisor per tick.
Every advertisement carries ttl_ms => ?ADVERTISEMENT_TTL_MS (4x the republish interval, matching the buffer macula_station_announcer's own 75%-of-TTL refresh leaves for stations) instead of the ~48h envelope default — a dead service's advertisement should age out on the order of minutes, not days. The handler-bearing path's ttl_ms has no live effect until macula ships past 10.11.1 (the fix to macula_direct_dial:adv_opts/1, which silently dropped ttl_ms before then); the record-only (no-handler) path builds the record directly and is unaffected by that gap.
lookup/1 resolves a capability by name: derive the same procedure key, read every advertisement stored there, verify each signature, return the {advertiser, serving_station} set.
list_org_capabilities/1 browses every capability an org has advertised — genuinely new, unlike lookup/1/call_capability: the bare key needs a capability NAME to look anything up, and so does the org-qualified key (discovery_key_org/3, Realm/Org/Name — Name is part of the key, not something a lookup can search past). There is no NAME-less "everything Org has" key to resolve. A DHT-composite-key design (publish the same record again under an org-prefix-only key) — the original plan for this — turned out infeasible: macula_record:storage_key/1 DERIVES a procedure_advertisement's storage key from its own procedure_uri payload, so a record cannot be stored under an independently-chosen key without its procedure_uri field lying about what it actually is. Building a second, genuinely separate DHT record type just for this (with its own write-conflict semantics for multiple advertisers of one org) was judged more than this needs. Instead: list_org_capabilities/1 is a CLIENT-SIDE filter over macula:find_records_by_type/2 (matched via macula_topic_pattern:matches/2) — the exact same local-relay-view, warm-start-only mechanism read_model_services.md already documents for bulk browsing, honestly inheriting its "one relay's local view, not authoritative" limitation rather than pretending to a DHT-wide index this record type cannot support.
Signing needs the service's stable keypair (hecate_om_identity:keypair/0); an ephemeral service cannot sign and is correctly not advertised.
Summary
Functions
Call a capability by name over the DIRECT-DIAL data path: resolve the providers of CapName UNDER Org specifically (their procedure_advertisement records, keyed Realm/Org/CapName — discovery_key_org/3), resolve one provider's serving station to a dialable endpoint, dial it directly and issue the CALL there. On failure (unresolvable endpoint, dead station, error reply) fail over to the next provider *of the same org* — this never silently falls over to a different org's own implementation of the same capability name.
Explicit-pool form (testable without hecate_om_identity). Opts: verify => boolean() (default false = open; when true, drop providers whose embedded service-cert chain does not verify to the realm CA, Slice 7c Direction B) and ucan_token => binary() (presented to a gated provider, Slice 7b).
Whether Cap carries a handler (and so should be advertised via advertise_direct, not just written to the DHT as a bare discovery record).
Every capability Org has advertised — {ok, [#{procedure_uri := binary(), advertiser := Pubkey, serving_station := Pubkey}]}. See moduledoc for why this is a client-side filter over find_records_by_type, not a DHT-indexed query: a WARM-START view of whatever this pool's connected station(s) locally hold, not an authoritative mesh-wide listing. Empty when nothing matches or the mesh is unreachable, same as lookup/1.
Resolve a capability by name to the providers advertising it. {ok, [#{advertiser := Pubkey, serving_station := Pubkey}]}. Empty when nothing is advertised or the mesh is unreachable.
Whether Uri (a procedure_advertisement's own procedure_uri field) matches the RealmHex/Org/*Pattern. Split purely on / -- Uri's own Name segment may itself contain . (weather.get_forecast) but never /, matching procedure_uri/3's own construction.
The org-qualified wire-level procedure string a handler-bearing capability's SECOND advertise_direct registration uses. Deliberately NOT procedure_uri/3's realm-hex-prefixed form: the ADVERTISE/CALL wire frames already carry realm as a separate field (macula_frame:advertise/1), so re-embedding it here would be redundant on every wire message. The DHT KEY still ends up realm-prefixed regardless — see moduledoc, discovery_uri/2 does that wrapping on the publish side, matching discovery_key_org/3 on the read side.
Every capability Org has advertised, under Realm — see moduledoc for why this is a client-side filter over find_records_by_type, not a DHT-indexed query.
The extra Opts entry an advertise_direct retry needs to reuse a prior call's factory supervisor instead of leaking a new one every republish tick. #{} on a capability's first-ever advertise.
Functions
-spec build_advertisement(macula_identity:key_pair(), binary(), binary(), hecate_om_service:capability(), macula_identity:pubkey()) -> map().
-spec build_advertisement(macula_identity:key_pair(), binary(), binary(), hecate_om_service:capability(), macula_identity:pubkey(), macula_record:procedure_advertisement_opts()) -> map().
-spec call_capability(binary(), binary(), term(), pos_integer(), map()) -> {ok, term()} | {error, term()}.
Call a capability by name over the DIRECT-DIAL data path: resolve the providers of CapName UNDER Org specifically (their procedure_advertisement records, keyed Realm/Org/CapName — discovery_key_org/3), resolve one provider's serving station to a dialable endpoint, dial it directly and issue the CALL there. On failure (unresolvable endpoint, dead station, error reply) fail over to the next provider *of the same org* — this never silently falls over to a different org's own implementation of the same capability name.
Falls back to the bare, any-provider key (discovery_key/2) only when Org has published no org-qualified advertisement at all — a fleet mid-migration onto this org-scoping keeps resolving exactly as it did before this existed. Once Org is genuinely irrelevant to you (any provider will do, e.g. a realm-wide commodity capability), pass whatever value is convenient; it only narrows the search, it never widens it beyond what an org-blind lookup would already find.
The CALL uses whichever wire-level procedure string actually resolved (resolve_full/4 tags each provider with it) — org_procedure(Org, CapName) on an org-scoped hit, the bare CapName only on the any-provider fallback — matching whichever registration that specific provider made via advertise_one/7. Org-scoping therefore changes both WHICH station gets dialed AND what's sent once dialed; a targeted call can only ever be answered by that org's own registration, never a different org's provider sharing the same station. Runs in the caller's process (not the capabilities gen_server), so a slow mesh never blocks capability registration.
-spec call_capability(pid(), binary(), binary(), binary(), term(), pos_integer(), map()) -> {ok, term()} | {error, term()}.
Explicit-pool form (testable without hecate_om_identity). Opts: verify => boolean() (default false = open; when true, drop providers whose embedded service-cert chain does not verify to the realm CA, Slice 7c Direction B) and ucan_token => binary() (presented to a gated provider, Slice 7b).
-spec has_handler(hecate_om_service:capability()) -> boolean().
Whether Cap carries a handler (and so should be advertised via advertise_direct, not just written to the DHT as a bare discovery record).
Every capability Org has advertised — {ok, [#{procedure_uri := binary(), advertiser := Pubkey, serving_station := Pubkey}]}. See moduledoc for why this is a client-side filter over find_records_by_type, not a DHT-indexed query: a WARM-START view of whatever this pool's connected station(s) locally hold, not an authoritative mesh-wide listing. Empty when nothing matches or the mesh is unreachable, same as lookup/1.
Resolve a capability by name to the providers advertising it. {ok, [#{advertiser := Pubkey, serving_station := Pubkey}]}. Empty when nothing is advertised or the mesh is unreachable.
Whether Uri (a procedure_advertisement's own procedure_uri field) matches the RealmHex/Org/*Pattern. Split purely on / -- Uri's own Name segment may itself contain . (weather.get_forecast) but never /, matching procedure_uri/3's own construction.
The org-qualified wire-level procedure string a handler-bearing capability's SECOND advertise_direct registration uses. Deliberately NOT procedure_uri/3's realm-hex-prefixed form: the ADVERTISE/CALL wire frames already carry realm as a separate field (macula_frame:advertise/1), so re-embedding it here would be redundant on every wire message. The DHT KEY still ends up realm-prefixed regardless — see moduledoc, discovery_uri/2 does that wrapping on the publish side, matching discovery_key_org/3 on the read side.
Every capability Org has advertised, under Realm — see moduledoc for why this is a client-side filter over find_records_by_type, not a DHT-indexed query.
The extra Opts entry an advertise_direct retry needs to reuse a prior call's factory supervisor instead of leaking a new one every republish tick. #{} on a capability's first-ever advertise.