Skip to content

Commit 67a40dc

Browse files
authored
Merge branch 'main' into security/preauth-signature-rate-limit
2 parents 06ab072 + b05001a commit 67a40dc

39 files changed

Lines changed: 1867 additions & 706 deletions

apps/android/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
Android notification forwarding now excludes native WhatsApp, Telegram, Telegram X, Discord, and Signal channel apps to prevent duplicate cross-session replies. (#48516)
6+
57
Assistant messages now offer a long-press Listen action with gateway TTS playback, on-device fallback, and tap-to-stop status.
68

79
The Settings About screen now shows the animated mascot with the app tagline plus Website, Docs, GitHub, and Discord links.

apps/android/app/src/main/java/ai/openclaw/app/NotificationForwardingPolicy.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ package ai.openclaw.app
33
import java.time.Instant
44
import java.time.ZoneId
55

6+
private val nativeChannelNotificationPackages =
7+
setOf(
8+
"com.discord",
9+
"com.whatsapp",
10+
"com.whatsapp.w4b",
11+
"org.telegram.messenger",
12+
"org.telegram.messenger.web",
13+
"org.thunderdog.challegram",
14+
"org.thoughtcrime.securesms",
15+
)
16+
617
/** Package-filter mode used before notification events are forwarded to the gateway. */
718
enum class NotificationPackageFilterMode(
819
val rawValue: String,
@@ -40,6 +51,11 @@ internal fun NotificationForwardingPolicy.allowsPackage(packageName: String): Bo
4051
if (self.isNotEmpty() && normalized == self) {
4152
return false
4253
}
54+
// Native channel sessions own these messages. Forwarding their notifications creates an
55+
// unbound duplicate that can be answered from the wrong conversation.
56+
if (normalized in nativeChannelNotificationPackages) {
57+
return false
58+
}
4359
return when (mode) {
4460
NotificationPackageFilterMode.Allowlist -> packages.contains(normalized)
4561
NotificationPackageFilterMode.Blocklist -> !packages.contains(normalized)

apps/android/app/src/test/java/ai/openclaw/app/NotificationForwardingPolicyTest.kt

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,45 @@ class NotificationForwardingPolicyTest {
9696
assertTrue(policy.allowsPackage("com.other.app"))
9797
}
9898

99+
@Test
100+
fun allowsPackage_neverForwardsNativeChannelPackages() {
101+
val nativeChannelPackages =
102+
setOf(
103+
"com.discord",
104+
"com.whatsapp",
105+
"com.whatsapp.w4b",
106+
"org.telegram.messenger",
107+
"org.telegram.messenger.web",
108+
"org.thunderdog.challegram",
109+
"org.thoughtcrime.securesms",
110+
)
111+
val policies =
112+
NotificationPackageFilterMode.entries.map { mode ->
113+
NotificationForwardingPolicy(
114+
enabled = true,
115+
mode = mode,
116+
packages =
117+
if (mode == NotificationPackageFilterMode.Allowlist) {
118+
nativeChannelPackages + "com.other.app"
119+
} else {
120+
nativeChannelPackages
121+
},
122+
quietHoursEnabled = false,
123+
quietStart = "22:00",
124+
quietEnd = "07:00",
125+
maxEventsPerMinute = 20,
126+
sessionKey = null,
127+
)
128+
}
129+
130+
policies.forEach { policy ->
131+
nativeChannelPackages.forEach { packageName ->
132+
assertFalse("$packageName must be owned by its native channel", policy.allowsPackage(packageName))
133+
}
134+
assertTrue(policy.allowsPackage("com.other.app"))
135+
}
136+
}
137+
99138
@Test
100139
fun isWithinQuietHours_handlesWindowCrossingMidnight() {
101140
val policy =

apps/android/app/src/test/java/ai/openclaw/app/SecurePrefsNotificationForwardingTest.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,18 +130,19 @@ class SecurePrefsNotificationForwardingTest {
130130
}
131131

132132
@Test
133-
fun getNotificationForwardingPolicy_blocksSelfPackageInAllowlistMode() {
133+
fun getNotificationForwardingPolicy_blocksOwnedPackagesInAllowlistMode() {
134134
val context = RuntimeEnvironment.getApplication()
135135
val plainPrefs = context.getSharedPreferences("openclaw.node", Context.MODE_PRIVATE)
136136
plainPrefs.edit().clear().commit()
137137

138138
val prefs = SecurePrefs(context)
139139
prefs.setNotificationForwardingMode(NotificationPackageFilterMode.Allowlist)
140-
prefs.setNotificationForwardingPackages(listOf("ai.openclaw.app", "com.other.app"))
140+
prefs.setNotificationForwardingPackages(listOf("ai.openclaw.app", "com.whatsapp", "com.other.app"))
141141

142142
val policy = prefs.getNotificationForwardingPolicy(appPackageName = "ai.openclaw.app")
143143

144144
assertFalse(policy.allowsPackage("ai.openclaw.app"))
145+
assertFalse(policy.allowsPackage("com.whatsapp"))
145146
assertTrue(policy.allowsPackage("com.other.app"))
146147
}
147148
}

docs/platforms/android.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,8 @@ Android can forward device notifications to the gateway as `node.event` items. T
320320
Notification forwarding requires the Android Notification Listener permission. The app prompts for this during setup.
321321
</Note>
322322

323+
WhatsApp, WhatsApp Business, Telegram, Telegram X, Discord, and Signal notifications are always excluded. Their messages are already owned by native OpenClaw channel sessions; forwarding the Android notification as a separate node event could route a reply through the wrong conversation.
324+
323325
## Related
324326

325327
- [iOS app](/platforms/ios)

extensions/diagnostics-otel/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Diagnostics Otel plugin entrypoint registers its OpenClaw integration.
22
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3-
import { createDiagnosticsOtelService } from "./src/service.js";
3+
import { createDiagnosticsOtelService } from "./runtime-api.js";
44

55
export default definePluginEntry({
66
id: "diagnostics-otel",
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// Diagnostics Otel runtime API exposes the service factory to trusted runtime callers.
2+
export { createDiagnosticsOtelService } from "./src/service.js";
3+
export type { OpenClawPluginServiceContext } from "./api.js";

extensions/qa-lab/src/coverage-report.test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ function scenarioWithCoverage(params: {
143143

144144
describe("qa coverage report", () => {
145145
it("groups scenario coverage metadata by theme and surface", () => {
146-
const inventory = buildQaCoverageInventory(readQaScenarioPack().scenarios);
146+
const scenarios = readQaScenarioPack().scenarios;
147+
const inventory = buildQaCoverageInventory(scenarios);
147148

148149
expect(inventory.scenarioCount).toBeGreaterThan(0);
149150
expect(inventory.coverageIdCount).toBeGreaterThan(0);
@@ -169,6 +170,22 @@ describe("qa coverage report", () => {
169170
).toMatchObject({
170171
channelDriver: "live",
171172
});
173+
for (const [categoryId, scenarioRef] of [
174+
["docker-podman-hosting.container-setup", "qa/scenarios/runtime/compose-setup.yaml"],
175+
[
176+
"docker-podman-hosting.image-release-and-validation",
177+
"qa/scenarios/runtime/docker-package-install.yaml",
178+
],
179+
] as const) {
180+
const category = inventory.scorecardTaxonomy.categories.find(
181+
(entry) => entry.id === categoryId,
182+
);
183+
expect(category?.profiles).toContain("release");
184+
expect(category?.profiles).not.toContain("smoke-ci");
185+
expect(scenarios.find((scenario) => scenario.sourcePath === scenarioRef)?.category).toBe(
186+
categoryId,
187+
);
188+
}
172189
expect(
173190
inventory.scorecardTaxonomy.profiles.find((profile) => profile.id === "all"),
174191
).toMatchObject({

extensions/qa-lab/src/scenario-catalog.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ describe("qa scenario catalog", () => {
222222
it("loads native test execution scenarios from YAML", () => {
223223
const scenario = readQaScenarioById("control-ui-chat-flow-playwright");
224224
const uxMatrix = readQaScenarioById("ux-matrix-evidence-dashboard");
225+
const otelSmoke = readQaScenarioById("qa-otel-smoke");
225226

226227
expect(scenario.execution.kind).toBe("playwright");
227228
if (scenario.execution.kind !== "playwright") {
@@ -238,6 +239,17 @@ describe("qa scenario catalog", () => {
238239
expect(uxMatrix.execution.args).toStrictEqual(["--artifact-base", "${outputDir}"]);
239240
expect(uxMatrix.execution.config).toBeUndefined();
240241
expect(uxMatrix.coverage?.primary).toContain("qa.artifact-safety");
242+
expect(otelSmoke.execution.kind).toBe("script");
243+
if (otelSmoke.execution.kind !== "script") {
244+
throw new Error(`expected script scenario, got ${otelSmoke.execution.kind}`);
245+
}
246+
expect(otelSmoke.execution.args).toStrictEqual([
247+
"--output-dir",
248+
"${outputDir}",
249+
"--logs-exporter",
250+
"both",
251+
]);
252+
expect(otelSmoke.coverage?.secondary).not.toContain("harness.qa-lab");
241253
});
242254

243255
it("loads helper-backed HTTP API scenarios as supporting taxonomy coverage", () => {

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,6 +1796,7 @@
17961796
"test:docker:cron-mcp-cleanup": "bash scripts/e2e/cron-mcp-cleanup-docker.sh",
17971797
"test:docker:codex-media-path": "bash scripts/e2e/codex-media-path-docker.sh",
17981798
"test:docker:doctor-switch": "bash scripts/e2e/doctor-install-switch-docker.sh",
1799+
"test:docker:compose-setup": "bash scripts/e2e/compose-setup.sh",
17991800
"test:docker:e2e-build": "bash scripts/e2e/build-image.sh",
18001801
"test:docker:gateway-network": "bash scripts/e2e/gateway-network-docker.sh",
18011802
"test:docker:kitchen-sink-plugin": "bash scripts/e2e/kitchen-sink-plugin-docker.sh",
@@ -1848,6 +1849,7 @@
18481849
"test:docker:openai-image-auth": "bash scripts/e2e/openai-image-auth-docker.sh",
18491850
"test:docker:openai-web-search-minimal": "bash scripts/e2e/openai-web-search-minimal-docker.sh",
18501851
"test:docker:openwebui": "bash scripts/e2e/openwebui-docker.sh",
1852+
"test:docker:package-install": "bash scripts/e2e/docker-package-install.sh",
18511853
"test:docker:plugin-binding-command-escape": "bash scripts/e2e/plugin-binding-command-escape-docker.sh",
18521854
"test:docker:plugin-lifecycle-matrix": "bash scripts/e2e/plugin-lifecycle-matrix-docker.sh",
18531855
"test:docker:plugin-update": "bash scripts/e2e/plugin-update-unchanged-docker.sh",

0 commit comments

Comments
 (0)