hecate_om_capabilities (hecate_om v0.16.1)

View Source

Advertises 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/CapNamediscovery_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

build_advertisement(KeyPair, Realm, Org, Cap, Station)

-spec build_advertisement(macula_identity:key_pair(),
                          binary(),
                          binary(),
                          hecate_om_service:capability(),
                          macula_identity:pubkey()) ->
                             map().

build_advertisement(KeyPair, Realm, Org, _, Station, CertOpts)

-spec build_advertisement(macula_identity:key_pair(),
                          binary(),
                          binary(),
                          hecate_om_service:capability(),
                          macula_identity:pubkey(),
                          macula_record:procedure_advertisement_opts()) ->
                             map().

call_capability(Org, CapName, Payload, TimeoutMs, Opts)

-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/CapNamediscovery_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.

call_capability(Pool, Realm, Org, CapName, Payload, TimeoutMs, Opts)

-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).

decode_resolved(Records)

-spec decode_resolved([map()]) -> [map()].

discovery_key(Realm, Name)

discovery_key_org(Realm, Org, Name)

handle_call(Msg, From, State)

handle_cast(Msg, S)

handle_info(Other, State)

has_handler(_)

-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).

init(_)

list()

list_org_capabilities(Org)

-spec list_org_capabilities(binary()) -> {ok, [map()]}.

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.

lookup(CapName)

-spec lookup(binary()) -> {ok, [map()]}.

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.

matches_org_pattern(Pattern, Uri)

-spec matches_org_pattern([binary()], binary()) -> boolean().

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.

org_capability_pattern(Realm, Org)

org_procedure(Org, Name)

-spec org_procedure(binary(), binary()) -> binary().

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.

org_scoped_full_or_any(OrgScoped, Pool, Realm, Org, CapName)

org_scoped_or_any(OrgScoped, Pool, Realm, CapName)

procedure_uri(Realm, Org, Name)

-spec procedure_uri(binary(), binary(), binary() | map()) -> binary().

publish()

register(Caps)

resolve_org_capabilities(Pool, Realm, Org)

-spec resolve_org_capabilities(pid(), binary(), binary()) -> [map()].

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.

reuse_sup_opts(Sup)

-spec reuse_sup_opts(pid() | undefined) -> map().

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.

start_link()

station_url(Host, Port)

-spec station_url(binary(), 1..65535) -> binary().

terminate(_, _)