Skip to content

feat(android): add authenticated presence alive beacons#73373

Merged
steipete merged 3 commits into
mainfrom
feat/android-background-presence-beacon
Apr 28, 2026
Merged

feat(android): add authenticated presence alive beacons#73373
steipete merged 3 commits into
mainfrom
feat/android-background-presence-beacon

Conversation

@steipete

Copy link
Copy Markdown
Contributor

Summary

  • Problem: Android nodes currently rely on live WebSocket presence; after disconnect, paired node metadata can lose the most recent authenticated app-alive signal.
  • Why it matters: this keeps Android parity with the newly landed iOS node.presence.alive path and makes node.list last-seen metadata useful after reconnects/background transitions.
  • What changed: Android publishes authenticated node.presence.alive events on node connect and throttled background transitions, checks handled: true, and documents the behavior.
  • What did NOT change (scope boundary): no Android push/wake transport was added; this only uses the existing authenticated node session and node.event RPC.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

N/A

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: NodePresenceAliveBeaconTest, GatewaySessionInvokeTest
  • Scenario the test should lock in: Android builds the presence payload, treats old { ok: true } acks as not durable, and preserves the structured node.event response.
  • Why this is the smallest reliable guardrail: it covers the Android-owned payload/response logic and the GatewaySession RPC framing without requiring a live device.
  • Existing test that already covers this (if any): gateway server tests already cover persistence and auth identity boundaries for node.presence.alive.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

Android paired nodes now publish durable last-seen metadata after node connect and background transitions when the gateway supports node.presence.alive.

Diagram (if applicable)

Before:
Android node connects -> live presence only -> disconnect hides newest alive signal

After:
Android node connects/backgrounds -> node.presence.alive -> paired metadata lastSeenAtMs/lastSeenReason

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? Yes
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation: Android sends an existing authenticated node.event RPC over the already-paired node session. Gateway persistence remains bounded by the authenticated node device identity.

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Android Gradle :app:testPlayDebugUnitTest, Android SDK at ~/Library/Android/sdk
  • Model/provider: N/A
  • Integration/channel (if any): Android node app
  • Relevant config (redacted): N/A

Steps

  1. Build the Android play debug unit test graph.
  2. Run NodePresenceAliveBeaconTest.
  3. Run GatewaySessionInvokeTest.

Expected

  • Beacon helper payload/response behavior passes.
  • GatewaySession sends node.event params with payloadJSON and returns the structured response.

Actual

  • Passed.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Focused command:

ANDROID_HOME="$HOME/Library/Android/sdk" ANDROID_SDK_ROOT="$HOME/Library/Android/sdk" ./gradlew :app:testPlayDebugUnitTest --tests ai.openclaw.app.node.NodePresenceAliveBeaconTest --tests ai.openclaw.app.gateway.GatewaySessionInvokeTest

Result after rebase:

BUILD SUCCESSFUL in 3s
30 actionable tasks: 30 up-to-date

Also ran git diff --check.

Note: ./gradlew :app:ktlintFormat is currently blocked by pre-existing Android ktlint debt across unrelated files; this PR did not include those cleanup changes.

Human Verification (required)

  • Verified scenarios: focused Android unit tests for payload generation, response decoding, old gateway ack handling, and node.event structured response framing.
  • Edge cases checked: recent-success throttling requires a connected gateway; old { "ok": true } responses do not count as handled.
  • What you did not verify: real-device background transition or full Android app install.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: Older gateways acknowledge node.event without durable presence support.
    • Mitigation: Android treats that as an RPC ack only and requires handled: true before recording a local success.

@aisle-research-bot

aisle-research-bot Bot commented Apr 28, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 1 potential security issue(s) in this PR:

# Severity Title
1 🔵 Low Untrusted gateway reason string written to logs (log injection / info disclosure)
1. 🔵 Untrusted gateway `reason` string written to logs (log injection / info disclosure)
Property Value
Severity Low
CWE CWE-117
Location apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt:704-707

Description

NodeRuntime.sendNodePresenceAliveBeacon logs the gateway-provided reason field when the node.presence.alive event is not handled.

  • Input (untrusted): response?.reason is parsed from result.payloadJson, which originates from the gateway RPC response.
  • Sink: Android Log.d(...) writes this string to device logs.
  • Issue: sanitizeReasonForLog() only replaces ISO control characters and truncates to 200 chars. It does not neutralize other potentially misleading/log-forging content (e.g., Unicode bidi override characters) and still allows the server to cause arbitrary text to be written to logs. If logs are collected (e.g., via adb, OEM tooling, or any future log-upload feature), this can also leak sensitive server-supplied details into diagnostic logs.

Vulnerable code:

Log.d(
  "OpenClawNode",
  "node.presence.alive not handled: ${NodePresenceAliveBeacon.sanitizeReasonForLog(response?.reason)}",
)

Recommendation

Avoid logging untrusted server-provided free-form text in production, or strongly neutralize it.

Options:

  1. Log only a stable code (preferred):
val reasonCode = response?.reason?.takeWhile { it.isLetterOrDigit() || it == '_' || it == '-' }?.take(50)
  ?: "unsupported"
Log.d("OpenClawNode", "node.presence.alive not handled; reasonCode=$reasonCode")
  1. If free-form text is required, escape/encode it before logging (e.g., JSON-string encode) and strip bidi/format characters:
fun safeForLog(raw: String?): String {
  val s = raw?.trim().orEmpty().take(200)
  val noControls = s.filterNot { it.isISOControl() || Character.getType(it) == Character.FORMAT.toInt() }
  return kotlinx.serialization.json.JsonPrimitive(noControls).toString() // quoted/escaped
}

Log.d("OpenClawNode", "node.presence.alive not handled: ${safeForLog(response?.reason)}")

Also consider gating the log behind a debug flag (or using Log.w with a fixed message) to reduce exposure in release builds.


Analyzed PR: #73373 at commit 4603e21

Last updated on: 2026-04-28T07:51:20Z

@cursor

cursor Bot commented Apr 28, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes connection-time/background network behavior and node.event RPC handling, which could affect presence reporting and error handling on flaky networks. Scope is limited to Android client code with added tests and backward-compatible response parsing.

Overview
Android node sessions now publish authenticated node.presence.alive beacons via node.event after connect and on background transitions (throttled by last successful record) to keep paired node lastSeen metadata durable across disconnects.

This introduces a NodePresenceAliveBeacon helper to build the payload and interpret gateway responses, and extends GatewaySession with sendNodeEventDetailed (structured RpcResult) while making sendNodeEvent treat a completed RPC as success even when the gateway returns an error payload.

Adds unit/seam tests for the new beacon payload/response handling and the node.event RPC framing, and updates the Android docs/README and changelog to describe the new presence beacon behavior and backward compatibility with older gateways.

Reviewed by Cursor Bugbot for commit 4603e21. Bugbot is set up for automated code reviews on this repo. Configure here.

@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation app: android App: android size: M maintainer Maintainer-authored PR labels Apr 28, 2026
@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds Android parity for node.presence.alive beacons: a new NodePresenceAliveBeacon helper builds and decodes the payload, GatewaySession gains a sendNodeEventDetailed variant that returns the full RpcResult, and NodeRuntime fires a beacon on connect and on background transitions with a 10-minute dedup guard.

  • nodePresenceAliveLastSuccessAtMs is declared as a plain var in a class whose coroutine scope uses Dispatchers.IO (multi-threaded). Without @Volatile, the JVM memory model does not guarantee cross-thread visibility, so a concurrent connect + background transition can both read a stale null, both bypass the throttle, and both emit — silently defeating the dedup guard.

Confidence Score: 3/5

Mostly safe; a missing @Volatile on the throttle timestamp can cause duplicate beacons under concurrent IO coroutines, which should be fixed before merging.

One P1 finding: nodePresenceAliveLastSuccessAtMs lacks @Volatile in a multi-threaded Dispatchers.IO scope, breaking the dedup throttle under concurrent coroutine execution. A P1 ceiling is 4/5; the concurrency issue is isolated to the throttle path (no data corruption or security impact), pulling the score to 3/5.

apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt — the nodePresenceAliveLastSuccessAtMs field needs @Volatile.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt
Line: 250

Comment:
**`nodePresenceAliveLastSuccessAtMs` not visible across IO threads**

`scope` is `CoroutineScope(SupervisorJob() + Dispatchers.IO)`, a multi-threaded pool. Without `@Volatile`, the JVM memory model does not guarantee that a write to this `var` in one IO coroutine is visible to a concurrent read in another. The connect beacon (coroutine C1) and an immediate background beacon (coroutine C2) can both read `null`, both skip the throttle, and both emit, silently defeating the 10-minute dedup guard.

```suggestion
  @Volatile private var nodePresenceAliveLastSuccessAtMs: Long? = null
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/app/node/NodePresenceAliveBeacon.kt
Line: 35-46

Comment:
**`isGatewayConnected` branch is dead code at the only call site**

`sendNodePresenceAliveBeacon` in `NodeRuntime` always passes `isGatewayConnected = true`, so the early-return on `!isGatewayConnected` (which returns `false` / "don't skip") is never exercised in production. The intent of the parameter is also unclear: returning `false` when not connected means "proceed to send", yet the caller's outer `if (!_nodeConnected.value) return` guard already handles that. Consider removing the parameter from `shouldSkipRecentSuccess` and relying solely on the existing connection guard in `sendNodePresenceAliveBeacon`.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: add Android presence alive beacons" | Re-trigger Greptile

private var gatewayAgents: List<GatewayAgentSummary> = emptyList()
private var didAutoRequestCanvasRehydrate = false
private val canvasRehydrateSeq = AtomicLong(0)
private var nodePresenceAliveLastSuccessAtMs: Long? = null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 nodePresenceAliveLastSuccessAtMs not visible across IO threads

scope is CoroutineScope(SupervisorJob() + Dispatchers.IO), a multi-threaded pool. Without @Volatile, the JVM memory model does not guarantee that a write to this var in one IO coroutine is visible to a concurrent read in another. The connect beacon (coroutine C1) and an immediate background beacon (coroutine C2) can both read null, both skip the throttle, and both emit, silently defeating the 10-minute dedup guard.

Suggested change
private var nodePresenceAliveLastSuccessAtMs: Long? = null
@Volatile private var nodePresenceAliveLastSuccessAtMs: Long? = null
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt
Line: 250

Comment:
**`nodePresenceAliveLastSuccessAtMs` not visible across IO threads**

`scope` is `CoroutineScope(SupervisorJob() + Dispatchers.IO)`, a multi-threaded pool. Without `@Volatile`, the JVM memory model does not guarantee that a write to this `var` in one IO coroutine is visible to a concurrent read in another. The connect beacon (coroutine C1) and an immediate background beacon (coroutine C2) can both read `null`, both skip the throttle, and both emit, silently defeating the 10-minute dedup guard.

```suggestion
  @Volatile private var nodePresenceAliveLastSuccessAtMs: Long? = null
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +35 to +46
fun shouldSkipRecentSuccess(
isGatewayConnected: Boolean,
nowMs: Long,
lastSuccessAtMs: Long?,
minIntervalMs: Long = MIN_SUCCESS_INTERVAL_MS,
): Boolean {
if (!isGatewayConnected) return false
val last = lastSuccessAtMs ?: return false
if (last <= 0) return false
val elapsed = nowMs - last
return elapsed >= 0 && elapsed < minIntervalMs
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 isGatewayConnected branch is dead code at the only call site

sendNodePresenceAliveBeacon in NodeRuntime always passes isGatewayConnected = true, so the early-return on !isGatewayConnected (which returns false / "don't skip") is never exercised in production. The intent of the parameter is also unclear: returning false when not connected means "proceed to send", yet the caller's outer if (!_nodeConnected.value) return guard already handles that. Consider removing the parameter from shouldSkipRecentSuccess and relying solely on the existing connection guard in sendNodePresenceAliveBeacon.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/android/app/src/main/java/ai/openclaw/app/node/NodePresenceAliveBeacon.kt
Line: 35-46

Comment:
**`isGatewayConnected` branch is dead code at the only call site**

`sendNodePresenceAliveBeacon` in `NodeRuntime` always passes `isGatewayConnected = true`, so the early-return on `!isGatewayConnected` (which returns `false` / "don't skip") is never exercised in production. The intent of the parameter is also unclear: returning `false` when not connected means "proceed to send", yet the caller's outer `if (!_nodeConnected.value) return` guard already handles that. Consider removing the parameter from `shouldSkipRecentSuccess` and relying solely on the existing connection guard in `sendNodePresenceAliveBeacon`.

How can I resolve this? If you propose a fix, please make it concise.

@steipete

Copy link
Copy Markdown
Contributor Author

Addressed the bot review findings in e287975d73:

  • Made nodePresenceAliveLastSuccessAtMs @Volatile for cross-thread visibility on Dispatchers.IO.
  • Removed the dead isGatewayConnected parameter from the Android throttle helper.
  • Stopped returning/logging raw exception messages from sendNodeEventDetailed.
  • Sanitized and bounded gateway reason before Android logging.

Validation rerun in Blacksmith Testbox tbx_01kq9ff6whsrq37vn9hb4ks40h:

cd apps/android && ./gradlew :app:testPlayDebugUnitTest --tests ai.openclaw.app.node.NodePresenceAliveBeaconTest --tests ai.openclaw.app.gateway.GatewaySessionInvokeTest

Result: BUILD SUCCESSFUL in 1m 45s.

@cursor cursor 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.

Cursor Bugbot has reviewed your changes and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e287975d73b615f72e822d7324a37579b499e363. Configure here.

Comment thread apps/android/app/src/main/java/ai/openclaw/app/node/NodePresenceAliveBeacon.kt Outdated
Comment thread apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt Outdated
@steipete

Copy link
Copy Markdown
Contributor Author

Follow-up in f3ab4e2fc4 addresses the remaining Android review items:

  • Bounded node.presence.alive response JSON before parsing to avoid oversized gateway payload work.
  • Restored sendNodeEvent(...) compatibility: existing callers now treat a completed node.event RPC as sent even if the gateway response has ok: false; sendNodeEventDetailed(...) still exposes strict ok/payload/error for the presence beacon.
  • Removed the unused Android trigger normalizer that only tests called.

Validation rerun in Blacksmith Testbox tbx_01kq9ff6whsrq37vn9hb4ks40h on the exact patched tree:

cd apps/android && ./gradlew :app:testPlayDebugUnitTest --tests ai.openclaw.app.node.NodePresenceAliveBeaconTest --tests ai.openclaw.app.gateway.GatewaySessionInvokeTest

Result: BUILD SUCCESSFUL in 4s.

@steipete
steipete force-pushed the feat/android-background-presence-beacon branch from f3ab4e2 to 4603e21 Compare April 28, 2026 07:49
@steipete

Copy link
Copy Markdown
Contributor Author

Rebased onto current origin/main to pick up the unrelated src/video-generation/provider-registry.test.ts isolation fix, then reran the Android focused gate in Blacksmith Testbox tbx_01kq9ff6whsrq37vn9hb4ks40h on the rebased head 4603e21af8:

cd apps/android && ./gradlew :app:testPlayDebugUnitTest --tests ai.openclaw.app.node.NodePresenceAliveBeaconTest --tests ai.openclaw.app.gateway.GatewaySessionInvokeTest

Result: BUILD SUCCESSFUL in 566ms.

@steipete
steipete merged commit 04e774e into main Apr 28, 2026
38 of 39 checks passed
@steipete
steipete deleted the feat/android-background-presence-beacon branch April 28, 2026 07:55
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
* feat: add Android presence alive beacons

* fix: harden Android presence beacon review findings

* fix: address Android presence review findings
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
* feat: add Android presence alive beacons

* fix: harden Android presence beacon review findings

* fix: address Android presence review findings
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
* feat: add Android presence alive beacons

* fix: harden Android presence beacon review findings

* fix: address Android presence review findings
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
* feat: add Android presence alive beacons

* fix: harden Android presence beacon review findings

* fix: address Android presence review findings
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
* feat: add Android presence alive beacons

* fix: harden Android presence beacon review findings

* fix: address Android presence review findings
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
* feat: add Android presence alive beacons

* fix: harden Android presence beacon review findings

* fix: address Android presence review findings
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: android App: android docs Improvements or additions to documentation maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant