diff --git a/AGENTS.md b/AGENTS.md index 39859d5..a5bf871 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -125,8 +125,17 @@ MobileClaw supports three backend modes, selectable in the setup dialog: MobileClaw connects to OpenClaw's gateway WebSocket. Protocol frames: 1. **Server sends** `event:connect.challenge` with nonce -2. **Client responds** with `req:connect` including auth token, capabilities -3. **Server responds** with `res:hello-ok` including server info, session snapshot -4. **Client requests** `req:chat.history` to load message history -5. **Messages flow** via `event:chat` (delta/final/aborted/error) and `event:agent` (content/tool/reasoning/lifecycle streams) -6. **Client sends** `req:chat.send` with user messages +2. **Client responds** with `req:connect` using protocol `3`, a v3 device-auth signature, and either a shared auth token or a cached `auth.deviceToken` +3. **Server responds** with `res:hello-ok` including server info, session snapshot, `features`, `policy`, and optional auth state +4. **Client persists** `hello-ok.auth.deviceToken` per normalized gateway URL and reuses it on later connects when the shared token hash still matches +5. **Client requests** `req:chat.history` to load message history +6. **Messages flow** via `event:chat` (delta/final/aborted/error) and `event:agent` (content/tool/reasoning/lifecycle streams) +7. **Client optionally subscribes** to `sessions.subscribe` and `sessions.messages.subscribe` when advertised, but still reconciles transcript state from `chat.history` +8. **Client sends** `req:chat.send` with user messages + +### Protocol Notes + +- Keep `lib/deviceIdentity.ts` and `ios/MobileClaw/Networking/DeviceIdentity.swift` aligned. They must sign the same v3 auth payload and normalize `platform` / `deviceFamily` the same way. +- Device-token cache is implemented in `lib/gatewayAuth.ts` under `mc-openclaw-device-auth-v1`. Native storage goes through `lib/nativeBridge.ts` and `ios/MobileClaw/Bridge/WebViewBridge.swift`. +- `session.message`, `session.tool`, and `sessions.changed` are used as invalidation signals. They should not directly mutate the rendered transcript. +- `AUTH_TOKEN_MISMATCH` may trigger one reconnect with cached device approval. `DEVICE_AUTH_*` errors should surface immediately. diff --git a/CHANGELOG.md b/CHANGELOG.md index 923baaa..93a564f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to MobileClaw are documented in this file. +## 2026-04-07 + +### Added +- OpenClaw gateway-auth cache (`mc-openclaw-device-auth-v1`) for persisted `hello-ok.auth.deviceToken` reuse across reconnects on web and in the native iOS shell +- Native bridge support for gateway-auth storage (`gatewayAuth:get`, `gatewayAuth:set`, `gatewayAuth:delete`) backed by Keychain on iOS +- Runtime tests covering hello-ok auth persistence, cached-device-token reuse, bounded `AUTH_TOKEN_MISMATCH` retry, and session invalidation events + +### Changed +- OpenClaw handshake now signs the v3 device-auth payload with `platform` and `deviceFamily` on both web and iOS; MobileClaw no longer silently downgrades when device signing fails +- `types/chat.ts` now models `hello-ok` auth/features/policy payloads plus newer gateway method and event names without hard-coding the full surface area +- `useOpenClawRuntime.ts` now capability-gates `sessions.subscribe` / `sessions.messages.subscribe`, treats `session.message` / `session.tool` / `sessions.changed` as invalidation signals, and surfaces gateway shutdown/auth guidance more explicitly +- Session switcher now marks session lists dirty and refreshes immediately when session-change events arrive + ## 2026-03-03 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index a3a65ef..7e8f41a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -52,11 +52,20 @@ MobileClaw has a native iOS app that wraps the webapp in a WKWebView. See [`ios/ MobileClaw connects to OpenClaw's gateway WebSocket. Protocol frames: 1. **Server sends** `event:connect.challenge` with nonce -2. **Client responds** with `req:connect` including auth token, capabilities -3. **Server responds** with `res:hello-ok` including server info, session snapshot -4. **Client requests** `req:chat.history` to load message history -5. **Messages flow** via `event:chat` (delta/final/aborted/error) and `event:agent` (content/tool/reasoning/lifecycle streams) -6. **Client sends** `req:chat.send` with user messages +2. **Client responds** with `req:connect` using protocol `3`, a v3 device-auth signature, and either a shared auth token or a cached `auth.deviceToken` +3. **Server responds** with `res:hello-ok` including server info, session snapshot, `features`, `policy`, and optional `auth.deviceToken` state +4. **Client persists** `hello-ok.auth.deviceToken` per normalized gateway URL and reuses it on later connects when the current shared token hash still matches +5. **Client requests** `req:chat.history` to load message history +6. **Messages flow** via `event:chat` (delta/final/aborted/error) and `event:agent` (content/tool/reasoning/lifecycle streams) +7. **Client optionally subscribes** to `sessions.subscribe` and `sessions.messages.subscribe` when the gateway advertises those methods, but still uses `chat.history` as the source of truth for transcript reconciliation +8. **Client sends** `req:chat.send` with user messages + +### Protocol Notes + +- `lib/deviceIdentity.ts` and `ios/MobileClaw/Networking/DeviceIdentity.swift` must stay aligned. Both build the same v3 auth payload: `v3|deviceId|clientId|clientMode|role|scopes|signedAtMs|token|nonce|platform|deviceFamily`. +- Device-token cache lives in `lib/gatewayAuth.ts` under `mc-openclaw-device-auth-v1`. Web uses `localStorage`; native uses the bridge-backed Keychain store. +- MobileClaw treats `session.message`, `session.tool`, and `sessions.changed` as invalidation signals only. The realtime UI still renders from `chat` and `agent` events. +- On `AUTH_TOKEN_MISMATCH`, MobileClaw retries once with a cached device token if the gateway says `canRetryWithDeviceToken`. `DEVICE_AUTH_*` errors are treated as client bugs and are surfaced directly. **ALWAYS use cicada-mcp tools for Elixir and Python code searches. NEVER use Grep/Find for these tasks.** diff --git a/CONCEPTS.md b/CONCEPTS.md index 7a1de0c..7068106 100644 --- a/CONCEPTS.md +++ b/CONCEPTS.md @@ -24,6 +24,7 @@ - **Hook**: `hooks/useAppMode.ts` — provides `isNative`, `isNativeRef` - **Activation**: URL param `?native=true` or `window.__nativeMode === true` - **Effect**: Adds `"native"` class to body, hides chrome, transparent background, uses `lib/nativeBridge.ts` for Swift ↔ WebView communication +- **Auth/storage bridge**: Native handles device signing and gateway-auth cache persistence through `identity:sign`, `gatewayAuth:get`, `gatewayAuth:set`, and `gatewayAuth:delete` - **Bootstrap**: Native bridge handler registered in `hooks/chat/useModeBootstrap.ts` --- @@ -142,7 +143,9 @@ All blocks are rendered inside `MessageRow.tsx`. Block data is represented as `C ### OpenClaw (Default) - **Runtime**: `hooks/chat/useOpenClawRuntime.ts` - **Protocol**: WebSocket (`lib/useWebSocket.ts`) -- **Flow**: Challenge → connect → hello-ok → history → event streams (`event:chat`, `event:agent`) +- **Flow**: Challenge → signed protocol-3 connect → hello-ok → history → event streams (`event:chat`, `event:agent`) +- **Auth**: `lib/deviceIdentity.ts` signs v3 connect challenges; `lib/gatewayAuth.ts` persists per-gateway device tokens and handles one-shot retry on `AUTH_TOKEN_MISMATCH` +- **Session freshness**: Uses `sessions.subscribe` / `sessions.messages.subscribe` when the gateway advertises them, but only as invalidation signals for `sessions.list` and `chat.history` - **Features**: Session switching, model selection, slash commands, full agent protocol ### LM Studio diff --git a/app/globals.css b/app/globals.css index 4b616a9..ccac33b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -128,6 +128,16 @@ --color-sidebar-ring: var(--sidebar-ring); } +[data-mobileclaw-embedded] { + --primary: oklch(0.313 0 0); + --primary-foreground: oklch(0.985 0 0); +} + +.dark [data-mobileclaw-embedded] { + --primary: oklch(0.258 0 0); + --primary-foreground: oklch(0.911 0 0); +} + @theme { /* Font-size scale — boosted so text-sm = 16px body baseline */ --text-2xs: 0.75rem; /* 12px */ @@ -148,6 +158,9 @@ * { @apply border-border outline-ring/50; } + html { + @apply bg-background text-foreground; + } body { @apply bg-background text-foreground; overflow: hidden; diff --git a/app/layout.tsx b/app/layout.tsx index b9f52c8..eec0ce5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -32,9 +32,15 @@ export const metadata: Metadata = { // Also registers the service worker for PWA support const headScript = ` (function() { + var isDev = ${JSON.stringify(process.env.NODE_ENV !== 'production')}; var params = new URLSearchParams(location.search); var detached = params.has('detached'); var detachedMode = detached ? params.get('mode') : null; + var host = location.hostname; + var isIpV4 = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/.test(host); + var isLocalHost = host === 'localhost' || host === '127.0.0.1' || host === '0.0.0.0' || host.endsWith('.local'); + var isPrivateLanIp = /^10\\./.test(host) || /^192\\.168\\./.test(host) || /^172\\.(1[6-9]|2\\d|3[0-1])\\./.test(host); + var shouldDisableServiceWorker = isDev || isLocalHost || (isIpV4 && isPrivateLanIp); try { if (detachedMode === 'dark') { document.documentElement.classList.add('dark'); @@ -53,8 +59,19 @@ const headScript = ` if (detached) { document.documentElement.classList.add('detached-loading'); } - if ('serviceWorker' in navigator && location.hostname !== 'localhost' && !window.__nativeMode) { + if ('serviceWorker' in navigator && !window.__nativeMode) { window.addEventListener('load', function() { + if (shouldDisableServiceWorker) { + navigator.serviceWorker.getRegistrations().then(function(registrations) { + registrations.forEach(function(registration) { registration.unregister(); }); + }); + if (window.caches && caches.keys) { + caches.keys().then(function(keys) { + keys.forEach(function(key) { caches.delete(key); }); + }); + } + return; + } navigator.serviceWorker.register('/sw.js'); }); } @@ -67,7 +84,7 @@ export default function RootLayout({ children: React.ReactNode }>) { return ( - +