feat(android): add authenticated presence alive beacons#73373
Conversation
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🔵 Untrusted gateway `reason` string written to logs (log injection / info disclosure)
Description
Vulnerable code: Log.d(
"OpenClawNode",
"node.presence.alive not handled: ${NodePresenceAliveBeacon.sanitizeReasonForLog(response?.reason)}",
)RecommendationAvoid logging untrusted server-provided free-form text in production, or strongly neutralize it. Options:
val reasonCode = response?.reason?.takeWhile { it.isLetterOrDigit() || it == '_' || it == '-' }?.take(50)
?: "unsupported"
Log.d("OpenClawNode", "node.presence.alive not handled; reasonCode=$reasonCode")
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 Analyzed PR: #73373 at commit Last updated on: 2026-04-28T07:51:20Z |
PR SummaryMedium Risk Overview This introduces a Adds unit/seam tests for the new beacon payload/response handling and the Reviewed by Cursor Bugbot for commit 4603e21. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryAdds Android parity for
Confidence Score: 3/5Mostly safe; a missing One P1 finding: apps/android/app/src/main/java/ai/openclaw/app/NodeRuntime.kt — the Prompt To Fix All With AIThis 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 |
There was a problem hiding this 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.
| 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.| 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 | ||
| } |
There was a problem hiding this 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.
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.|
Addressed the bot review findings in
Validation rerun in Blacksmith Testbox cd apps/android && ./gradlew :app:testPlayDebugUnitTest --tests ai.openclaw.app.node.NodePresenceAliveBeaconTest --tests ai.openclaw.app.gateway.GatewaySessionInvokeTestResult: |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ 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.
|
Follow-up in
Validation rerun in Blacksmith Testbox cd apps/android && ./gradlew :app:testPlayDebugUnitTest --tests ai.openclaw.app.node.NodePresenceAliveBeaconTest --tests ai.openclaw.app.gateway.GatewaySessionInvokeTestResult: |
f3ab4e2 to
4603e21
Compare
|
Rebased onto current cd apps/android && ./gradlew :app:testPlayDebugUnitTest --tests ai.openclaw.app.node.NodePresenceAliveBeaconTest --tests ai.openclaw.app.gateway.GatewaySessionInvokeTestResult: |
* feat: add Android presence alive beacons * fix: harden Android presence beacon review findings * fix: address Android presence review findings
* feat: add Android presence alive beacons * fix: harden Android presence beacon review findings * fix: address Android presence review findings
* feat: add Android presence alive beacons * fix: harden Android presence beacon review findings * fix: address Android presence review findings
* feat: add Android presence alive beacons * fix: harden Android presence beacon review findings * fix: address Android presence review findings
* feat: add Android presence alive beacons * fix: harden Android presence beacon review findings * fix: address Android presence review findings
* feat: add Android presence alive beacons * fix: harden Android presence beacon review findings * fix: address Android presence review findings

Summary
node.presence.alivepath and makesnode.listlast-seen metadata useful after reconnects/background transitions.node.presence.aliveevents on node connect and throttled background transitions, checkshandled: true, and documents the behavior.node.eventRPC.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause (if applicable)
N/A
Regression Test Plan (if applicable)
NodePresenceAliveBeaconTest,GatewaySessionInvokeTest{ ok: true }acks as not durable, and preserves the structurednode.eventresponse.node.presence.alive.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)
Security Impact (required)
Yes, explain risk + mitigation: Android sends an existing authenticatednode.eventRPC over the already-paired node session. Gateway persistence remains bounded by the authenticated node device identity.Repro + Verification
Environment
:app:testPlayDebugUnitTest, Android SDK at~/Library/Android/sdkSteps
NodePresenceAliveBeaconTest.GatewaySessionInvokeTest.Expected
node.eventparams withpayloadJSONand returns the structured response.Actual
Evidence
Focused command:
Result after rebase:
Also ran
git diff --check.Note:
./gradlew :app:ktlintFormatis currently blocked by pre-existing Android ktlint debt across unrelated files; this PR did not include those cleanup changes.Human Verification (required)
node.eventstructured response framing.{ "ok": true }responses do not count as handled.Review Conversations
Compatibility / Migration
Risks and Mitigations
node.eventwithout durable presence support.handled: truebefore recording a local success.