The exposed-method contract every hub_kernel interface gem builds on: what a hub exposes, who may call it, and calling it in an account's scope. An interface gem, such as a JSON API or an MCP server, depends on this gem alone rather than on all of hub_kernel.
A hub lists the methods outside callers may reach by name, the values each takes, and whether it writes:
module Supplies
extend HubKernel::Exposes
exposes :price_of, takes: %i[item], writes: false
def self.price_of(item:) = "#{item} costs 3"
endThe host sets the permission check, asked with the person, hub:method and the account, and
the account scope, wrapped around every call:
HubKernel::Authz.check = ->(person, action, account) { true }
HubKernel::Context.scope = ->(account, &call) { call.call }An interface gem lists the methods a person may call on an account, and calls one by name:
Supplies.exposures_for(person: person, account: account)
Supplies.call_exposed("price_of", values: { item: "soap" }, person: person, account: account)A call is refused with HubKernel::UnexposedMethodError for a name the hub does not expose,
HubKernel::NotAllowed when the permission check refuses, and HubKernel::MissingArgumentError
for a missing person, account or required value. A hub refuses with HubKernel::Refused and a
reason a person can read. A call made while the permission check or the account scope is unset
raises HubKernel::UnwiredPortError.
Every interface serves every hub that exposes a method, each at its module name in snake
case, unless the host names the hubs to serve, once. A hub listed alone is served at its
module name in snake case, and a hub listed as a one-pair hash is served only at the name
given. Setting the list to nil serves every exposing hub again. An interface gem finds the
hub served at a name, or nil when none is:
HubKernel::Interface.hubs = [ Supplies, { "money" => Billing::Ledger } ]
HubKernel::Interface.find("supplies") # => Supplies
HubKernel::Interface.find("money") # => Billing::Ledger
HubKernel::Interface.served # => { "supplies" => Supplies, "money" => Billing::Ledger }The permission check is still asked about a hub served under a chosen name by its own name,
such as ledger:record_spend.
The host names the methods that give the calling person and account once, here, for an interface gem to read:
HubKernel::Interface.person_method = :hub_person
HubKernel::Interface.account_method = :hub_accountAn interface gem's boot check runs HubKernel::Interface.check!, which raises
HubKernel::Interface::UnservableHubError naming every problem in the served list at once:
an entry that exposes no methods, two entries that answer at the same name, and each
problem in a served hub's exposed list.
An interface gem checks a call's values before calling, and describes a record the call named that does not exist, so every interface gives the same reasons:
HubKernel::Interface::CallReasons.refuse_unlisted_values(Supplies, "price_of", values: values, person: person, account: account)
HubKernel::Interface::CallReasons.missing_record(error) # => "No consumable has the id 7"refuse_unlisted_values does nothing for a call that sends only listed values, and asks the
permission check nothing. For a call that sends any other value, it asks the permission check
once about the called method, raises HubKernel::NotAllowed when it refuses, and otherwise
raises HubKernel::Refused naming the method and every value it is not listed with.
Supplies.allows?("price_of", person: person, account: account) asks the permission check
about that one method. missing_record reads the error's model and id.
Supplies.exposure_problems names each exposed method the hub has no method for, or lists
with values it does not take. HubKernel::Interface::ExposingHubs.list holds every hub that
declares an exposed method.
gem "hub_kernel-interface"hub_kernel releases before 0.19.0 define this contract themselves. Loading hub_kernel-interface
beside one raises HubKernel::Interface::OlderHubKernelError, which names the hub_kernel
release to upgrade to. An app with no hub_kernel, or with hub_kernel 0.19.0 or later, loads it
as usual.
The gem is available as open source under the terms of the MIT License.