The org.bluez.Agent1 pairing agent backing Bluez.Gatt's pair/1
(Phase 2 of the Bluetooth proxy).
bluetoothd resolves pairing IO through an agent: when Device1.Pair()
is called it uses the agent registered by the calling D-Bus
connection, falling back to the bus-wide default agent. The Pair
call is made on Bluez.Gatt's connection (which registers no agent),
so this process must be the default agent — RegisterAgent alone is
not enough; RequestDefaultAgent is what routes Gatt's pairings here.
Capability is NoInputNoOutput, so bluetoothd negotiates Just Works
with every peripheral — the same IO posture as an ESP32 ESPHome proxy.
Just Works pairing normally completes without any agent callback;
everything below is for the exceptional paths.
Security posture
A default agent answers for all pairing on the adapter, including a
hypothetical inbound attempt (the adapter is never made discoverable,
so none is expected). Authorization-style callbacks
(RequestConfirmation, RequestAuthorization, AuthorizeService)
are therefore confirmed only for device paths with an in-flight
Device1.Pair() we initiated — Bluez.Gatt brackets each Pair call
with expect_pairing/1 / pairing_done/1 — and rejected otherwise.
PIN/passkey callbacks are always rejected: we have no IO to display or
collect one, and under NoInputNoOutput bluetoothd should never send
them.
Like Bluez.Client, this is both a D-Bus client (registration calls)
and a service (bluetoothd calls back into our exported object) on its
own rebus connection — its failure domain is pairing only; scanning
and GATT traffic are untouched. If the Agent is down, Pair() still
proceeds for devices that need no interaction; anything needing the
agent fails cleanly on the BlueZ side.
Summary
Functions
Object path our Agent1 implementation is exported at.
Returns a specification to start this module under a supervisor.
Decide the reply for an inbound org.bluez.Agent1 call. Pure — exposed
for tests.
Mark device_path as having an in-flight Device1.Pair() we initiated,
so authorization callbacks for it are confirmed.
Remove device_path from the in-flight pairing set.
Functions
@spec agent_path() :: String.t()
Object path our Agent1 implementation is exported at.
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec decide(String.t(), list(), %{optional(String.t()) => reference()}) :: :ack | {:reject, String.t()} | :unknown
Decide the reply for an inbound org.bluez.Agent1 call. Pure — exposed
for tests.
Returns :ack (empty success reply), {:reject, error_name}, or
:unknown (not an Agent1 member we implement). expected is the
in-flight pairing map (device_path => expiry ref).
@spec expect_pairing(String.t()) :: :ok
Mark device_path as having an in-flight Device1.Pair() we initiated,
so authorization callbacks for it are confirmed.
Synchronous on purpose: the caller issues Pair() right after, and a
cast could still be in our mailbox when bluetoothd's authorization
callback arrives — rejecting a pairing we initiated. The return is :ok
even when the Agent isn't running (pairing then degrades to whatever
BlueZ can do agent-less rather than crashing the pair Task) or the path
is refused (the pairing fails closed on the BlueZ side).
Expectations expire after 40000 ms as a backstop for a pair
Task that died without calling pairing_done/1.
@spec pairing_done(String.t()) :: :ok
Remove device_path from the in-flight pairing set.