Skip to content

Commit 7dd7b73

Browse files
Auto-merge upstream openclaw/openclaw
2 parents 4acdeee + 42f6de1 commit 7dd7b73

221 files changed

Lines changed: 9934 additions & 157181 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.

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ ui/src/ui/views/__screenshots__
136136
ui/.vitest-attachments
137137
docs/superpowers
138138

139+
# Generated docs baseline artifacts (locally generated, only hashes tracked)
140+
docs/.generated/*.json
141+
docs/.generated/*.jsonl
142+
139143
# Deprecated changelog fragment workflow
140144
changelog/fragments/
141145

CHANGELOG.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,15 @@ Docs: https://docs.openclaw.ai
2222
- Providers/request overrides: add shared model and media request transport overrides across OpenAI-, Anthropic-, Google-, and compatible provider paths, including headers, auth, proxy, and TLS controls. (#60200)
2323
- Matrix/exec approvals: add Matrix-native exec approval prompts with account-scoped approvers, channel-or-DM delivery, and room-thread aware resolution handling. (#58635) Thanks @gumadeiras.
2424
- Agents/Claude CLI: expose OpenClaw tools to background Claude CLI runs through a loopback MCP bridge that reuses gateway tool policy, honors session/account/channel scoping, and only advertises the bridge when the local runtime is actually live. (#35676) Thanks @mylukin.
25-
- Prompt caching: keep prompt prefixes more reusable across transport fallback, compaction, and embedded image history so follow-up turns hit cache more reliably. (#59054, #60603, #60691)
25+
- Prompt caching: keep prompt prefixes more reusable across transport fallback, deterministic MCP tool ordering, compaction, and embedded image history so follow-up turns hit cache more reliably. (#58036, #58037, #58038, #59054, #60603, #60691) Thanks @bcherny.
26+
- Agents/cache: diagnostics: add prompt-cache break diagnostics, trace live cache scenarios through embedded runner paths, and show cache reuse explicitly in `openclaw status --verbose`. Thanks @vincentkoc.
27+
- Agents/cache: stabilize cache-relevant system prompt fingerprints by normalizing equivalent structured prompt whitespace, line endings, hook-added system context, and runtime capability ordering so semantically unchanged prompts reuse KV/cache more reliably. Thanks @vincentkoc.
2628

2729
### Fixes
2830

31+
- MiniMax: advertise image input on bundled `MiniMax-M2.7` and `MiniMax-M2.7-highspeed` model definitions so image-capable flows can route through the M2.7 family correctly. (#54843) Thanks @MerlinMiao88888888.
32+
- Agents/exec approvals: let `exec-approvals.json` agent security override stricter gateway tool defaults so approved subagents can use `security: "full"` without falling back to allowlist enforcement again. (#60310) Thanks @lml2468.
33+
- Tasks/maintenance: mark stale cron runs and CLI tasks backed only by long-lived chat sessions as lost again so task cleanup does not keep dead work alive indefinitely. (#60310) Thanks @lml2468.
2934
- Providers/OpenAI: preserve native `reasoning.effort: "none"` and strict tool schemas on direct OpenAI-family endpoints, keep compat routes on compat shaping, fix Responses WebSocket warm-up behavior, keep stable session and turn metadata, and fall back more gracefully after early WebSocket failures.
3035
- Providers/OpenAI Codex: split native `contextWindow` from runtime `contextTokens`, keep the default effective cap at `272000`, and expose a per-model `contextTokens` override on `models.providers.*.models[]`.
3136
- Providers/compat: stop forcing OpenAI-only defaults on proxy and custom OpenAI-compatible routes, preserve native vendor-specific reasoning/tool/streaming behavior across Anthropic-compatible, Moonshot, Mistral, ModelStudio, OpenRouter, xAI, and Z.ai endpoints, and route GitHub Copilot Claude models through Anthropic Messages instead of OpenAI Responses.
@@ -54,6 +59,9 @@ Docs: https://docs.openclaw.ai
5459
- Exec approvals: reuse durable exact-command `allow-always` approvals in allowlist mode so identical reruns stop prompting, and tighten Windows interpreter/path approval handling so wrapper and malformed-path cases fail closed more consistently. (#59880, #59780, #58040, #59182)
5560
- Agents/runtime: make default subagent allowlists, inherited skills/workspaces, and duplicate session-id resolution behave more predictably, and include value-shape hints in missing-parameter tool errors. (#59944, #59992, #59858, #55317)
5661
- Update/npm: prefer the npm binary that owns the installed global OpenClaw prefix so mixed Homebrew-plus-nvm setups update the right install. (#60153) Thanks @jayeshp19.
62+
- Gateway/plugin routes: keep gateway-auth plugin runtime routes on write-only fallback scopes unless a trusted-proxy caller explicitly declares narrower `x-openclaw-scopes`, so plugin HTTP handlers no longer mint admin-level runtime scopes on missing or untrusted HTTP scope headers. (#59815) Thanks @pgondhi987.
63+
- Agents/exec approvals: let `exec-approvals.json` agent security override stricter gateway tool defaults so approved subagents can use `security: "full"` without falling back to allowlist enforcement again. (#60310) Thanks @lml2468.
64+
- Tasks/maintenance: reconcile stale cron and chat-backed CLI task rows against live cron-job and agent-run ownership instead of treating any persisted session key as proof that the task is still running. (#60310) Thanks @lml2468.
5765

5866
## 2026.4.2
5967

@@ -170,6 +178,7 @@ Docs: https://docs.openclaw.ai
170178
- Channels/WhatsApp: pass inbound message timestamp to model context so the AI can see when WhatsApp messages were sent. (#58590) Thanks @Maninae
171179
- QQBot/voice: lazy-load `silk-wasm` in `audio-convert.ts` so qqbot still starts when the optional voice dependency is missing, while voice encode/decode degrades gracefully instead of crashing at module load time. (#58829) Thanks @WideLee.
172180
- WhatsApp/groups: fix bot waking up on self-number quoted replies in groups with `selfChatMode` enabled. (#60148) Thanks @lurebat
181+
- Device pairing: require `operator.pairing` or `operator.admin` for internal `/pair` setup-code, QR, and cleanup commands so lower-privilege gateway callers cannot mint or revoke pairing bootstrap material. (#60491) Thanks @eleqtrizit.
173182

174183
## 2026.3.31
175184

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

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class NodeRuntime(
7171

7272
private val identityStore = DeviceIdentityStore(appContext)
7373
private var connectedEndpoint: GatewayEndpoint? = null
74+
private var activeGatewayAuth: GatewayConnectAuth? = null
7475

7576
private val cameraHandler: CameraHandler = CameraHandler(
7677
appContext = appContext,
@@ -299,6 +300,11 @@ class NodeRuntime(
299300
_canvasRehydrateErrorText.value = null
300301
updateStatus()
301302
showLocalCanvasOnConnect()
303+
val endpoint = connectedEndpoint
304+
val auth = activeGatewayAuth
305+
if (endpoint != null && auth != null) {
306+
maybeStartOperatorSessionAfterNodeConnect(endpoint, auth)
307+
}
302308
},
303309
onDisconnected = { message ->
304310
_nodeConnected.value = false
@@ -800,25 +806,24 @@ class NodeRuntime(
800806
auth: GatewayConnectAuth,
801807
reconnect: Boolean = false,
802808
) {
809+
activeGatewayAuth = auth
803810
val tls = connectionManager.resolveTlsParams(endpoint)
804-
val connectOperator =
805-
shouldConnectOperatorSession(
806-
auth.token,
807-
auth.bootstrapToken,
808-
auth.password,
809-
loadStoredRoleDeviceToken("operator"),
811+
val operatorAuth =
812+
resolveOperatorSessionConnectAuth(
813+
auth = auth,
814+
storedOperatorToken = loadStoredRoleDeviceToken("operator"),
810815
)
811-
if (!connectOperator) {
816+
if (operatorAuth == null) {
812817
operatorConnected = false
813818
operatorStatusText = "Offline"
814819
operatorSession.disconnect()
815820
updateStatus()
816821
} else {
817822
operatorSession.connect(
818823
endpoint,
819-
auth.token,
820-
auth.bootstrapToken,
821-
auth.password,
824+
operatorAuth.token,
825+
operatorAuth.bootstrapToken,
826+
operatorAuth.password,
822827
connectionManager.buildOperatorConnectOptions(),
823828
tls,
824829
)
@@ -831,7 +836,7 @@ class NodeRuntime(
831836
connectionManager.buildNodeConnectOptions(),
832837
tls,
833838
)
834-
if (reconnect && connectOperator) {
839+
if (reconnect && operatorAuth != null) {
835840
operatorSession.reconnect()
836841
}
837842
if (reconnect) {
@@ -929,8 +934,33 @@ class NodeRuntime(
929934
return deviceAuthStore.loadToken(deviceId, role)
930935
}
931936

937+
private fun maybeStartOperatorSessionAfterNodeConnect(
938+
endpoint: GatewayEndpoint,
939+
auth: GatewayConnectAuth,
940+
) {
941+
if (operatorConnected || operatorStatusText == "Connecting…") {
942+
return
943+
}
944+
val operatorAuth =
945+
resolveOperatorSessionConnectAuth(
946+
auth = auth,
947+
storedOperatorToken = loadStoredRoleDeviceToken("operator"),
948+
) ?: return
949+
operatorStatusText = "Connecting…"
950+
updateStatus()
951+
operatorSession.connect(
952+
endpoint,
953+
operatorAuth.token,
954+
operatorAuth.bootstrapToken,
955+
operatorAuth.password,
956+
connectionManager.buildOperatorConnectOptions(),
957+
connectionManager.resolveTlsParams(endpoint),
958+
)
959+
}
960+
932961
fun disconnect() {
933962
connectedEndpoint = null
963+
activeGatewayAuth = null
934964
_pendingGatewayTrust.value = null
935965
operatorSession.disconnect()
936966
nodeSession.disconnect()
@@ -1266,18 +1296,47 @@ class NodeRuntime(
12661296

12671297
}
12681298

1299+
internal fun resolveOperatorSessionConnectAuth(
1300+
auth: NodeRuntime.GatewayConnectAuth,
1301+
storedOperatorToken: String?,
1302+
): NodeRuntime.GatewayConnectAuth? {
1303+
val explicitToken = auth.token?.trim()?.takeIf { it.isNotEmpty() }
1304+
if (explicitToken != null) {
1305+
return NodeRuntime.GatewayConnectAuth(
1306+
token = explicitToken,
1307+
bootstrapToken = null,
1308+
password = null,
1309+
)
1310+
}
1311+
1312+
val explicitPassword = auth.password?.trim()?.takeIf { it.isNotEmpty() }
1313+
if (explicitPassword != null) {
1314+
return NodeRuntime.GatewayConnectAuth(
1315+
token = null,
1316+
bootstrapToken = null,
1317+
password = explicitPassword,
1318+
)
1319+
}
1320+
1321+
val storedToken = storedOperatorToken?.trim()?.takeIf { it.isNotEmpty() }
1322+
if (storedToken != null) {
1323+
// Bootstrap can seed the operator token, but operator should reconnect
1324+
// through the stored device-token path rather than bootstrap auth itself.
1325+
return NodeRuntime.GatewayConnectAuth(
1326+
token = null,
1327+
bootstrapToken = null,
1328+
password = null,
1329+
)
1330+
}
1331+
1332+
return null
1333+
}
1334+
12691335
internal fun shouldConnectOperatorSession(
1270-
token: String?,
1271-
bootstrapToken: String?,
1272-
password: String?,
1336+
auth: NodeRuntime.GatewayConnectAuth,
12731337
storedOperatorToken: String?,
12741338
): Boolean {
1275-
return (
1276-
!token.isNullOrBlank() ||
1277-
!bootstrapToken.isNullOrBlank() ||
1278-
!password.isNullOrBlank() ||
1279-
!storedOperatorToken.isNullOrBlank()
1280-
)
1339+
return resolveOperatorSessionConnectAuth(auth, storedOperatorToken) != null
12811340
}
12821341

12831342
private enum class HomeCanvasGatewayState {
Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,92 @@
11
package ai.openclaw.app.gateway
22

33
import ai.openclaw.app.SecurePrefs
4+
import kotlinx.serialization.Serializable
5+
import kotlinx.serialization.encodeToString
6+
import kotlinx.serialization.json.Json
7+
8+
data class DeviceAuthEntry(
9+
val token: String,
10+
val role: String,
11+
val scopes: List<String>,
12+
val updatedAtMs: Long,
13+
)
14+
15+
@Serializable
16+
private data class PersistedDeviceAuthMetadata(
17+
val scopes: List<String> = emptyList(),
18+
val updatedAtMs: Long = 0L,
19+
)
420

521
interface DeviceAuthTokenStore {
6-
fun loadToken(deviceId: String, role: String): String?
7-
fun saveToken(deviceId: String, role: String, token: String)
22+
fun loadEntry(deviceId: String, role: String): DeviceAuthEntry?
23+
fun loadToken(deviceId: String, role: String): String? = loadEntry(deviceId, role)?.token
24+
fun saveToken(deviceId: String, role: String, token: String, scopes: List<String> = emptyList())
825
fun clearToken(deviceId: String, role: String)
926
}
1027

1128
class DeviceAuthStore(private val prefs: SecurePrefs) : DeviceAuthTokenStore {
12-
override fun loadToken(deviceId: String, role: String): String? {
29+
private val json = Json { ignoreUnknownKeys = true }
30+
31+
override fun loadEntry(deviceId: String, role: String): DeviceAuthEntry? {
1332
val key = tokenKey(deviceId, role)
14-
return prefs.getString(key)?.trim()?.takeIf { it.isNotEmpty() }
33+
val token = prefs.getString(key)?.trim()?.takeIf { it.isNotEmpty() } ?: return null
34+
val normalizedRole = normalizeRole(role)
35+
val metadata =
36+
prefs.getString(metadataKey(deviceId, role))
37+
?.let { raw ->
38+
runCatching { json.decodeFromString<PersistedDeviceAuthMetadata>(raw) }.getOrNull()
39+
}
40+
return DeviceAuthEntry(
41+
token = token,
42+
role = normalizedRole,
43+
scopes = metadata?.scopes ?: emptyList(),
44+
updatedAtMs = metadata?.updatedAtMs ?: 0L,
45+
)
1546
}
1647

17-
override fun saveToken(deviceId: String, role: String, token: String) {
48+
override fun saveToken(deviceId: String, role: String, token: String, scopes: List<String>) {
49+
val normalizedScopes = normalizeScopes(scopes)
1850
val key = tokenKey(deviceId, role)
1951
prefs.putString(key, token.trim())
52+
prefs.putString(
53+
metadataKey(deviceId, role),
54+
json.encodeToString(
55+
PersistedDeviceAuthMetadata(
56+
scopes = normalizedScopes,
57+
updatedAtMs = System.currentTimeMillis(),
58+
),
59+
),
60+
)
2061
}
2162

2263
override fun clearToken(deviceId: String, role: String) {
2364
val key = tokenKey(deviceId, role)
2465
prefs.remove(key)
66+
prefs.remove(metadataKey(deviceId, role))
2567
}
2668

2769
private fun tokenKey(deviceId: String, role: String): String {
28-
val normalizedDevice = deviceId.trim().lowercase()
29-
val normalizedRole = role.trim().lowercase()
70+
val normalizedDevice = normalizeDeviceId(deviceId)
71+
val normalizedRole = normalizeRole(role)
3072
return "gateway.deviceToken.$normalizedDevice.$normalizedRole"
3173
}
74+
75+
private fun metadataKey(deviceId: String, role: String): String {
76+
val normalizedDevice = normalizeDeviceId(deviceId)
77+
val normalizedRole = normalizeRole(role)
78+
return "gateway.deviceTokenMeta.$normalizedDevice.$normalizedRole"
79+
}
80+
81+
private fun normalizeDeviceId(deviceId: String): String = deviceId.trim().lowercase()
82+
83+
private fun normalizeRole(role: String): String = role.trim().lowercase()
84+
85+
private fun normalizeScopes(scopes: List<String>): List<String> {
86+
return scopes
87+
.map { it.trim() }
88+
.filter { it.isNotEmpty() }
89+
.distinct()
90+
.sorted()
91+
}
3292
}

apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewaySession.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -451,8 +451,8 @@ class GatewaySession(
451451
token: String,
452452
scopes: List<String>,
453453
) {
454-
if (filteredBootstrapHandoffScopes(role, scopes) == null) return
455-
deviceAuthStore.saveToken(deviceId, role, token)
454+
val filteredScopes = filteredBootstrapHandoffScopes(role, scopes) ?: return
455+
deviceAuthStore.saveToken(deviceId, role, token, filteredScopes)
456456
}
457457

458458
private fun persistIssuedDeviceToken(
@@ -467,7 +467,7 @@ class GatewaySession(
467467
persistBootstrapHandoffToken(deviceId, role, token, scopes)
468468
return
469469
}
470-
deviceAuthStore.saveToken(deviceId, role, token)
470+
deviceAuthStore.saveToken(deviceId, role, token, scopes)
471471
}
472472

473473
private fun handleConnectSuccess(

0 commit comments

Comments
 (0)