Policy Admission Chain
August 3, 2026 · View on GitHub
HiveMind's admission control is a chain of policy plugins. Every Mycroft
Message and every binary payload a client sends crosses the chain
before reaching the agent bus.
Architecture
PolicyChain(inhivemind_core.policy) holds an ordered list ofPolicyPlugininstances and exposes three hooks:review,review_binary, andobserve.- For each
review, the chain calls every policy in order. A policy returns aVerdict: either an allow (optionally carrying typedMutationobjects) or a deny (with a stablecode, human-readablereason, and structureddata). - The first deny short-circuits the chain. Mutations from earlier allow verdicts are applied to the message before the next policy sees it.
- Always fail-closed. Any exception from a policy or from
Mutation.applyis converted toVerdict.deny("policy_error", ...)with the offending policy/mutation name indata. There is no operator knob to flip this. The bus is unauthenticated, so lenient- on-error handling would be a security risk. is_adminon a client is informational only. The chain runner gives it no special treatment. Policies that want admin-exemption branch onclient.is_adminthemselves.- After a successful emit, the chain calls
observe()on every policy for counters / audit logs.observeexceptions are swallowed.
Authoring a Policy Plugin
Subclass hivemind_plugin_manager.PolicyPlugin and register under the
hivemind.policy entry-point group:
[project.entry-points."hivemind.policy"]
my-quota-policy = "my_pkg.policy:QuotaPolicy"
from hivemind_plugin_manager import PolicyPlugin, Verdict, DenyCodes
class QuotaPolicy(PolicyPlugin):
def review(self, message, client):
if self._over_quota(client):
return Verdict.deny(
"quota_exceeded", "monthly cap reached",
limit=self.config.get("limit", 100),
)
return Verdict.allow()
Use stable, machine-readable code strings. Common codes are exposed
as DenyCodes members. Pick your own for plugin-specific reasons.
A policy can return one or more Mutation objects on an allow verdict
to change the message before downstream policies and the bus see it.
Mutation classes are agent-specific and live with the agent plugin
(e.g. hivemind_ovos_agent_plugin ships AddBlacklistedSkill,
RewriteUtterance, etc.).
Built-in Policies
MessageTypeACLPolicy: always present, non-removable, runs first. It enforces the per-clientallowed_typeswhitelist. An empty whitelist denies everything (deny-by-default). Refreshes the whitelist from the database on every admission sohivemind-core allow-msgtakes effect without a reconnect. Caches the resolved client row on the connection (client.resolve_user) so downstream policies skip a second DB hit. A failed database lookup denies withcode="policy_error"; it never falls back to the whitelist captured when the client connected, because that would keep a revoked grant alive while the database is unreachable. Binary payloads cross the same gate: a client with an empty whitelist is denied binary too. The whitelist holds message types only, so there is no per-bin_typegranularity yet.DefaultSessionPolicy: always present, non-removable, runs second. It denies any message from a non-admin client whosecontext["session"]["session_id"]is the reserveddefault. That id addresses the host's own device-local session, so a peer must not be able to write into it. Admins are exempt.DenyAllPolicy: fail-closed fallback installed whenPolicyChain.from_configraises. Denies every message and binary payload withcode="policy_chain_unavailable".OVOSAgentPolicy(inhivemind-ovos-agent-plugin): optional, OVOS-specific. Enforces skill/intent blacklists, session-id rules, and ships agent-specificMutationclasses.
Operator Configuration
policy:
chain:
- module: my-quota-policy
config:
limit: 100
- module: hivemind-ovos-agent-policy
- module: my-experimental-policy
optional: true
MessageTypeACLPolicy and DefaultSessionPolicy are implicit and
always first. Do not list them.
optional flag
Per-chain-entry optional: true marks the policy as non-load-bearing:
if its review / review_binary raises, the chain logs a warning and
treats the verdict as allow (no mutations, chain continues). The default
is false, so exceptions fail closed with policy_error. The implicit built-in
policies are always mandatory and ignore this flag.
allowed_types is the canonical admission whitelist for a client.
Grant message types with hivemind-core allow-msg <msg_type> <node_id>.
Revoke with blacklist-msg. Empty whitelist ⇒ deny everything.
There is no policy.fail_open knob. The chain is fail-closed by
design: a misbehaving policy denies rather than admits.
Deny-Code Reference
Codes returned by built-in policies and the chain runner:
| Code | Source | Meaning |
|---|---|---|
acl_disallowed_type | MessageTypeACLPolicy | msg_type not in allowed_types |
policy_error | PolicyChain | A policy or mutation raised. Data carries policy, error, and optionally mutation |
policy_chain_unavailable | DenyAllPolicy | Chain construction failed at startup |
session_id_default_forbidden | DefaultSessionPolicy | Client tried to use the reserved default session id |
backend_unavailable | HiveMindListenerProtocol | The message passed admission, but the agent bus is unreachable, so nothing was forwarded. Not a policy decision — retry later |
malformed_payload | HiveMindListenerProtocol | The payload of the message can not be reconstructed, at any nesting level: a QUERY, BROADCAST, PROPAGATE, ESCALATE or CASCADE that does not carry a nested HiveMessage as HIVEMIND-MSG-1 §4 requires, a BUS or SHARED_BUS whose payload is not a bus Message, or a payload that is not a dict. The node catches every Exception the reconstruction raises. Not a policy decision — the frame is unusable and the sender must fix it |
Plugin authors are free to mint their own code values. Reuse a
built-in code only when the semantics match.
CLI
hivemind-core policy list: print the loaded chain (built-in first, then configured plugins).hivemind-core policy test <api_key> <msg_type>: dry-run a fake message through the chain and print the verdict as JSON.
Wire Format: hive.policy.denied
When the chain returns a deny verdict, the client receives a
HiveMessageType.BUS message of type hive.policy.denied. The
payload:
{
"denied_type": "<original msg_type>",
"code": "<DenyCode value>",
"reason": "<human-readable>",
"data": { "...": "policy-specific structured fields" }
}
denied_type: themsg_typeof the message that was rejected.code: a stable, machine-readable identifier. See the Deny-Code Reference above.reason: a human-readable explanation for logs and UIs.data: a free-form structured payload set by the policy (msg_type,allowed,policy,error, and so on, depending on the code).
Clients SHOULD branch on code. reason is informational only.