Bluez.Agent (bluez v0.1.0)

Copy Markdown View Source

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

agent_path()

@spec agent_path() :: String.t()

Object path our Agent1 implementation is exported at.

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

decide(member, body, expected)

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

expect_pairing(device_path)

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

pairing_done(device_path)

@spec pairing_done(String.t()) :: :ok

Remove device_path from the in-flight pairing set.

start_link(opts \\ [])