Skip to content

agent: fix NoInputNoOutput agents rejecting all pairing requests#190

Open
abezukor wants to merge 1 commit into
bluez:masterfrom
MaticianInc:agent-noinputnooutput
Open

agent: fix NoInputNoOutput agents rejecting all pairing requests#190
abezukor wants to merge 1 commit into
bluez:masterfrom
MaticianInc:agent-noinputnooutput

Conversation

@abezukor

@abezukor abezukor commented Jun 11, 2026

Copy link
Copy Markdown

Problem

It is currently impossible to register a working NoInputNoOutput agent, because the capability string passed to RegisterAgent is derived automatically from which handler fields are set:

  • All handlers None → capability is NoInputNoOutput, but BlueZ still calls RequestAuthorization for incoming just-works pairing (and AuthorizeService for service connections). Since the handler is None, bluer replies with org.bluez.Error.Rejected — so every pairing attempt fails.
  • Setting request_authorization to accept → the derivation flips the capability to DisplayYesNo, so MITM-capable centrals (iOS in particular sets the MITM flag) negotiate numeric comparison instead of just-works. The phone then shows a "confirm this code" dialog for a code that a headless device cannot display anywhere.

This also contradicts the existing Agent documentation, which states:

Setting all handlers to [None] (the default) will result in a NoInputNoOutput handler that accepts all requests.

The all-None agent rejected all requests instead, so the documented behavior never worked.

Use case

A headless device (no display, no input) exposing a GATT service with encrypt-read/encrypt-write characteristics. Phones must be able to pair via just-works with a single consent tap and no passkey dialog. This requires advertising NoInputNoOutput and accepting RequestAuthorization/AuthorizeService.

Fix

  1. Exclude request_authorization and authorize_service from the capability derivation. Per the BlueZ agent API, RequestAuthorization is called "to request the user to authorize an incoming pairing attempt which would in other circumstances trigger the just-works model" — it is a local authorization policy decision, invoked regardless of the published capability, not an input/output capability. The same applies to AuthorizeService. Only request_confirmation now implies yes/no capability. With this change, an agent with only authorization handlers correctly advertises NoInputNoOutput.

  2. Make the all-None default match the documented behavior. When no handlers are set, default RequestAuthorization and AuthorizeService handlers that accept all requests are installed at registration, so the default Agent is the documented "NoInputNoOutput handler that accepts all requests". Code-input requests (RequestPinCode, RequestPasskey, etc.) still reject — BlueZ does not send them to a NoInputNoOutput agent.

Behavior changes

  • An agent with only request_authorization/authorize_service set now publishes NoInputNoOutput instead of DisplayYesNo. Such configurations were effectively broken before, since they had no request_confirmation to satisfy numeric comparison.
  • An all-None agent now accepts incoming just-works pairing instead of rejecting it, matching the existing documentation.

cargo build and cargo clippy --all-features pass with no new warnings.

🤖 Generated with Claude Code - I (a Human) did manually verify the issue and audit the code.

The Agent documentation states:

    Setting all handlers to [None] (the default) will result in a
    `NoInputNoOutput` handler that accepts all requests.

This did not work. An all-None Agent published the NoInputNoOutput
capability, but rejected the RequestAuthorization call that BlueZ
makes for incoming just-works pairing, so every pairing attempt
failed. This change makes the code match the documented behavior.

It was also impossible to fix from user code: setting
request_authorization to accept the pairing changed the published
capability to DisplayYesNo, causing MITM-capable peers (e.g. iOS)
to negotiate numeric comparison instead of just-works.

Fix both issues by:

  - Excluding request_authorization and authorize_service from the
    capability derivation, since both are local authorization policy
    decisions that BlueZ invokes regardless of the published input
    and output capabilities. Only request_confirmation now implies
    yes/no capability.

  - Installing default RequestAuthorization and AuthorizeService
    handlers that accept all requests when no handlers are set, so
    an all-None Agent behaves as documented.

Note for existing code: an agent with only request_authorization or
authorize_service set now publishes NoInputNoOutput instead of
DisplayYesNo, and an all-None agent now accepts incoming just-works
pairing instead of rejecting it, as the documentation always stated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant