diff --git a/samples/CameraAccess/CameraAccess/OpenClaw/OpenClawBridge.swift b/samples/CameraAccess/CameraAccess/OpenClaw/OpenClawBridge.swift index 1f48ac6f..06e30801 100644 --- a/samples/CameraAccess/CameraAccess/OpenClaw/OpenClawBridge.swift +++ b/samples/CameraAccess/CameraAccess/OpenClaw/OpenClawBridge.swift @@ -38,25 +38,58 @@ class OpenClawBridge: ObservableObject { return } connectionState = .checking - guard let url = URL(string: "\(GeminiConfig.openClawHost):\(GeminiConfig.openClawPort)/v1/chat/completions") else { + + // Step 1: Check gateway health endpoint + guard let healthURL = URL(string: "\(GeminiConfig.openClawHost):\(GeminiConfig.openClawPort)/health") else { connectionState = .unreachable("Invalid URL") return } - var request = URLRequest(url: url) - request.httpMethod = "GET" - request.setValue("Bearer \(GeminiConfig.openClawGatewayToken)", forHTTPHeaderField: "Authorization") - request.setValue("glass", forHTTPHeaderField: "x-openclaw-message-channel") + var healthRequest = URLRequest(url: healthURL) + healthRequest.httpMethod = "GET" do { - let (_, response) = try await pingSession.data(for: request) - if let http = response as? HTTPURLResponse, (200...499).contains(http.statusCode) { - connectionState = .connected - NSLog("[OpenClaw] Gateway reachable (HTTP %d)", http.statusCode) - } else { - connectionState = .unreachable("Unexpected response") + let (_, healthResponse) = try await pingSession.data(for: healthRequest) + if let http = healthResponse as? HTTPURLResponse, !(200...299).contains(http.statusCode) { + connectionState = .unreachable("Gateway not running (HTTP \(http.statusCode))") + NSLog("[OpenClaw] Gateway health check failed: HTTP %d", http.statusCode) + return } } catch { - connectionState = .unreachable(error.localizedDescription) + connectionState = .unreachable("Gateway unreachable: \(error.localizedDescription)") NSLog("[OpenClaw] Gateway unreachable: %@", error.localizedDescription) + return + } + + // Step 2: Verify chat completions endpoint is enabled + guard let chatURL = URL(string: "\(GeminiConfig.openClawHost):\(GeminiConfig.openClawPort)/v1/chat/completions") else { + connectionState = .unreachable("Invalid URL") + return + } + var chatRequest = URLRequest(url: chatURL) + chatRequest.httpMethod = "GET" + chatRequest.setValue("Bearer \(GeminiConfig.openClawGatewayToken)", forHTTPHeaderField: "Authorization") + chatRequest.setValue("glass", forHTTPHeaderField: "x-openclaw-message-channel") + do { + let (_, chatResponse) = try await pingSession.data(for: chatRequest) + if let http = chatResponse as? HTTPURLResponse { + switch http.statusCode { + case 200...299, 405: + // 405 Method Not Allowed on GET is expected — endpoint exists and is enabled + connectionState = .connected + NSLog("[OpenClaw] Gateway connected (HTTP %d)", http.statusCode) + case 401, 403: + connectionState = .unreachable("Authentication failed (HTTP \(http.statusCode)) — check your gateway token") + NSLog("[OpenClaw] Auth failed: HTTP %d", http.statusCode) + case 404: + connectionState = .unreachable("chatCompletions endpoint disabled — enable it in openclaw.json") + NSLog("[OpenClaw] Endpoint disabled: HTTP 404. Set gateway.http.endpoints.chatCompletions.enabled = true in ~/.openclaw/openclaw.json") + default: + connectionState = .unreachable("Unexpected response (HTTP \(http.statusCode))") + NSLog("[OpenClaw] Unexpected status: HTTP %d", http.statusCode) + } + } + } catch { + connectionState = .unreachable(error.localizedDescription) + NSLog("[OpenClaw] Chat endpoint check failed: %@", error.localizedDescription) } } @@ -92,6 +125,7 @@ class OpenClawBridge: ObservableObject { request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue(sessionKey, forHTTPHeaderField: "x-openclaw-session-key") request.setValue("glass", forHTTPHeaderField: "x-openclaw-message-channel") + request.setValue("operator.write", forHTTPHeaderField: "x-openclaw-scopes") let body: [String: Any] = [ "model": "openclaw", diff --git a/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/openclaw/OpenClawBridge.kt b/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/openclaw/OpenClawBridge.kt index 4310ca8c..eafa7b0f 100644 --- a/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/openclaw/OpenClawBridge.kt +++ b/samples/CameraAccessAndroid/app/src/main/java/com/meta/wearable/dat/externalsampleapps/cameraaccess/openclaw/OpenClawBridge.kt @@ -51,28 +51,69 @@ class OpenClawBridge { } _connectionState.value = OpenClawConnectionState.Checking - val url = "${GeminiConfig.openClawHost}:${GeminiConfig.openClawPort}/v1/chat/completions" + // Step 1: Check gateway health endpoint + val healthUrl = "${GeminiConfig.openClawHost}:${GeminiConfig.openClawPort}/health" try { - val request = Request.Builder() - .url(url) + val healthRequest = Request.Builder() + .url(healthUrl) + .get() + .build() + + val healthResponse = pingClient.newCall(healthRequest).execute() + val healthCode = healthResponse.code + healthResponse.close() + + if (healthCode !in 200..299) { + _connectionState.value = OpenClawConnectionState.Unreachable("Gateway not running (HTTP $healthCode)") + Log.d(TAG, "Gateway health check failed: HTTP $healthCode") + return@withContext + } + } catch (e: Exception) { + _connectionState.value = OpenClawConnectionState.Unreachable("Gateway unreachable: ${e.message}") + Log.d(TAG, "Gateway unreachable: ${e.message}") + return@withContext + } + + // Step 2: Verify chat completions endpoint is enabled + val chatUrl = "${GeminiConfig.openClawHost}:${GeminiConfig.openClawPort}/v1/chat/completions" + try { + val chatRequest = Request.Builder() + .url(chatUrl) .get() .addHeader("Authorization", "Bearer ${GeminiConfig.openClawGatewayToken}") .addHeader("x-openclaw-message-channel", "glass") .build() - val response = pingClient.newCall(request).execute() - val code = response.code - response.close() - - if (code in 200..499) { - _connectionState.value = OpenClawConnectionState.Connected - Log.d(TAG, "Gateway reachable (HTTP $code)") - } else { - _connectionState.value = OpenClawConnectionState.Unreachable("Unexpected response") + val chatResponse = pingClient.newCall(chatRequest).execute() + val code = chatResponse.code + chatResponse.close() + + when (code) { + in 200..299, 405 -> { + // 405 Method Not Allowed on GET is expected — endpoint exists and is enabled + _connectionState.value = OpenClawConnectionState.Connected + Log.d(TAG, "Gateway connected (HTTP $code)") + } + 401, 403 -> { + _connectionState.value = OpenClawConnectionState.Unreachable( + "Authentication failed (HTTP $code) — check your gateway token" + ) + Log.d(TAG, "Auth failed: HTTP $code") + } + 404 -> { + _connectionState.value = OpenClawConnectionState.Unreachable( + "chatCompletions endpoint disabled — enable it in openclaw.json" + ) + Log.d(TAG, "Endpoint disabled: HTTP 404. Set gateway.http.endpoints.chatCompletions.enabled = true in ~/.openclaw/openclaw.json") + } + else -> { + _connectionState.value = OpenClawConnectionState.Unreachable("Unexpected response (HTTP $code)") + Log.d(TAG, "Unexpected status: HTTP $code") + } } } catch (e: Exception) { _connectionState.value = OpenClawConnectionState.Unreachable(e.message ?: "Unknown error") - Log.d(TAG, "Gateway unreachable: ${e.message}") + Log.d(TAG, "Chat endpoint check failed: ${e.message}") } } @@ -123,6 +164,7 @@ class OpenClawBridge { .addHeader("Content-Type", "application/json") .addHeader("x-openclaw-session-key", sessionKey) .addHeader("x-openclaw-message-channel", "glass") + .addHeader("x-openclaw-scopes", "operator.write") .build() val response = client.newCall(request).execute()