Skip to content

feat(mobile): add Personal pairing and Android transport foundation#1538

Merged
SingleMai merged 16 commits into
mainfrom
feat/mobile-agentgui-personal-transport
Jul 23, 2026
Merged

feat(mobile): add Personal pairing and Android transport foundation#1538
SingleMai merged 16 commits into
mainfrom
feat/mobile-agentgui-personal-transport

Conversation

@SingleMai

@SingleMai SingleMai commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add the provisional shared packages/device-link ICE/QUIC transport and Android gomobile boundary
  • connect Personal tuttid to the deployed same-account device registration, QR pairing, and paired-device rendezvous control plane
  • add Desktop pairing management with QR plus copyable manual pairing code
  • gate Desktop mobile remote access settings behind a default-off Developer switch so account login can ship independently
  • add the Android-first React Native app with system-browser GitHub login plus email fallback, device/workspace/session navigation, conversation rendering, composer, stop, and structured Interaction answers
  • reuse generated Agent DTOs and the pure AgentGUI interaction-answer model instead of introducing parallel entities
  • harden request deadlines, frame limits, revoke/auth cleanup, foreground/background closure, stable submit identity, and native generation fencing

Scope

This PR validates the Tutti Personal direct P2P lane first. It intentionally does not upgrade TSH, add Relay fallback, push notifications, long-running background operation, or the full Desktop side-panel surface.

Desktop account login remains visible independently. The phone pairing and remote-access block in Account settings is hidden by default and appears only when Developer settings enables the durable mobile.remoteAccessSettings feature flag. The flag controls presentation only; it does not create another login state, device entity, or protocol path.

The Mobile UI follows one consistent hierarchy:

  1. same-account login
  2. paired device list
  3. workspace selection (single workspace enters automatically)
  4. conversation-first workspace with a session drawer

The mobile client consumes the generated @tutti-os/client-tuttid-ts contracts. Session, Turn, Message, Interaction, and submit identity remain Host-owned facts; the mobile app does not introduce a second Agent entity model.

Security and lifecycle

  • account cookies and Ed25519 private keys never cross the daemon/Mobile native security boundaries
  • Android creates the Ed25519 signing key inside Android Keystore and exports only the raw public key and signatures
  • QR challenge secrets remain ephemeral
  • paired-device requests use mutually fingerprint-pinned QUIC and an allowlisted Agent HTTP surface
  • request bodies, response bodies, and framed Base64 envelopes have independent limits
  • auth loss and local revoke cancel active attempts
  • Android closes the link after a 15-second background grace period
  • prepare/connect use an opaque native generation token so canceled work cannot operate on a replacement link
  • ambiguous sends retain the exact agentSessionId and clientSubmitId for safe retry

Android physical-device fixes

Physical-device testing exposed two independent blockers after the initial emulator validation:

  • Google Code Scanner depended on a Play Services dynamic module that was unavailable on the tested domestic Android ROM, so scan returned immediately without an app camera permission request. Mobile now uses the bundled ZXing capture Activity, declares CAMERA, and surfaces denied permission separately.
  • the default Ed25519 generator selected AndroidKeyStore without initialization, and the OEM KeyStore also rejected its own Ed25519 key when reconstructed through PrivateKeyEntry. Mobile now initializes the system Ed25519 curve explicitly, reads the private key and certificate public key separately, validates the exact Ed25519 SPKI prefix, and signs through the Android Keystore operation provider.

Android now reuses the existing desktop-bridge transfer-code protocol for GitHub login. The system browser owns provider credentials; a short-lived loopback bridge returns only the one-time transfer code, and the redeemed session is installed into React Native Android’s native CookieManager before account requests. Pairing errors now remain visible above the footer actions instead of being rendered off-screen inside the device-list scroller. Sanitized native/JS diagnostics identify identity, refresh, and pairing failures without logging credentials, pairing payloads, keys, or Agent content.

The same run also found that changed-aware lane fingerprinting inherited Node's 1 MiB synchronous subprocess output limit. A bounded 64 MiB Git buffer and a 2 MiB regression fixture allow this PR's 1.43 MiB diff to reach its actual validation lanes.

Review

Three focused Codex reviewers covered:

  • Go concurrency, attempt TTL, auth/revoke cleanup, framing, and storage/model boundaries
  • Android native lifecycle, scanner AppState transitions, deadlines, and generation races
  • Mobile Agent semantics, target availability, Interaction identity, polling, and stable submission retry

All reported blockers were resolved. The final targeted reviews reported no remaining blocker for the Personal direct-lane MVP.

Validation

  • pnpm check:changed: 30/30 selected lanes passed
  • pre-push pnpm check:changed -- --push-ready: 33/33 selected lanes passed
  • Desktop suite: 1569 passed, 2 platform-skipped
  • Desktop typecheck, i18n parity (3968 keys), and production build with renderer CSS contracts
  • Android four-ABI app:assembleDebug
  • physical vivo Android/API 36 install and cold start\n- physical-device GitHub system-browser login, loopback callback, transfer-code redemption, native cookie installation, and return to the Mobile device page
  • physical-device Android Keystore identity creation, signing, and deployed control-plane registration
  • first-use Android camera permission Activity and bundled ZXing CaptureActivity
  • manual-code form expansion and persistent visible invalid-code feedback
  • read-only production verification that the existing Android user_device row contains ed25519, a 32-byte public key, identity revision 1, registered and not revoked
  • go test ./... and go test -race ./authenticated ./mobile in packages/device-link
  • go test ./service/mobileremote and go test -race ./service/mobileremote in services/tuttid
  • AgentGUI typecheck and suite, Mobile typecheck/Jest, DeviceLink Android contract, Metro production bundle, and repository boundary/generated-contract gates through the changed-aware runs

Acceptance remaining

The Android app is installed on the physical device with identity registration, permission, scanner launch, and manual-entry error behavior verified. The remaining Personal MVP acceptance requires fresh external pairing state:

  • enable Show mobile remote access settings under Developer settings
  • generate a fresh Desktop QR/manual pairing code and complete same-account claim/confirm
  • establish physical Android-to-Desktop direct P2P across the intended Wi-Fi/cellular/NAT paths
  • verify workspace/session read, send, stop, and Interaction submission on the real account
  • verify revoke and 15-second background disconnect behavior

Relay fallback, event streaming, automatic foreground reconnect, and TSH cutover remain follow-up milestones rather than blockers for this direct-lane PR.

Documentation impact

Updated the AgentGUI ownership/data-flow architecture, DeviceLink responsibility, full mobile design, beginner Android development/validation guide, and changed-aware static-analysis convention.

@SingleMai
SingleMai force-pushed the feat/mobile-agentgui-personal-transport branch from 80b43f9 to 0e385bc Compare July 23, 2026 03:49
Signed-off-by: liying <422107224@qq.com>
Signed-off-by: liying <422107224@qq.com>
@SingleMai
SingleMai force-pushed the feat/mobile-agentgui-personal-transport branch from 0e385bc to f0b8244 Compare July 23, 2026 06:53
@SingleMai SingleMai changed the title feat(device-link): add Personal-first Android transport foundation feat(mobile): add Personal pairing and Android transport foundation Jul 23, 2026
SingleMai added 14 commits July 23, 2026 15:17
Signed-off-by: liying <422107224@qq.com>
Signed-off-by: liying <422107224@qq.com>
Signed-off-by: liying <422107224@qq.com>
Signed-off-by: liying <422107224@qq.com>
…personal-transport

Signed-off-by: liying <422107224@qq.com>

# Conflicts:
#	services/tuttid/go.mod
#	services/tuttid/wiring_daemon_api.go
Signed-off-by: liying <422107224@qq.com>
Signed-off-by: liying <422107224@qq.com>
Signed-off-by: liying <422107224@qq.com>
Signed-off-by: liying <422107224@qq.com>
Signed-off-by: liying <422107224@qq.com>
Signed-off-by: liying <422107224@qq.com>
Signed-off-by: liying <422107224@qq.com>
Signed-off-by: liying <422107224@qq.com>
Signed-off-by: liying <422107224@qq.com>
@SingleMai
SingleMai marked this pull request as ready for review July 23, 2026 18:12
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@SingleMai
SingleMai merged commit dcac9de into main Jul 23, 2026
11 of 12 checks passed

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

Open in Devin Review

Comment on lines +610 to +615
if (cors) {
writer.write("Access-Control-Allow-Origin: *\r\n")
writer.write("Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n")
writer.write("Access-Control-Allow-Headers: Content-Type\r\n")
writer.write("Access-Control-Allow-Private-Network: true\r\n")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 Loopback auth bridge returns wildcard CORS with private-network access enabled

The loopback GitHub-login bridge writes Access-Control-Allow-Origin: * together with Access-Control-Allow-Private-Network: true and Access-Control-Allow-Methods: GET, POST, OPTIONS on every JSON/CORS response (apps/mobile/android/app/src/main/java/dev/tutti/mobile/MobileBrowserAuthBridge.kt:610-615). While the bridge only listens on 127.0.0.1 and the sensitive /oauth/health response is gated behind the secret attempt_id+token pair, the wildcard origin plus private-network allowance lets any web origin loaded in the system browser issue cross-origin requests to the loopback server during the short login window. The OPTIONS handler restricts origins to the auth origin, but the actual JSON responses do not echo that restriction, so the CORS grant is broader than the preflight check implies.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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