diff --git a/CHANGELOG.md b/CHANGELOG.md index 5058a6ce3..cf57a27e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ Docs: https://docs.clawd.bot - CLI: keep `clawdbot logs` output resilient to broken pipes while preserving progress output. - Model catalog: avoid caching import failures, log transient discovery errors, and keep partial results. (#1332) — thanks @dougvk. - Doctor: clarify plugin auto-enable hint text in the startup banner. +- Gateway: allow mobile node client ids for iOS + Android handshake validation. (#1354) — thanks @vignesh07. - Gateway: clarify unauthorized handshake responses with token/password mismatch guidance. - Gateway: clarify connect/validation errors for gateway params. (#1347) — thanks @vignesh07. - Gateway: preserve restart wake routing + thread replies across restarts. (#1337) — thanks @John-Rood. diff --git a/apps/android/app/src/main/java/com/clawdbot/android/NodeRuntime.kt b/apps/android/app/src/main/java/com/clawdbot/android/NodeRuntime.kt index 8d051a421..603e4b82b 100644 --- a/apps/android/app/src/main/java/com/clawdbot/android/NodeRuntime.kt +++ b/apps/android/app/src/main/java/com/clawdbot/android/NodeRuntime.kt @@ -529,7 +529,7 @@ class NodeRuntime(context: Context) { caps = buildCapabilities(), commands = buildInvokeCommands(), permissions = emptyMap(), - client = buildClientInfo(clientId = "node-host", clientMode = "node"), + client = buildClientInfo(clientId = "clawdbot-android", clientMode = "node"), userAgent = buildUserAgent(), ) } diff --git a/src/cli/nodes-cli/register.status.ts b/src/cli/nodes-cli/register.status.ts index 4f8e2ae76..3c89cc0fc 100644 --- a/src/cli/nodes-cli/register.status.ts +++ b/src/cli/nodes-cli/register.status.ts @@ -46,7 +46,13 @@ function formatNodeVersions(node: { function parseSinceMs(raw: unknown, label: string): number | undefined { if (raw === undefined || raw === null) return undefined; - const value = String(raw).trim(); + const value = + typeof raw === "string" ? raw.trim() : typeof raw === "number" ? String(raw).trim() : null; + if (value === null) { + defaultRuntime.error(`${label}: invalid duration value`); + defaultRuntime.exit(1); + return undefined; + } if (!value) return undefined; try { return parseDurationMs(value); diff --git a/src/gateway/protocol/client-info.ts b/src/gateway/protocol/client-info.ts index 54720bcc5..d855e463d 100644 --- a/src/gateway/protocol/client-info.ts +++ b/src/gateway/protocol/client-info.ts @@ -5,6 +5,8 @@ export const GATEWAY_CLIENT_IDS = { CLI: "cli", GATEWAY_CLIENT: "gateway-client", MACOS_APP: "clawdbot-macos", + IOS_APP: "clawdbot-ios", + ANDROID_APP: "clawdbot-android", NODE_HOST: "node-host", TEST: "test", FINGERPRINT: "fingerprint", diff --git a/src/gateway/server.ios-client-id.test.ts b/src/gateway/server.ios-client-id.test.ts new file mode 100644 index 000000000..64f87abcd --- /dev/null +++ b/src/gateway/server.ios-client-id.test.ts @@ -0,0 +1,87 @@ +import { test } from "vitest"; +import WebSocket from "ws"; + +import { PROTOCOL_VERSION } from "./protocol/index.js"; +import { getFreePort, onceMessage, startGatewayServer } from "./test-helpers.server.js"; + +function connectReq( + ws: WebSocket, + params: { clientId: string; platform: string; token?: string; password?: string }, +): Promise<{ ok: boolean; error?: { message?: string } }> { + const id = `c-${Math.random().toString(16).slice(2)}`; + ws.send( + JSON.stringify({ + type: "req", + id, + method: "connect", + params: { + minProtocol: PROTOCOL_VERSION, + maxProtocol: PROTOCOL_VERSION, + client: { + id: params.clientId, + version: "dev", + platform: params.platform, + mode: "node", + }, + auth: { + token: params.token, + password: params.password, + }, + role: "node", + scopes: [], + caps: ["canvas"], + commands: ["system.notify"], + permissions: {}, + }, + }), + ); + + return onceMessage( + ws, + (o) => (o as { type?: string }).type === "res" && (o as { id?: string }).id === id, + ); +} + +test("accepts clawdbot-ios as a valid gateway client id", async () => { + const port = await getFreePort(); + const server = await startGatewayServer(port); + const ws = new WebSocket(`ws://127.0.0.1:${port}`); + await new Promise((resolve) => ws.once("open", resolve)); + + const res = await connectReq(ws, { clientId: "clawdbot-ios", platform: "ios" }); + // We don't care if auth fails here; we only care that schema validation accepts the client id. + // A schema rejection would close the socket before sending a response. + if (!res.ok) { + // allow unauthorized error when gateway requires auth + // but reject schema validation errors + const message = String(res.error?.message ?? ""); + if (message.includes("invalid connect params")) { + throw new Error(message); + } + } + + ws.close(); + await server.close(); +}); + +test("accepts clawdbot-android as a valid gateway client id", async () => { + const port = await getFreePort(); + const server = await startGatewayServer(port); + const ws = new WebSocket(`ws://127.0.0.1:${port}`); + await new Promise((resolve) => ws.once("open", resolve)); + + const res = await connectReq(ws, { clientId: "clawdbot-android", platform: "android" }); + // We don't care if auth fails here; we only care that schema validation accepts the client id. + // A schema rejection would close the socket before sending a response. + if (!res.ok) { + // allow unauthorized error when gateway requires auth + // but reject schema validation errors + const message = String(res.error?.message ?? ""); + if (message.includes("invalid connect params")) { + throw new Error(message); + } + } + + ws.close(); + await server.close(); +});