Skip to content

Accept device-initiated Bluetooth Just Works pairing - #12897

Open
Chessing234 wants to merge 3 commits into
omacom:quattrofrom
Chessing234:fix/10771-bt-agent-authorization
Open

Chessing234 wants to merge 3 commits into
omacom:quattrofrom
Chessing234:fix/10771-bt-agent-authorization

Conversation

@Chessing234

@Chessing234 Chessing234 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Replace bluez-tools bt-agent with omarchy-bluetooth-agent, which returns success from RequestAuthorization / RequestConfirmation / AuthorizeService so Xbox controllers and BLE keyboards can bond.
  • Trust a device only after BlueZ reports Bonded: yes, and route trusted-but-unbonded devices back through pair in the panel.
  • Keep the adapter Pairable only while the Bluetooth panel is open so Just Works auto-accept is not a standing window.

Fixes #10771.
Fixes #7889.

Test plan

  • bash test/shell.d/bluetooth-test.sh
  • bash test/shell.d/systemd-test.sh
  • Pair an Xbox Wireless Controller from the Bluetooth panel; confirm Bonded: yes and HID input
  • Pair a BLE keyboard; confirm it stays bonded across reconnects

@llstrk

llstrk commented Sep 22, 2026

Copy link
Copy Markdown

Two actionable issues remain at 898dad1a: the claimed pairing-window safeguard is absent, and the replacement agent is not activated in already-running sessions.

Verified: On a private D-Bus with synthetic BlueZ objects and unattended stdin, the old agent rejected RequestAuthorization; the new agent accepted it. The real device helper now withholds trust until a bond is observed, and the focused Bluetooth/systemd suites pass. Physical controller and keyboard pairing remain untested.

Auto-accept is not restricted to the claimed pairing window

omarchy-bluetooth-agent:65-74 accepts RequestAuthorization, RequestConfirmation, and AuthorizeService unconditionally. Its safety comment says the adapter is pairable only while the panel is open, but the panel controls discovery, not Pairable; no implementation of that pairing-window guard exists in the pinned repository.

Panel closes          -> stops discovery, does not clear Pairable
Authorization arrives -> new agent returns success, no user-intent check

This is a source-derived flow, with the success replies also tested over private D-Bus without a panel running. BlueZ documents Pairable=true and PairableTimeout=0 defaults; stopping discovery is not a pairing-policy change.

Impact: Incoming requests that reach the agent can be authorized outside a user pairing action. The inaccurate comment existed previously, but replacing rejection with success makes the missing guard consequential. No over-the-air pairing or exploit was demonstrated.

Suggested change: Establish an explicit, bounded pairing-consent policy, including closed/startup/error states. If relying on Pairable, actually manage that property rather than discovery alone. Give AuthorizeService a separate device-authorization policy that preserves legitimate known-device reconnects: BlueZ specifies that Pairable affects incoming pairing requests, not all service authorization.

Existing sessions keep the old rejecting agent

The unit change updates ExecStart, but adds no activation migration. The normal update path refreshes the shell, not bt-agent.service; the generic user-unit package hook reloads/reexecutes its manager without replacing the running agent.

Upgrade without agent restart/reboot
  new unit + new helper + refreshed panel
  old /usr/bin/bt-agent process still running

Impact: Users already running the old agent still encounter its rejection when retesting the fix. This is an upgrade-activation gap, not a new pairing regression; a fresh agent start uses the replacement.

Suggested change: Add a focused migration to reload the user manager and restart the active agent, leaving intentionally inactive services alone. 1785608166.sh provides a related user-service activation pattern.


Review information

Test scope: Pinned source, real agents/clients against synthetic BlueZ on private D-Bus, bond-state controls, and focused repository suites. No physical Bluetooth/HID, rendered panel, or actual package-upgrade test; upgrade behavior is source-derived.

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

Automated AI review: Astra initial inspection, independent Opus 5 and Astra technical reviews, followed by verification of claims against targeted evidence and technical adjudication.

The panel raises Pairable only while open (and clears BlueZ's default at
load), the agent rejects pairing outside that window, and a migration
restarts an already-active bt-agent so upgrades leave the old rejector.
@Chessing234

Copy link
Copy Markdown
Contributor Author

addressed both points:

  • panel now owns Pairable (on while open, off at load/close). agent only accepts RequestAuthorization/RequestConfirmation when an adapter is Pairable. AuthorizeService stays open so bonded devices can still reconnect.
  • migration 1790385200.sh daemon-reloads and restarts an already-active bt-agent.service so upgrades don't keep the old rejector.

@Chessing234

Copy link
Copy Markdown
Contributor Author

also covers #7889 — pairable only while the panel is open

@llstrk

llstrk commented Sep 27, 2026

Copy link
Copy Markdown

Automated AI review

Community review: Independent automated community review, unaffiliated with the Omarchy team, intended to help prepare PRs for their review.

Follow-up to the earlier review and the author's reply, checked at 87c21cf5.

Resolved: "Existing sessions keep the old rejecting agent". migrations/1790385200.sh reloads the user manager and restarts bt-agent.service only when it is active.

Partly addressed: "Auto-accept is not restricted to the claimed pairing window". The agent now rejects pairing requests when no adapter is Pairable, and the panel clears Pairable on the default adapter when it loads and closes. Pairable can still be on with every panel closed in three states, described below. Turning Pairable off also has a new side effect: pairing started outside the panel no longer bonds.

Verified: the agents at both heads, called as bluetoothd would call them, against a synthetic BlueZ on a private D-Bus (device under hci0):

Adapter state Previous head 898dad1a Current head 87c21cf5
No adapter Pairable all pairing requests accepted all rejected; AuthorizeService accepted
hci0 Pairable accepted accepted
Only hci1 Pairable accepted accepted
GetManagedObjects fails accepted all rejected (fails closed)

"Pairing requests" means RequestAuthorization, RequestConfirmation, RequestPinCode and RequestPasskey. With a stub systemctl, run under bash -euo pipefail like omarchy-migrate, the migration:

  • exits 0 without a user manager;
  • exits 1 (so it is retried) when the manager socket exists but does not respond, or when the reload, the state check or the restart fails;
  • leaves an inactive, activating or failed unit alone;
  • restarts an active unit after daemon-reload.

Pairable can still be on with every panel closed

In Omarchy, only the panel clears Pairable, and only on its own load, open and close events (Panel.qml:442-449, :586-609). It does not react when Pairable changes. BlueZ 5.87 sets Pairable on again by itself on every adapter whenever an agent registers as the default agent, because AlwaysPairable defaults to false and Omarchy does not change it (src/agent.c:139-151, src/adapter.c:9152-9167). The agent accepts if any adapter is Pairable (omarchy-bluetooth-agent:63-89).

1. Agent restarts after the shell has loaded
   panel load clears Pairable -> bt-agent restarts -> BlueZ sets Pairable on -> no panel handler
   Triggers: this PR's migration run from the login notification or a manual omarchy-migrate,
   Restart=on-failure, a manual restart. (omarchy update restarts the shell afterwards, which
   clears it again, unless that restart is refused, for example while the session is locked.)
2. Two adapters
   BlueZ raises Pairable on every adapter -> panel clears only Bluetooth.defaultAdapter
   -> hci1 stays Pairable -> agent accepts requests for devices on hci0 as well (table above)
3. Bluetooth widget not in the bar (removed from the layout, plugin disabled, replacement bar)
   nothing ever clears Pairable for the session

The agent's decision covers LE Just Works and PIN/passkey pairing. For Classic devices without a stored key, the kernel accepts SSP bonding by itself whenever Pairable is on, as it did at the base. Source reading of the kernel and bluetoothd also limits who can reach these requests: links the host makes or accepts (known devices, devices the user connects to), not arbitrary nearby devices. No over-the-air test was done.

Impact: in these states, pairing is not limited to the time a panel is open. That is the guarantee the agent header and unit comment describe and the new #7889 claim relies on. The #7889 reproduction on the default controller (open the panel, close it, bluetoothctl show) should now report Pairable: no. In state 3, device-initiated LE Just Works changes from rejected at the base to accepted for the whole session.

Suggested change: let the always-running agent own the closed default. After RegisterAgent/RequestDefaultAgent and whenever an Adapter1 appears, set Pairable false on every adapter. Gate each request on the Pairable of the requesting device's own adapter (its object path or Device1.Adapter), not on any adapter. The panel then only raises Pairable while open. A finite PairableTimeout would bound any remaining leak, though the panel would then need to refresh it while open.

Pairing started outside the panel no longer bonds

When the adapter is not Pairable, the kernel removes bonding even from pairing the host starts: Classic in net/bluetooth/hci_event.c:5339-5341, LE in smp.c:635-641. bluetoothd's Pair() does not check Pairable. At the base, Pairable stayed on while bt-agent was registered. Now it is off whenever no panel is open.

Pairable off (panel closed)
  bluetoothctl pair / omarchy-bluetooth-device pair from a terminal
  -> pairing completes without bonding -> wait_for_bond fails -> no trust
  -> the pairing is dropped on disconnect

The panel's own Pair action is affected the same way if the panel is closed before the device bonds. omarchy-bluetooth-device pair can run for about 55 s of timeouts or longer, and its comment notes that some peripherals bond only during connect. #11198 describes this failure mode.

Impact: pairing from a terminal or another tool with no panel open, which bonded at the base, now leaves a device that has to be paired again after each disconnect. This comes from source reading; it was not run.

Suggested change: raise Pairable for the duration of a host-initiated pair in omarchy-bluetooth-device and restore it afterwards, as #11198 does. That also covers closing the panel mid-pair.

AuthorizeService: the author's reasoning holds for reconnects, because BlueZ asks the agent only about untrusted devices (src/adapter.c:7783-7786). The new agent accepts every device (omarchy-bluetooth-agent:115-118), while the base agent accepted only devices with Paired: yes (bluez-tools src/lib/agent-helper.c:67-79, the commit Arch's bluez-tools 0.2.0-6 builds). With BlueZ and PipeWire defaults, the only extra path found is sixaxis USB cable pairing, which needs physical access and may be wanted. Source only. If the intent is bonded reconnects only, as the code comment states, a Bonded check matches it; it would also keep rejecting cable-paired sixaxis controllers, as the base did.

Optional test improvement: test/shell.d/bluetooth-test.sh still passes with the agent's Pairable check deleted, and separately with syncPairable() changed to never write false. A test that calls the agent over a private D-Bus, like the table above, would catch the first; the second needs a check of the panel's Pairable writes.

Migration file mode: 1790385200.sh is committed as 100755; agents/skills/migrations.md:123-124 requires 0644. Harmless at runtime.

Related open PRs:

  • #9965 adds a different agent (it forwards pairing prompts to the panel) at the same bin/omarchy-bluetooth-agent path and changes the same unit.
  • #11198 raises Pairable in omarchy-bluetooth-device during pair and restores it afterwards.
  • #7880 also changes how trusted-only devices are handled, in omarchy-bluetooth-device, Model.js and Panel.qml.
  • #13258 and #11937 change bt-agent.service.

Review information

Test scope: follow-up limited to the Pairable gate, the panel's Pairable handling, AuthorizeService and the new migration. The current and previous agents ran against a synthetic BlueZ on a private D-Bus; the migration ran with a stub systemctl. The repository Bluetooth and systemd suites pass at the current head. BlueZ 5.87, Linux 7.2, Quickshell 0.3.1 and bluez-tools behaviour is source-derived, including the bonding finding. No real Bluetooth hardware, bluetoothd, over-the-air pairing or rendered panel was tested; the synthetic BlueZ does not reproduce BlueZ's own Pairable changes.

AI process: Opus 5.5 Medium coordination and synthesis, Opus 5.5 Xhigh technical review and final fact check, GPT 6 Sol Xhigh search for related issues, Opus 5.5 Medium editorial check.

Opt out: To stop receiving these reviews, reply to this comment saying so.

@Chessing234

Copy link
Copy Markdown
Contributor Author

pairable ownership (agent vs panel) and host-initiated pair still open. overlaps #12114/#11198 — need a single approach before more churn.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

3 participants