Skip to content

Commit c7de078

Browse files
Merge branch 'main' into fix/discord-message-listener-perf
2 parents 6dd2088 + fc97393 commit c7de078

447 files changed

Lines changed: 13944 additions & 6298 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.agents/skills/openclaw-parallels-smoke/SKILL.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
1616
- Pass `--json` for machine-readable summaries.
1717
- Per-phase logs land under `/tmp/openclaw-parallels-*`.
1818
- Do not run local and gateway agent turns in parallel on the same fresh workspace or session.
19+
- For `prlctl exec`, pass the VM name before `--current-user` (`prlctl exec "$VM" --current-user ...`), not the other way around.
20+
21+
## npm install then update
22+
23+
- Preferred entrypoint: `pnpm test:parallels:npm-update`
24+
- Flow: fresh snapshot -> install npm package baseline -> smoke -> install current main tgz on the same guest -> smoke again.
25+
- Same-guest update verification should set the default model explicitly to `openai/gpt-5.4` before the agent turn and use a fresh explicit `--session-id` so old session model state does not leak into the check.
26+
- On Windows same-guest update checks, restart the gateway after the npm upgrade before `gateway status` / `agent`; in-place global npm updates can otherwise leave stale hashed `dist/*` module imports alive in the running service.
27+
- Linux same-guest update verification should also export `HOME=/root`, pass `OPENAI_API_KEY` via `prlctl exec ... /usr/bin/env`, and use `openclaw agent --local`; the fresh Linux baseline does not rely on persisted gateway credentials.
1928

2029
## macOS flow
2130

@@ -34,7 +43,9 @@ Use this skill for Parallels guest workflows and smoke interpretation. Do not lo
3443
- Always use `prlctl exec --current-user`; plain `prlctl exec` lands in `NT AUTHORITY\\SYSTEM`.
3544
- Prefer explicit `npm.cmd` and `openclaw.cmd`.
3645
- Use PowerShell only as the transport with `-ExecutionPolicy Bypass`, then call the `.cmd` shims from inside it.
46+
- Windows installer/tgz phases now retry once after guest-ready recheck; keep new Windows smoke steps idempotent so a transport-flake retry is safe.
3747
- Keep onboarding and status output ASCII-clean in logs; fancy punctuation becomes mojibake in current capture paths.
48+
- If you hit an older run with `rc=255` plus an empty `fresh.install-main.log` or `upgrade.install-main.log`, treat it as a likely `prlctl exec` transport drop after guest start-up, not immediate proof of an npm/package failure.
3849

3950
## Linux flow
4051

CHANGELOG.md

Lines changed: 188 additions & 174 deletions
Large diffs are not rendered by default.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ class MainViewModel(app: Application) : AndroidViewModel(app) {
237237
ensureRuntime().handleCanvasA2UIActionFromWebView(payloadJson)
238238
}
239239

240+
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean {
241+
return ensureRuntime().isTrustedCanvasActionUrl(rawUrl)
242+
}
243+
240244
fun requestCanvasRehydrate(source: String = "screen_tab") {
241245
ensureRuntime().requestCanvasRehydrate(source = source, force = true)
242246
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,10 @@ class NodeRuntime(
904904
}
905905
}
906906

907+
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean {
908+
return a2uiHandler.isTrustedCanvasActionUrl(rawUrl)
909+
}
910+
907911
fun loadChat(sessionKey: String) {
908912
val key = sessionKey.trim().ifEmpty { resolveMainSessionKey() }
909913
chat.load(key)

apps/android/app/src/main/java/ai/openclaw/app/node/A2UIHandler.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ class A2UIHandler(
1313
private val getNodeCanvasHostUrl: () -> String?,
1414
private val getOperatorCanvasHostUrl: () -> String?,
1515
) {
16+
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean {
17+
return CanvasActionTrust.isTrustedCanvasActionUrl(
18+
rawUrl = rawUrl,
19+
trustedA2uiUrls = listOfNotNull(resolveA2uiHostUrl()),
20+
)
21+
}
22+
1623
fun resolveA2uiHostUrl(): String? {
1724
val nodeRaw = getNodeCanvasHostUrl()?.trim().orEmpty()
1825
val operatorRaw = getOperatorCanvasHostUrl()?.trim().orEmpty()
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package ai.openclaw.app.node
2+
3+
import java.net.URI
4+
5+
object CanvasActionTrust {
6+
const val scaffoldAssetUrl: String = "file:///android_asset/CanvasScaffold/scaffold.html"
7+
8+
fun isTrustedCanvasActionUrl(rawUrl: String?, trustedA2uiUrls: List<String>): Boolean {
9+
val candidate = rawUrl?.trim().orEmpty()
10+
if (candidate.isEmpty()) return false
11+
if (candidate == scaffoldAssetUrl) return true
12+
13+
val candidateUri = parseUri(candidate) ?: return false
14+
if (candidateUri.scheme.equals("file", ignoreCase = true)) {
15+
return false
16+
}
17+
18+
return trustedA2uiUrls.any { trusted ->
19+
isTrustedA2uiPage(candidateUri, trusted)
20+
}
21+
}
22+
23+
private fun isTrustedA2uiPage(candidateUri: URI, trustedUrl: String): Boolean {
24+
val trustedUri = parseUri(trustedUrl) ?: return false
25+
if (!candidateUri.scheme.equals(trustedUri.scheme, ignoreCase = true)) return false
26+
if (candidateUri.host?.equals(trustedUri.host, ignoreCase = true) != true) return false
27+
if (effectivePort(candidateUri) != effectivePort(trustedUri)) return false
28+
29+
val trustedPath = trustedUri.rawPath?.takeIf { it.isNotBlank() } ?: return false
30+
val candidatePath = candidateUri.rawPath?.takeIf { it.isNotBlank() } ?: return false
31+
val trustedPrefix = if (trustedPath.endsWith("/")) trustedPath else "$trustedPath/"
32+
return candidatePath == trustedPath || candidatePath.startsWith(trustedPrefix)
33+
}
34+
35+
private fun effectivePort(uri: URI): Int {
36+
if (uri.port >= 0) return uri.port
37+
return when (uri.scheme?.lowercase()) {
38+
"https" -> 443
39+
"http" -> 80
40+
else -> -1
41+
}
42+
}
43+
44+
private fun parseUri(raw: String): URI? =
45+
try {
46+
URI(raw)
47+
} catch (_: Throwable) {
48+
null
49+
}
50+
}

apps/android/app/src/main/java/ai/openclaw/app/ui/CanvasScreen.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ import androidx.compose.ui.viewinterop.AndroidView
2222
import androidx.webkit.WebSettingsCompat
2323
import androidx.webkit.WebViewFeature
2424
import ai.openclaw.app.MainViewModel
25+
import java.util.concurrent.atomic.AtomicReference
2526

2627
@SuppressLint("SetJavaScriptEnabled")
2728
@Composable
2829
fun CanvasScreen(viewModel: MainViewModel, visible: Boolean, modifier: Modifier = Modifier) {
2930
val context = LocalContext.current
3031
val isDebuggable = (context.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
3132
val webViewRef = remember { mutableStateOf<WebView?>(null) }
33+
val currentPageUrlRef = remember { AtomicReference<String?>(null) }
3234

3335
DisposableEffect(viewModel) {
3436
onDispose {
@@ -68,6 +70,14 @@ fun CanvasScreen(viewModel: MainViewModel, visible: Boolean, modifier: Modifier
6870
isHorizontalScrollBarEnabled = true
6971
webViewClient =
7072
object : WebViewClient() {
73+
override fun onPageStarted(
74+
view: WebView,
75+
url: String?,
76+
favicon: android.graphics.Bitmap?,
77+
) {
78+
currentPageUrlRef.set(url)
79+
}
80+
7181
override fun onReceivedError(
7282
view: WebView,
7383
request: WebResourceRequest,
@@ -90,6 +100,7 @@ fun CanvasScreen(viewModel: MainViewModel, visible: Boolean, modifier: Modifier
90100
}
91101

92102
override fun onPageFinished(view: WebView, url: String?) {
103+
currentPageUrlRef.set(url)
93104
if (isDebuggable) {
94105
Log.d("OpenClawWebView", "onPageFinished: $url")
95106
}
@@ -122,7 +133,12 @@ fun CanvasScreen(viewModel: MainViewModel, visible: Boolean, modifier: Modifier
122133
}
123134
}
124135

125-
val bridge = CanvasA2UIActionBridge { payload -> viewModel.handleCanvasA2UIActionFromWebView(payload) }
136+
val bridge =
137+
CanvasA2UIActionBridge(
138+
isTrustedPage = { viewModel.isTrustedCanvasActionUrl(currentPageUrlRef.get()) },
139+
) { payload ->
140+
viewModel.handleCanvasA2UIActionFromWebView(payload)
141+
}
126142
addJavascriptInterface(bridge, CanvasA2UIActionBridge.interfaceName)
127143
viewModel.canvas.attach(this)
128144
webViewRef.value = this
@@ -147,11 +163,15 @@ private fun disableForceDarkIfSupported(settings: WebSettings) {
147163
WebSettingsCompat.setForceDark(settings, WebSettingsCompat.FORCE_DARK_OFF)
148164
}
149165

150-
private class CanvasA2UIActionBridge(private val onMessage: (String) -> Unit) {
166+
private class CanvasA2UIActionBridge(
167+
private val isTrustedPage: () -> Boolean,
168+
private val onMessage: (String) -> Unit,
169+
) {
151170
@JavascriptInterface
152171
fun postMessage(payload: String?) {
153172
val msg = payload?.trim().orEmpty()
154173
if (msg.isEmpty()) return
174+
if (!isTrustedPage()) return
155175
onMessage(msg)
156176
}
157177

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package ai.openclaw.app.node
2+
3+
import org.junit.Assert.assertFalse
4+
import org.junit.Assert.assertTrue
5+
import org.junit.Test
6+
7+
class CanvasActionTrustTest {
8+
@Test
9+
fun acceptsBundledScaffoldAsset() {
10+
assertTrue(CanvasActionTrust.isTrustedCanvasActionUrl(CanvasActionTrust.scaffoldAssetUrl, emptyList()))
11+
}
12+
13+
@Test
14+
fun acceptsTrustedA2uiPageOnAdvertisedCanvasHost() {
15+
assertTrue(
16+
CanvasActionTrust.isTrustedCanvasActionUrl(
17+
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android",
18+
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
19+
),
20+
)
21+
}
22+
23+
@Test
24+
fun rejectsDifferentOriginEvenIfPathMatches() {
25+
assertFalse(
26+
CanvasActionTrust.isTrustedCanvasActionUrl(
27+
rawUrl = "https://evil.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android",
28+
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
29+
),
30+
)
31+
}
32+
33+
@Test
34+
fun rejectsUntrustedCanvasPagePathOnTrustedOrigin() {
35+
assertFalse(
36+
CanvasActionTrust.isTrustedCanvasActionUrl(
37+
rawUrl = "https://canvas.example.com:9443/untrusted/index.html",
38+
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
39+
),
40+
)
41+
}
42+
}

docs/.generated/config-baseline.json

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31097,6 +31097,26 @@
3109731097
"tags": [],
3109831098
"hasChildren": false
3109931099
},
31100+
{
31101+
"path": "channels.synology-chat.dangerouslyAllowInheritedWebhookPath",
31102+
"kind": "channel",
31103+
"type": "boolean",
31104+
"required": false,
31105+
"deprecated": false,
31106+
"sensitive": false,
31107+
"tags": [],
31108+
"hasChildren": false
31109+
},
31110+
{
31111+
"path": "channels.synology-chat.dangerouslyAllowNameMatching",
31112+
"kind": "channel",
31113+
"type": "boolean",
31114+
"required": false,
31115+
"deprecated": false,
31116+
"sensitive": false,
31117+
"tags": [],
31118+
"hasChildren": false
31119+
},
3110031120
{
3110131121
"path": "channels.telegram",
3110231122
"kind": "channel",
@@ -45733,7 +45753,7 @@
4573345753
"storage"
4573445754
],
4573545755
"label": "Node Browser Proxy Allowed Profiles",
45736-
"help": "Optional allowlist of browser profile names exposed through node proxy routing. Leave empty to expose all configured profiles, or use a tight list to enforce least-privilege profile access.",
45756+
"help": "Optional allowlist of browser profile names exposed through node proxy routing. Leave empty to preserve the default full profile surface, including profile create/delete routes. When set, OpenClaw enforces least-privilege profile access and blocks persistent profile create/delete through the proxy.",
4573745757
"hasChildren": true
4573845758
},
4573945759
{
@@ -46044,7 +46064,7 @@
4604446064
"advanced"
4604546065
],
4604646066
"label": "Expected acpx Version",
46047-
"help": "Exact version to enforce (for example 0.1.16) or \"any\" to skip strict version matching.",
46067+
"help": "Exact version to enforce or \"any\" to skip strict version matching.",
4604846068
"hasChildren": false
4604946069
},
4605046070
{

docs/.generated/config-baseline.jsonl

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5617}
1+
{"generatedBy":"scripts/generate-config-doc-baseline.ts","recordType":"meta","totalPaths":5619}
22
{"recordType":"path","path":"acp","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACP","help":"ACP runtime controls for enabling dispatch, selecting backends, constraining allowed agent targets, and tuning streamed turn projection behavior.","hasChildren":true}
33
{"recordType":"path","path":"acp.allowedAgents","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access"],"label":"ACP Allowed Agents","help":"Allowlist of ACP target agent ids permitted for ACP runtime sessions. Empty means no additional allowlist restriction.","hasChildren":true}
44
{"recordType":"path","path":"acp.allowedAgents.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
@@ -2804,6 +2804,8 @@
28042804
{"recordType":"path","path":"channels.slack.webhookPath","kind":"channel","type":"string","required":true,"defaultValue":"/slack/events","deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
28052805
{"recordType":"path","path":"channels.synology-chat","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Synology Chat","help":"Connect your Synology NAS Chat to OpenClaw with full agent capabilities.","hasChildren":true}
28062806
{"recordType":"path","path":"channels.synology-chat.*","kind":"channel","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
2807+
{"recordType":"path","path":"channels.synology-chat.dangerouslyAllowInheritedWebhookPath","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
2808+
{"recordType":"path","path":"channels.synology-chat.dangerouslyAllowNameMatching","kind":"channel","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
28072809
{"recordType":"path","path":"channels.telegram","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["channels","network"],"label":"Telegram","help":"simplest way to get started — register a bot with @BotFather and get going.","hasChildren":true}
28082810
{"recordType":"path","path":"channels.telegram.accounts","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
28092811
{"recordType":"path","path":"channels.telegram.accounts.*","kind":"channel","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
@@ -4058,7 +4060,7 @@
40584060
{"recordType":"path","path":"models.providers.*.models.*.reasoning","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
40594061
{"recordType":"path","path":"nodeHost","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Node Host","help":"Node host controls for features exposed from this gateway node to other nodes or clients. Keep defaults unless you intentionally proxy local capabilities across your node network.","hasChildren":true}
40604062
{"recordType":"path","path":"nodeHost.browserProxy","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Node Browser Proxy","help":"Groups browser-proxy settings for exposing local browser control through node routing. Enable only when remote node workflows need your local browser profiles.","hasChildren":true}
4061-
{"recordType":"path","path":"nodeHost.browserProxy.allowProfiles","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","network","storage"],"label":"Node Browser Proxy Allowed Profiles","help":"Optional allowlist of browser profile names exposed through node proxy routing. Leave empty to expose all configured profiles, or use a tight list to enforce least-privilege profile access.","hasChildren":true}
4063+
{"recordType":"path","path":"nodeHost.browserProxy.allowProfiles","kind":"core","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":["access","network","storage"],"label":"Node Browser Proxy Allowed Profiles","help":"Optional allowlist of browser profile names exposed through node proxy routing. Leave empty to preserve the default full profile surface, including profile create/delete routes. When set, OpenClaw enforces least-privilege profile access and blocks persistent profile create/delete through the proxy.","hasChildren":true}
40624064
{"recordType":"path","path":"nodeHost.browserProxy.allowProfiles.*","kind":"core","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":false}
40634065
{"recordType":"path","path":"nodeHost.browserProxy.enabled","kind":"core","type":"boolean","required":false,"deprecated":false,"sensitive":false,"tags":["network"],"label":"Node Browser Proxy Enabled","help":"Expose the local browser control server through node proxy routing so remote clients can use this host's browser capabilities. Keep disabled unless remote automation explicitly depends on it.","hasChildren":false}
40644066
{"recordType":"path","path":"plugins","kind":"core","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Plugins","help":"Plugin system controls for enabling extensions, constraining load scope, configuring entries, and tracking installs. Keep plugin policy explicit and least-privilege in production environments.","hasChildren":true}
@@ -4082,7 +4084,7 @@
40824084
{"recordType":"path","path":"plugins.entries.acpx.config","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"ACPX Runtime Config","help":"Plugin-defined config payload for acpx.","hasChildren":true}
40834085
{"recordType":"path","path":"plugins.entries.acpx.config.command","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"acpx Command","help":"Optional path/command override for acpx (for example /home/user/repos/acpx/dist/cli.js). Leave unset to use plugin-local bundled acpx.","hasChildren":false}
40844086
{"recordType":"path","path":"plugins.entries.acpx.config.cwd","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Default Working Directory","help":"Default cwd for ACP session operations when not set per session.","hasChildren":false}
4085-
{"recordType":"path","path":"plugins.entries.acpx.config.expectedVersion","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Expected acpx Version","help":"Exact version to enforce (for example 0.1.16) or \"any\" to skip strict version matching.","hasChildren":false}
4087+
{"recordType":"path","path":"plugins.entries.acpx.config.expectedVersion","kind":"plugin","type":"string","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"Expected acpx Version","help":"Exact version to enforce or \"any\" to skip strict version matching.","hasChildren":false}
40864088
{"recordType":"path","path":"plugins.entries.acpx.config.mcpServers","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":["advanced"],"label":"MCP Servers","help":"Named MCP server definitions to inject into ACPX-backed session bootstrap. Each entry needs a command and can include args and env.","hasChildren":true}
40874089
{"recordType":"path","path":"plugins.entries.acpx.config.mcpServers.*","kind":"plugin","type":"object","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}
40884090
{"recordType":"path","path":"plugins.entries.acpx.config.mcpServers.*.args","kind":"plugin","type":"array","required":false,"deprecated":false,"sensitive":false,"tags":[],"hasChildren":true}

0 commit comments

Comments
 (0)