agent-orchestrator/backend
Prasad Ware 073419ce68
feat(mobile): Connect Mobile: password-authenticated LAN bridge (#2533)
* docs(mobile): capture Connect Mobile design (CONTEXT, ADR, plan) + scope loopback rule

(cherry picked from commit 0d3642ec4806faf6d24bbddb00220819d362723b)

* feat(mobile): mobilebridge config store + rotating password

(cherry picked from commit f2617934cac9f2d681e522c92a380fcc60e8a90b)

* feat(mobile): autopick private LAN IPv4

(cherry picked from commit e030474bceff8322cf2a7e814e0a560db1cd8c54)

* feat(mobile): bearer auth middleware with per-source lockout

(cherry picked from commit 461d3c22be4441e6405286979f3e7b371cb171ae)

* feat(mobile): test lockout is per-source (never global)

(cherry picked from commit 47e7d9770511c863ad5bd724386e8ab774661700)

* feat(mobile): runtime-controlled LAN listener manager

(cherry picked from commit a10fd4b6e460f234107a89c4d0fad92932b40bf0)

* feat(mobile): mobile control endpoints + bridge service

(cherry picked from commit c0bb792f040344a5597fd3190ff035c51a988edf)

* feat(mobile): mount loopback-gated mobile control routes + regen API

(cherry picked from commit 376b5f1b9041392feb03bff65f8ec85353b9f076)

* feat(mobile): wire LAN listener into daemon with restore-on-boot

(cherry picked from commit 1a65adbf058db1606d9f8df4c3c794387e08e7dc)

* fix(mobile): bound LAN listener shutdown ctx; assert restore happy path

(cherry picked from commit a2c051bcff9d1ccf19f1e39d84900d2e69a957aa)

* feat(ui): add shadcn dialog primitive

(cherry picked from commit 27e690005362a34af9c1d5c41313a4076749bdcd)

* feat(mobile): desktop Connect Mobile modal with QR, password, warning

(cherry picked from commit 33df1d0fb86a114ce849eb8bdd2e1d672933d445)

* feat(mobile): send Authorization bearer on REST + mux

(cherry picked from commit b4322a4d2397f07977b8f54fae4fb8a14f949297)

* feat(mobile): QR scan pairing + password popup

(cherry picked from commit 42d8624efc368eec10e2c7dfed2819a69c318956)

* docs(mobile): scope loopback-only rule to loopback listener; document LAN listener

(cherry picked from commit 03fd7ac3fdf86566838b5d19d138e58988c88d6d)

* fix(mobile): block loopback-only control routes on the LAN listener

(cherry picked from commit 0d721a6a4fd448547441631363955aa397f4186d)

* fix(mobile): let the desktop renderer reach the mobile control routes

The mobile control routes were gated by localControlRequest, which rejects any
request carrying an Origin header. The desktop renderer is a browser context and
always sends an Origin, so every enable/disable/status call from the desktop got
a 403. Drop that gate: the loopback listener protects these routes via CORS like
every other API route, and the phone is already blocked from them on the LAN
listener by lanControlBlock (a transport-based 404 that cannot be spoofed).

(cherry picked from commit 0fbddfecd7bd4b89599d941014f29b83e83cdf13)

* feat(mobile): persist the connection password so the desktop can show it

State stored only the password hash, so GET /mobile/status returned an empty
password and the desktop modal could not display it. Persist the 8-char password
in plaintext in ~/.ao/mobile/config.json (0600) and return it from status while
the bridge is enabled; derive the auth hash in memory (HashPassword). Restore on
boot re-derives the hash from the persisted plaintext, so a paired phone keeps
working across restarts with no rotation.

(cherry picked from commit 1ff0c35059f2cb4503407507e3af20f714a0f580)

* fix(mobile): reset the per-source lockout counter when the cooldown expires

Previously the fail counter was only cleared on a successful auth, so after the
60s cooldown the very next failure re-locked immediately for another full minute
— and a client that keeps polling with a bad password would stay locked out
forever. Clear the counter (and prune the entry) once the cooldown elapses, so
each source gets a fresh 5-attempt window.

(cherry picked from commit eeb7bd30a7ff42e00037a1232a8a424d231e000a)

* feat(mobile): move Connect Mobile to the sidebar Settings menu; toggle modal

Connect Mobile is now its own item in the sidebar Settings dropdown (its own
section above Project/Global settings, in both expanded and collapsed menus)
instead of a card on the Global Settings page. The modal is redesigned around a
single 'Enable mobile' toggle: flipping it on starts the bridge and reveals the
pairing details (QR, address, password + copy, warning, regenerate) below the
toggle row; off tears it down. The QR now encodes the password too, so one scan
connects with no typing. Adds a shadcn Switch primitive; removes the old
ConnectMobileButton card.

(cherry picked from commit 36c617d18573efbf8ba3454ee8bd2e3a965e28ed)

* feat(mobile): scan-to-connect and hardened connection UX

- Parse the password from the pairing QR and, on scan, save host/port/password
  and reconnect — one scan connects with no typing.
- 'Save' now behaves like connect: it prompts for the password when empty and
  reports the result, so it can no longer persist a passwordless config silently.
- Reload the server config on screen focus so returning from the scanner repaints
  the fields.
- Stop the background poller on an auth failure (401/429) instead of hammering —
  a bad password otherwise kept the daemon's lockout armed forever.
- Clear a stale error banner once the connection goes live.

(cherry picked from commit 7cf5db29bd23c4fa9af9c1b3a68dbac399c5f10d)

* chore(mobile): drop fork-only shared API schema (upstream generates to frontend/src/api)

* chore(mobile): sync lockfiles for qrcode.react (desktop) and expo-camera (mobile)

* fix(mobile): store connection password in SecureStore, not AsyncStorage

The Bearer secret authorizes terminal input, spawn/kill and PR actions, but
was persisted inside the AsyncStorage config blob (unencrypted app sandbox).
Split it into expo-secure-store (iOS Keychain / Android Keystore) and keep
AsyncStorage for the non-secret host/port/TLS config. loadConfig migrates any
legacy password out of the blob on first read.

* fix(mobile): roll back LAN listener when enable fails to persist

enableWithPassword armed the new hash and opened the 0.0.0.0 listener before
saving durable state. If mobilebridge.Save failed, the endpoint returned 500
but the LAN listener stayed live with the new password while persisted state
and the UI still reported the bridge as disabled — a network-exposed control
surface no state tracked.

Snapshot the prior hash and running state, then on the error paths undo the
in-memory side effects: restore the hash on a Start failure, and on a Save
failure stop a freshly-started listener (a rotate on an already-running
listener keeps serving on the prior hash). Add PasswordHash() to LANController
for the snapshot and a regression test for the Save-failure rollback.

* fix(mobile): satisfy gofmt, golangci-lint, and Prettier CI

- gofmt: auth.go, auth_test.go, netiface_test.go
- golangci-lint: add doc comments on exported mobile-bridge / LAN-listener
  symbols (revive), check the LAN listener TCPAddr assertion and drop
  os.Remove/tmp.Close returns (errcheck), and justify the deliberate
  0.0.0.0 bind (G102) and password persistence (G117) with nolint
- Prettier: format the 6 changed docs/frontend/mobile files
2026-07-10 02:16:27 +05:30
..
cmd refactor: move project manager to service layer (#68) 2026-06-02 01:26:48 +05:30
internal feat(mobile): Connect Mobile: password-authenticated LAN bridge (#2533) 2026-07-10 02:16:27 +05:30
.golangci.yml feat(agents): agent platform — registry, activity hooks, harness allowlist (#119) 2026-06-07 00:52:40 +05:30
go.mod feat(preview): agent driven browser previews + Markdown rendering (#2455) 2026-07-09 03:55:06 +05:30
go.sum feat(preview): agent driven browser previews + Markdown rendering (#2455) 2026-07-09 03:55:06 +05:30
main.go feat(backend): add cobra cli foundation 2026-06-01 01:44:43 +05:30
sqlc.yaml feat(review): configurable AO code review backend (V1) (#192) (#197) 2026-06-15 01:17:17 +05:30