Compare commits

..

512 Commits

Author SHA1 Message Date
Peter Steinberger
c535f6d3f3 docs: document daemon shared helper contracts 2026-06-01 13:15:52 -04:00
Peter Steinberger
baaf6f8d1c docs: document CLI startup runtime helpers 2026-06-01 13:13:41 -04:00
Peter Steinberger
01e7733deb docs: document config set helper contracts 2026-06-01 13:12:12 -04:00
Peter Steinberger
66425c3406 docs: document CLI utility helper contracts 2026-06-01 13:10:43 -04:00
Peter Steinberger
f6d05d604b docs: document CLI banner contracts 2026-06-01 13:09:39 -04:00
Peter Steinberger
92a676fc71 docs: document CLI completion contracts 2026-06-01 13:08:22 -04:00
Peter Steinberger
4e5835c038 docs: document CLI command policy contracts 2026-06-01 13:07:15 -04:00
Peter Steinberger
c4686e50c2 docs: document CLI invocation display helpers 2026-06-01 13:05:49 -04:00
Peter Steinberger
3764ff6b84 docs: document CLI argv helpers 2026-06-01 13:04:38 -04:00
Peter Steinberger
242995a3af docs: document CLI root option helpers 2026-06-01 13:03:06 -04:00
Peter Steinberger
4ce258ae9b docs: document approval handler runtime contracts 2026-06-01 13:01:14 -04:00
Peter Steinberger
b132ca0183 docs: document command carrier exports 2026-06-01 12:59:09 -04:00
Peter Steinberger
4c556fc09f docs: document offsetless datetime parsing 2026-06-01 12:57:30 -04:00
Peter Steinberger
98e05f8754 docs: document duration formatter contract 2026-06-01 12:55:42 -04:00
Peter Steinberger
05df67dd70 docs: document lsof resolver contracts 2026-06-01 12:53:09 -04:00
Peter Steinberger
30d6a53681 docs: document port diagnostics contracts 2026-06-01 12:51:27 -04:00
Peter Steinberger
3c25345fd5 docs: document binary detection contract 2026-06-01 12:49:41 -04:00
Peter Steinberger
11d7a51844 docs: document git root contracts 2026-06-01 12:48:01 -04:00
Peter Steinberger
38da14ac55 docs: document package json contracts 2026-06-01 12:46:27 -04:00
Peter Steinberger
eb7ec0e620 docs: document runtime status contract 2026-06-01 12:44:36 -04:00
Peter Steinberger
9fe0862e4b docs: document WebSocket raw data contract 2026-06-01 12:43:10 -04:00
Peter Steinberger
4667b7cca2 docs: document secure random contracts 2026-06-01 12:41:28 -04:00
Peter Steinberger
a8b695a944 docs: document JSON byte contracts 2026-06-01 12:40:06 -04:00
Peter Steinberger
ba6af56f48 docs: document path prepend contracts 2026-06-01 12:38:42 -04:00
Peter Steinberger
412fb4b32e docs: document port probe contract 2026-06-01 12:37:14 -04:00
Peter Steinberger
6fa07e83bd docs: document install target contracts 2026-06-01 12:35:45 -04:00
Peter Steinberger
889fc5fa91 docs: document update channel contracts 2026-06-01 12:34:00 -04:00
Peter Steinberger
9bfb81d64e docs: document update check contracts 2026-06-01 12:31:39 -04:00
Peter Steinberger
08b953d111 docs: document update restart sentinel handoff 2026-06-01 12:28:52 -04:00
Peter Steinberger
d10427f45c docs: document restart sentinel continuations 2026-06-01 12:27:22 -04:00
Peter Steinberger
c6a49588aa docs: document entrypoint detection 2026-06-01 12:25:37 -04:00
Peter Steinberger
187cfdf385 docs: document npm registry spec contracts 2026-06-01 12:24:09 -04:00
Peter Steinberger
724bdbb1bd docs: document secret input normalization 2026-06-01 12:22:48 -04:00
Peter Steinberger
5a6a6db65d docs: document inline directive helpers 2026-06-01 12:21:36 -04:00
Peter Steinberger
daf2b631e0 docs: document delivery context helpers 2026-06-01 12:20:18 -04:00
Peter Steinberger
ba9993229f docs: document message channel helpers 2026-06-01 12:18:51 -04:00
Peter Steinberger
021252e214 docs: document shared utils contracts 2026-06-01 12:17:31 -04:00
Peter Steinberger
5c00de15f5 docs: document ssrf contracts 2026-06-01 12:16:19 -04:00
Peter Steinberger
ba97b0484d docs: document local-origin bypass contracts 2026-06-01 12:15:13 -04:00
Peter Steinberger
e2c5e19876 docs: document proxy fetch contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
b3d9bf8f55 docs: document runtime fetch contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
59e8c8a166 docs: document fetch compatibility contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
a8019540bd docs: document web push contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
55a44bb7ae docs: document Homebrew resolution contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
2821654f38 docs: document gateway discovery targets 2026-06-01 12:13:49 -04:00
Peter Steinberger
22855ab94e docs: document gateway process contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
10eec59169 docs: document host env security contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
affa47c13b docs: document restart sentinel contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
d39becd739 docs: document port diagnostics contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
eb8a1b6877 docs: document heartbeat cooldown contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
f3608d08b4 docs: document OpenAI tool choice contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
e6dec97e75 docs: document temp directory contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
80e2bfbd16 docs: document executable path contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
a5c8558689 docs: document MCP loopback contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
aca296e92b docs: document infra utility contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
dba817386a docs: document npm registry spec contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
f8d93befac docs: document gateway shared auth contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
887ebc95fa docs: document gateway method scope contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
54ebb9d08f docs: document agent stream safety contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
ce4f471206 docs: document CLI session reuse contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
ae606118b4 docs: document node pairing surface contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
30c0c1352f docs: document node pairing persistence contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
94a92a78f4 docs: document gateway security helper contracts 2026-06-01 12:13:49 -04:00
Peter Steinberger
e4539d2756 docs: document gateway update mutation contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
42f1c9e3d4 docs: document node wake rpc contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
ade91600bc docs: document node approval pairing contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
5fe49e3f9d docs: document gateway node registry contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
518eff785e docs: document gateway startup runtime contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
f1635142d8 docs: document gateway startup config contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
ee39f5d282 docs: document gateway talk session registry contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
17b35107a9 docs: document gateway talk relay contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
3d798f4e8e docs: document gateway session history contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
81dcefe261 docs: document gateway subagent lineage contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
c847c89dbb docs: document session lifecycle event contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
690d79b32a docs: document gateway active session shutdown contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
593d97b9ca docs: document gateway transcript ownership contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
5e88b8b5af docs: document gateway session patch contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
c57c27016a docs: document gateway auth resolution contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
1f0c7847a6 docs: document gateway auth secret materialization 2026-06-01 12:13:48 -04:00
Peter Steinberger
8a475b6631 docs: document gateway explicit connection policy 2026-06-01 12:13:48 -04:00
Peter Steinberger
d48a8e53bb docs: document control ui routing contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
845851cc78 docs: document gateway utility contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
03abdfea2c docs: document gateway run session key contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
eb16425492 docs: document node invoke sanitizer contract 2026-06-01 12:13:48 -04:00
Peter Steinberger
64fcdba480 docs: document gateway rate limit contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
ca0e791b4a docs: document gateway request context contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
76b69cfecb docs: document gateway shared auth generation 2026-06-01 12:13:48 -04:00
Peter Steinberger
080b453592 docs: document gateway session key contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
fa4f5044f5 docs: document gateway credential secret contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
e144252720 docs: document gateway method contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
646522aaa3 docs: document plugin main api contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
10c39f6da5 docs: document plugin node service contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
c07ae4e067 docs: document plugin command gateway contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
db0d6d750f docs: document speech realtime provider contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
e51e9c327c docs: document provider plugin tail contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
c057a31564 docs: document provider plugin identity contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
59cb1a2be3 docs: document provider setup prompt contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
ece65f4e24 docs: document provider runtime hook contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
266380f6c0 docs: document provider catalog type contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
84914e4dc8 docs: document provider auth method contracts 2026-06-01 12:13:48 -04:00
Peter Steinberger
5f51677454 docs: document provider auth type contracts 2026-06-01 12:13:47 -04:00
Peter Steinberger
39fa88a1e4 docs: document migration provider type contracts 2026-06-01 12:13:47 -04:00
Peter Steinberger
8a42725d38 docs: document migrate prompt option APIs 2026-06-01 12:13:47 -04:00
Peter Steinberger
ed4f308d28 docs: document migrate command entrypoint 2026-06-01 12:13:47 -04:00
Peter Steinberger
e58cba2797 docs: document migrate provider context helpers 2026-06-01 12:13:47 -04:00
Peter Steinberger
088d228e71 docs: document migrate output helpers 2026-06-01 12:13:47 -04:00
Peter Steinberger
cb55aa2ab1 docs: document migrate selection helpers 2026-06-01 12:13:47 -04:00
Peter Steinberger
1705b12dea docs: document plugin SDK migration runtime 2026-06-01 12:13:47 -04:00
Peter Steinberger
a605c11b6f docs: document plugin SDK migration helpers 2026-06-01 12:13:47 -04:00
Peter Steinberger
d31966726b docs: document provider tool schema compatibility 2026-06-01 12:13:47 -04:00
Peter Steinberger
9e45e0c9b6 docs: document outbound text chunking 2026-06-01 12:13:47 -04:00
Peter Steinberger
c9f51ad18d docs: document channel route identity helpers 2026-06-01 12:13:47 -04:00
Peter Steinberger
a5b42a7500 docs: document inbound debounce policy 2026-06-01 12:13:47 -04:00
Peter Steinberger
0cff3edb56 docs: document channel config matching helpers 2026-06-01 12:13:47 -04:00
Peter Steinberger
fb756242be docs: document plugin host cleanup timeout 2026-06-01 12:13:47 -04:00
Peter Steinberger
fe384065fe docs: document provider catalog helpers 2026-06-01 12:13:47 -04:00
Peter Steinberger
a8bbff2f9e docs: document channel model override resolution 2026-06-01 12:13:47 -04:00
Peter Steinberger
cceb080869 docs: document channel config helper contracts 2026-06-01 12:13:47 -04:00
Peter Steinberger
e87e873017 docs: document account snapshot field projection 2026-06-01 12:13:47 -04:00
Peter Steinberger
7764e91417 docs: document thread binding policy helpers 2026-06-01 12:13:47 -04:00
Peter Steinberger
535616c292 docs: document channel target parsing contracts 2026-06-01 12:13:47 -04:00
Peter Steinberger
62eb7259c3 docs: document direct dm access bridge 2026-06-01 12:13:47 -04:00
Peter Steinberger
e5d7cf2efc docs: document provider catalog text projection 2026-06-01 12:13:47 -04:00
Peter Steinberger
ed43f9090d docs: document gateway startup plugin scopes 2026-06-01 12:13:47 -04:00
Peter Steinberger
e634c7459e docs: document embedding provider runtime lookup 2026-06-01 12:13:47 -04:00
Peter Steinberger
2ad26392c5 docs: document memory embedding provider runtime 2026-06-01 12:13:47 -04:00
Peter Steinberger
378146c9bc docs: document memory runtime helpers 2026-06-01 12:13:47 -04:00
Peter Steinberger
d67ff3e041 docs: document bundle config helpers 2026-06-01 12:13:47 -04:00
Peter Steinberger
db2df9dd79 docs: document plugin config state 2026-06-01 12:13:47 -04:00
Peter Steinberger
d43ba91710 docs: document plugin activation context 2026-06-01 12:13:47 -04:00
Peter Steinberger
d6145ad4c2 docs: document trusted tool policy flow 2026-06-01 12:13:47 -04:00
Peter Steinberger
0c3b71ba23 docs: document provider install catalog 2026-06-01 12:13:47 -04:00
Peter Steinberger
d58a649a33 docs: document web search provider helpers 2026-06-01 12:13:47 -04:00
Peter Steinberger
c41b710ae9 docs: document web fetch provider helpers 2026-06-01 12:13:47 -04:00
Peter Steinberger
8c3e7eddfd docs: document web provider resolution 2026-06-01 12:13:47 -04:00
Peter Steinberger
09d8eae1e2 docs: document manifest model suppression 2026-06-01 12:13:47 -04:00
Peter Steinberger
786d5c1042 docs: document manifest owner policy 2026-06-01 12:13:47 -04:00
Peter Steinberger
fd88ce0039 docs: document manifest tool availability 2026-06-01 12:13:47 -04:00
Peter Steinberger
b487a2dfbb docs: document plugin scheduled turn contracts 2026-06-01 12:13:47 -04:00
Peter Steinberger
1802ed180a docs: document uninstall helper contracts 2026-06-01 12:13:47 -04:00
Peter Steinberger
0c790251e1 docs: document effective plugin id resolution 2026-06-01 12:13:47 -04:00
Peter Steinberger
52ad1b26ef docs: document optional plugin manifest rule 2026-06-01 12:13:46 -04:00
Peter Steinberger
88cefa4d3f docs: document web search credential detection 2026-06-01 12:13:46 -04:00
Peter Steinberger
ef6e5aa961 docs: document hook agent channel context 2026-06-01 12:13:46 -04:00
Peter Steinberger
2c04aea604 docs: document host hook cleanup timeout 2026-06-01 12:13:46 -04:00
Peter Steinberger
1cb93fee3e docs: document agent tool result middleware helpers 2026-06-01 12:13:46 -04:00
Peter Steinberger
247a67320a docs: document plugin source display 2026-06-01 12:13:46 -04:00
Peter Steinberger
02aee615de docs: document plugin control plane context 2026-06-01 12:13:46 -04:00
Peter Steinberger
ba7e68b271 docs: document plugin scope helpers 2026-06-01 12:13:46 -04:00
Peter Steinberger
f3b723fd9a docs: document provider auth input mode 2026-06-01 12:13:46 -04:00
Peter Steinberger
58a5c1e512 docs: document installed plugin index store paths 2026-06-01 12:13:46 -04:00
Peter Steinberger
43212e574c docs: document plugin HTTP path normalization 2026-06-01 12:13:46 -04:00
Peter Steinberger
95bd60001d docs: document provider config owner hints 2026-06-01 12:13:46 -04:00
Peter Steinberger
ac206252fa docs: document plugin conversation binding runtime 2026-06-01 12:13:46 -04:00
Peter Steinberger
7ad843234f docs: document interactive registry helpers 2026-06-01 12:13:46 -04:00
Peter Steinberger
e52c366a07 docs: document plugin tool descriptor cache 2026-06-01 12:13:46 -04:00
Peter Steinberger
681a0863f1 docs: document plugin cache primitives 2026-06-01 12:13:46 -04:00
Peter Steinberger
ee23d27ce2 docs: document plugin root cache inputs 2026-06-01 12:13:46 -04:00
Peter Steinberger
175db3e84d docs: document package entrypoint candidates 2026-06-01 12:13:46 -04:00
Peter Steinberger
07693abbca docs: document bundled public surface resolution 2026-06-01 12:13:46 -04:00
Peter Steinberger
27359abe70 docs: document host hook JSON boundary 2026-06-01 12:13:46 -04:00
Peter Steinberger
41ea42f864 docs: document setup wizard flow 2026-06-01 12:13:46 -04:00
Peter Steinberger
30f3fd75b1 docs: clarify configured binding matching 2026-06-01 12:13:46 -04:00
Peter Steinberger
4518c7f673 docs: document stateful target driver contract 2026-06-01 12:13:46 -04:00
Peter Steinberger
39b93679b5 docs: document configured binding consumer contract 2026-06-01 12:13:46 -04:00
Peter Steinberger
8eb8eef88e docs: document configured binding skips 2026-06-01 12:13:46 -04:00
Peter Steinberger
8b02c78f46 docs: document chat target prefix helpers 2026-06-01 12:13:46 -04:00
Peter Steinberger
a181224d0a docs: document status approval utilities 2026-06-01 12:13:46 -04:00
Peter Steinberger
ef8f96aeca docs: document channel module loader 2026-06-01 12:13:46 -04:00
Peter Steinberger
5f143b6361 docs: document config write ambiguity 2026-06-01 12:13:46 -04:00
Peter Steinberger
db4fb64e2f docs: document group policy warning helpers 2026-06-01 12:13:46 -04:00
Peter Steinberger
23426e4d26 docs: document channel utility entrypoints 2026-06-01 12:13:46 -04:00
Peter Steinberger
6fd7ffd4c4 docs: document status issue helpers 2026-06-01 12:13:46 -04:00
Peter Steinberger
962cae0bf9 docs: document setup wizard type contracts 2026-06-01 12:13:46 -04:00
Peter Steinberger
c2364779e0 docs: document channel adapter type contracts 2026-06-01 12:13:46 -04:00
Peter Steinberger
a17b95e2dc docs: document channel plugin type contracts 2026-06-01 12:13:46 -04:00
Peter Steinberger
2987e9bc82 docs: document outbound config types 2026-06-01 12:13:46 -04:00
Peter Steinberger
9011a31d56 docs: document setup wizard helpers 2026-06-01 12:13:46 -04:00
Peter Steinberger
55f124ed01 docs: document setup promotion helpers 2026-06-01 12:13:46 -04:00
Peter Steinberger
2a42a0e2fe docs: document bundled read helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
6a8090b7d8 docs: document approval config schema helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
88c1abb9b5 docs: document dm pairing helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
a9f3e35813 docs: document channel runtime helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
39b3364ae5 docs: document media helper APIs 2026-06-01 12:13:45 -04:00
Peter Steinberger
a9176b3e3c docs: document threading target helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
df4512571f docs: document channel state helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
ba95ba46da docs: document channel catalog helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
30678b2812 docs: document bundled channel loader 2026-06-01 12:13:45 -04:00
Peter Steinberger
6477e3c75a docs: document channel registry helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
69763d0d0e docs: document outbound loader APIs 2026-06-01 12:13:45 -04:00
Peter Steinberger
bd3683052d docs: document presentation limit invariants 2026-06-01 12:13:45 -04:00
Peter Steinberger
061cddc829 docs: document outbound helper APIs 2026-06-01 12:13:45 -04:00
Peter Steinberger
112a78b070 docs: document message action helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
c665944276 docs: document message tool APIs 2026-06-01 12:13:45 -04:00
Peter Steinberger
fd883d2eb4 docs: document message action discovery 2026-06-01 12:13:45 -04:00
Peter Steinberger
69583e9f15 docs: document stateful target builtins 2026-06-01 12:13:45 -04:00
Peter Steinberger
6234092a66 docs: document configured binding consumers 2026-06-01 12:13:45 -04:00
Peter Steinberger
d69a72f98e docs: document binding public APIs 2026-06-01 12:13:45 -04:00
Peter Steinberger
bfaaac79b6 docs: document stateful target drivers 2026-06-01 12:13:45 -04:00
Peter Steinberger
bc36755609 docs: document configured binding registry 2026-06-01 12:13:45 -04:00
Peter Steinberger
a1223825a2 docs: document binding routing helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
c05687aa34 docs: document directory adapter helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
d032288a77 docs: document directory config helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
7e71a0b4a4 docs: document setup wizard prompt helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
187dd18674 docs: document setup wizard policy helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
f2e6163788 docs: document setup wizard helper parsing 2026-06-01 12:13:45 -04:00
Peter Steinberger
f54ee04c05 docs: document setup wizard delegation 2026-06-01 12:13:45 -04:00
Peter Steinberger
d2b8293236 docs: document setup promotion keys 2026-06-01 12:13:45 -04:00
Peter Steinberger
10c99178c6 docs: document setup promotion helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
9fdc022ad0 docs: document plugin config helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
bbbb3ad27b docs: document config write wrappers 2026-06-01 12:13:45 -04:00
Peter Steinberger
824abf5fa1 docs: document config write policy helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
3de4e9e00f docs: document channel account helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
bda364fb74 docs: document account action gate 2026-06-01 12:13:45 -04:00
Peter Steinberger
0785082b8d docs: document target resolver helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
80dd8c390e docs: document channel route helper invariants 2026-06-01 12:13:45 -04:00
Peter Steinberger
df637ed2f8 docs: document channel target parser contracts 2026-06-01 12:13:45 -04:00
Peter Steinberger
69b1b3fdd3 docs: document chat target prefix helpers 2026-06-01 12:13:45 -04:00
Peter Steinberger
6ae61ffaef docs: document allowlist matcher invariants 2026-06-01 12:13:45 -04:00
Peter Steinberger
8b9b4ce082 docs: document allowlist resolution helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
75223a869d docs: document plugin allow-from helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
e3514e8d71 docs: document channel gating helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
4088a58674 docs: document channel ingress sender gates 2026-06-01 12:13:44 -04:00
Peter Steinberger
ae3f41f6c3 docs: document channel ingress allowlist helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
52e1d14e94 docs: document channel ingress identity helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
0b41911c70 docs: document channel ingress runtime invariants 2026-06-01 12:13:44 -04:00
Peter Steinberger
c3fa7f2148 docs: document channel turn helper invariants 2026-06-01 12:13:44 -04:00
Peter Steinberger
e20d87cfc3 docs: document delivery result compatibility 2026-06-01 12:13:44 -04:00
Peter Steinberger
fb94dac19d docs: document durable reply delivery 2026-06-01 12:13:44 -04:00
Peter Steinberger
c959f82d5c docs: document channel turn kernel invariants 2026-06-01 12:13:44 -04:00
Peter Steinberger
8502427352 docs: document inbound reply dispatch delivery 2026-06-01 12:13:44 -04:00
Peter Steinberger
853e32fef3 docs: document reply prefix context 2026-06-01 12:13:44 -04:00
Peter Steinberger
bbee5e456c docs: document channel typing internals 2026-06-01 12:13:44 -04:00
Peter Steinberger
e4e3a8dbc4 docs: document channel typing lifecycle 2026-06-01 12:13:44 -04:00
Peter Steinberger
596ee3c2a8 docs: document channel reply pipeline contracts 2026-06-01 12:13:44 -04:00
Peter Steinberger
e836cd8b71 docs: document channel inbound compatibility 2026-06-01 12:13:44 -04:00
Peter Steinberger
3f313b0ca9 docs: document channel config helper contracts 2026-06-01 12:13:44 -04:00
Peter Steinberger
7759b44638 docs: document channel policy helper contracts 2026-06-01 12:13:44 -04:00
Peter Steinberger
fa3e1067a6 docs: document channel send result helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
95eaf32b61 docs: document channel route helper contracts 2026-06-01 12:13:44 -04:00
Peter Steinberger
902c2d685c docs: document status helper contracts 2026-06-01 12:13:44 -04:00
Peter Steinberger
e5d1ce4f84 docs: document webhook target helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
1f6058f495 docs: document webhook memory guards 2026-06-01 12:13:44 -04:00
Peter Steinberger
95a84d98e4 docs: document webhook request guards 2026-06-01 12:13:44 -04:00
Peter Steinberger
3534f68068 docs: document plugin runtime store 2026-06-01 12:13:44 -04:00
Peter Steinberger
9d71225d39 docs: document agent harness task runtime 2026-06-01 12:13:44 -04:00
Peter Steinberger
02043fe89b docs: document agent harness task runtime scope 2026-06-01 12:13:44 -04:00
Peter Steinberger
32abf56791 docs: document detached task runtime state 2026-06-01 12:13:44 -04:00
Peter Steinberger
750bbdf09f docs: document detached task runtime dispatch 2026-06-01 12:13:44 -04:00
Peter Steinberger
afe95da1f7 docs: document detached task runtime contract 2026-06-01 12:13:44 -04:00
Peter Steinberger
da42fb0a81 docs: document task registry types 2026-06-01 12:13:44 -04:00
Peter Steinberger
ba65ce48a0 docs: document task registry summaries 2026-06-01 12:13:44 -04:00
Peter Steinberger
d7a35e7079 docs: document task domain view mappers 2026-06-01 12:13:44 -04:00
Peter Steinberger
8016ce9999 docs: document task status helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
1f9a80ca61 docs: document required completion contract 2026-06-01 12:13:44 -04:00
Peter Steinberger
a21a7ee883 docs: document task executor policy helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
1d0f43a709 docs: document blocked taskflow retry helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
f1ecfbe08f docs: document taskflow executor helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
301c84204d docs: document task owner access helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
ca78f99c96 docs: document taskflow owner access helpers 2026-06-01 12:13:44 -04:00
Peter Steinberger
1d4f70a8cd docs: document managed taskflow runtime guards 2026-06-01 12:13:44 -04:00
Peter Steinberger
a8113c72f6 docs: document managed taskflow runtime types 2026-06-01 12:13:43 -04:00
Peter Steinberger
4f48cd1413 docs: document plugin runtime task DTOs 2026-06-01 12:13:43 -04:00
Peter Steinberger
843dfafaa8 docs: document plugin runtime task types 2026-06-01 12:13:43 -04:00
Peter Steinberger
0db557a6dc docs: document plugin runtime llm types 2026-06-01 12:13:43 -04:00
Peter Steinberger
d598a239ca docs: document plugin runtime core types 2026-06-01 12:13:43 -04:00
Peter Steinberger
037cf3ed86 docs: document plugin runtime types 2026-06-01 12:13:43 -04:00
Peter Steinberger
89203a47dd docs: document runtime context registry 2026-06-01 12:13:43 -04:00
Peter Steinberger
0160c650e6 docs: document channel runtime surface types 2026-06-01 12:13:43 -04:00
Peter Steinberger
d92e91373c docs: document channel runtime contexts 2026-06-01 12:13:43 -04:00
Peter Steinberger
0f4eedd32a docs: document approval handler bootstrap 2026-06-01 12:13:43 -04:00
Peter Steinberger
a1a836f2bb docs: document exec approval channel runtime 2026-06-01 12:13:43 -04:00
Peter Steinberger
c4a8e1be9b docs: document exec approval runtime types 2026-06-01 12:13:43 -04:00
Peter Steinberger
7a070e6ca2 docs: document approval native runtime helpers 2026-06-01 12:13:43 -04:00
Peter Steinberger
904f84df05 docs: document approval route coordinator 2026-06-01 12:13:43 -04:00
Peter Steinberger
fbb050028d docs: document exec approval surface states 2026-06-01 12:13:43 -04:00
Peter Steinberger
fb78550cbb docs: document approval request filters 2026-06-01 12:13:43 -04:00
Peter Steinberger
96e9d73a64 docs: document exec approval display sanitizers 2026-06-01 12:13:43 -04:00
Peter Steinberger
365b63de19 docs: document exec approval session targets 2026-06-01 12:13:43 -04:00
Peter Steinberger
410bf91087 docs: document approval account binding helpers 2026-06-01 12:13:43 -04:00
Peter Steinberger
4d9d9d3e42 docs: document approval gateway resolver 2026-06-01 12:13:43 -04:00
Peter Steinberger
c1d56cb9b3 docs: document approval error detection 2026-06-01 12:13:43 -04:00
Peter Steinberger
6b8fd7a3cd docs: document approval turn-source routing 2026-06-01 12:13:43 -04:00
Peter Steinberger
5f9926b7fd docs: document approval view model builders 2026-06-01 12:13:43 -04:00
Peter Steinberger
4becd8dbfe docs: document approval view model unions 2026-06-01 12:13:43 -04:00
Peter Steinberger
a8a2be4f33 docs: document approval handler adapters 2026-06-01 12:13:43 -04:00
Peter Steinberger
d688f72752 docs: document lazy approval runtime adapter 2026-06-01 12:13:43 -04:00
Peter Steinberger
19d0073e5f docs: document approval handler runtime types 2026-06-01 12:13:43 -04:00
Peter Steinberger
74eacd9742 docs: document approval native runtime types 2026-06-01 12:13:43 -04:00
Peter Steinberger
22518f9820 docs: document approval native target keys 2026-06-01 12:13:43 -04:00
Peter Steinberger
3f04d320ad docs: document approval native delivery planner 2026-06-01 12:13:43 -04:00
Peter Steinberger
31420c16e1 docs: document approval native route notices 2026-06-01 12:13:43 -04:00
Peter Steinberger
4276ba3b60 docs: document approval display path helper 2026-06-01 12:13:43 -04:00
Peter Steinberger
a7b2cd5be2 docs: document exec approval surface helpers 2026-06-01 12:13:43 -04:00
Peter Steinberger
1cf7ea66e5 docs: document safe-bin runtime policy 2026-06-01 12:13:43 -04:00
Peter Steinberger
97026eab56 docs: document safe-bin argv validator 2026-06-01 12:13:43 -04:00
Peter Steinberger
6fa4e7ceb0 docs: document safe-bin semantic helpers 2026-06-01 12:13:43 -04:00
Peter Steinberger
1fe2d34e01 docs: document safe-bin profile helpers 2026-06-01 12:13:43 -04:00
Peter Steinberger
2751480168 docs: document safe-bin trust helpers 2026-06-01 12:13:43 -04:00
Peter Steinberger
930b1fc082 docs: document exec allowlist pattern matching 2026-06-01 12:13:43 -04:00
Peter Steinberger
ba9825795b docs: document shell wrapper APIs 2026-06-01 12:13:43 -04:00
Peter Steinberger
3f5bf3ac35 docs: document executable path helpers 2026-06-01 12:13:43 -04:00
Peter Steinberger
8197cdcac4 docs: document exec command resolution APIs 2026-06-01 12:13:43 -04:00
Peter Steinberger
38306a7695 docs: document exec command analysis APIs 2026-06-01 12:13:43 -04:00
Peter Steinberger
fd7b7a09d8 docs: document exec allowlist result APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
1752e50eb1 docs: document system-run approval match result 2026-06-01 12:13:42 -04:00
Peter Steinberger
92138702fb docs: document exec approval request config APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
934bf883c1 docs: document exec approval prompt policy 2026-06-01 12:13:42 -04:00
Peter Steinberger
42b0b53efa docs: document exec approval allowlist APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
6d478c61cf docs: document exec approval store APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
4a48b7efe7 docs: document exec approval decision APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
b51b9cbbf4 docs: document dispatch wrapper resolution APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
fa568259e4 docs: document exec approval reply APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
1e26fa770d docs: document dotenv loader contracts 2026-06-01 12:13:42 -04:00
Peter Steinberger
3693916c0c docs: document update channel contracts 2026-06-01 12:13:42 -04:00
Peter Steinberger
5b313c819a docs: document home directory helpers 2026-06-01 12:13:42 -04:00
Peter Steinberger
cafea5c3ef docs: document exec approval policy combinators 2026-06-01 12:13:42 -04:00
Peter Steinberger
ab6bc8d109 docs: document exec approval policy APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
db0470aece docs: document error kind contract 2026-06-01 12:13:42 -04:00
Peter Steinberger
cf49d56b74 docs: document exec approval session target 2026-06-01 12:13:42 -04:00
Peter Steinberger
2eeabc4e12 docs: document exec approval surface APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
f0af33a0ff docs: document event session routing APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
20133a58a9 docs: document approval request filters 2026-06-01 12:13:42 -04:00
Peter Steinberger
2a69d62245 docs: document container environment helpers 2026-06-01 12:13:42 -04:00
Peter Steinberger
14440032bd docs: document clipboard helper 2026-06-01 12:13:42 -04:00
Peter Steinberger
ea516f648b docs: document clawhub spec parser 2026-06-01 12:13:42 -04:00
Peter Steinberger
b71792767e docs: document channel summary APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
7939c408cf docs: document channel runtime context APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
d017bacc5a docs: document bonjour discovery APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
86ad6d9772 docs: document diagnostic event APIs 2026-06-01 12:13:42 -04:00
Peter Steinberger
0dcb3ce86b docs: document channel activity helpers 2026-06-01 12:13:42 -04:00
Peter Steinberger
a864715dd0 docs: document agent event contracts 2026-06-01 12:13:42 -04:00
Peter Steinberger
474cdce26c docs: document source reply mirror 2026-06-01 12:13:42 -04:00
Peter Steinberger
5917d8ba45 docs: document outbound session context 2026-06-01 12:13:42 -04:00
Peter Steinberger
1bc4ba9908 docs: document outbound delivery substrate 2026-06-01 12:13:42 -04:00
Peter Steinberger
6f16ee9266 docs: document session binding service 2026-06-01 12:13:42 -04:00
Peter Steinberger
2c7c7bf7f9 docs: document delivery queue storage 2026-06-01 12:13:42 -04:00
Peter Steinberger
7580daf705 docs: document delivery recovery helpers 2026-06-01 12:13:42 -04:00
Peter Steinberger
e6c50fd771 docs: document outbound message API 2026-06-01 12:13:42 -04:00
Peter Steinberger
f3aae8a380 docs: document message action runner API 2026-06-01 12:13:42 -04:00
Peter Steinberger
34c5d059aa docs: document agent delivery helpers 2026-06-01 12:13:42 -04:00
Peter Steinberger
eab3b1a6a2 docs: document npm install env helpers 2026-06-01 12:13:42 -04:00
Peter Steinberger
0edb913c13 docs: document OpenClaw exec env marker 2026-06-01 12:13:41 -04:00
Peter Steinberger
74d98e1fd7 docs: document shell env fallback 2026-06-01 12:13:41 -04:00
Peter Steinberger
082c443015 docs: document binary prerequisite helper 2026-06-01 12:13:41 -04:00
Peter Steinberger
8340b1151c docs: document control UI asset helpers 2026-06-01 12:13:41 -04:00
Peter Steinberger
89daadd478 docs: document channel status issues 2026-06-01 12:13:41 -04:00
Peter Steinberger
3d335e402a docs: document brew resolution 2026-06-01 12:13:41 -04:00
Peter Steinberger
8b5a6bda51 docs: document CLI root option parsing 2026-06-01 12:13:41 -04:00
Peter Steinberger
3ac62666ed docs: document diagnostics timeline 2026-06-01 12:13:41 -04:00
Peter Steinberger
8d5a2f5fa9 docs: document diagnostic LLM content policy 2026-06-01 12:13:41 -04:00
Peter Steinberger
0b5ead9f37 docs: document diagnostic trace context 2026-06-01 12:13:41 -04:00
Peter Steinberger
67e6f9aaba docs: document disk space helpers 2026-06-01 12:13:41 -04:00
Peter Steinberger
983c5a664c docs: document diagnostic flags 2026-06-01 12:13:41 -04:00
Peter Steinberger
b5ee774d68 docs: document embedded mode flag 2026-06-01 12:13:41 -04:00
Peter Steinberger
9676536668 docs: document package manager detection 2026-06-01 12:13:41 -04:00
Peter Steinberger
441a7cf792 docs: document browser open helpers 2026-06-01 12:13:41 -04:00
Peter Steinberger
3a35c1e806 docs: document map size pruning 2026-06-01 12:13:41 -04:00
Peter Steinberger
fbeaf41dc2 docs: document abort signal helper 2026-06-01 12:13:41 -04:00
Peter Steinberger
5590a45e7e docs: document channel activity 2026-06-01 12:13:41 -04:00
Peter Steinberger
9a551d49f3 docs: document approval gateway runtime 2026-06-01 12:13:41 -04:00
Peter Steinberger
fdae22dfea docs: document approval turn source auth 2026-06-01 12:13:41 -04:00
Peter Steinberger
552fa03822 docs: document approval display errors 2026-06-01 12:13:41 -04:00
Peter Steinberger
7e97b42a95 docs: document approval view model 2026-06-01 12:13:41 -04:00
Peter Steinberger
d3b9c5aa3e docs: document approval handler runtime 2026-06-01 12:13:41 -04:00
Peter Steinberger
78172b720b docs: document native approval runtime 2026-06-01 12:13:41 -04:00
Peter Steinberger
9ea00cf73a docs: document native approval delivery 2026-06-01 12:13:41 -04:00
Peter Steinberger
a5013c5574 docs: document native approval route notices 2026-06-01 12:13:41 -04:00
Peter Steinberger
ec7ae4fc9a docs: document approval request binding filters 2026-06-01 12:13:41 -04:00
Peter Steinberger
a9e6e4c5e3 docs: document exec approval channel runtime 2026-06-01 12:13:41 -04:00
Peter Steinberger
92e6368860 docs: document exec approval forwarder 2026-06-01 12:13:41 -04:00
Peter Steinberger
6757a52944 docs: document exec approval reply routing 2026-06-01 12:13:41 -04:00
Peter Steinberger
e011559750 docs: document system-run approval binding 2026-06-01 12:13:41 -04:00
Peter Steinberger
4d63f1ea8c docs: document system-run approval context 2026-06-01 12:13:41 -04:00
Peter Steinberger
36d1080d83 docs: document system-run command contracts 2026-06-01 12:13:41 -04:00
Peter Steinberger
d3e8a89959 docs: document exec allowlist entry contracts 2026-06-01 12:13:41 -04:00
Peter Steinberger
8b3c5d898a docs: document exec allowlist contracts 2026-06-01 12:13:41 -04:00
Peter Steinberger
0c5b962a29 docs: document safe-bin profile contracts 2026-06-01 12:13:41 -04:00
Peter Steinberger
1a43a00def docs: document safe-bin trust contracts 2026-06-01 12:13:41 -04:00
Peter Steinberger
439904eef4 docs: document safe-bin runtime policy contracts 2026-06-01 12:13:41 -04:00
Peter Steinberger
9bd5808fda docs: document exec approval policy snapshots 2026-06-01 12:13:41 -04:00
Peter Steinberger
32bf8712e9 docs: document approval display contracts 2026-06-01 12:13:41 -04:00
Peter Steinberger
2466798a08 docs: document executable path contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
797e503fe8 docs: document exec command resolution contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
c7e238a862 docs: document shell inline command scanners 2026-06-01 12:13:40 -04:00
Peter Steinberger
0097a6fb46 docs: document shell wrapper trust contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
8a3bda61e1 docs: document dispatch wrapper trust contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
fd715e0eee docs: document command carrier helper contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
f83ff78bb8 docs: document command explainer span contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
9094429658 docs: document command policy analysis contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
909d521602 docs: document command explanation summary contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
44cef2a792 docs: document command risk carrier contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
0d4dec734d docs: document inline eval detector contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
41aee0429c docs: document TCP port parser contract 2026-06-01 12:13:40 -04:00
Peter Steinberger
e1f1045d46 docs: document port diagnostics contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
a98b9ceb37 docs: document package tag input contract 2026-06-01 12:13:40 -04:00
Peter Steinberger
b6064d1cf5 docs: document update channel contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
fbbf2e6237 docs: document inline option token contract 2026-06-01 12:13:40 -04:00
Peter Steinberger
2f2c77e192 docs: document prototype key guard contract 2026-06-01 12:13:40 -04:00
Peter Steinberger
d64c80daae docs: document environment helper contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
db9ced7b9d docs: document PATH bootstrap contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
11576303ab docs: document fetch header normalization contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
c2f5594555 docs: document secret file compatibility contract 2026-06-01 12:13:40 -04:00
Peter Steinberger
df1e4177e4 docs: document JSON file helper contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
a7d11dd3c7 docs: document HTTP body guard contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
493e4ab2f9 docs: document fixed-window rate limiter contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
0f4fa29d78 docs: document backoff helper contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
f25cbad91b docs: document retry engine contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
713d4cd355 docs: document retry policy contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
2ed5feffef docs: document number coercion thresholds 2026-06-01 12:13:40 -04:00
Peter Steinberger
99d6f0f8c1 docs: document map and numeric option helpers 2026-06-01 12:13:40 -04:00
Peter Steinberger
dcbe7e30d9 docs: document dedupe cache contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
917d24f5c9 docs: document diagnostic error metadata helpers 2026-06-01 12:13:40 -04:00
Peter Steinberger
be922af1e6 docs: document shared error helpers 2026-06-01 12:13:40 -04:00
Peter Steinberger
bc3165647f docs: document reasoning tag partitioner contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
9f36c0f00c docs: document trajectory path helpers 2026-06-01 12:13:40 -04:00
Peter Steinberger
35ee75ec6b docs: document trajectory runtime writer contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
6a14ad3189 docs: document trajectory cleanup guards 2026-06-01 12:13:40 -04:00
Peter Steinberger
3451c03366 docs: document trajectory export contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
5b3d73bc90 docs: document trajectory metadata contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
97620910ef docs: document support bundle writer contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
e58cb30c44 docs: document diagnostic support export contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
158dd20e24 docs: document support log sanitizer invariants 2026-06-01 12:13:40 -04:00
Peter Steinberger
16d872f02d docs: document support redaction contracts 2026-06-01 12:13:40 -04:00
Peter Steinberger
3edc65d397 docs: document logging redaction API contracts 2026-06-01 12:13:39 -04:00
Peter Steinberger
86260867ad docs: document bounded concurrency helper contracts 2026-06-01 12:13:39 -04:00
Peter Steinberger
dd062c655c docs: document fetch timeout abort contracts 2026-06-01 12:13:39 -04:00
Peter Steinberger
b8244deddb docs: document gateway client public contracts 2026-06-01 12:13:39 -04:00
Peter Steinberger
b1fa7f0e16 docs: document device auth payload contracts 2026-06-01 12:13:39 -04:00
Peter Steinberger
eed3735edd docs: document gateway client readiness start rules 2026-06-01 12:13:39 -04:00
Peter Steinberger
9d1edb4c00 docs: document event loop readiness probe fields 2026-06-01 12:13:39 -04:00
Peter Steinberger
d39c2051d0 docs: document gateway timeout clamp rules 2026-06-01 12:13:39 -04:00
Peter Steinberger
d008a425c2 docs: document live plugin config fallback 2026-06-01 12:13:39 -04:00
Peter Steinberger
7fc4dd9d14 docs: document node presence reasons 2026-06-01 12:13:39 -04:00
Peter Steinberger
c3697c2ac1 docs: document avatar policy constants 2026-06-01 12:13:39 -04:00
Peter Steinberger
de85fcd978 docs: document OpenAI Codex auth helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
8fb987c565 docs: document plugin command runner 2026-06-01 12:13:39 -04:00
Peter Steinberger
8cacdce95e docs: document tool send target fallback 2026-06-01 12:13:39 -04:00
Peter Steinberger
fd12d434ba docs: document telegram account facade 2026-06-01 12:13:39 -04:00
Peter Steinberger
a70b17e5cb docs: document runtime store helper 2026-06-01 12:13:39 -04:00
Peter Steinberger
283dff0c19 docs: document webhook request guard helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
e2878dcf33 docs: document keyed async queue helper 2026-06-01 12:13:39 -04:00
Peter Steinberger
af9f15074f docs: document tool payload helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
e7e7e4f2f1 docs: document plugin SDK runtime helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
0ba732cf5e docs: document browser maintenance helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
a9120d2df6 docs: document safe record helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
d3106d2209 docs: document approval reaction helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
e174ddaaeb docs: document provider catalog helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
8b3a9a5617 docs: document provider onboard helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
c6fed61806 docs: document allowlist config helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
602a8e2d10 docs: document provider tool compat helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
1c31afac81 docs: document persistent dedupe helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
5ceb45d38e docs: document account setup helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
ff326e9ca5 docs: document auth and chunk helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
2991ae6fc9 docs: document assistant text helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
4f12fa1d70 docs: document command status runtime 2026-06-01 12:13:39 -04:00
Peter Steinberger
a73f42096e docs: document provider model helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
c11a3a0d78 docs: document browser config helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
c4520714c8 docs: document QA scenario helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
157e893b77 docs: document migration helper contracts 2026-06-01 12:13:39 -04:00
Peter Steinberger
f260f1bc06 docs: document status helper payloads 2026-06-01 12:13:39 -04:00
Peter Steinberger
8cba61f985 docs: document extension shared helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
48f2eef53b docs: document session policy payloads 2026-06-01 12:13:39 -04:00
Peter Steinberger
692dbb7b3f docs: document webhook guard helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
fc2e6ab07e docs: document channel diagnostics helpers 2026-06-01 12:13:39 -04:00
Peter Steinberger
00465096ce docs: document thread binding label helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
7ee37a45c4 docs: document reply prefix typing guard helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
102f1427e9 docs: document inbound debounce stream helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
bf14891ff3 docs: document direct dm allow-from helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
fc84fd8f26 docs: document channel match allowlist helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
6a10a55114 docs: document channel snapshot presence helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
6e1e89cbe9 docs: document channel config helper contracts 2026-06-01 12:13:38 -04:00
Peter Steinberger
df725c5b4e docs: document channel turn adapter contracts 2026-06-01 12:13:38 -04:00
Peter Steinberger
0c7f9ea6be docs: document channel turn delivery helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
216a2daf23 docs: document inbound reply bridge helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
a48823f18b docs: document message receive capability helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
c4d88ffc3e docs: document durable ingress queue helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
02e12555bb docs: document message outbound bridge helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
9888974144 docs: document durable message state fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
c78717b229 docs: document live message contracts 2026-06-01 12:13:38 -04:00
Peter Steinberger
d5d0090865 docs: document message receipt fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
f407e71101 docs: document channel turn helper fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
5d713e20ec docs: document inbound event context helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
c291eb6c6c docs: document conversation resolution helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
36c53e66ef docs: document route projection helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
6810dfd575 docs: document direct dm guard policy 2026-06-01 12:13:38 -04:00
Peter Steinberger
9b40fcd056 docs: document ack reaction helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
f529df5b97 docs: document channel target policy helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
f9c86a65a6 docs: document thread binding policy fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
e47f45e322 docs: document command gating fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
7ef4d676c9 docs: document mention gating fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
0c2dc54eae docs: document entry status fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
b21b889017 docs: document usage timeseries fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
6991205bd8 docs: document usage payload fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
dfdcd2aa97 docs: document node list payload fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
dc0cc1b7c1 docs: document thread binding lifecycle fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
25ce9fbb31 docs: document runtime requirement fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
ca6fd41b95 docs: document requirement metadata fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
6a6930983b docs: document store writer queue fields 2026-06-01 12:13:38 -04:00
Peter Steinberger
a1ff03b634 docs: document scoped id cache helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
4d8686c24e docs: document custom command config helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
5802610280 docs: document final tag parser helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
51c0ca2aa6 docs: document device auth store helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
f3a313bfd1 docs: document node matching helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
afa810271a docs: document shared runtime policy helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
11d9b2780b docs: document shared utility helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
4a2ce15e59 docs: document assistant error format helpers 2026-06-01 12:13:38 -04:00
Peter Steinberger
c1e7449b28 docs: document shared json schema helpers 2026-06-01 12:13:37 -04:00
Peter Steinberger
49156048f0 docs: document markdown table chunk helpers 2026-06-01 12:13:37 -04:00
Peter Steinberger
1c212ee73f docs: document markdown ir helpers 2026-06-01 12:13:37 -04:00
Peter Steinberger
63625093a1 docs: document markdown code span helpers 2026-06-01 12:13:37 -04:00
Peter Steinberger
fb59ac217c docs: document markdown render helpers 2026-06-01 12:13:37 -04:00
16245 changed files with 41903 additions and 160898 deletions

View File

@@ -22,8 +22,6 @@ Use when:
- Read dependency docs/source/types when the finding depends on external behavior.
- Reject unrealistic edge cases, speculative risks, broad rewrites, and fixes that over-complicate the codebase.
- Prefer small fixes at the right ownership boundary; no refactor unless it clearly improves the bug class.
- When an accepted finding shows a bug class or repeated pattern, inspect the current PR scope for sibling instances before fixing.
- Fix the scoped bug class at once when practical; stop at touched surfaces, owner boundaries, and clear follow-up territory.
- Keep going until structured review returns no accepted/actionable findings.
- If a review-triggered fix changes code, rerun focused tests and rerun the structured review helper.
- For security-audit suppression changes, verify accepted findings remain auditable: suppressed findings stay in structured output, active output keeps an unsuppressible suppression notice, and aggregate findings cannot hide unrelated active risk.

View File

@@ -1,8 +1,6 @@
#!/usr/bin/env node
/**
* Secret scanning alert handler for OpenClaw maintainers.
* Usage: node secret-scanning.mjs <command> [options]
*/
// Secret scanning alert handler for OpenClaw maintainers.
// Usage: node secret-scanning.mjs <command> [options]
import { spawnSync } from "node:child_process";
import crypto from "node:crypto";
@@ -59,7 +57,6 @@ function isBodyLocationType(locationType) {
return locationType === "issue_body" || locationType === "pull_request_body";
}
/** Decides whether redacting an issue/PR body requires notifying the reporter. */
export function decideBodyRedaction(currentBody, redactedBody) {
const bodyChanged = String(currentBody) !== String(redactedBody);
return {
@@ -68,7 +65,6 @@ export function decideBodyRedaction(currentBody, redactedBody) {
};
}
/** Loads redaction-result metadata for issue/PR body secret locations. */
export function loadBodyRedactionResult(locationType, resultFile) {
if (!isBodyLocationType(locationType)) {
return { notify_required: true };

View File

@@ -1,7 +1,4 @@
#!/usr/bin/env node
/**
* Heap snapshot diff utility for OpenClaw test memory leak investigations.
*/
import fs from "node:fs";
import path from "node:path";

View File

@@ -16,10 +16,6 @@ Use this with `$release-openclaw-maintainer` and `$openclaw-testing` when a rele
- Watch one parent run plus compact child summaries. Avoid broad `gh run view` polling loops; REST quota is easy to burn.
- Fetch logs only for failed or currently-blocking jobs. If quota is low, stop polling and wait for reset.
- Treat live-provider flakes separately from code failures: prove key validity, provider HTTP status, retry evidence, and exact failing lane before editing code.
- Full Release Validation parent monitors fail fast: once a required child job
fails, the parent cancels the remaining child matrix and prints the failed
job summary. Inspect that first red job instead of waiting for unrelated
matrix tails.
## Preflight
@@ -77,9 +73,6 @@ gh workflow run full-release-validation.yml \
```
Use `release_profile=stable` unless the operator explicitly asks for the broad advisory provider/media matrix. Use narrow `rerun_group` after focused fixes.
Publish with `openclaw-release-publish.yml` using `release_profile=from-validation`
unless a maintainer intentionally wants to cross-check a specific profile; the
publish workflow reads the effective profile from the full-validation manifest.
## Watch

View File

@@ -1,8 +1,4 @@
#!/usr/bin/env node
/**
* Release CI summary helper that prints parent and child workflow status for a
* full release run.
*/
import { execFileSync } from "node:child_process";
import process from "node:process";

View File

@@ -1,8 +1,4 @@
#!/usr/bin/env node
/**
* Release preflight helper that verifies required provider API keys can reach
* their model-list endpoints without printing secret values.
*/
import process from "node:process";
const args = new Map();

View File

@@ -49,21 +49,17 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
the next beta number until the matching npm package has actually published.
If a published beta needs a fix, commit the fix on the release branch and
increment to the next `-beta.N`.
- For a beta release train, keep Full Release Validation as a pre-publish gate
unless the operator explicitly waives it. Run the fast local preflight, npm
preflight, full release validation, and performance in parallel where safe.
If anything fails before npm publish, fix it on the release branch,
forward-port the fix to `main`, move the unpublished beta tag/prerelease to
the fixed commit, and rerun the affected pre-publish gates. If anything fails
after npm publish, fix it, forward-port to `main`, increment beta number, and
repeat. After each beta publish, run the published-package roster focused on
install/update/Docker/Parallels/NPM Telegram. For later beta attempts, rerun
only lanes whose evidence changed unless the fix touches broad release,
install/update, plugin, Docker, Parallels, or live QA behavior. After each
beta is live, scan current `main` once for critical fixes that landed after
the release branch cut and backport only important low-risk fixes. Operators
may authorize up to 4 autonomous beta attempts; after 4 failed beta attempts,
stop and report.
- For a beta release train, run the fast local preflight first, publish the
beta to npm `beta`, then run the expensive published-package roster focused
on install/update/Docker/Parallels/NPM Telegram. If anything fails, fix it on
the release branch, commit/push/pull, increment beta number, and repeat. Run
the full expensive roster at least once before stable/latest promotion; for
later beta attempts, rerun only lanes whose evidence changed unless the fix
touches broad release, install/update, plugin, Docker, Parallels, or live QA
behavior. After each beta is published, scan current `main` once for critical
fixes that landed after the release branch cut and backport only important
low-risk fixes. Operators may authorize up to 4 autonomous beta attempts;
after 4 failed beta attempts, stop and report.
- As soon as the release candidate SHA exists, dispatch `OpenClaw Performance`
with `target_ref=<release-sha>` in parallel with the other release work. Do
not wait for full release validation to start the performance signal.
@@ -111,10 +107,9 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
- For fallback correction tags like `vYYYY.M.D-N`, the repo version locations still stay at `YYYY.M.D`.
- “Bump version everywhere” means all version locations above except `appcast.xml`.
- Release signing and notary credentials live outside the repo in the private maintainer docs.
- Every stable OpenClaw release ships the npm package, macOS app, and signed
Windows Hub installers together. Beta releases normally ship npm/package
artifacts first and skip native app build/sign/notarize/promote unless the
operator requests native beta validation.
- Every stable OpenClaw release ships the npm package and macOS app together.
Beta releases normally ship npm/package artifacts first and skip mac app
build/sign/notarize unless the operator requests mac beta validation.
- Do not let the slower macOS signing/notary path block npm publication once
the npm preflight has passed. Keep mac validation/publish running in
parallel, publish npm from the successful npm preflight, then start published
@@ -144,17 +139,6 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
at `YYYY.M.D`, but the mac release must use a strictly higher numeric
`APP_BUILD` / Sparkle build than the original release so existing installs
see it as newer.
- Stable Windows Hub release closeout requires the signed
`OpenClawCompanion-Setup-x64.exe`, `OpenClawCompanion-Setup-arm64.exe`, and
`OpenClawCompanion-SHA256SUMS.txt` assets on the canonical
`openclaw/openclaw` GitHub Release. Use the public `Windows Node Release`
workflow after the matching `openclaw/openclaw-windows-node` release exists;
it verifies Authenticode signatures on Windows before uploading assets.
- Website Windows Hub download links should target exact canonical
`openclaw/openclaw/releases/download/vYYYY.M.D/...` assets for the current
stable release, or `releases/latest/download/...` only after verifying the
redirect resolves to that same tag, so the installable signed Windows artifact
is visible from both the GitHub release page and openclaw.ai.
## Build changelog-backed release notes
@@ -190,13 +174,6 @@ Use this skill for release and publish-time workflow. Load `$release-private` if
`CHANGELOG.md` version section, not highlights or an excerpt. When creating
or editing a release, extract from `## YYYY.M.D` through the line before the
next level-2 heading and use that complete block as the release notes.
- To update an existing GitHub Release body, resolve the numeric release id and
patch that resource with the notes file as the `body` field:
`gh api repos/openclaw/openclaw/releases/tags/vYYYY.M.D --jq .id`, then
`gh api -X PATCH repos/openclaw/openclaw/releases/<id> -F body=@/tmp/notes.md`.
Do not trust `gh release edit --notes-file` or `--input` JSON if verification
disagrees; verify with `gh api repos/openclaw/openclaw/releases/<id>` because
the tag lookup and `gh release view` can lag or show stale body text.
- When preparing release notes, scan `src/plugins/compat/registry.ts` and
`src/commands/doctor/shared/deprecation-compat.ts` for compatibility records
with `warningStarts` or `removeAfter` within 7 days after the release date.
@@ -491,10 +468,8 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
- The npm workflow and the private mac publish workflow accept
`preflight_only=true` to run validation/build/package steps without uploading
public release assets.
- Real npm publish requires a prior successful npm preflight run id and the
successful Full Release Validation run id for the same tag/SHA so the publish
job promotes the prepared tarball instead of rebuilding it and attaches the
correct release evidence.
- Real npm publish requires a prior successful npm preflight run id so the
publish job promotes the prepared tarball instead of rebuilding it.
- Real private mac publish requires a prior successful private mac preflight
run id so the publish job promotes the prepared artifacts instead of
rebuilding or renotarizing them again.
@@ -524,12 +499,11 @@ node --import tsx scripts/openclaw-npm-postpublish-verify.ts <published-version>
instead of uploading public GitHub release assets.
- Private smoke-test runs upload ad-hoc, non-notarized build artifacts as
workflow artifacts and intentionally skip stable `appcast.xml` generation.
- For stable releases, npm preflight, Full Release Validation, public mac
validation, private mac validation, and private mac preflight must all pass
before any real publish run starts. For beta releases, npm preflight and Full
Release Validation must pass before npm publish unless the operator explicitly
waives the full gate; mac beta validation is still only required when
requested.
- For stable releases, npm preflight, public mac validation, private mac
validation, and private mac preflight must all pass before any real publish
run starts. For beta releases, npm preflight plus the selected Docker,
install/update, Parallels, and release-check lanes are sufficient unless mac
beta validation was explicitly requested.
- Real publish runs may be dispatched from `main` or from a
`release/YYYY.M.D` branch. For release-branch runs, the tag must be contained
in that release branch, and the real publish must reuse a successful preflight

View File

@@ -1,156 +0,0 @@
name: Blacksmith ARM Testbox
on:
workflow_dispatch:
inputs:
testbox_id:
type: string
description: "Testbox session ID"
required: true
pull_request:
paths:
- ".github/workflows/**"
permissions:
contents: read
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
jobs:
check-arm:
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
permissions:
contents: read
name: "check-arm"
runs-on: blacksmith-16vcpu-ubuntu-2404-arm
timeout-minutes: 120
steps:
- name: Begin Testbox
uses: useblacksmith/begin-testbox@d0e04585c26905fdd92c94a09c159544c7ee1b67
with:
testbox_id: ${{ inputs.testbox_id }}
- name: Verify ARM runner
shell: bash
run: |
set -euo pipefail
runner_arch="$(uname -m)"
echo "check-arm runner architecture: ${runner_arch}"
case "$runner_arch" in
aarch64 | arm64)
;;
*)
echo "check-arm requires an ARM64 runner; got ${runner_arch}" >&2
exit 1
;;
esac
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
if [[ -z "$CHECKOUT_TOKEN" ]]; then
echo "checkout token is missing" >&2
exit 1
fi
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 5 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Prepare Testbox shell
shell: bash
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"
PNPM
sudo chmod 0755 /usr/local/bin/pnpm
- name: Hydrate Testbox provider env helper
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Run Testbox
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: success()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -139,3 +139,139 @@ jobs:
if: success()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
check-arm:
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.draft }}
permissions:
contents: read
name: "check-arm"
runs-on: blacksmith-16vcpu-ubuntu-2404-arm
timeout-minutes: 120
steps:
- name: Begin Testbox
uses: useblacksmith/begin-testbox@d0e04585c26905fdd92c94a09c159544c7ee1b67
with:
testbox_id: ${{ inputs.testbox_id }}
- name: Verify ARM runner
shell: bash
run: |
set -euo pipefail
runner_arch="$(uname -m)"
echo "check-arm runner architecture: ${runner_arch}"
case "$runner_arch" in
aarch64 | arm64)
;;
*)
echo "check-arm requires an ARM64 runner; got ${runner_arch}" >&2
exit 1
;;
esac
- name: Checkout
shell: bash
env:
CHECKOUT_REPO: ${{ github.repository }}
CHECKOUT_SHA: ${{ github.sha }}
CHECKOUT_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
workdir="$GITHUB_WORKSPACE"
if [[ -z "$CHECKOUT_TOKEN" ]]; then
echo "checkout token is missing" >&2
exit 1
fi
auth_header="$(printf 'x-access-token:%s' "$CHECKOUT_TOKEN" | base64 | tr -d '\n')"
reset_checkout_dir() {
mkdir -p "$workdir"
find "$workdir" -mindepth 1 -maxdepth 1 -exec rm -rf {} +
}
checkout_attempt() {
local attempt="$1"
reset_checkout_dir
git init "$workdir" >/dev/null
git config --global --add safe.directory "$workdir"
git -C "$workdir" remote add origin "https://github.com/${CHECKOUT_REPO}"
git -C "$workdir" config gc.auto 0
timeout --signal=TERM --kill-after=10s 30s git -C "$workdir" \
-c protocol.version=2 \
-c "http.extraheader=AUTHORIZATION: basic ${auth_header}" \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/ci-target" || return 1
git -C "$workdir" checkout --force --detach "$CHECKOUT_SHA" || return 1
test -f "$workdir/.github/actions/setup-node-env/action.yml" || return 1
echo "checkout attempt ${attempt}/5 succeeded"
}
for attempt in 1 2 3 4 5; do
if checkout_attempt "$attempt"; then
exit 0
fi
echo "checkout attempt ${attempt}/5 failed"
sleep $((attempt * 5))
done
echo "checkout failed after 5 attempts" >&2
exit 1
- name: Setup Node environment
uses: ./.github/actions/setup-node-env
with:
install-bun: "false"
- name: Prepare Testbox shell
shell: bash
run: |
set -euo pipefail
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
sudo ln -sf "$node_bin/node" /usr/local/bin/node
sudo ln -sf "$node_bin/npm" /usr/local/bin/npm
sudo ln -sf "$node_bin/npx" /usr/local/bin/npx
sudo ln -sf "$node_bin/corepack" /usr/local/bin/corepack
sudo tee /usr/local/bin/pnpm >/dev/null <<'PNPM'
#!/usr/bin/env bash
exec /usr/local/bin/corepack pnpm "$@"
PNPM
sudo chmod 0755 /usr/local/bin/pnpm
- name: Hydrate Testbox provider env helper
shell: bash
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}
KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }}
MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}
MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}
MOONSHOT_API_KEY: ${{ secrets.MOONSHOT_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: ${{ secrets.OPENAI_BASE_URL }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
QWEN_API_KEY: ${{ secrets.QWEN_API_KEY }}
TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}
XAI_API_KEY: ${{ secrets.XAI_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
Z_AI_API_KEY: ${{ secrets.Z_AI_API_KEY }}
run: bash scripts/ci-hydrate-testbox-env.sh
- name: Run Testbox
uses: useblacksmith/run-testbox@5ca05834db1d3813554d1dd109e5f2087a8d7cbc
if: success()
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"

View File

@@ -92,7 +92,7 @@ jobs:
for attempt in 1 2 3; do
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=2 origin \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${ref}:refs/remotes/origin/checkout" && return 0
fetch_status="$?"
if [ "$fetch_status" != "124" ] && [ "$fetch_status" != "137" ]; then
@@ -146,12 +146,12 @@ jobs:
if [ "${{ github.event_name }}" = "push" ]; then
BASE="${{ github.event.before }}"
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
else
BASE="${{ github.event.pull_request.base.sha }}"
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD --merge-head-first-parent
fi
node scripts/ci-changed-scope.mjs --base "$BASE" --head HEAD
- name: Build CI manifest
id: manifest
env:
@@ -605,19 +605,7 @@ jobs:
restore-keys: |
${{ runner.os }}-build-all-v3-
- name: Restore dist build cache
id: dist_build_cache
uses: actions/cache/restore@v5
with:
path: |
dist/
dist-runtime/
extensions/*/src/host/**/.bundle.hash
extensions/*/src/host/**/*.bundle.js
key: ${{ runner.os }}-dist-build-${{ needs.preflight.outputs.checkout_revision }}
- name: Build dist
if: steps.dist_build_cache.outputs.cache-hit != 'true'
env:
NODE_OPTIONS: --max-old-space-size=8192
run: pnpm build:ci-artifacts
@@ -626,6 +614,14 @@ jobs:
if: needs.preflight.outputs.run_control_ui_i18n == 'true'
run: pnpm ui:i18n:check
- name: Cache dist build
uses: actions/cache@v5
with:
path: |
dist/
dist-runtime/
key: ${{ runner.os }}-dist-build-${{ needs.preflight.outputs.checkout_revision }}
- name: Pack built runtime artifacts
run: tar --posix -cf dist-runtime-build.tar.zst --use-compress-program zstdmt dist dist-runtime
@@ -755,18 +751,6 @@ jobs:
done
exit "$failures"
- name: Save dist build cache
if: steps.dist_build_cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
continue-on-error: true
with:
path: |
dist/
dist-runtime/
extensions/*/src/host/**/.bundle.hash
extensions/*/src/host/**/*.bundle.js
key: ${{ steps.dist_build_cache.outputs.cache-primary-key }}
- name: Upload gateway watch regression artifacts
if: always() && needs.preflight.outputs.run_check_additional == 'true'
uses: actions/upload-artifact@v7
@@ -1167,8 +1151,7 @@ jobs:
OPENCLAW_NODE_TEST_CONFIGS_JSON: ${{ toJson(matrix.configs) }}
OPENCLAW_NODE_TEST_INCLUDE_PATTERNS_JSON: ${{ toJson(matrix.includePatterns) }}
OPENCLAW_VITEST_SHARD_NAME: ${{ matrix.shard_name }}
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "300000"
OPENCLAW_VITEST_NO_OUTPUT_RETRY: "1"
OPENCLAW_VITEST_NO_OUTPUT_TIMEOUT_MS: "900000"
OPENCLAW_TEST_PROJECTS_PARALLEL: "2"
shell: bash
run: |

View File

@@ -32,11 +32,11 @@ permissions:
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true"
PNPM_CONFIG_CHILD_CONCURRENCY: "1"
PNPM_CONFIG_MODULES_DIR: "/var/tmp/openclaw-pnpm/node_modules"
PNPM_CONFIG_MODULES_DIR: "/tmp/openclaw-pnpm-node-modules"
PNPM_CONFIG_NETWORK_CONCURRENCY: "1"
PNPM_CONFIG_STORE_DIR: "/var/cache/crabbox/pnpm/store"
PNPM_CONFIG_STORE_DIR: "/tmp/openclaw-pnpm-store"
PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN: "false"
PNPM_CONFIG_VIRTUAL_STORE_DIR: "/var/tmp/openclaw-pnpm/virtual-store"
PNPM_CONFIG_VIRTUAL_STORE_DIR: "/tmp/openclaw-pnpm-virtual-store"
jobs:
hydrate:
@@ -120,27 +120,6 @@ jobs:
append_pnpm_option_arg PNPM_CONFIG_MODULES_DIR modules-dir
append_pnpm_option_arg PNPM_CONFIG_NETWORK_CONCURRENCY network-concurrency
append_pnpm_option_arg PNPM_CONFIG_VIRTUAL_STORE_DIR virtual-store-dir
require_safe_writable_dir() {
local dir="$1"
if [ -L "$dir" ] || [ ! -d "$dir" ] || [ ! -w "$dir" ]; then
echo "::error::Refusing unsafe pnpm directory: $dir"
exit 1
fi
}
prepare_crabbox_pnpm_dirs() {
local volatile_root="/var/tmp/openclaw-pnpm"
case "${PNPM_CONFIG_MODULES_DIR:?}" in "$volatile_root"/*) ;; *) echo "::error::PNPM_CONFIG_MODULES_DIR must stay under $volatile_root"; exit 1 ;; esac
case "${PNPM_CONFIG_VIRTUAL_STORE_DIR:?}" in "$volatile_root"/*) ;; *) echo "::error::PNPM_CONFIG_VIRTUAL_STORE_DIR must stay under $volatile_root"; exit 1 ;; esac
rm -rf -- "$volatile_root"
mkdir -p "$volatile_root" "$PNPM_CONFIG_STORE_DIR"
require_safe_writable_dir "$volatile_root"
require_safe_writable_dir "$PNPM_CONFIG_STORE_DIR"
mkdir -p "$PNPM_CONFIG_MODULES_DIR" "$PNPM_CONFIG_VIRTUAL_STORE_DIR"
}
prepare_crabbox_pnpm_dirs
if [ -L node_modules ] && [ "$(readlink node_modules)" = "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
rm -f node_modules
fi
if [ -n "${PNPM_CONFIG_MODULES_DIR:-}" ]; then
mkdir -p "$PNPM_CONFIG_MODULES_DIR"
ln -sfn . "$PNPM_CONFIG_MODULES_DIR/node_modules"
@@ -378,10 +357,9 @@ jobs:
$env:XDG_CACHE_HOME = Join-Path $cacheRoot "cache"
$env:COREPACK_HOME = Join-Path $env:XDG_CACHE_HOME "corepack"
$env:PNPM_HOME = Join-Path $cacheRoot "pnpm-home"
$pnpmCacheRoot = Join-Path $cacheRoot "openclaw-pnpm"
$env:PNPM_CONFIG_STORE_DIR = Join-Path $pnpmCacheRoot "store"
$env:PNPM_CONFIG_MODULES_DIR = Join-Path $pnpmCacheRoot "node_modules"
$env:PNPM_CONFIG_VIRTUAL_STORE_DIR = Join-Path $pnpmCacheRoot "virtual-store"
$env:PNPM_CONFIG_STORE_DIR = Join-Path $cacheRoot "openclaw-pnpm-store"
$env:PNPM_CONFIG_MODULES_DIR = Join-Path $cacheRoot "openclaw-pnpm-node-modules"
$env:PNPM_CONFIG_VIRTUAL_STORE_DIR = Join-Path $env:PNPM_CONFIG_MODULES_DIR ".pnpm"
$env:PNPM_CONFIG_CHILD_CONCURRENCY = "4"
$env:PNPM_CONFIG_NETWORK_CONCURRENCY = "8"
$env:PNPM_CONFIG_VERIFY_DEPS_BEFORE_RUN = "false"

View File

@@ -4,7 +4,6 @@ on:
push:
tags:
- "v*"
- "!v*-alpha.*"
paths-ignore:
- "docs/**"
- "**/*.md"
@@ -39,11 +38,7 @@ jobs:
RELEASE_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
if [[ "${RELEASE_TAG}" == *"-alpha."* ]]; then
echo "Docker alpha image publishing is disabled."
exit 1
fi
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-beta\.[1-9][0-9]*)?$ ]]; then
if [[ ! "${RELEASE_TAG}" =~ ^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*(-(alpha|beta)\.[1-9][0-9]*)?$ ]]; then
echo "Invalid release tag: ${RELEASE_TAG}"
exit 1
fi

View File

@@ -229,7 +229,7 @@ jobs:
needs: [resolve_target]
if: inputs.rerun_group == 'all'
runs-on: ubuntu-24.04
timeout-minutes: 20
timeout-minutes: 45
permissions:
contents: read
steps:
@@ -245,11 +245,54 @@ jobs:
DOCKER_BUILDKIT: "1"
run: |
set -euo pipefail
timeout --kill-after=30s 15m docker build \
timeout --kill-after=30s 35m docker build \
--target runtime-assets \
--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \
.
- name: Build and smoke test final Docker runtime image
env:
DOCKER_BUILDKIT: "1"
TARGET_SHA: ${{ needs.resolve_target.outputs.sha }}
run: |
set -euo pipefail
image_ref="openclaw-release-runtime-smoke:${TARGET_SHA}"
timeout --kill-after=30s 35m docker build \
--build-arg OPENCLAW_EXTENSIONS="diagnostics-otel,codex" \
-t "${image_ref}" \
.
docker run --rm --entrypoint /bin/sh "${image_ref}" -lc '
set -eu
test -f /app/src/agents/templates/HEARTBEAT.md
temp_root="$(mktemp -d)"
trap "rm -rf \"${temp_root}\"" EXIT
mkdir -p "${temp_root}/home" "${temp_root}/cwd"
cd "${temp_root}/cwd"
set +e
HOME="${temp_root}/home" \
USERPROFILE="${temp_root}/home" \
OPENCLAW_HOME="${temp_root}/home" \
OPENCLAW_NO_ONBOARD=1 \
OPENCLAW_SUPPRESS_NOTES=1 \
OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 \
OPENCLAW_DISABLE_BUNDLED_ENTRY_SOURCE_FALLBACK=1 \
AWS_EC2_METADATA_DISABLED=true \
AWS_SHARED_CREDENTIALS_FILE="${temp_root}/home/.aws/credentials" \
AWS_CONFIG_FILE="${temp_root}/home/.aws/config" \
node /app/openclaw.mjs agent --message "workspace bootstrap smoke" --session-id "workspace-bootstrap-smoke" --local --timeout 1 --json \
>"${temp_root}/out.log" 2>&1
status="$?"
set -e
if grep -F "Missing workspace template:" "${temp_root}/out.log"; then
cat "${temp_root}/out.log"
exit 1
fi
test -f "${temp_root}/home/.openclaw/workspace/HEARTBEAT.md"
if [ "${status}" -ne 0 ]; then
cat "${temp_root}/out.log"
fi
'
normal_ci:
name: Run normal full CI
needs: [resolve_target, docker_runtime_assets_preflight]
@@ -337,21 +380,6 @@ jobs:
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
}
fail_fast_failed_jobs() {
local failed_jobs_json
failed_jobs_json="$(
fetch_child_jobs |
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
)"
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
cancel_child
trap - EXIT INT TERM
exit 1
fi
}
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
@@ -367,9 +395,6 @@ jobs:
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 2 == 0 )); then
fail_fast_failed_jobs
fi
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
@@ -485,21 +510,6 @@ jobs:
gh_with_retry api --paginate "repos/${GITHUB_REPOSITORY}/actions/runs/${run_id}/jobs?per_page=100" --jq '.jobs[]'
}
fail_fast_failed_jobs() {
local failed_jobs_json
failed_jobs_json="$(
fetch_child_jobs |
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
)"
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
cancel_child
trap - EXIT INT TERM
exit 1
fi
}
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
@@ -515,9 +525,6 @@ jobs:
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 2 == 0 )); then
fail_fast_failed_jobs
fi
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
@@ -683,24 +690,6 @@ jobs:
[[ "$saw_advisory" == "1" && "$failed" == "0" ]]
}
fail_fast_failed_jobs() {
local failed_jobs_json
if [[ "$workflow" == "openclaw-release-checks.yml" && "$CHILD_WORKFLOW_REF" =~ ^tideclaw/alpha/[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}Z$ ]]; then
return 0
fi
failed_jobs_json="$(
fetch_child_jobs |
jq -s '[.[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
)"
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
echo "::error::${workflow} has failed child jobs before the workflow completed; cancelling the remaining matrix."
jq '.[] | {name, conclusion, url: .html_url}' <<< "$failed_jobs_json"
cancel_child
trap - EXIT INT TERM
exit 1
fi
}
cancel_child() {
if [[ -n "${run_id:-}" ]]; then
echo "Cancelling child workflow ${workflow}: ${run_id}" >&2
@@ -716,9 +705,6 @@ jobs:
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 2 == 0 )); then
fail_fast_failed_jobs
fi
if (( poll_count % 10 == 0 )); then
echo "Still waiting on ${workflow}: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
fetch_child_jobs | jq 'select(.status != "completed") | {name, status, url: .html_url}' || true
@@ -976,21 +962,6 @@ jobs:
}
trap cancel_child EXIT INT TERM
fail_fast_failed_jobs() {
local failed_jobs_json
failed_jobs_json="$(
gh_with_retry run view "$run_id" --json jobs \
--jq '[.jobs[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]'
)"
if jq -e 'length > 0' <<< "$failed_jobs_json" >/dev/null; then
echo "::error::npm-telegram-beta-e2e.yml has failed child jobs before the workflow completed; cancelling the remaining run."
jq '.[] | {name, conclusion, url}' <<< "$failed_jobs_json"
cancel_child
trap - EXIT INT TERM
exit 1
fi
}
poll_count=0
while true; do
status="$(gh_with_retry run view "$run_id" --json status --jq '.status')"
@@ -998,9 +969,6 @@ jobs:
break
fi
poll_count=$((poll_count + 1))
if (( poll_count % 2 == 0 )); then
fail_fast_failed_jobs
fi
if (( poll_count % 10 == 0 )); then
echo "Still waiting on npm-telegram-beta-e2e.yml: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}"
gh_with_retry run view "$run_id" --json jobs --jq '.jobs[] | select(.status != "completed") | {name, status, url}' || true

View File

@@ -563,7 +563,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_repo_e2e && inputs.live_suite_filter == ''
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ inputs.release_test_profile == 'full' && 90 || 60 }}
env:
OPENCLAW_VITEST_MAX_WORKERS: "2"
@@ -595,7 +595,7 @@ jobs:
needs: validate_selected_ref
if: inputs.include_repo_e2e && (inputs.live_suite_filter == '' || inputs.live_suite_filter == 'openshell-e2e')
continue-on-error: ${{ inputs.advisory }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-32vcpu-ubuntu-2404' }}
runs-on: ${{ inputs.use_github_hosted_runners && 'ubuntu-24.04' || 'blacksmith-8vcpu-ubuntu-2404' }}
timeout-minutes: ${{ matrix.timeout_minutes }}
strategy:
fail-fast: false

View File

@@ -46,12 +46,11 @@ on:
default: true
type: boolean
release_profile:
description: Release coverage profile used for release evidence summaries; default reads it from the validation manifest
description: Release coverage profile used for release evidence summaries
required: false
default: from-validation
default: beta
type: choice
options:
- from-validation
- beta
- stable
- full
@@ -136,9 +135,9 @@ jobs:
exit 1
fi
case "$RELEASE_PROFILE" in
from-validation|beta|stable|full) ;;
beta|stable|full) ;;
*)
echo "release_profile must be one of: from-validation, beta, stable, full" >&2
echo "release_profile must be one of: beta, stable, full" >&2
exit 1
;;
esac
@@ -260,7 +259,6 @@ jobs:
echo "sha=$release_sha" >> "$GITHUB_OUTPUT"
- name: Validate full release validation manifest
id: full_manifest
if: ${{ inputs.publish_openclaw_npm }}
env:
GH_TOKEN: ${{ github.token }}
@@ -291,7 +289,7 @@ jobs:
echo "Full release validation target SHA mismatch: expected $EXPECTED_SHA, got $target_sha" >&2
exit 1
fi
if [[ "$EXPECTED_RELEASE_PROFILE" != "from-validation" && "$release_profile" != "$EXPECTED_RELEASE_PROFILE" ]]; then
if [[ "$release_profile" != "$EXPECTED_RELEASE_PROFILE" ]]; then
echo "Full release validation profile mismatch: expected $EXPECTED_RELEASE_PROFILE, got $release_profile" >&2
exit 1
fi
@@ -299,7 +297,6 @@ jobs:
echo "Full release validation must run rerun_group=all before npm publish; got $rerun_group" >&2
exit 1
fi
echo "release_profile=$release_profile" >> "$GITHUB_OUTPUT"
- name: Validate release tag is reachable from a trusted release branch
env:
@@ -335,7 +332,7 @@ jobs:
env:
RELEASE_TAG: ${{ inputs.tag }}
TARGET_SHA: ${{ steps.manifest.outputs.sha || steps.ref.outputs.sha }}
RELEASE_PROFILE: ${{ steps.full_manifest.outputs.release_profile || inputs.release_profile }}
RELEASE_PROFILE: ${{ inputs.release_profile }}
FULL_RELEASE_VALIDATION_RUN_ID: ${{ inputs.full_release_validation_run_id }}
run: |
{
@@ -504,7 +501,7 @@ jobs:
wait_for_run() {
local workflow="$1"
local run_id="$2"
local status conclusion url updated_at created_at duration_seconds duration_label last_state failed_json
local status conclusion url updated_at created_at duration_seconds duration_label last_state
last_state=""
while true; do
@@ -513,14 +510,6 @@ jobs:
if [[ "$status" == "completed" ]]; then
break
fi
failed_json="$(gh run view --repo "$GITHUB_REPOSITORY" "$run_id" --json jobs \
--jq '[.jobs[] | select(.status == "completed" and .conclusion != "success" and .conclusion != "skipped")]' || true)"
if [[ -n "${failed_json}" ]] && jq -e 'length > 0' <<< "$failed_json" >/dev/null; then
echo "${workflow} has failed jobs before the workflow completed: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${run_id}" >&2
jq '.[] | {name, conclusion, url}' <<< "$failed_json" >&2 || true
print_failed_run_summary "${run_id}"
return 1
fi
url="$(printf '%s' "$run_json" | jq -r '.url')"
updated_at="$(printf '%s' "$run_json" | jq -r '.updatedAt')"
state="${status}:${updated_at}"

View File

@@ -44,7 +44,7 @@ jobs:
uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
fetch-depth: 2
fetch-depth: 1
fetch-tags: false
persist-credentials: false
submodules: false
@@ -74,7 +74,6 @@ jobs:
- name: Run opengrep on PR diff
env:
OPENCLAW_OPENGREP_BASE_REF: ${{ github.event.pull_request.base.sha }}...HEAD
OPENCLAW_OPENGREP_MERGE_HEAD_FIRST_PARENT: "1"
# Findings from precise rules block this workflow. Pull requests scan
# changed first-party source paths only so findings stay attributable to
# the PR diff. Test/fixture/QA path exclusions live in `.semgrepignore`

View File

@@ -818,7 +818,6 @@ jobs:
OPENCLAW_QA_CONVEX_SECRET_CI: ${{ secrets.OPENCLAW_QA_CONVEX_SECRET_CI }}
OPENCLAW_QA_REDACT_PUBLIC_METADATA: "1"
OPENCLAW_QA_SLACK_CAPTURE_CONTENT: "1"
OPENCLAW_QA_TRANSPORT_READY_TIMEOUT_MS: "180000"
INPUT_SCENARIO: ${{ github.event_name == 'workflow_dispatch' && inputs.slack_scenario || '' }}
run: |
set -euo pipefail

View File

@@ -27,9 +27,7 @@ env:
jobs:
tui-pty:
runs-on: ubuntu-24.04
timeout-minutes: 8
env:
OPENCLAW_TUI_PTY_INCLUDE_LOCAL: "1"
timeout-minutes: 5
steps:
- name: Checkout
uses: actions/checkout@v6
@@ -40,4 +38,4 @@ jobs:
install-bun: "false"
- name: Run TUI PTY tests
run: timeout --kill-after=30s 240s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts
run: timeout --kill-after=30s 120s node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts

View File

@@ -1,126 +0,0 @@
name: Windows Node Release
on:
workflow_dispatch:
inputs:
tag:
description: Existing OpenClaw release tag to receive Windows Hub installers, for example v2026.6.1
required: true
type: string
windows_node_tag:
description: openclaw-windows-node release tag to promote, or latest
required: true
default: latest
type: string
permissions:
contents: write
concurrency:
group: windows-node-release-${{ inputs.tag }}
cancel-in-progress: false
jobs:
promote_signed_windows_installers:
name: Promote signed Windows installers
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: Validate inputs
shell: pwsh
env:
RELEASE_TAG: ${{ inputs.tag }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
GH_TOKEN: ${{ github.token }}
run: |
if ($env:RELEASE_TAG -notmatch '^v[0-9]{4}\.[1-9][0-9]*\.[1-9][0-9]*((-(alpha|beta)\.[1-9][0-9]*)|(-[1-9][0-9]*))?$') {
throw "Invalid OpenClaw release tag: $env:RELEASE_TAG"
}
if ($env:WINDOWS_NODE_TAG -ne "latest" -and $env:WINDOWS_NODE_TAG -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$') {
throw "Invalid openclaw-windows-node release tag: $env:WINDOWS_NODE_TAG"
}
gh release view $env:RELEASE_TAG --repo $env:GITHUB_REPOSITORY | Out-Null
- name: Download Windows Hub release installers
shell: pwsh
env:
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
GH_TOKEN: ${{ github.token }}
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
$tagArgs = @()
if ($env:WINDOWS_NODE_TAG -ne "latest") {
$tagArgs += $env:WINDOWS_NODE_TAG
}
gh release download @tagArgs `
--repo openclaw/openclaw-windows-node `
--pattern "OpenClawCompanion-Setup-*.exe" `
--dir dist
$expected = @(
"dist/OpenClawCompanion-Setup-x64.exe",
"dist/OpenClawCompanion-Setup-arm64.exe"
)
foreach ($file in $expected) {
if (-not (Test-Path -LiteralPath $file)) {
throw "Missing expected Windows installer: $file"
}
}
- name: Verify Authenticode signatures
shell: pwsh
run: |
Get-ChildItem -LiteralPath dist -Filter "OpenClawCompanion-Setup-*.exe" | ForEach-Object {
$signature = Get-AuthenticodeSignature -LiteralPath $_.FullName
if ($signature.Status -ne "Valid") {
throw "$($_.Name) Authenticode signature was $($signature.Status)."
}
if (-not $signature.SignerCertificate) {
throw "$($_.Name) has no signer certificate."
}
[pscustomobject]@{
File = $_.Name
Signer = $signature.SignerCertificate.Subject
Thumbprint = $signature.SignerCertificate.Thumbprint
} | Format-List
}
- name: Write SHA-256 manifest
shell: pwsh
run: |
Get-ChildItem -LiteralPath dist -Filter "OpenClawCompanion-Setup-*.exe" |
Sort-Object Name |
ForEach-Object {
$hash = Get-FileHash -Algorithm SHA256 -LiteralPath $_.FullName
"$($hash.Hash.ToLowerInvariant()) $($_.Name)"
} | Set-Content -Encoding utf8NoBOM -Path dist/OpenClawCompanion-SHA256SUMS.txt
- name: Upload to OpenClaw release
shell: pwsh
env:
RELEASE_TAG: ${{ inputs.tag }}
GH_TOKEN: ${{ github.token }}
run: |
gh release upload $env:RELEASE_TAG `
dist/OpenClawCompanion-Setup-x64.exe `
dist/OpenClawCompanion-Setup-arm64.exe `
dist/OpenClawCompanion-SHA256SUMS.txt `
--repo $env:GITHUB_REPOSITORY `
--clobber
- name: Summary
shell: pwsh
env:
RELEASE_TAG: ${{ inputs.tag }}
WINDOWS_NODE_TAG: ${{ inputs.windows_node_tag }}
run: |
@"
## Windows Hub installers promoted
OpenClaw release: $env:RELEASE_TAG
Source release: openclaw/openclaw-windows-node@$env:WINDOWS_NODE_TAG
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-Setup-x64.exe
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-Setup-arm64.exe
- https://github.com/openclaw/openclaw/releases/download/$env:RELEASE_TAG/OpenClawCompanion-SHA256SUMS.txt
"@ >> $env:GITHUB_STEP_SUMMARY

View File

@@ -2,65 +2,25 @@
Docs: https://docs.openclaw.ai
## 2026.6.2
### Highlights
- Plugin and skill installs now use an operator install policy instead of the old dangerous-code scanner path, with clearer doctor, CLI, ClawHub, and troubleshooting surfaces for package, archive, source, upload, and marketplace installs. (#89516) Thanks @joshavant.
- Telegram, Feishu, Discord, WhatsApp, and outbound delivery paths got safer around duplicate transcript mirrors, Telegram admin writeback, streamed-final previews, approval allowlists, setup runtime state, poll modifiers, Discord voice errors, and internal progress traces. (#88973, #89626, #89812, #89035, #89814, #89813, #89601) Thanks @pgondhi987, @Petru2224, @zhangguiping-xydt, @codezz, and @takhoffman.
- Chat, Control UI, Skill Workshop, Workboard, Android companion shell, and WebChat flows now preserve visible streaming text, reconcile completed sends, expose ACK timing, add Workboard keyboard movement, harden dialog accessibility, lazy-load usage views, keep current chat toggles working, and improve Android companion-first shell navigation. (#89801, #89777, #89802) Thanks @vincentkoc.
- Security, policy, and config recovery now reject corrupt shell snapshots, unsupported policy keys, unsafe exec approval precheck environments, malformed script limits, and suspicious gateway startup configs while adding data-handling conformance checks. (#89701, #87074, #81488, #87056, #89480) Thanks @RomneyDa, @giodl73-repo, and @mmaps.
- Gateway, agent, Codex, provider, model, and memory paths now recover session write-lock release failures, abandoned Codex app-server startups, stream-to-parent ACP spawns, custom-provider runtime fanout, bundled provider aliases, prompt-cache boundaries, Gemini stop sequences, Kimi cache markers, and watcher pressure warnings. (#89811, #89244) Thanks @RomneyDa and @takhoffman.
- Release, CI, Docker, Crabbox/Testbox, package, and E2E validation lanes now bound more network calls, malformed numeric limits, process groups, cleanup leaks, package hydration paths, Windows installer publishing, release asset verification, and log drains so failures produce bounded proof instead of hanging.
### Changes
- Plugins/security: replace dangerous-code scanner enforcement with operator install policy, install-policy context, doctor checks, install/update CLI wiring, ClawHub metadata paths, and package/archive/source/upload lifecycle coverage. (#89516) Thanks @joshavant.
- Policy: add data-handling conformance checks and reject unsupported policy keys. (#87056, #87074) Thanks @giodl73-repo.
- Telegram/channels: show commentary and reasoning in progress drafts, share progress draft compositors across channel plugins, and keep Telegram polling stop/reset boundaries cheaper and more reliable.
- UI/mobile: add Workboard keyboard movement controls, tighten Workboard card operations, improve Android companion-first shell UX, and document chat ACK timing metadata. (#89802) Thanks @vincentkoc.
- Release metadata: align the root package, publishable plugin manifests, generated shrinkwraps, appcast, iOS, Android, macOS, Matrix plugin changelog, and docs/generated baselines with the 2026.6.2 beta train.
- Release/packaging: promote Windows node installer publishing, require verified Windows release asset links, and document GitHub release-note edits.
- Docs: refresh Windows Hub setup guidance and document Gateway, CLI, and plugin SDK helper contracts.
### Fixes
- Channels/outbound: keep channel sends durable when transcript mirroring fails, stop schema-padded poll modifiers from blocking normal sends, preserve WebChat `sessions_send` handoffs, preserve Discord channel-label suppression while hiding internal agent failure traces, match Discord libopus error shapes, and sanitize Discord tool progress scaffolding. (#89626, #89812, #89601) Thanks @Petru2224, @codezz, and @takhoffman.
- Telegram/Feishu: require admin rights for Telegram target writeback, keep Telegram DM exec approval allowlists working with `ask:off`, prevent Telegram preview duplication across streaming modes, isolate verbose status after streamed finals, cancel clean restart stop timers, slow polling restart storms, and wire Feishu setup runtime setters. (#88973, #89035, #89813, #89814) Thanks @pgondhi987, @zhangguiping-xydt, and @takhoffman.
- Feishu: preserve full streaming card content by sending the merged text on each update instead of only the latest delta, so card readers see complete output when intermediate frames are missed. (#90181) Thanks @mushuiyu886.
- Chat/UI/Gateway: preserve visible chat stream text, clear stale stream buffers before terminal commits, reconcile completed sends, scroll pending sends into view, harden Workboard dialog accessibility, stabilize WebChat prompt-cache affinity, overlap chat catalog startup, render chat history incrementally, lazy-load usage dashboard, and report gateway health auth diagnostics. (#89337) Thanks @RomneyDa.
- Agents/Codex/providers/models: release session write locks when prompt-release fence reads fail, retire abandoned Codex app-server startups, keep stream-to-parent ACP spawns registered, close Codex startup clients on timeout, recover bundled provider aliases, avoid custom-provider runtime fanout, preserve provider prompt-cache boundaries, forward Gemini stop sequences, and strip Kimi-incompatible Anthropic cache markers. (#89811) Thanks @takhoffman.
- Memory/build/update: warn after startup watcher pressure checks, externalize optional Baileys image backends, restore and pin Canvas A2UI compatibility assets, keep plugin repair fetch failures nonblocking, restore Skill Workshop view switching, and keep the current chat toggle active after awaited session switches. (#89244) Thanks @RomneyDa.
- Plugins/auth: keep Hermes migration reports pointed at SQLite auth-profile stores and keep plugin auth-profile reuse tests on the current store path.
- Plugins/CLI: avoid importing the runtime plugin loader only to clear in-process caches after short-lived plugin install, enable, disable, update, and uninstall commands refresh registry metadata.
- Security/config/tooling: reject corrupt shell snapshots, suspicious gateway startup configs, malformed release/test/tooling/Docker/perf numeric limits, oversized audit responses, unsafe exec precheck env, and invalid pending-agent SQLite scaffold denials. (#89701, #89705, #89480, #81488) Thanks @RomneyDa and @mmaps.
- Release/CI/E2E: restore package changelog extraction after the post-2026.6.1 version bump, keep hydrated pnpm modules under `node_modules` for ARM/Linux package lifecycle scripts, keep OpenAI live-cache prerequisites advisory while Anthropic prerequisites stay blocking, retry Windows Parallels background log appends on transient file-lock errors, bound candidate GitHub and cross-OS Discord fetches, harden ARM smoke/browser checks, show Docker build heartbeats, reset Crabbox pnpm hydrate state, and isolate Testbox/Docker/release journey artifacts.
- Release/CI/E2E: keep Crabbox hydrate pnpm stores on the persistent cache volume while still resetting volatile modules, reducing cold installs and runner memory churn.
- Release/CI/E2E: fail secret-provider proof startup immediately when the gateway exits by signal instead of waiting for the readiness timeout.
- Release/CI/E2E: report plugin gateway gauntlet command-log write failures as failed rows instead of crashing the harness from child-process callbacks.
- Release/CI/E2E: abort stalled Kitchen Sink RPC readiness probes as soon as the gateway exits so proof failures return promptly.
- Release/CI/E2E: keep Parallels JSON-mode progress on stderr so macOS, Linux, Windows, and aggregate update smoke summaries stay parseable on stdout.
- Release/CI/E2E: fail Crabbox sparse-sync runs clearly when their temporary full checkout disappears while the child process is running, instead of pretending the child's deleted cwd can be repaired.
- Release/CI/E2E: fail PTY-backed E2E commands when transcript logs cannot be written instead of letting missing proof capture crash around a live child process.
- Release/CI/E2E: fail mock OpenAI request-log write errors with clear HTTP responses instead of leaving provider proof clients waiting on a broken socket.
- Release/CI/E2E: fail Parallels host-command log write errors through the command result path instead of leaving streaming smoke phases unresolved.
## 2026.6.1
### Highlights
- Agents and CLI-backed runtimes recover more cleanly from interrupted tool calls, stale session bindings, compaction handoffs, auth-profile failover, reasoning-tag cleanup, and media delivery retries. (#85798, #87484, #88129, #88136, #88141, #88162, #88182, #88924, #89220) Thanks @RomneyDa, @neeravmakwana, and @omarshahine.
- Channels and mobile delivery are steadier across Telegram, WhatsApp, iMessage, Slack, Discord, Microsoft Teams, Google Chat, Google Meet, QQBot, and iOS realtime Talk. (#88096, #88105, #88183, #88749, #88866, #88948, #88984, #89015, #88231) Thanks @omarshahine, @Jensenwgd, and @sliverp.
- Provider and plugin requests now bound more timers, retries, OAuth/device-code lifetimes, media downloads, local service probes, generated-content polling, provider-catalog failures, reasoning output, and model catalog paths before they can hang a run. (#88480, #88512, #88767, #88781, #88851, #88860, #89343, #89379, #89400) Thanks @vincentkoc, @charles-openclaw, @zz327455573, @849261680, and @XuZehan-iCenter.
- Skills, Skill Workshop, and plugin loading now handle proposal review, stale disabled snapshots, support-file approvals, locale/routing fixes, and loader failures more clearly, so channel turns avoid disabled SecretRefs and operators get better recovery guidance. (#79072, #79173, #88734) Thanks @zeus1959 and @shakkernerd.
- Workboard, SecretRef plugin manifests, hosted iOS push relay, typed presentation command actions, and external Copilot/Tokenjuice packaging add broader orchestration, integration, SDK, and plugin delivery surfaces. (#82326, #87469, #87796, #88107, #88117, #88721, #89336) Thanks @RomneyDa.
- Chat and Control UI startup paths keep sends alive through history loading, stream deltas incrementally, skip markdown work while streaming, keep drafts local while typing, clear the composer after sends, trace first-output latency, cache transcript renders, prioritize first connect, and expose calmer composer controls and notification settings. (#74715, #88772, #88825, #88952, #88960, #88998, #89030, #89106) Thanks @VladyslavLevchuk, @vincentkoc, and @sallyom.
- iMessage monitor state, inbound queues, Discord thread bindings, plugin install ledgers, session metadata, gateway runtime state, plugin metadata, memory watchers, and store writes moved toward SQLite-backed or cached state so restarts and hot paths do less repeated work. (#88794, #88797, #88866, #89075, #89185, #89188, #85351) Thanks @RomneyDa and @NianJiuZst.
- Release, CI, Docker, E2E, plugin install, update, doctor, diagnostics, and security lanes now cap more logs, response bodies, readiness probes, artifact checks, status polling, child workflow waits, docker package cleanup, quiet test stalls, downgrade repair, and health probes so failures report bounded proof instead of stalling. (#84988, #87914, #87952, #88966, #89169, #89701, #89731) Thanks @LibraHo, @Niriakot, @MukundaKatta, and @RomneyDa.
- Agents and CLI-backed runtimes recover more cleanly from interrupted tool calls, stale session bindings, compaction handoffs, and media delivery retries. (#88129, #88136, #88141, #88162, #88182)
- Channels and mobile delivery are steadier across Telegram, WhatsApp, iMessage, Slack, Discord, Microsoft Teams, Google Chat, Google Meet, and iOS realtime Talk. (#88096, #88105, #88183, #88231)
- Provider and plugin requests now bound more timers, retries, OAuth/device-code lifetimes, media downloads, local service probes, and generated-content polling paths before they can hang a run.
- Skills, session metadata, gateway runtime state, plugin metadata, and store writes do less repeated work on hot paths while keeping config and dispatch behavior stable.
- Skills and plugin loading now handle stale disabled snapshots and loader failures more clearly, so channel turns avoid disabled SecretRefs and operators get better recovery guidance. (#79072, #79173) Thanks @zeus1959.
- Workboard, SecretRef plugin manifests, hosted iOS push relay, and external Copilot/Tokenjuice packaging add broader orchestration, integration, and plugin delivery surfaces. (#82326, #87469, #87796, #88107, #88117)
- Skill Workshop now has a fuller Control UI flow with proposal lists, today actions, revision handoff, searchable file previews, review states, locale coverage, and reusable session routing.
- Chat and Control UI startup paths keep sends alive through history loading, stream deltas incrementally, skip markdown work while streaming, keep drafts local while typing, trace first-output latency, and expose calmer composer controls. (#88772, #88825, #88998) Thanks @vincentkoc.
- Provider coverage and model metadata now include MiniMax M3, account OAuth endpoints, Google/Vertex catalog fixes, OpenRouter SQLite model caching, Copilot Claude 1M capabilities, Foundry reasoning alignment, and OpenAI response replay guards. (#88480, #88512, #88851, #88860)
- iMessage monitor state, inbound queues, and plugin install ledgers moved toward SQLite-backed state so restarts and local monitors recover with less duplicate filesystem scanning. (#88794, #88797)
- Release, CI, Docker, E2E, plugin install, and diagnostics lanes now cap more logs, response bodies, readiness probes, artifact checks, status polling, and rollback snapshots so failures report bounded proof instead of stalling.
### Changes
- Docs: add a dedicated Skill Workshop guide covering governed skill creation, reviewable proposals, CLI, Gateway, agent tool behavior, approval policy, support files, and recovery; refresh ClawHub cards; and add ClawHub CLI, iMessage SSH-wrapper TCC, Android helper, diff-language, and host-local media-send guidance. (#79658, #88734, #88758, #88865, #89297) Thanks @simplyclever914, @shakkernerd, @vyctorbrzezowski, @TurboTheTurtle, @RomneyDa, and @Wang-Yeah623.
- Docs: add a dedicated Skill Workshop guide covering governed skill creation, reviewable proposals, CLI, Gateway, agent tool behavior, approval policy, support files, and recovery. Thanks @shakkernerd.
- Skills: let the `skill_workshop` agent tool apply, reject, and quarantine explicit proposals through the guarded review flow. Thanks @shakkernerd.
- Skills: let proposals carry approved support files under standard skill folders, with scanner, hash, and rollback safeguards. Thanks @shakkernerd.
- Skills: let pending proposals be revised in place with versioned, dated proposal frontmatter before approval. Thanks @shakkernerd.
@@ -70,83 +30,59 @@ Docs: https://docs.openclaw.ai
- Plugins: externalize the GitHub Copilot agent runtime as the official `@openclaw/copilot` plugin with npm and ClawHub publish metadata.
- iOS: add hosted push relay defaults, realtime Talk playback, and a guarded WebSocket ping path for more reliable mobile sessions. (#88096, #88105, #88231)
- iOS: support native iPad display layouts.
- Android: add installed-app inspection commands, notification picker helpers, and updated-system-app classification.
- Workboard: add orchestration primitives and agent coordination tools for multi-agent planning and run tracking. (#87469)
- Workboard: wire task-backed board runs and show task comments in the edit modal.
- Code mode: add internal namespaces for scoped agent/global sessions and exact namespace tool dispatch. (#88043)
- Code mode: add MCP API files and docs for code-mode integrations.
- Gateway: support Tailscale Serve service names for local service routing.
- Control UI: add a Dreaming-tab agent selector and propagate the selected agent through Dreaming status, diary, and diary actions. (#78748) Thanks @stevenepalmer.
- Control UI: add calmer chat composer controls, local draft typing state, and first-output latency instrumentation for active chat entry. (#88772, #88998) Thanks @vincentkoc.
- Plugins: add a SecretRef provider integration manifest contract and extract shared LLM core packages for provider/plugin reuse. (#82326, #88117)
- Plugin SDK: add typed presentation command actions and the bounded `resolve_exec_env` hook for plugin-provided exec environment contributions. (#88721)
- Plugins: persist the plugin install index in SQLite so installed package lookup survives reloads with less filesystem scanning. (#88794)
- Providers: add MiniMax M3 model support. (#88860)
- Tools/media: allow validated host-local text document media sends while keeping unsafe plain-text media sends blocked. (#79658) Thanks @simplyclever914.
- Doctor: add disk space health checks and stabilize post-upgrade JSON probes.
- Channels: store inbound queues in SQLite and migrate iMessage monitor state to SQLite-backed tracking. (#88797)
- Skills: add the core skills index and centralize skills runtime loading, status, filtering, and prompt formatting.
### Fixes
- Release/CI/E2E: fail early when Crabbox sparse-sync full checkouts do not have enough local disk, with guidance for moving the sync root.
- Build: render independent CLI startup metadata help snapshots concurrently to cut cold build-all metadata time.
- Plugins: stop timed-out package-boundary prep steps by process group so descendant TypeScript/helper processes do not survive local check cleanup.
- Control UI: serve static assets asynchronously after safe-open checks so large UI files do not block Gateway request handling.
- Scripts/UI: forward direct wrapper SIGHUP shutdown to child processes so terminal hangups do not leave wrapped dev commands running.
- Gateway: return the post-expiration pending-work revision from node drains so reconnecting nodes do not observe stale queue revisions after expired items are pruned.
- Release/CI/E2E: keep temporary full-sync checkouts alive while slow Crabbox leases boot, so sparse worktree runs do not lose their sync source before file-list generation.
- Release/CI/E2E: normalize inherited Linux `C.UTF-8` locale settings before raw AWS macOS Crabbox bootstrap commands, avoiding macOS locale warnings during package-manager hydration.
- Release/CI/E2E: keep gateway watch regression checks from copying large static plugin assets inside the measured idle window.
- Update: keep core updates nonblocking when missing external plugin repair downloads or soft plugin repair warnings would otherwise stall, pin post-core plugin compatibility to the downgraded core version, and still block installed active plugin payload smoke failures. (#84431, #87914, #87952) Thanks @TurboTheTurtle, @Niriakot, and @MukundaKatta.
- Agents/providers: keep streaming tool-call argument parsing record-shaped when providers emit valid non-object JSON such as `null` or arrays.
- Release/CI/E2E: reset incremental log readers when watched log files rotate without shrinking, so same-size replacements do not hide new readiness or RPC lines.
- Talk: preserve explicit `null` payloads on controller-created turn and output-audio lifecycle events.
- Agents/TUI: keep local custom provider runs from loading plugin runtime and auth alias metadata when plugins are disabled.
- Agents/TUI: restore in-flight TUI run switch-back behavior, keep no-policy native hook fallback available, guard vanished workspaces, and keep lightweight isolated subagents lightweight.
- Agents/media: keep async image, music, and video generation starts from ending the Codex turn, avoid duplicate generated-media fallbacks, and let mixed requests continue with summaries or other work while media renders in the background. (#89220) Thanks @omarshahine.
- Agents/media: keep async image, music, and video generation starts from ending the Codex turn, so mixed requests can continue with summaries or other work while media renders in the background.
- Agents/Codex: keep public OpenAI API-key profiles from being treated as native Codex app-server auth while preserving persisted Codex OAuth sessions.
- Agents/Codex: stream Codex app-server final-answer partials to live reply previews, preserve ACP metadata in SQLite, prefer real tool results over synthetic repair output, prevent aborted app-server turn handles from lingering, migrate legacy OpenAI Codex `lastGood` auth state, and preserve workspace/session metadata through ACP runtime refactors. (#88405, #88724, #88730) Thanks @vincentkoc.
- Control UI: keep collapsed tool cards labeled with the tool name and action instead of generic output text. Thanks @shakkernerd.
- Agents/Codex: surface Skill Workshop guidance in Codex app-server prompts when `skill_workshop` is available. Thanks @shakkernerd.
- Skill Workshop: restore and localize the Control UI board/today view switcher so review workflows keep their intended layout toggle across locales. Thanks @shakkernerd.
- Agents/auth: write auth profiles atomically, dispatch auth failures by type, add force re-login and exhausted-failover recovery, clear legacy auto fallback pins, preserve workspaces during state-only uninstall, and compact before oversized turns so recovery paths avoid partial state. (#85798, #87484, #89181) Thanks @RomneyDa and @neeravmakwana.
- Agents/auth: write auth profiles atomically, add force re-login recovery, preserve workspaces during state-only uninstall, and compact before oversized turns so recovery paths avoid partial state.
- Skills: skip disabled skill env overrides from stale persisted snapshots so disabled skill `apiKey` SecretRefs cannot abort embedded or channel turns. (#79072, #79173) Thanks @zeus1959.
- Skill Workshop: render the Control UI tab from filtered navigation state and keep filtered fallback routing stable.
- CLI: avoid live catalog validation during `openclaw agents add`, so adding a secondary agent no longer depends on provider catalog availability. (#76284, #88314) Thanks @zhangguiping-xydt.
- CLI: harden CLI and plugin edge cases, and keep `plugins list --json` on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph. (#88896)
- CLI/desktop: bridge WSL clipboard operations through the shell, recognize manual-update launchd jobs, and keep machine-readable startup output parseable during progress setup. (#88764, #88689) Thanks @alexzhu0.
- CLI: keep `plugins list --json` on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph.
- CLI/desktop: bridge WSL clipboard operations through the shell and recognize manual-update launchd jobs. (#88764)
- Plugins: make PixVerse external-plugin ClawHub metadata explicit and keep it out of bundled dist builds.
- Plugins: clarify plugin loader failure guidance and treat soft plugin repair warnings as nonfatal so missing or incompatible plugin packages point operators at the right repair path without blocking unrelated work. (#84431) Thanks @TurboTheTurtle.
- Plugins: preserve npm plugin roots after blocked installs, skip plugin-local `openclaw` peer symlinks during rollback snapshots, relink those peers after restore, isolate cached tool runtime siblings, isolate provider catalog projections and web-provider factory failures, and keep private LLM-core declarations bundled so one bad plugin does not poison sibling runtime paths. (#77237, #88767, #88807, #89336) Thanks @vincentkoc and @RomneyDa.
- Cron: keep SQLite cron migrations compatible with legacy run-log tables, archived job stores, diagnostic cron names, single-job run-history names, startup cron retries, and legacy one-shot delete-after-run behavior. (#88285, #88294, #89075) Thanks @kip-claw.
- Plugins: clarify plugin loader failure guidance so missing or incompatible plugin packages point operators at the right repair path.
- Plugins: preserve npm plugin roots after blocked installs, skip plugin-local `openclaw` peer symlinks during rollback snapshots, relink those peers after restore, isolate cached tool runtime siblings, and isolate web-provider factory failures so one bad plugin does not poison sibling runtime paths. (#77237, #88807)
- Cron: keep SQLite cron migrations compatible with legacy run-log tables, archived job stores, diagnostic cron names, and legacy one-shot delete-after-run behavior. (#88285)
- Cron: keep update delivery validation scoped, harden restart state, and retire MCP runtimes on isolated cron cleanup.
- Auto-reply: guard dispatcher failure-count probes so missing optional counters do not break SDK-typed recovery paths. (#89318) Thanks @Alix-007 and @takhoffman.
- Memory: serialize QMD update/embed writes per store, reduce Linux watcher fan-out, avoid noisy gateway watcher warnings, retry transient FileProvider-backed reads, preserve phase signals on read errors, harden envelope metadata sanitization, reattach Linux native watchers when directories are recreated, and rewrite generated transcript paths on rollover so memory/search state survives concurrent gateway and CLI activity. (#66339, #85931, #89185, #89188, #89246, #85351) Thanks @openperf, @amittell, @RomneyDa, and @NianJiuZst.
- Memory: keep vector-disabled FTS indexes from resolving embedding providers during sync and search.
- Memory: serialize QMD update/embed writes per store, preserve phase signals on read errors, harden envelope metadata sanitization, and rewrite generated transcript paths on rollover so memory/search state survives concurrent gateway and CLI activity. (#66339, #85931) Thanks @openperf and @amittell.
- Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.
- Providers: resolve Google defaults to `google-generative-ai`, register Vertex static catalog rows and `gemini-3.1-flash-lite`, align Foundry reasoning metadata, skip DeepSeek V4 thinking params on Foundry fallback, use MiniMax account OAuth endpoints, preserve Copilot Claude 1M capabilities, suppress disabled Ollama reasoning output, forward Gemini stop sequences, switch direct Gemini reasoning to native mode, strip provider self-prefixes and Kimi-incompatible Anthropic cache markers, keep OpenAI stop-finished tool calls, and avoid replay ids when the Responses store is disabled. (#88480, #88512, #88781, #89343, #89379, #89400, #76612) Thanks @coder999999999, @BryanTegomoh, @vliuyt, @charles-openclaw, @zz327455573, @849261680, and @XuZehan-iCenter.
- Providers: resolve Google defaults to `google-generative-ai`, register Vertex static catalog rows, align Foundry reasoning metadata, skip DeepSeek V4 thinking params on Foundry fallback, use MiniMax account OAuth endpoints, preserve Copilot Claude 1M capabilities, suppress disabled Ollama reasoning output, keep OpenAI stop-finished tool calls, and avoid replay ids when the Responses store is disabled. (#88480, #88512)
- Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.
- Cron: retry recurring jobs after transient model rate limits before waiting for the next scheduled slot.
- Agents/Codex: keep live session locks during cleanup, recover interrupted CLI tool transcripts, preserve Codex auth and compaction session identity, clear orphan tool state, cap app-server idle timers, and keep media completion delivery retryable. (#88129, #88136, #88141, #88162, #88182)
- Chat/UI: show Gateway chat failures as visible assistant messages in the Control UI instead of only setting an invisible error state.
- Channels: cap Telegram, Discord, WhatsApp, Signal, Feishu, Google Chat, Microsoft Teams, QQBot, Nostr, Zalo, Zalouser, and Nextcloud-style request/retry timers; preserve SMS approval reply routes; keep iMessage typing active during tool work; allow RFC2544 benchmark ranges for QQBot token fetches; and retry WhatsApp QR login 408 timeouts. (#88183, #88948, #88984, #89015) Thanks @omarshahine, @Jensenwgd, and @sliverp.
- Security/config parsing: reject unsafe OAuth/token lifetimes, retry-after delays, inbound timestamps, response body sizes, command timeout config, sandbox observer token TTLs, corrupt shell snapshots, untrusted workspace setup-only channel loads, remote media reference overreads, trajectory export leaks, hooks-token auth reuse, and gateway WebSocket calls after close. (#86953, #87376, #88974, #89354, #89701) Thanks @hxy91819, @coygeek, @pgondhi987, and @RomneyDa.
- Channels: cap Telegram, Discord, WhatsApp, Signal, Feishu, Google Chat, Microsoft Teams, QQBot, Nostr, Zalo, Zalouser, and Nextcloud-style request/retry timers; preserve SMS approval reply routes; and retry WhatsApp QR login 408 timeouts. (#88183)
- Security/config parsing: reject unsafe OAuth/token lifetimes, retry-after delays, inbound timestamps, response body sizes, command timeout config, sandbox observer token TTLs, and gateway WebSocket calls after close.
- Providers/media: cap local service, model, usage, queue, generated media, TTS, music, workflow polling, and provider OAuth request timers across hosted and local providers.
- Release/CI/E2E: bound release candidate reads, beta smoke REST calls, plugin npm verification commands, changelog restore, cross-OS process groups, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Telegram credential timeouts, Control UI i18n and CLI startup metadata generation, Vitest routing, dependency guard admin approvals, child workflow failure detection, quiet Node test shard stalls, dist cache restores, Docker base-image/package cleanup, and mainline test flakes. (#84988, #88127, #88137, #88155, #88160, #88966, #89169) Thanks @LibraHo and @RomneyDa.
- Release/CI/E2E: bound release candidate reads, beta smoke REST calls, plugin npm verification commands, changelog restore, cross-OS process groups, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Telegram credential timeouts, Control UI i18n and CLI startup metadata generation, Vitest routing, and mainline test flakes. (#88127, #88137, #88155, #88160)
- Release/CI/E2E: keep Kitchen Sink live plugin MCP probes resolving source-checkout workspace packages and align the live gauntlet with current Kitchen Sink diagnostics.
- Backup: accept root-relative hardlink targets during backup verification. (#89328) Thanks @abnershang.
- Release/CI/E2E: run the secret-provider integration proof through the repo pnpm runner so native macOS and Windows validation use the hydrated package-manager shim.
- Release/CI/E2E: run the Telegram desktop proof gateway through the repo pnpm runner so native macOS proof uses the hydrated package-manager shim.
- Docs/CI: run Mintlify anchor checks through the repo pnpm runner so docs link validation works when pnpm is only available through the hydrated package-manager shim.
- Agents: keep configured fallback model metadata typed so provider params, context-token caps, and media input limits do not break changed-gate typechecks.
- Agents: accept hidden `sessions_send` body aliases before validation while keeping the model-facing `message` schema canonical. (#88229) Thanks @zhangguiping-xydt.
- Chat/UI: preserve startup chat sends during history loading, unblock the initial Control UI chat send, stream chat deltas incrementally, skip markdown parsing while streaming, keep drafts local while typing, guard composer rerenders, cache chat transcript renders, record pending-send paint timing, show the Communication Notifications tab, honor Chromium executable overrides, and detect system Chromium for E2E. (#74715, #88952, #88960, #88998) Thanks @VladyslavLevchuk and @vincentkoc.
- Channels: stop schema-padded poll modifiers from turning normal `send` actions into invalid poll sends. (#89601) Thanks @codezz and @takhoffman.
- Channels: preserve long Feishu streaming replies, recover failed progress draft starts, send visible fallbacks when accepted Feishu turns produce no final reply, preserve external `sessions_send` routes, persist Discord thread bindings in SQLite, tolerate iMessage self-chat timestamp skew, preserve colon-prefixed slash commands in mention parsing, decode Nostr `npub` allowlists correctly, and suppress raw provider errors during channel delivery. (#87896, #88749, #88803, #88866) Thanks @MonkeyLeeT.
- Config/status/doctor: skip unresolved shell references in state-dir dotenv files, resolve gateway auth secrets during deep status audits, surface disabled Codex plugin routes in doctor lint, respect explicit PI runtime policy, report runtime tool-schema and gateway health credential errors, clear recovered embedded-run activity, migrate voice-call call logs through doctor, and keep post-upgrade JSON stable. (#88731, #88761, #88820, #88288, #89731) Thanks @brokemac79, @openperf, and @RomneyDa.
- Gateway/session state: list commands from the Gateway plugin registry, harden MCP loopback tool schemas, hide phantom agent-store rows from `sessions.list`, make task persistence failures explicit, support Tailscale Serve service names, guard Browser/Chrome pending attach aborts, and carry session UUIDs on interactive dispatch events. (#88305) Thanks @rohitjavvadi.
- Gateway/plugins: narrow plugin lookup memoization to the stable plugin/runtime inputs, avoiding repeated lookup work without mixing disabled or filtered plugin state.
- Chat/UI: preserve startup chat sends during history loading, unblock the initial Control UI chat send, stream chat deltas incrementally, skip markdown parsing while streaming, keep drafts local while typing, guard composer rerenders, honor Chromium executable overrides, and detect system Chromium for E2E. (#88998) Thanks @vincentkoc.
- Channels: preserve long Feishu streaming replies, send visible fallbacks when accepted Feishu turns produce no final reply, tolerate iMessage self-chat timestamp skew, preserve colon-prefixed slash commands in mention parsing, decode Nostr `npub` allowlists correctly, and suppress raw provider errors during channel delivery. (#87896)
- Config/status/doctor: skip unresolved shell references in state-dir dotenv files, resolve gateway auth secrets during deep status audits, respect explicit PI runtime policy, report runtime tool-schema errors, and keep post-upgrade JSON stable. (#88288)
- Gateway/session state: list commands from the Gateway plugin registry, harden MCP loopback tool schemas, hide phantom agent-store rows from `sessions.list`, make task persistence failures explicit, and carry session UUIDs on interactive dispatch events.
- OpenAI/TTS: handle speed directives for OpenAI TTS voices. (#74089)
- CI/Crabbox: keep default runner capacity on the Azure credit-backed on-demand D4 lane with the Azure SSH port and a Git-independent full check job, so broad validation avoids low-priority spot quota stalls, hydrate port mismatches, non-Git hydrated workspaces, and stale AWS region hints.
- CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.
@@ -721,7 +657,6 @@ Docs: https://docs.openclaw.ai
- Gateway/sessions: allow shared-secret bearer callers to read and stream session history without an explicit scope header. (#81815) Thanks @medns.
- Agents/embedded runner: classify HTML auth provider responses as `auth_html` and return a re-authentication hint instead of the CDN-blocked copy that `upstream_html` returns. Cloudflare Access login pages, nginx basic-auth challenges, and gateway login walls all produce HTML auth bodies that were previously misdiagnosed as transient CDN blocks. (#79900) Thanks @martingarramon.
- TUI/streaming watchdog: dismiss the `This response is taking longer than expected` notice as soon as a chat event for the same run arrives, so the message no longer sits next to the recovered response when the run was only briefly silent. Refs #67052, #69081 (closed), prior attempt #69026. Thanks @jpruit20 and @romneyda.
- Agents/auth profiles: replace the bare `No available auth profile for <provider> (all in cooldown or unavailable)` TUI error with plain-language copy that explains what happened in user terms (sign-in expired, provider asking us to slow down, billing issue on the account, etc.) and suggests the matching `openclaw models auth login --provider <provider>` recovery command for sign-in and billing causes, while falling back to the underlying provider error for cases without a clear recovery path. Thanks @romneyda.
- Agents/Pi: tolerate OpenClaw-owned transcript writes while embedded prompts are released for model I/O, keeping long-running Feishu, Slack, Telegram, and cron turns from failing with false session-takeover errors. Fixes #84059. (#84250) Thanks @tianxiaochannel-oss88.
## 2026.5.20

View File

@@ -9,18 +9,18 @@
# Build stages use full bookworm; the runtime image is always bookworm-slim.
ARG OPENCLAW_EXTENSIONS=""
ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="docker.io/library/node:24-bookworm@sha256:8530f76a96d88820d288761f022e318970dda93d01536919fbc16076b7983e63"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="docker.io/library/node:24-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:8530f76a96d88820d288761f022e318970dda93d01536919fbc16076b7983e63"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:242549cd46785b480c832479a730f4f2a20865d61ea2e404fdb2a5c3d3b73ecf"
# Keep in sync with .github/actions/setup-node-env/action.yml bun-version.
# To update: docker buildx imagetools inspect docker.io/oven/bun:<version> and use the manifest-list digest.
ARG OPENCLAW_BUN_IMAGE="docker.io/oven/bun:1.3.13@sha256:87416c977a612a204eb54ab9f3927023c2a3c971f4f345a01da08ea6262ae30e"
# To update: docker buildx imagetools inspect oven/bun:<version> and use the manifest-list digest.
ARG OPENCLAW_BUN_IMAGE="oven/bun:1.3.13@sha256:87416c977a612a204eb54ab9f3927023c2a3c971f4f345a01da08ea6262ae30e"
# Base images are pinned to SHA256 digests for reproducible builds.
# Dependabot refreshes these blessed digests; release builds consume the
# reviewed base snapshot instead of mutating distro state on every build.
# To update, run: docker buildx imagetools inspect docker.io/library/node:24-bookworm and
# docker.io/library/node:24-bookworm-slim (or podman) and replace the digests below with the
# To update, run: docker buildx imagetools inspect node:24-bookworm and
# node:24-bookworm-slim (or podman) and replace the digests below with the
# current multi-arch manifest list entries.
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS workspace-deps

View File

@@ -30,8 +30,7 @@ Supported channels include: WhatsApp, Telegram, Slack, Discord, Google Chat, Sig
New install? Start here: [Getting started](https://docs.openclaw.ai/start/getting-started)
Preferred setup: run `openclaw onboard` in your terminal.
OpenClaw Onboard guides you step by step through setting up the gateway, workspace, channels, and skills. It is the recommended CLI setup path and works on **macOS, Linux, and Windows**.
Windows desktop users can start with the native [Windows Hub](https://docs.openclaw.ai/platforms/windows) companion app for setup, tray status, chat, node mode, and local MCP mode.
OpenClaw Onboard guides you step by step through setting up the gateway, workspace, channels, and skills. It is the recommended CLI setup path and works on **macOS, Linux, and Windows (via WSL2; strongly recommended)**.
Works with npm, pnpm, or bun.
## Sponsors
@@ -165,7 +164,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
- **[Voice Wake](https://docs.openclaw.ai/nodes/voicewake) + [Talk Mode](https://docs.openclaw.ai/nodes/talk)** — wake words on macOS/iOS and continuous voice on Android (ElevenLabs + system TTS fallback).
- **[Live Canvas](https://docs.openclaw.ai/platforms/mac/canvas)** — agent-driven visual workspace with [A2UI](https://docs.openclaw.ai/platforms/mac/canvas#canvas-a2ui).
- **[First-class tools](https://docs.openclaw.ai/tools)** — browser, canvas, nodes, cron, sessions, and Discord/Slack actions.
- **[Companion apps](https://docs.openclaw.ai/platforms)** — Windows Hub, macOS menu bar app, and iOS/Android [nodes](https://docs.openclaw.ai/nodes).
- **[Companion apps](https://docs.openclaw.ai/platforms/macos)** — macOS menu bar app + iOS/Android [nodes](https://docs.openclaw.ai/nodes).
- **[Onboarding](https://docs.openclaw.ai/start/wizard) + [skills](https://docs.openclaw.ai/tools/skills)** — onboarding-driven setup with bundled/managed/workspace skills.
## Security model (important)
@@ -186,7 +185,7 @@ Run `openclaw doctor` to surface risky/misconfigured DM policies.
- New here: [Getting started](https://docs.openclaw.ai/start/getting-started), [Onboarding](https://docs.openclaw.ai/start/wizard), [Updating](https://docs.openclaw.ai/install/updating)
- Channel setup: [Channels index](https://docs.openclaw.ai/channels), [WhatsApp](https://docs.openclaw.ai/channels/whatsapp), [Telegram](https://docs.openclaw.ai/channels/telegram), [Discord](https://docs.openclaw.ai/channels/discord), [Slack](https://docs.openclaw.ai/channels/slack)
- Apps + nodes: [Windows Hub](https://docs.openclaw.ai/platforms/windows), [macOS](https://docs.openclaw.ai/platforms/macos), [iOS](https://docs.openclaw.ai/platforms/ios), [Android](https://docs.openclaw.ai/platforms/android), [Nodes](https://docs.openclaw.ai/nodes)
- Apps + nodes: [macOS](https://docs.openclaw.ai/platforms/macos), [iOS](https://docs.openclaw.ai/platforms/ios), [Android](https://docs.openclaw.ai/platforms/android), [Nodes](https://docs.openclaw.ai/nodes)
- Config + security: [Configuration](https://docs.openclaw.ai/gateway/configuration), [Security](https://docs.openclaw.ai/gateway/security), [Exposure runbook](https://docs.openclaw.ai/gateway/security/exposure-runbook), [Sandboxing](https://docs.openclaw.ai/gateway/sandboxing)
- Remote + web: [Gateway](https://docs.openclaw.ai/gateway), [Remote access](https://docs.openclaw.ai/gateway/remote), [Tailscale](https://docs.openclaw.ai/gateway/tailscale), [Web surfaces](https://docs.openclaw.ai/web)
- Tools + automation: [Tools](https://docs.openclaw.ai/tools), [Skills](https://docs.openclaw.ai/tools/skills), [Cron jobs](https://docs.openclaw.ai/automation/cron-jobs), [Webhooks](https://docs.openclaw.ai/automation/webhook), [Gmail Pub/Sub](https://docs.openclaw.ai/automation/gmail-pubsub)

View File

@@ -2,133 +2,6 @@
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
<channel>
<title>OpenClaw</title>
<item>
<title>2026.6.1</title>
<pubDate>Wed, 03 Jun 2026 21:26:22 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026060190</sparkle:version>
<sparkle:shortVersionString>2026.6.1</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.6.1</h2>
<h3>Highlights</h3>
<ul>
<li>Agents and CLI-backed runtimes recover more cleanly from interrupted tool calls, stale session bindings, compaction handoffs, and media delivery retries. (#88129, #88136, #88141, #88162, #88182)</li>
<li>Channels and mobile delivery are steadier across Telegram, WhatsApp, iMessage, Slack, Discord, Microsoft Teams, Google Chat, Google Meet, and iOS realtime Talk. (#88096, #88105, #88183, #88231)</li>
<li>Provider and plugin requests now bound more timers, retries, OAuth/device-code lifetimes, media downloads, local service probes, and generated-content polling paths before they can hang a run.</li>
<li>Skills, session metadata, gateway runtime state, plugin metadata, memory watchers, and store writes do less repeated work on hot paths while keeping config, dispatch, and Linux file-watch behavior stable. (#89185, #89188, #85351) Thanks @RomneyDa and @NianJiuZst.</li>
<li>Skills and plugin loading now handle stale disabled snapshots and loader failures more clearly, so channel turns avoid disabled SecretRefs and operators get better recovery guidance. (#79072, #79173) Thanks @zeus1959.</li>
<li>Workboard, SecretRef plugin manifests, hosted iOS push relay, and external Copilot/Tokenjuice packaging add broader orchestration, integration, and plugin delivery surfaces. (#82326, #87469, #87796, #88107, #88117)</li>
<li>Skill Workshop now has a fuller Control UI flow with proposal lists, today actions, revision handoff, searchable file previews, review states, locale coverage, and reusable session routing.</li>
<li>Chat and Control UI startup paths keep sends alive through history loading, stream deltas incrementally, skip markdown work while streaming, keep drafts local while typing, clear the composer after sends, trace first-output latency, prioritize first connect, and expose calmer composer controls. (#88772, #88825, #88998, #89030, #89106) Thanks @vincentkoc and @sallyom.</li>
<li>Provider coverage and model metadata now include MiniMax M3, account OAuth endpoints, Google/Vertex catalog fixes, OpenRouter SQLite model caching, Copilot Claude 1M capabilities, Foundry reasoning alignment, and OpenAI response replay guards. (#88480, #88512, #88851, #88860)</li>
<li>iMessage monitor state, inbound queues, and plugin install ledgers moved toward SQLite-backed state so restarts and local monitors recover with less duplicate filesystem scanning. (#88794, #88797)</li>
<li>Release, CI, Docker, E2E, plugin install, and diagnostics lanes now cap more logs, response bodies, readiness probes, artifact checks, status polling, child workflow waits, docker package cleanup, quiet test stalls, and rollback snapshots so failures report bounded proof instead of stalling. (#88966) Thanks @RomneyDa.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>Docs: add a dedicated Skill Workshop guide covering governed skill creation, reviewable proposals, CLI, Gateway, agent tool behavior, approval policy, support files, and recovery, and refresh the ClawHub showcase cards. (#88734) Thanks @shakkernerd and @vyctorbrzezowski.</li>
<li>Skills: let the <code>skill_workshop</code> agent tool apply, reject, and quarantine explicit proposals through the guarded review flow. Thanks @shakkernerd.</li>
<li>Skills: let proposals carry approved support files under standard skill folders, with scanner, hash, and rollback safeguards. Thanks @shakkernerd.</li>
<li>Skills: let pending proposals be revised in place with versioned, dated proposal frontmatter before approval. Thanks @shakkernerd.</li>
<li>Skills: add Skill Workshop with pending proposals, CLI/Gateway review actions, rollback metadata, and the <code>skill_workshop</code> agent tool. Thanks @shakkernerd.</li>
<li>Skill Workshop: add the Control UI navigation, styled dashboard, proposal today view, revision dialog, file preview modal, searchable preview files, reusable session handoff, and localized strings.</li>
<li>Plugins: externalize Tokenjuice as the official <code>@openclaw/tokenjuice</code> plugin with npm and ClawHub publish metadata.</li>
<li>Plugins: externalize the GitHub Copilot agent runtime as the official <code>@openclaw/copilot</code> plugin with npm and ClawHub publish metadata.</li>
<li>iOS: add hosted push relay defaults, realtime Talk playback, and a guarded WebSocket ping path for more reliable mobile sessions. (#88096, #88105, #88231)</li>
<li>iOS: support native iPad display layouts.</li>
<li>Workboard: add orchestration primitives and agent coordination tools for multi-agent planning and run tracking. (#87469)</li>
<li>Workboard: wire task-backed board runs and show task comments in the edit modal.</li>
<li>Code mode: add internal namespaces for scoped agent/global sessions and exact namespace tool dispatch. (#88043)</li>
<li>Code mode: add MCP API files and docs for code-mode integrations.</li>
<li>Control UI: add a Dreaming-tab agent selector and propagate the selected agent through Dreaming status, diary, and diary actions. (#78748) Thanks @stevenepalmer.</li>
<li>Control UI: add calmer chat composer controls, local draft typing state, and first-output latency instrumentation for active chat entry. (#88772, #88998) Thanks @vincentkoc.</li>
<li>Plugins: add a SecretRef provider integration manifest contract and extract shared LLM core packages for provider/plugin reuse. (#82326, #88117)</li>
<li>Plugins: persist the plugin install index in SQLite so installed package lookup survives reloads with less filesystem scanning. (#88794)</li>
<li>Providers: add MiniMax M3 model support. (#88860)</li>
<li>Doctor: add disk space health checks and stabilize post-upgrade JSON probes.</li>
<li>Channels: store inbound queues in SQLite and migrate iMessage monitor state to SQLite-backed tracking. (#88797)</li>
<li>Skills: add the core skills index and centralize skills runtime loading, status, filtering, and prompt formatting.</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Release/CI/E2E: fail early when Crabbox sparse-sync full checkouts do not have enough local disk, with guidance for moving the sync root.</li>
<li>Build: render independent CLI startup metadata help snapshots concurrently to cut cold build-all metadata time.</li>
<li>Plugins: stop timed-out package-boundary prep steps by process group so descendant TypeScript/helper processes do not survive local check cleanup.</li>
<li>Control UI: serve static assets asynchronously after safe-open checks so large UI files do not block Gateway request handling.</li>
<li>Scripts/UI: forward direct wrapper SIGHUP shutdown to child processes so terminal hangups do not leave wrapped dev commands running.</li>
<li>Gateway: return the post-expiration pending-work revision from node drains so reconnecting nodes do not observe stale queue revisions after expired items are pruned.</li>
<li>Release/CI/E2E: keep temporary full-sync checkouts alive while slow Crabbox leases boot, so sparse worktree runs do not lose their sync source before file-list generation.</li>
<li>Release/CI/E2E: normalize inherited Linux <code>C.UTF-8</code> locale settings before raw AWS macOS Crabbox bootstrap commands, avoiding macOS locale warnings during package-manager hydration.</li>
<li>Release/CI/E2E: keep gateway watch regression checks from copying large static plugin assets inside the measured idle window.</li>
<li>Update: keep core updates nonblocking when a missing external plugin repair download stalls, while still blocking installed active plugin payload smoke failures.</li>
<li>Agents/providers: keep streaming tool-call argument parsing record-shaped when providers emit valid non-object JSON such as <code>null</code> or arrays.</li>
<li>Release/CI/E2E: reset incremental log readers when watched log files rotate without shrinking, so same-size replacements do not hide new readiness or RPC lines.</li>
<li>Talk: preserve explicit <code>null</code> payloads on controller-created turn and output-audio lifecycle events.</li>
<li>Agents/TUI: keep local custom provider runs from loading plugin runtime and auth alias metadata when plugins are disabled.</li>
<li>Agents/TUI: restore in-flight TUI run switch-back behavior, keep no-policy native hook fallback available, guard vanished workspaces, and keep lightweight isolated subagents lightweight.</li>
<li>Agents/media: keep async image, music, and video generation starts from ending the Codex turn, so mixed requests can continue with summaries or other work while media renders in the background.</li>
<li>Agents/Codex: keep public OpenAI API-key profiles from being treated as native Codex app-server auth while preserving persisted Codex OAuth sessions.</li>
<li>Agents/Codex: stream Codex app-server final-answer partials to live reply previews, preserve ACP metadata in SQLite, prefer real tool results over synthetic repair output, prevent aborted app-server turn handles from lingering, migrate legacy OpenAI Codex <code>lastGood</code> auth state, and preserve workspace/session metadata through ACP runtime refactors. (#88405, #88724, #88730) Thanks @vincentkoc.</li>
<li>Control UI: keep collapsed tool cards labeled with the tool name and action instead of generic output text. Thanks @shakkernerd.</li>
<li>Agents/Codex: surface Skill Workshop guidance in Codex app-server prompts when <code>skill_workshop</code> is available. Thanks @shakkernerd.</li>
<li>Skill Workshop: restore and localize the Control UI board/today view switcher so review workflows keep their intended layout toggle across locales. Thanks @shakkernerd.</li>
<li>Agents/auth: write auth profiles atomically, dispatch auth failures by type, add force re-login recovery, preserve workspaces during state-only uninstall, and compact before oversized turns so recovery paths avoid partial state. (#89181) Thanks @RomneyDa.</li>
<li>Skills: skip disabled skill env overrides from stale persisted snapshots so disabled skill <code>apiKey</code> SecretRefs cannot abort embedded or channel turns. (#79072, #79173) Thanks @zeus1959.</li>
<li>Skill Workshop: render the Control UI tab from filtered navigation state and keep filtered fallback routing stable.</li>
<li>CLI: avoid live catalog validation during <code>openclaw agents add</code>, so adding a secondary agent no longer depends on provider catalog availability. (#76284, #88314) Thanks @zhangguiping-xydt.</li>
<li>CLI: keep <code>plugins list --json</code> on the snapshot-only path so plugin sweeps avoid loading the full runtime status graph.</li>
<li>CLI/desktop: bridge WSL clipboard operations through the shell, recognize manual-update launchd jobs, and keep machine-readable startup output parseable during progress setup. (#88764, #88689) Thanks @alexzhu0.</li>
<li>Plugins: make PixVerse external-plugin ClawHub metadata explicit and keep it out of bundled dist builds.</li>
<li>Plugins: clarify plugin loader failure guidance so missing or incompatible plugin packages point operators at the right repair path.</li>
<li>Plugins: preserve npm plugin roots after blocked installs, skip plugin-local <code>openclaw</code> peer symlinks during rollback snapshots, relink those peers after restore, isolate cached tool runtime siblings, and isolate web-provider factory failures so one bad plugin does not poison sibling runtime paths. (#77237, #88807)</li>
<li>Cron: keep SQLite cron migrations compatible with legacy run-log tables, archived job stores, diagnostic cron names, and legacy one-shot delete-after-run behavior. (#88285)</li>
<li>Cron: keep update delivery validation scoped, harden restart state, and retire MCP runtimes on isolated cron cleanup.</li>
<li>Memory: serialize QMD update/embed writes per store, reduce Linux watcher fan-out, retry transient FileProvider-backed reads, preserve phase signals on read errors, harden envelope metadata sanitization, reattach Linux native watchers when directories are recreated, and rewrite generated transcript paths on rollover so memory/search state survives concurrent gateway and CLI activity. (#66339, #85931, #89185, #89188, #85351) Thanks @openperf, @amittell, @RomneyDa, and @NianJiuZst.</li>
<li>Memory: keep vector-disabled FTS indexes from resolving embedding providers during sync and search.</li>
<li>Providers: bound generated media downloads from OpenAI, Runway, xAI, MiniMax, BytePlus, DashScope-compatible, FAL, OpenRouter, Google, Vydra, and Comfy providers.</li>
<li>Providers: resolve Google defaults to <code>google-generative-ai</code>, register Vertex static catalog rows, align Foundry reasoning metadata, skip DeepSeek V4 thinking params on Foundry fallback, use MiniMax account OAuth endpoints, preserve Copilot Claude 1M capabilities, suppress disabled Ollama reasoning output, forward Gemini stop sequences, strip Kimi-incompatible Anthropic cache markers, keep OpenAI stop-finished tool calls, and avoid replay ids when the Responses store is disabled. (#88480, #88512, #76612) Thanks @coder999999999, @BryanTegomoh, and @vliuyt.</li>
<li>Providers: cap GitHub Copilot OAuth request timeouts before creating abort signals.</li>
<li>Cron: retry recurring jobs after transient model rate limits before waiting for the next scheduled slot.</li>
<li>Agents/Codex: keep live session locks during cleanup, recover interrupted CLI tool transcripts, preserve Codex auth and compaction session identity, clear orphan tool state, cap app-server idle timers, and keep media completion delivery retryable. (#88129, #88136, #88141, #88162, #88182)</li>
<li>Chat/UI: show Gateway chat failures as visible assistant messages in the Control UI instead of only setting an invisible error state.</li>
<li>Channels: cap Telegram, Discord, WhatsApp, Signal, Feishu, Google Chat, Microsoft Teams, QQBot, Nostr, Zalo, Zalouser, and Nextcloud-style request/retry timers; preserve SMS approval reply routes; and retry WhatsApp QR login 408 timeouts. (#88183)</li>
<li>Security/config parsing: reject unsafe OAuth/token lifetimes, retry-after delays, inbound timestamps, response body sizes, command timeout config, sandbox observer token TTLs, and gateway WebSocket calls after close.</li>
<li>Providers/media: cap local service, model, usage, queue, generated media, TTS, music, workflow polling, and provider OAuth request timers across hosted and local providers.</li>
<li>Release/CI/E2E: bound release candidate reads, beta smoke REST calls, plugin npm verification commands, changelog restore, cross-OS process groups, kitchen-sink and bundled plugin readiness probes, secret-provider probes, Telegram credential timeouts, Control UI i18n and CLI startup metadata generation, Vitest routing, dependency guard admin approvals, child workflow failure detection, quiet Node test shard stalls, docker package cleanup, and mainline test flakes. (#88127, #88137, #88155, #88160, #88966) Thanks @RomneyDa.</li>
<li>Release/CI/E2E: keep Kitchen Sink live plugin MCP probes resolving source-checkout workspace packages and align the live gauntlet with current Kitchen Sink diagnostics.</li>
<li>Release/CI/E2E: run the secret-provider integration proof through the repo pnpm runner so native macOS and Windows validation use the hydrated package-manager shim.</li>
<li>Release/CI/E2E: run the Telegram desktop proof gateway through the repo pnpm runner so native macOS proof uses the hydrated package-manager shim.</li>
<li>Docs/CI: run Mintlify anchor checks through the repo pnpm runner so docs link validation works when pnpm is only available through the hydrated package-manager shim.</li>
<li>Agents: keep configured fallback model metadata typed so provider params, context-token caps, and media input limits do not break changed-gate typechecks.</li>
<li>Agents: accept hidden <code>sessions_send</code> body aliases before validation while keeping the model-facing <code>message</code> schema canonical. (#88229) Thanks @zhangguiping-xydt.</li>
<li>Chat/UI: preserve startup chat sends during history loading, unblock the initial Control UI chat send, stream chat deltas incrementally, skip markdown parsing while streaming, keep drafts local while typing, guard composer rerenders, honor Chromium executable overrides, and detect system Chromium for E2E. (#88998) Thanks @vincentkoc.</li>
<li>Channels: stop schema-padded poll modifiers from turning normal <code>send</code> actions into invalid poll sends. (#89601) Thanks @codezz.</li>
<li>Channels: preserve long Feishu streaming replies, send visible fallbacks when accepted Feishu turns produce no final reply, tolerate iMessage self-chat timestamp skew, preserve colon-prefixed slash commands in mention parsing, decode Nostr <code>npub</code> allowlists correctly, and suppress raw provider errors during channel delivery. (#87896)</li>
<li>Config/status/doctor: skip unresolved shell references in state-dir dotenv files, resolve gateway auth secrets during deep status audits, respect explicit PI runtime policy, report runtime tool-schema errors, and keep post-upgrade JSON stable. (#88288)</li>
<li>Gateway/session state: list commands from the Gateway plugin registry, harden MCP loopback tool schemas, hide phantom agent-store rows from <code>sessions.list</code>, make task persistence failures explicit, and carry session UUIDs on interactive dispatch events.</li>
<li>Gateway/plugins: narrow plugin lookup memoization to the stable plugin/runtime inputs, avoiding repeated lookup work without mixing disabled or filtered plugin state.</li>
<li>OpenAI/TTS: handle speed directives for OpenAI TTS voices. (#74089)</li>
<li>CI/Crabbox: keep default runner capacity on the Azure credit-backed on-demand D4 lane with the Azure SSH port and a Git-independent full check job, so broad validation avoids low-priority spot quota stalls, hydrate port mismatches, non-Git hydrated workspaces, and stale AWS region hints.</li>
<li>CI/Crabbox: route Crabbox wrapper and Testbox workflow edits to their regression tests so changed-test gates do not silently run zero specs.</li>
<li>CI/workflows: route workflow sanity helper edits to their guard tests and cover composite-action input interpolation checks.</li>
<li>CI/tooling: route CI scope, dependency, changelog, and docs helper edits to their owner tests instead of silently skipping changed-test coverage.</li>
<li>CI/tooling: route package, release, and install helper edits to their owner tests so changed-test gates cover publish and installer script changes.</li>
<li>CI/tooling: route shared script library edits through their owner tests so lock, process, safety, and scan helpers do not skip changed-test coverage.</li>
<li>CI/tooling: skip expensive import-graph scans once a changed diff already requires broad fallback, keeping local changed-test planning fast while still collecting explicit owner tests.</li>
<li>CI/tooling: route script edits through conventional owner tests when matching <code>test/scripts</code> or <code>src/scripts</code> coverage already exists.</li>
<li>CI/tooling: honor option terminators in the memory FD repro script so follow-on arguments are not reparsed.</li>
<li>Release/CI/E2E: assert plugin lifecycle runtime inspect output instead of only capturing it.</li>
<li>Release/CI/E2E: make gateway-network prove the advertised health RPC and retry early WebSocket closes without burning full open timeouts.</li>
<li>Release/CI/E2E: honor option terminators across release, Parallels smoke, plugin gauntlet, and extension-memory scripts.</li>
<li>Release/CI/E2E: fail plugin gateway gauntlet QA chunks when the requested suite summary is missing or invalid.</li>
<li>Performance: prebuild QA runtime probes with generated plugin assets but without CLI startup metadata.</li>
<li>Performance: skip declaration bundling for runtime-only CLI startup and gateway watch build profiles.</li>
<li>Performance: reuse prepared provider handles, strict tool schemas, gateway runtime metadata, session maintenance config, plugin metadata, bundled skill allowlists, package-local plugin artifacts, single-entry store writes, and validated/serialized session prompt blobs.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.6.1/OpenClaw-2026.6.1.zip" length="55062100" type="application/octet-stream" sparkle:edSignature="PVp8E2HBCvikB/0LCr36lFEyHPAzoFA2ScT6LW27FlzvP+m4r1AEuVN2UrtgWlpkGSsn4Eav0kPJe32u4ObNBw=="/>
</item>
<item>
<title>2026.5.28</title>
<pubDate>Sat, 30 May 2026 21:21:09 +0000</pubDate>
@@ -240,5 +113,214 @@
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.27/OpenClaw-2026.5.27.zip" length="54488811" type="application/octet-stream" sparkle:edSignature="c5w2T1UO6vpPs70hyYH93cIyWEOd5sl5z2NkhU53E+XQBSd+jAr+xd0qf3KzWbeX2mfXYMQmnx+VMls3L22EDg=="/>
</item>
<item>
<title>2026.5.26</title>
<pubDate>Wed, 27 May 2026 12:24:26 +0000</pubDate>
<link>https://raw.githubusercontent.com/openclaw/openclaw/main/appcast.xml</link>
<sparkle:version>2026052690</sparkle:version>
<sparkle:shortVersionString>2026.5.26</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<h2>OpenClaw 2026.5.26</h2>
<h3>Highlights</h3>
<ul>
<li>Faster Gateway and replies: startup avoids repeated plugin, channel, session, usage-cost, warning, scheduled-service, and filesystem scans; visible replies separate user-facing sends from slower follow-up work; Gateway runtime/session caches churn less under load.</li>
<li>Transcripts are core: transcript-backed meeting summaries, source-provider chunks, cleaned user turns, media provenance, Codex mirrors, WebChat replies, and CLI/TUI replay now use one more reliable transcript path.</li>
<li>More channels are production-ready: Telegram keeps typing/progress context and forum topics, iMessage handles attachment roots, remote media staging, and duplicate local Messages sources, WhatsApp restores group/media behavior, Discord improves voice playback and model picking, and Signal/iMessage/WhatsApp get reaction approvals.</li>
<li>Better voice and Talk: realtime Talk runs can be inspected, steered, cancelled, or followed up from Web UI and Discord voice; wake-name handling is more tolerant without letting ambient speech trigger agents.</li>
<li>Safer content boundaries: Browser snapshot reads honor SSRF policy, system-event text cannot spoof nested prompt markers, fetched file text is wrapped as external content, ClickClack inbound sender allowlists run before agent dispatch, stale device tokens are rejected, and serialized tool-call text is scrubbed from replies.</li>
<li>Providers, Codex, and local models are steadier: named auth profiles, OpenAI sampling params, Codex app-server resume/timeout/usage-limit recovery, dynamic tool-schema guards, xAI usage-limit surfacing, Ollama top-p normalization, and local approval resolution reduce provider-specific dead ends.</li>
<li>More reliable install/update/release paths: Alpine installs, trusted runtime fallback roots, stable update channels, Docker/package timeouts, Windows Scheduled Tasks, Windows/macOS proof lanes, Testbox/Crabbox delegation, plugin publish checks, and macOS runner bootstraps all got hardened.</li>
<li>Better observability: Activity tab, gateway secret-prep traces, tool/model stream progress, explicit fast-mode status, systemd Gateway hygiene, OpenTelemetry LLM spans, release performance evidence, and richer telemetry signals make failures easier to inspect.</li>
</ul>
<h3>Changes</h3>
<ul>
<li>Transcripts: add core transcript capture and source-provider support for transcript-backed meeting summaries, including the renamed Transcripts docs, CLI surface, source-provider chunks, and cleaned user-turn persistence.</li>
<li>Auth: add named model login profiles and supported credential migration for Hermes, OpenCode, and Codex auth profiles, with explicit opt-out and non-interactive controls. (#85667) Thanks @fuller-stack-dev.</li>
<li>Diagnostics: trace gateway secret preparation, classify skill/tool usage, surface model stream progress, add OpenTelemetry LLM content spans, and expose alertable telemetry for blocked tools, failover, stale sessions, liveness, oversized payloads, and webhook ingress. (#83019, #80370, #86191)</li>
<li>Channels: add Signal reaction approvals, iMessage thumb approval reactions, and WhatsApp thumb approval reaction support so mobile approval flows work without textual <code>/approve</code> commands. (#85894, #85952, #85477)</li>
<li>Agents/API: forward OpenAI sampling params through the Gateway and expose estimated context-budget status for active agent runs. (#84094)</li>
<li>TUI/status: queue prompts submitted while an agent is busy and show explicit fast-mode state plus richer systemd Gateway hygiene in status output. (#86722, #87115, #86976)</li>
<li>Exec approvals: hide durable approval actions that are unavailable for the current prompt and keep approval runtime tokens local-only so stale prompts cannot offer misleading controls. (#86270, #86359)</li>
<li>Plugin SDK: add reaction approval helpers and keep diagnostic event root exports discoverable across function-name and alias-bound module graphs. (#86735, #87084)</li>
<li>Android/iOS: add the Android pair-new-gateway action and improve mobile Talk mode surfaces, including iOS realtime Talk mode and Android offline voice/gateway recovery. (#86798, #86355) Thanks @ngutman.</li>
<li>Performance: cache plugin metadata snapshots, package realpaths, stable gateway metadata, model cost indexes, channel resolution, usage-cost indexes, and session/auth hot-path facts so common Gateway and reply paths do less rediscovery. (#84649, #85843, #86517, #86678)</li>
<li>Voice: expose shared realtime turn-context tracking through the realtime voice SDK and reuse it for Discord speaker attribution and wake-name context recovery.</li>
<li>Voice: reuse shared realtime output activity tracking in Google Meet command and node audio bridges, including recent-output checks for local barge-in detection.</li>
<li>Voice: expose shared realtime output activity tracking through the realtime voice SDK and reuse it for Discord playback activity and barge-in decisions.</li>
<li>Voice: expose shared realtime consult question matching, speakable-result extraction, and alias-aware forced-consult coordination through the realtime voice SDK, then reuse it in Gateway Talk, Voice Call, and Discord voice paths.</li>
<li>Voice: share activation-name matching and consult-transcript screening through the realtime voice SDK so Discord, browser voice, and meeting surfaces can reuse one implementation.</li>
<li>Cron: default <code>cron.maxConcurrentRuns</code> to 8 so scheduled automations and their isolated agent turns can make progress in parallel without explicit configuration.</li>
<li>QA-Lab: add <code>qa coverage --match <query></code> so focused proof selection can discover matching scenarios from existing metadata before running live or remote lanes.</li>
<li>Discord/model picker: surface an alpha-bucket select (e.g. <code>AG (12) · HN (18) · OZ (5)</code>) when the provider list or a provider's model list exceeds 25 items, so configs with <code>provider/*</code> wildcards stay one click from the right page instead of paginating through prev/next; falls back to numeric chunks when every item shares the same first letter.</li>
<li>Control UI: add an ephemeral Activity tab for sanitized live tool activity summaries without persisting raw telemetry. Fixes #12831. Thanks @BunsDev.</li>
<li>Build: include <code>ui:build</code> in the <code>full</code> and <code>ciArtifacts</code> profiles of <code>scripts/build-all.mjs</code> so <code>pnpm build</code> always rebuilds <code>dist/control-ui</code> after <code>tsdown</code> cleans <code>dist</code>, removing the second-command requirement and the missing-asset failure mode for source/runtime installs and CI artifact uploads. (#85206)</li>
<li>iOS: improve Talk mode with direct realtime voice sessions, compact toolbar status, and responsive voice waveform feedback. (#86355) Thanks @ngutman.</li>
<li>Media: replace the Sharp image backend with Rastermill for metadata, resizing, EXIF orientation, and PNG alpha-preserving optimization so OpenClaw no longer installs Sharp or the WhatsApp Jimp fallback for image processing. (#86437)</li>
<li>Codex: update the bundled Codex CLI to 0.134.0 and keep native compaction disabled for budget-triggered app-server turns so OpenClaw owns the recovery boundary. (#86772)</li>
</ul>
<h3>Fixes</h3>
<ul>
<li>Memory/security: reject prompt-like text submitted through the explicit <code>memory_store</code> tool before embedding or storage, matching the existing auto-capture prompt-injection filter. (#87142)</li>
<li>Gateway/security: enable the default auth rate limiter for remote non-browser and HTTP gateway auth failures when <code>gateway.auth.rateLimit</code> is unset, while preserving the loopback exemption. (#87148)</li>
<li>Security/content boundaries: validate Browser snapshot tab URLs against SSRF policy before ChromeMCP or direct CDP reads, sanitize queued system-event text so untrusted plugin/channel labels cannot spoof nested prompt markers, wrap fetched file text and metadata as external content, apply ClickClack <code>allowFrom</code> sender allowlists before agent dispatch, reject RPCs from invalidated device-token clients during rotation, require staged sandbox media refs, and scrub serialized tool-call text from replies. (#78526, #87094, #87062, #83741, #70707, #86924) Thanks @zsxsoft, @ttzero25, and @mmaps.</li>
<li>Transcripts/user turns: persist CLI, WebChat, media, follow-up, hook, and Codex-mirror user turns to the admitted session target; keep cleaned transcript text, inline image routing, provenance metadata, replay hooks, and fallback paths idempotent when runtimes fail or restart.</li>
<li>TUI/status/onboarding/UI: queue busy TUI prompts instead of dropping them, preserve the configured default model during onboarding, show failed tool results as errors, show config-open failures in Control UI, keep status JSON plugin scans healthy, preserve xAI usage-limit errors locally, and expose explicit fast-mode/systemd state. (#86722, #87000, #85786, #87108, #87001, #86614, #87115, #86976)</li>
<li>Plugin commands/SDK: preserve plugin LLM command auth, bind native plugin command dispatch to the host agent's LLM auth, keep <code>onDiagnosticEvent</code> exports discoverable through <code>Function.name</code>, stabilize diagnostic event root aliases, correlate pathless read diagnostics, suppress transient runner failures in channel command paths, and repair local approval resolution. (#85936, #87084, #86977, #87069, #86771)</li>
<li>Codex/providers: keep WebChat delivery hints out of user prompts, avoid false queued-terminal idle timeouts, share the native hook relay registry, quarantine unsupported dynamic tool schemas, preserve Claude resumed-session system prompts, normalize greedy Ollama <code>top_p</code>, preserve per-agent thinking defaults for ingress runs, and avoid native compaction takeover on budget-triggered Codex turns. (#87096, #73950, #87049, #86689, #86772)</li>
<li>Gateway/perf/release: reuse startup-warning metadata and prepared auth stores, avoid cloning live-switch and lifecycle session caches on read paths, defer warning and scheduled-service fallback imports, trim Gateway session/startup/runtime CPU churn, skip duplicate turn session touches, stop chat timeout fallback cascades, drop stale subagent announce history, bound benchmark/watch/kitchen-sink teardown waits, bound macOS/package/onboarding/plugin smoke commands, bound install finalization probes, resolve Parallels npm-update commands from guest <code>PATH</code>, and bootstrap raw AWS macOS Node/pnpm commands through <code>/usr/bin/env</code>. (#86997)</li>
<li>Reply/perf: reduce visible reply delivery latency by preserving Telegram typing/progress context, lazy-loading slash-command startup metadata, avoiding hot-path model hydration, flag-gating Codex profiler timing, deferring context compaction maintenance, and tracking delivery timing. (#86989, #86990, #86991, #86992, #86993, #86994) Thanks @keshavbotagent.</li>
<li>Reply/source delivery: keep TUI, Control UI, media, TTS, transcript, and Codex source-reply finals live without duplicate terminal events or stale replay artifacts.</li>
<li>Agents/replay: repair legacy tool results before replay, preserve <code>sessions_spawn</code> transcript payloads, restore current guard checks, stage sandboxed workspace media, and keep duplicate transcripts tool display metadata from reappearing. (#82203, #86934, #87025) Thanks @martingarramon, @vincentkoc, and @joshavant.</li>
<li>Agents/sessions: handle active-fallback failures in <code>sessions_send</code> so fallback routing reports the real failure and does not leave callers with an ambiguous dropped send. (#86638)</li>
<li>Agents/hooks/subagents: enforce default hook agent allowlists, recover failed subagent lifecycle completions, and keep node task lifecycle cleanup from closing the Gateway listener. (#86101)</li>
<li>Codex: project newer OpenClaw chat history into resumed app-server threads and keep Codex turn timeouts inside the Codex runtime boundary so timeouts do not poison shared app-server clients or fall through to unrelated provider fallback. (#86677, #86476) Thanks @TurboTheTurtle and @pashpashpash.</li>
<li>Config/doctor/update: narrow profiled tool-section doctor repair, keep runtime-injected legacy web-search provider config out of user-authored config validation, and keep prerelease tags excluded from stable updater resolution. (#87030, #86818, #86559) Thanks @joshavant, @luoyanglang, and @stevenepalmer.</li>
<li>CLI/Windows: add a Windows-only stack-size respawn for stack-heavy startup paths, default CLI logs to local timestamps, and validate timeout/banner TTY state more strictly. (#87031, #85387) Thanks @giodl73-repo and @vincentkoc.</li>
<li>Locking/security: require owner identity proof before stale plugin lock removal, memoize session lock owner arguments, and avoid writing default exec approval stores unless policy state actually changed. (#86814, #86964) Thanks @Alix-007 and @vincentkoc.</li>
<li>Install/release: bound Docker package build, inventory, pack, and tarball preparation with process-group timeouts; pin shrinkwrap patch drift to the pnpm lock; harden macOS restart and dSYM packaging; and run release Docker/live timeout wrappers in the foreground so child processes cannot wedge gates.</li>
<li>Telegram/network: treat <code>ENETDOWN</code> as a transient pre-connect network failure so Telegram sends, gateway unhandled-rejection handling, and cron network retries follow the same recovery path as sibling network outages. (#86762) Thanks @TurboTheTurtle.</li>
<li>Telegram: preserve inbound text entities, overlapping DM replies, account topic cache sidecars, outbound reply context, targeted bot-command mentions, durable group retry targets, forum topic names, and native progress callbacks. (#83873, #85361, #85555, #85656, #85709, #86299, #86553) Thanks @SebTardif, @luoyanglang, and @neeravmakwana.</li>
<li>iMessage: read image attachments from local Messages attachment roots, dedupe duplicate local Messages-source accounts, seed direct DM history, fix image/group media attachment commands, advance catchup cursors after live handling, and keep slash-command acknowledgements in the source conversation. (#82642, #85475, #86569, #86705, #86706, #86770) Thanks @homer-byte, @TurboTheTurtle, @swang430, and @OmarShahine.</li>
<li>WhatsApp/QQ/Twitch/IRC/Slack: restore WhatsApp ack identity and group-drop warnings, make QQ Bot media respect <code>OPENCLAW_HOME</code>, serialize Twitch auth disconnects, store IRC channel routes canonically, and keep Slack downloaded files out of reply media. (#83833, #85309, #85777, #85794, #85906, #86318, #86697) Thanks @sliverp, @neeravmakwana, and @Kailigithub.</li>
<li>Discord/voice: improve voice playback and wake replies, bucket large model picker menus, merge media captions into one message, route metadata through configured proxies, restore numeric channel sends, suppress self-reply echoes, and tighten wake matching without breaking fuzzy wake phrases. (#80227, #86238, #86487, #86571, #86595, #86601)</li>
<li>Codex: preserve native web-search metadata, keep oversized native thread reuse, bridge CLI API-key auth into the app server, preserve sandbox bootstrap path style, recover context-window prompt errors, honor yolo approval policy, disable native thread personality, and route compaction through Codex auth. (#85378, #85542, #85891, #85909, #86408)</li>
<li>Agents/runtime: enforce session lock max-hold reclaim, release embedded-attempt locks on all exits, treat aborted subagent runs as terminal, avoid runtime model hydration on hot paths, disclose scoped session list counts, derive overflow budgets from provider errors, and keep fallback errors scoped to the active model candidate. (#70473, #85764, #86014, #86134, #86427, #86944) Thanks @openperf, @fuller-stack-dev, @zhangguiping-xydt, and @ferminquant.</li>
<li>Config/update/doctor: retry config recovery after failed backup restore, skip shell env fallback on Windows, exclude prerelease tags from the stable git channel, support deep config edits, warn instead of aborting on unreadable cron stores, prune stale bundled plugin paths, and avoid duplicate restart prompts when the Gateway is already healthy. (#85739, #85787, #86060, #86260, #86384, #86533) Thanks @liaoyl830.</li>
<li>Install/release: support Alpine CLI installs and runtime floors, prefer trusted startup argv runtime fallback roots, reject stale CLI node runtimes, avoid npm <code>min-release-age</code> installer failures, bound npm/package/Docker install phases, restore config parent ownership in Docker, seed Docker lockfile package tarballs before prune, make release/plugin prerelease checks fail closed instead of hanging or false-greening, and use host-visible Crabbox local work roots for Docker-backed proof. (#85491)</li>
<li>Windows daemon: keep Scheduled Task gateway launches running on battery power and avoid workgroup-machine prompts for a domain user during task installation. (#59299)</li>
<li>Security: avoid printing Gateway tokens in Docker, validate plugin model-pattern regexes safely, escape transcript metadata field names, harden session allowlist glob matching, audit Claude permission overrides under YOLO, and require explicit allow for ACP auto approvals. (#85849, #85934, #86046, #86557)</li>
<li>Media/images: replace Sharp with Rastermill, keep EXIF normalization best-effort, normalize HEIC/HEIF before image descriptions, route Codex image API keys through OpenAI, preserve image compression metadata, and auto-scale live tool result caps. (#85776, #86037, #86437, #86857, #86923)</li>
<li>Memory: prevent semantic vector indexes from silently degrading when embeddings are unavailable, stop doctor OOMs on large session stores, preserve sidecar hooks/artifacts, write fallback dream diaries, use CJK-aware dreaming dedupe, and avoid per-file watcher FD fan-out. (#80613, #82928, #85060, #85704, #85967, #86701) Thanks @brokemac79, @openperf, and @yaaboo-gif.</li>
<li>Agents/sessions: include visibility metadata on restricted <code>sessions_list</code> results so scoped counts are clearly reported without widening access or exposing hidden-session counts. (#86944) Thanks @ferminquant.</li>
<li>Gateway/DNS: validate wide-area discovery domains before deriving zone paths or writing zone files, so invalid <code>discovery.wideArea.domain</code> and <code>dns setup --domain</code> values fail with a DNS-name diagnostic instead of falling through to unrelated configuration errors. Thanks @mmaps.</li>
<li>Agents/BTW: route fallback side-question streams through the embedded stream resolver so Anthropic-compatible MiniMax requests use the same capped transport as normal chat. (#86312) Thanks @neeravmakwana.</li>
<li>Telegram: treat <code>/command@TargetBot</code> bot-command entities as explicit mentions for the addressed bot so <code>requireMention</code> groups no longer drop targeted commands or captions. Fixes #84462. (#86553) Thanks @luoyanglang.</li>
<li>CI: bound Docker/Bash E2E tarball npm installs with <code>OPENCLAW_E2E_NPM_INSTALL_TIMEOUT</code> so package, onboarding, plugin, and upgrade lanes fail instead of hanging on a stuck npm install.</li>
<li>CI: fail Parallels npm-update smoke jobs after the guest command timeout and cleanup backstop instead of only logging a timeout line.</li>
<li>CI: bound kitchen-sink RPC HTTP probes so stalled gateway readiness or response bodies fail and retry instead of wedging the walker.</li>
<li>CI: keep <code>OPENCLAW_TESTBOX=1 pnpm check:changed</code> delegating to Blacksmith Testbox through Crabbox without forwarding local Testbox or worker env into the remote command.</li>
<li>CI: send KILL after the TERM grace period for manual checkout fetch timeouts so stuck Testbox and workflow checkout retries cannot hang behind a wedged <code>git fetch</code>.</li>
<li>CI: send KILL after the TERM grace period for Bun global install smoke command timeouts so trapped <code>openclaw</code> child processes cannot wedge the scheduled install smoke.</li>
<li>iMessage: thread current channel/account inbound attachment roots into the image tool so iMessage-saved attachments under <code>~/Library/Messages/Attachments</code> (including the wildcard <code>/Users/*/Library/Messages/Attachments</code> root) are read through the existing inbound path policy instead of being rejected as <code>path-not-allowed</code>. Literal <code>localRoots</code> stays workspace-scoped. Fixes #30170. (#86569)</li>
<li>QQ Bot: respect <code>OPENCLAW_HOME</code> for outbound media path resolution so <code><qqmedia></code> sends no longer silently fail when <code>HOME</code> and <code>OPENCLAW_HOME</code> differ (Docker / multi-user hosts). Persisted QQ Bot data (sessions, known users, refs) stays anchored on the OS home for upgrade compatibility. Fixes #83562. Thanks @sliverp.</li>
<li>Update: report the primary malformed <code>openclaw.extensions</code> payload error without adding a duplicate missing-main diagnostic. (#86596) Thanks @ferminquant.</li>
<li>Control UI: keep host-local Markdown file paths inert while preserving app-relative links. (#86620) Thanks @BryanTegomoh.</li>
<li>Gateway: dampen repeated unauthenticated device-required probes per URL while preserving explicit-auth and paired recovery paths. (#86575) Thanks @ferminquant.</li>
<li>IRC: store inbound channel routes with the canonical <code>channel:#name</code> target and join transient channel sends before writing. (#85906) Thanks @Kailigithub.</li>
<li>Usage: surface unknown all-zero model pricing as missing cost entries instead of a confident <code>$0</code> total. (#85882) Thanks @MichaelZelbel.</li>
<li>Agents/Codex: honor yolo app-server approval policy only for the full <code>never</code> plus <code>danger-full-access</code> case. (#85909) Thanks @earlvanze.</li>
<li>Gateway/Gmail: clear Gmail watcher renewal intervals on re-entry so hot reloads do not leak lifecycle timers. (#82947) Thanks @SebTardif.</li>
<li>Logging: exit cleanly on broken stdout/stderr pipes without masking existing failure exit codes. (#80059) Thanks @pavelzak.</li>
<li>Gateway/security: escape transcript metadata field names while extracting oversized session line prefixes. (#85934) Thanks @SebTardif.</li>
<li>Plugins/security: validate manifest model pattern regexes with the safe-regex compiler so unsafe patterns are ignored before matching. (#86046) Thanks @SebTardif.</li>
<li>Discord: route gateway metadata REST lookups through the configured Discord proxy so proxied accounts do not fall back to direct <code>discord.com</code> connections before opening the WebSocket. Fixes #80227. Thanks @Clivilwalker.</li>
<li>Agents/media: hydrate current-turn image attachments from filename-derived MIME types so active vision can see generated or forwarded images whose source omitted an image content type. (#84812) Thanks @marchpure.</li>
<li>Agents/fs: point workspace-only scratch-path guidance at in-workspace temp directories while keeping host-root writes rejected by the tool guard. (#86501) Thanks @tianxiaochannel-oss88.</li>
<li>Agents/media: keep async cron media completions scoped to their run session while preserving direct delivery for stale generated-media success and failure notifications. (#86529) Thanks @ai-hpc.</li>
<li>Gateway: emit plugin <code>session_end</code>/<code>session_start</code> hooks when <code>agent.send</code> rotates or replaces a session id, keeping hook lifecycle state aligned with <code>sessions.changed</code> notifications. Fixes #83507. (#85875) Thanks @brokemac79.</li>
<li>OpenShell/SSH: reject malformed generated exec commands before sandbox/session setup so unresolved workflow placeholders fail fast instead of reaching the remote shell. Fixes #72373. Thanks @brokemac79.</li>
<li>Google: stop normalizing <code>gemini-3.1-flash-lite</code> to the retired preview endpoint and update Flash Lite alias guidance to the GA model id. Fixes #86151. (#86240) Thanks @SebTardif.</li>
<li>Installer: make Alpine apk installs cover Git, verify the Node runtime floor, try <code>nodejs-current</code>, and report Alpine version guidance when repositories only provide older Node packages.</li>
<li>Agents/status: prefer the active Claude CLI OAuth auth label over an unused Anthropic env API-key label for equivalent runtime aliases. Fixes #80184. (#86570) Thanks @brokemac79.</li>
<li>Agents/media: send direct fallback for generated media still missing after an active requester wake fails. (#85489) Thanks @fuller-stack-dev.</li>
<li>Agents: derive overflow compaction budgets from provider-reported and synthetic over-budget token counts so confirmed context overflows compact before retrying. (#70473) Thanks @fuller-stack-dev.</li>
<li>Agents/Codex: recover Codex context-window prompt errors through overflow compaction and surface reset guidance when recovery is exhausted. (#85542) Thanks @fuller-stack-dev.</li>
<li>Agents/Codex: allow Codex app-server runs to bootstrap from <code>CODEX_API_KEY</code> or <code>OPENAI_API_KEY</code> when no Codex auth profile is configured.</li>
<li>Agents/Codex: keep selected Codex runtime routing on OpenAI-Codex while preserving direct OpenAI API-key compaction fallback. (#86408) Thanks @funmerlin and @VACInc.</li>
<li>Agent transcript: include OpenClaw agent session logs when finding local transcript candidates.</li>
<li>Crabbox: bootstrap raw AWS macOS shell commands wrapped in absolute <code>time</code> paths so RSS probes can run Node and pnpm on fresh macOS runners.</li>
<li>Crabbox: bootstrap raw AWS macOS shell commands even when setup statements precede Node or pnpm usage.</li>
<li>TUI/local: skip unnecessary secret resolution, gateway model catalog loading, bootstrap, and skill scans in explicit local-model runs so startup reaches the model request faster.</li>
<li>Sessions/doctor: load large session stores without clone amplification during read-only doctor checks and reclaim stale <code>sessions.json.*.tmp</code> sidecars. Fixes #56827. Thanks @openperf.</li>
<li>Tests: clean successful plugin gateway gauntlet isolated temp roots while keeping an explicit preservation switch for failed/debug runs.</li>
<li>Plugins/perf: reuse derived plugin metadata snapshots for the lifetime of the process so reply-time skill setup no longer rescans plugin metadata on every turn.</li>
<li>Discord/OpenAI voice: keep wake-name master consults using the current speaker context after ignored ambient transcripts and shorten the default capture silence grace.</li>
<li>Doctor: skip redundant Gateway restart prompts when a recent supervisor restart leaves the Gateway healthy. Fixes #86518. (#86533) Thanks @liaoyl830.</li>
<li>Cron: restore suspended cron lanes to the configured/default concurrency instead of falling back to one after quota or circuit-breaker auto-resume.</li>
<li>Gateway: keep session-only Control UI tool-start mirrors flowing during diagnostic queue pressure instead of silently dropping non-terminal tool updates.</li>
<li>Agents/memory: return optional not-found context for missing date-only daily memory reads instead of logging benign first-run <code>ENOENT</code> failures. Fixes #82928. Thanks @galiniliev.</li>
<li>Discord: merge streamed text captions into following media block replies so captions and attachments send as one message. (#86487) Thanks @neeravmakwana.</li>
<li>Gateway: avoid sending duplicate tool-event frames to Control UI connections that are subscribed by both run and session.</li>
<li>Discord/OpenAI voice: accept broader edge-position fuzzy wake-name transcripts while keeping ambient speech gated.</li>
<li>Discord/OpenAI voice: accept longer leading wake-name mistranscripts such as "Open Club" for OpenClaw.</li>
<li>Agents/OpenAI-compatible: stop ModelStudio-compatible chat requests before sending system/tool-only payloads that have no usable user or assistant turn. (#86177) Thanks @TurboTheTurtle.</li>
<li>Gateway/plugins: reuse plugin package realpath checks while building installed plugin indexes so startup avoids repeated filesystem resolution work.</li>
<li>Kilo Gateway: send string <code>stop</code> sequences as arrays so Kilo accepts OpenAI-compatible chat completions. (#86461) Thanks @SebTardif.</li>
<li>Discord/OpenAI voice: accept leading fuzzy wake-name transcripts such as "Monty" or "Moti" for a Molty agent while keeping ambient speech gated.</li>
<li>Media understanding: convert HEIC and HEIF images to JPEG before image description providers run so iPhone photos work in direct and configured image-description flows. (#86037)</li>
<li>Agents: release embedded-attempt session locks from outer teardown so post-prompt exceptions cannot wedge later requests behind <code>SessionWriteLockTimeoutError</code>. Fixes #86014. Thanks @openperf.</li>
<li>Discord/OpenAI voice: rotate Realtime sessions at provider max duration without logging the expected session-expiry event as an error.</li>
<li>Sessions: skip metadata-only entries during QMD-slugified session lookup so one incomplete row does not block transcript hit resolution. (#86327) Thanks @abnershang.</li>
<li>Agents/media: derive bundled plugin local-media trust from plugin tool metadata instead of importing the full plugin registry on subscription paths. (#84409) Thanks @samzong.</li>
<li>Image tool: keep config-backed custom-provider API keys usable for auto-discovered vision models, including deferred image-tool execution without env keys or auth profiles. (#85733)</li>
<li>Memory/local embeddings: run local GGUF embeddings in an isolated worker sidecar and degrade to configured fallback or keyword search on worker failure so native embedding crashes do not take down the Gateway. (#85348) Thanks @osolmaz.</li>
<li>Gateway: clear the runtime config snapshot before <code>SIGUSR1</code> in-process restarts so config changes survive the next gateway loop. (#86388) Thanks @XuZehan-iCenter.</li>
<li>Models: show OAuth delegation markers as configured <code>models.json</code> auth while keeping runtime route usability checks strict. (#86378) Thanks @rohitjavvadi.</li>
<li>Cron: seed active scheduled and manual cron task rows with a progress summary so status surfaces do not look blank while jobs run. (#86313) Thanks @ferminquant.</li>
<li>Cron: preserve unsupported persisted cron payload rows during routine store writes while keeping those rows non-runnable. Fixes #84922. (#86415) Thanks @IWhatsskill.</li>
<li>Updater: exclude prerelease git tags from stable channel resolution so source updates do not check out newer alpha/rc/preview/canary tags. (#86260) Thanks @stevenepalmer.</li>
<li>Security/Audit: flag webhook <code>hooks.token</code> reuse of active Gateway password auth in <code>openclaw security audit</code> while keeping password-mode startup compatibility. (#84338) Thanks @coygeek.</li>
<li>QQBot: derive the outbound reply watchdog from configured agent and provider timeouts so slow local model replies are not cut off at five minutes. Fixes #85267. (#85271) Thanks @SymbolStar.</li>
<li>Agents/heartbeat: stop heartbeat turns after the first valid <code>heartbeat_respond</code> so repeated response loops do not burn tokens. (#86357) Thanks @udaymanish6.</li>
<li>Tasks: keep retained lost tasks out of default status health counts, explain their cleanup window during maintenance, and prune lost task records after 24 hours instead of the general 7-day terminal retention.</li>
<li>Memory-core: keep REM dreaming focused on live light-staged memories and mark staged entries as considered so old recall history no longer dominates fresh candidates. (#86302) Thanks @SebTardif.</li>
<li>Memory: abort sync instead of downgrading an existing semantic vector index to FTS-only when the configured embedding provider is temporarily unavailable. (#85704) Thanks @yaaboo-gif.</li>
<li>Telegram: propagate forum topic names through the account-scoped topic cache for native command context and topic create/edit actions. (#86299) Thanks @SebTardif.</li>
<li>Slack: keep downloaded read-only files out of reply media so Slack file reads do not echo files back to the conversation. (#86318) Thanks @neeravmakwana.</li>
<li>Cron: accept leading-plus relative durations such as <code>+5m</code> for one-shot <code>--at</code> schedules. (#86341) Thanks @mushuiyu886.</li>
<li>Agents/media: preserve async-started media tool metadata so background generation starts no longer surface generic incomplete-turn warnings while replay stays unsafe. (#85933) Thanks @fuller-stack-dev.</li>
<li>Docker E2E: dedupe scheduler lane resources so npm/service package lanes are not over-counted and serialized unnecessarily.</li>
<li>QA/diagnostics: add a collector-backed OpenTelemetry smoke lane, make the OTLP payload leak check scenario-aware, and keep source QA builds from failing on optional dependency imports resolved through pnpm's temp module path.</li>
<li>Crabbox: bootstrap Git metadata for sparse remote changed gates so raw synced workspaces can run <code>pnpm check:changed</code> from the intended diff.</li>
<li>xAI/LM Studio: avoid buffering ordinary bracketed or <code>final</code> prose until stream completion while watching for plain-text tool-call fallbacks.</li>
<li>Doctor: warn and continue when the cron job store exists but cannot be read so later health checks still run. Fixes #86102. (#86384) Thanks @1052326311.</li>
<li>Discord: suppress a bot's previous reply body and referenced media from prompt context when a user replies to that bot message, while keeping reply metadata for routing. (#86238) Thanks @fuller-stack-dev.</li>
<li>Discord: restore bare numeric channel IDs for outbound message-tool sends while keeping explicit DM targets unambiguous. (#86571) Thanks @joshavant.</li>
<li>Docker E2E: avoid rebuilding the Control UI twice while preparing the shared OpenClaw package tarball for package-backed scenario runs.</li>
<li>Tests: avoid rebuilding the Control UI twice during the installer Docker smoke now that <code>pnpm build</code> includes <code>ui:build</code>.</li>
<li>Tests: give QA config mutation RPCs enough native Windows budget to finish gateway config writes and restart settle after hot scenario runs.</li>
<li>Tests: keep the gateway restart-inflight QA scenario focused on restart recovery on native Windows by allowing expected embedded prompt handoff errors and using the Windows-safe timeout budget.</li>
<li>QA-Lab: make the synthetic OpenAI provider honor generic <code>reply exactly:</code> directives after required kickoff reads so restart-recovery scenarios do not fall through to generic repo-summary prose.</li>
<li>Gateway: abort active <code>agent</code> RPC runs during forced restart shutdown so stale in-process turns cannot keep writing a session after the Gateway lifecycle restarts.</li>
<li>Crabbox: sync clean sparse worktrees through a temporary full checkout even when reusing an existing lease so tracked build-time files are not omitted.</li>
<li>Build: route <code>scripts/ui.js</code> through the shared pnpm runner and keep Control UI chunking helpers in sparse-included source so native Windows Corepack builds can produce <code>dist/control-ui</code>.</li>
<li>Tests: give the memory fallback QA scenario enough turn budget to exercise native Windows gateway runs instead of failing on the client timeout while the mock agent is still dispatching.</li>
<li>Tests: collect QA gateway CPU/RSS metrics on native Windows and give the channel baseline enough turn budget to report slow gateway runs instead of timing out before proof.</li>
<li>Install/update: bypass npm <code>min-release-age</code> policies with <code>--min-release-age=0</code> instead of <code>--before</code> so hosted installers keep working on npm versions that reject the combined config. (#84749) Thanks @TeodoroRodrigo.</li>
<li>Diagnostics: reclaim wedged session lanes when stale active-run bookkeeping blocks queued work despite no forward progress. Fixes #85639. Thanks @openperf.</li>
<li>WebChat: keep message-tool replies visible in the chat while still summarizing internal tool results for the model. Fixes #86347. Thanks @shakkernerd.</li>
<li>Gateway/perf: fail startup benchmark samples when the Gateway process exits before benchmark teardown, including signal deaths after readiness probes.</li>
<li>Gateway/perf: fail restart benchmark samples when the Gateway exits before benchmark teardown, including clean exits and signal deaths after successful restart probes.</li>
<li>Agents/tests: keep model catalog visibility on static selection helpers so catalog visibility checks avoid the broad model-selection barrel import.</li>
<li>Agents/commitments: serialize commitment store load-modify-save writes so concurrent heartbeat and CLI updates no longer lose dismissal, sent, or attempt state. (#81153) Thanks @ai-hpc.</li>
<li>xAI/LM Studio: promote plain-text tool-call fallbacks into structured tool calls and strip leaked internal tool syntax before user-facing delivery. (#86222) Thanks @fuller-stack-dev.</li>
<li>CLI: suppress benign self-update version-skew warnings during package post-update finalization.</li>
<li>Gateway/perf: tighten restart and startup benchmark failure handling so long profiling runs, failed probes, and fresh Linux runners no longer produce false passing or <code>n/a</code> results.</li>
<li>Checks: keep intentional Knip unused-file findings optional so full CI and sparse proof workspaces stay aligned.</li>
<li>Docker: restore writable <code>~/.config</code> in runtime images. Fixes #85968. Thanks @hkoessler and @Bartok9.</li>
<li>Plugin SDK: keep legacy root diagnostic subscriptions connected when built plugin SDK aliases resolve diagnostic helpers through a separate module graph.</li>
<li>Diagnostics: export alertable OTel and Prometheus signals for blocked tools, model failover, stale sessions, liveness warnings, oversized payloads, and webhook ingress while fixing shared OTLP endpoints with query strings.</li>
<li>Tests: normalize macOS canonical temp paths in exec allowlists, fs-safe trash assertions, installed plugin matching, Telegram topic-name stores, and built ACPX MCP server expectations so native macOS proof runners cover the intended behavior.</li>
<li>Codex/app-server: preserve message-tool-only source reply delivery mode on active runs so sub-agent completion wakeups can steer the active Codex turn instead of being rejected. (#86287) Thanks @ferminquant.</li>
<li>Tests: sample the Windows kitchen-sink RPC gateway directly and serialize RSS probes so native runs keep the memory guard active.</li>
<li>Tests: normalize bundled plugin lifecycle probe paths and state-root lookup so native Windows release sweeps accept valid packaged plugin installs.</li>
<li>Agents/Claude CLI: route live native Bash permission requests through OpenClaw exec policy so Claude turns no longer stall on <code>control_request</code>, and document that OpenClaw exec policy is authoritative. Fixes #80819. (#86330, from #81971) Thanks @guthirry and @sallyom.</li>
<li>Security audit: warn when YOLO OpenClaw exec policy overrides a restrictive raw Claude <code>--permission-mode</code> for managed live sessions. (#86557) Thanks @sallyom.</li>
<li>Config: keep benign legacy metadata write anomalies out of default doctor and config command output while preserving explicit anomaly logging for diagnostics.</li>
<li>Codex: log when implicit app-server <code>never</code> approvals are promoted for OpenClaw tool policy, including whether the trigger was a <code>before_tool_call</code> hook or trusted tool policy.</li>
<li>Codex harness: make subscription usage-limit errors without reset times explain that OpenClaw cannot determine the reset and point users to wait until Codex is available, use another Codex account, or switch to another configured model/provider. Thanks @amknight.</li>
<li>Google Vertex: support production ADC modes such as Workload Identity Federation, service-account credentials, and metadata-server ADC for the native Vertex transport. (#83971) Thanks @damianFelixPago.</li>
<li>Telegram: route normal <code>[telegram][diag]</code> polling diagnostics through <code>runtime.log</code> while keeping non-diag warnings and persistence failures on <code>runtime.error</code>, so healthy polling startup no longer looks like an error. Fixes #82957. (#82958) Thanks @galiniliev.</li>
<li>Providers/Ollama: strip inline Kimi cloud reasoning prefixes from streamed and final visible replies while keeping ordinary Kimi answers append-only. (#86286) Thanks @jason-allen-oneal.</li>
</ul>
<ul>
<li>Gateway: require Talk secret authority before setup-code handoff can include Talk secrets. (#85690) Thanks @ngutman.</li>
<li>Agents: keep fallback error reporting scoped to the active model candidate so stale prior-provider quota/auth text is not reported for later fallback attempts. (#86134) Thanks @zhangguiping-xydt.</li>
<li>iMessage: dedupe watcher startup when <code>channels.imessage.accounts</code> lists both <code>default</code> and a named account that point at the same local Messages source, so the gateway no longer spawns two <code>imsg rpc</code> processes or doubles inbound replies; the dedupe is scoped to watcher startup, leaving duplicate accounts addressable for outbound sends, status, and capability listings, and <code>openclaw doctor</code> flags the redundant account with a rebinding hint. Fixes #65141. (#86705) Thanks @swang430.</li>
</ul>
<p><a href="https://github.com/openclaw/openclaw/blob/main/CHANGELOG.md">View full changelog</a></p>
]]></description>
<enclosure url="https://github.com/openclaw/openclaw/releases/download/v2026.5.26/OpenClaw-2026.5.26.zip" length="54484748" type="application/octet-stream" sparkle:edSignature="y4WXG7JT8ktJ+K7YDgllY7u5Z9BSKR/SwGiwEh0gikOJ/SWqwcQd+z2tWa2zgwvCJKWsAUFwJs1ATor880SUBg=="/>
</item>
</channel>
</rss>

View File

@@ -218,7 +218,6 @@ Current OpenClaw Android implication:
- Google Play build excludes SMS send/search, Call Log search, and recent-photo access unless the product is intentionally positioned and approved under the relevant policy exception.
- The repo now ships this split as Android product flavors:
- `play`: removes `READ_SMS`, `SEND_SMS`, `READ_CALL_LOG`, `READ_MEDIA_IMAGES`, `READ_MEDIA_VISUAL_USER_SELECTED`, and `READ_EXTERNAL_STORAGE`; hides SMS, Call Log, and Photos surfaces in onboarding, settings, and advertised node capabilities.
- Installed-app listing is user controlled. `device.apps` is advertised only after the user enables **Settings > Phone Capabilities > Installed Apps**. The command defaults to launcher-visible apps and does not require `QUERY_ALL_PACKAGES`.
- `thirdParty`: keeps the full permission set and the existing SMS / Call Log / Photos functionality.
Policy links:
@@ -253,9 +252,9 @@ Pre-req checklist:
4) Open the app **Screen** tab and keep it active during the run (canvas/A2UI commands require the canvas WebView attached there).
5) Grant runtime permissions for capabilities you expect to pass (camera/mic/location/notification listener/location, etc.).
6) No interactive system dialogs should be pending before test start.
7) Canvas host is enabled and reachable from the device for remote Canvas checks (do not run gateway with `OPENCLAW_SKIP_CANVAS_HOST=1`; startup logs should include `canvas host mounted at .../__openclaw__/`).
7) Canvas host is enabled and reachable from the device (do not run gateway with `OPENCLAW_SKIP_CANVAS_HOST=1`; startup logs should include `canvas host mounted at .../__openclaw__/`).
8) Local operator test client pairing is approved. If first run fails with `pairing required`, preview the latest pending request, approve the printed request ID, then rerun:
9) For A2UI checks, keep the app on **Screen** tab; the node uses its bundled app-owned A2UI page for message application.
9) For A2UI checks, keep the app on **Screen** tab; the node now auto-refreshes canvas capability once on first A2UI reachability failure (TTL-safe retry).
```bash
openclaw devices list
@@ -287,8 +286,8 @@ Common failure quick-fixes:
- `pairing required` before tests start:
- list pending requests (`openclaw devices list`), then approve with the exact ID (`openclaw devices approve <requestId>`) and rerun.
- `A2UI host not reachable` / `A2UI_HOST_UNAVAILABLE`:
- keep the app foregrounded on the **Screen** tab and rerun. A2UI commands use the bundled app-owned A2UI page; the Gateway Canvas host is still needed for remote Canvas checks, but not for A2UI message application.
- `A2UI host not reachable` / `A2UI_HOST_NOT_CONFIGURED`:
- ensure the Canvas plugin host is running and reachable, keep the app on the **Screen** tab. The app refreshes the Canvas plugin surface URL once before failing; if it still fails, reconnect app and rerun.
- `NODE_BACKGROUND_UNAVAILABLE: canvas unavailable`:
- app is not effectively ready for canvas commands; keep app foregrounded and **Screen** tab active.

View File

@@ -65,8 +65,8 @@ android {
applicationId = "ai.openclaw.app"
minSdk = 31
targetSdk = 36
versionCode = 2026060201
versionName = "2026.6.2"
versionCode = 2026053101
versionName = "2026.6.1"
ndk {
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")

View File

@@ -148,7 +148,6 @@ class MainViewModel(
val gatewayBootstrapToken: StateFlow<String> = prefs.gatewayBootstrapToken
val onboardingCompleted: StateFlow<Boolean> = prefs.onboardingCompleted
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
val installedAppsSharingEnabled: StateFlow<Boolean> = prefs.installedAppsSharingEnabled
val speakerEnabled: StateFlow<Boolean> = prefs.speakerEnabled
val voiceCaptureMode: StateFlow<VoiceCaptureMode> = runtimeState(initial = VoiceCaptureMode.Off) { it.voiceCaptureMode }
val micEnabled: StateFlow<Boolean> = runtimeState(initial = false) { it.micEnabled }
@@ -300,10 +299,6 @@ class MainViewModel(
prefs.setCanvasDebugStatusEnabled(value)
}
fun setInstalledAppsSharingEnabled(value: Boolean) {
ensureRuntime().setInstalledAppsSharingEnabled(value)
}
fun setNotificationForwardingEnabled(value: Boolean) {
ensureRuntime().setNotificationForwardingEnabled(value)
}

View File

@@ -189,6 +189,8 @@ class NodeRuntime(
A2UIHandler(
canvas = canvas,
json = json,
getNodeCanvasHostUrl = { nodeSession.currentCanvasHostUrl() },
getOperatorCanvasHostUrl = { operatorSession.currentCanvasHostUrl() },
)
private val connectionManager: ConnectionManager =
@@ -205,7 +207,6 @@ class NodeRuntime(
callLogAvailable = { SensitiveFeatureConfig.callLogEnabled },
photosAvailable = { SensitiveFeatureConfig.photosEnabled },
hasRecordAudioPermission = { hasRecordAudioPermission() },
installedAppsSharingEnabled = { installedAppsSharingEnabled.value },
manualTls = { manualTls.value },
)
@@ -244,7 +245,6 @@ class NodeRuntime(
smsTelephonyAvailable = { sms.hasTelephonyFeature() },
callLogAvailable = { SensitiveFeatureConfig.callLogEnabled },
photosAvailable = { SensitiveFeatureConfig.photosEnabled },
installedAppsSharingEnabled = { installedAppsSharingEnabled.value },
debugBuild = { BuildConfig.DEBUG },
onCanvasA2uiPush = {
_canvasA2uiHydrated.value = true
@@ -252,6 +252,7 @@ class NodeRuntime(
_canvasRehydrateErrorText.value = null
},
onCanvasA2uiReset = { _canvasA2uiHydrated.value = false },
refreshCanvasHostUrl = { nodeSession.refreshCanvasHostUrl() },
motionActivityAvailable = { motionHandler.isActivityAvailable() },
motionPedometerAvailable = { motionHandler.isPedometerAvailable() },
)
@@ -865,7 +866,6 @@ class NodeRuntime(
val lastDiscoveredStableId: StateFlow<String> = prefs.lastDiscoveredStableId
val canvasDebugStatusEnabled: StateFlow<Boolean> = prefs.canvasDebugStatusEnabled
val installedAppsSharingEnabled: StateFlow<Boolean> = prefs.installedAppsSharingEnabled
val notificationForwardingEnabled: StateFlow<Boolean> = prefs.notificationForwardingEnabled
val notificationForwardingMode: StateFlow<NotificationPackageFilterMode> =
prefs.notificationForwardingMode
@@ -1077,12 +1077,6 @@ class NodeRuntime(
prefs.setCanvasDebugStatusEnabled(value)
}
fun setInstalledAppsSharingEnabled(value: Boolean) {
if (prefs.installedAppsSharingEnabled.value == value) return
prefs.setInstalledAppsSharingEnabled(value)
refreshNodeSurfaceAfterSharingChange()
}
fun setNotificationForwardingEnabled(value: Boolean) {
prefs.setNotificationForwardingEnabled(value)
}
@@ -1420,11 +1414,6 @@ class NodeRuntime(
connectWithAuth(endpoint = endpoint, auth = resolveGatewayConnectAuth(), reconnect = true)
}
private fun refreshNodeSurfaceAfterSharingChange() {
val endpoint = connectedEndpoint ?: return
connectWithAuth(endpoint = endpoint, auth = resolveGatewayConnectAuth(), reconnect = true)
}
private fun connectWithAuth(
endpoint: GatewayEndpoint,
auth: GatewayConnectAuth,
@@ -2085,7 +2074,6 @@ class NodeRuntime(
id = id,
name = obj["name"].asStringOrNull()?.trim()?.takeIf { it.isNotEmpty() } ?: id,
provider = provider,
available = obj.optionalBoolean("available"),
supportsVision = "image" in inputTypes,
supportsAudio = "audio" in inputTypes,
supportsDocuments = "document" in inputTypes,
@@ -2702,7 +2690,6 @@ data class GatewayModelSummary(
val id: String,
val name: String,
val provider: String,
val available: Boolean?,
val supportsVision: Boolean,
val supportsAudio: Boolean,
val supportsDocuments: Boolean,
@@ -2885,15 +2872,6 @@ private fun JsonObject?.double(key: String): Double? = (this?.get(key) as? JsonP
private fun JsonObject?.boolean(key: String): Boolean = (this?.get(key) as? JsonPrimitive)?.content?.trim() == "true"
private fun JsonObject?.optionalBoolean(key: String): Boolean? =
(this?.get(key) as? JsonPrimitive)?.content?.trim()?.lowercase()?.let { value ->
when (value) {
"true" -> true
"false" -> false
else -> null
}
}
internal fun cronJobLastRunStatus(state: JsonObject?): String? =
state
.cronStatus("lastStatus")

View File

@@ -40,13 +40,11 @@ class SecurePrefs(
private const val notificationsForwardingMaxEventsPerMinuteKey =
"notifications.forwarding.maxEventsPerMinute"
private const val notificationsForwardingSessionKeyKey = "notifications.forwarding.sessionKey"
private const val installedAppsSharingEnabledKey = "device.apps.sharing.enabled"
private const val voiceMicEnabledKey = "voice.micEnabled"
}
private val appContext = context.applicationContext
private val json = Json { ignoreUnknownKeys = true }
// Non-secret UI/runtime preferences stay readable for migration and backup behavior.
private val plainPrefs: SharedPreferences =
appContext.getSharedPreferences(plainPrefsName, Context.MODE_PRIVATE)
@@ -116,10 +114,6 @@ class SecurePrefs(
MutableStateFlow(plainPrefs.getBoolean("canvas.debugStatusEnabled", false))
val canvasDebugStatusEnabled: StateFlow<Boolean> = _canvasDebugStatusEnabled
private val _installedAppsSharingEnabled =
MutableStateFlow(plainPrefs.getBoolean(installedAppsSharingEnabledKey, false))
val installedAppsSharingEnabled: StateFlow<Boolean> = _installedAppsSharingEnabled
private val _notificationForwardingEnabled =
MutableStateFlow(plainPrefs.getBoolean(notificationsForwardingEnabledKey, defaultNotificationForwardingEnabled))
val notificationForwardingEnabled: StateFlow<Boolean> = _notificationForwardingEnabled
@@ -258,11 +252,6 @@ class SecurePrefs(
_canvasDebugStatusEnabled.value = value
}
fun setInstalledAppsSharingEnabled(value: Boolean) {
plainPrefs.edit { putBoolean(installedAppsSharingEnabledKey, value) }
_installedAppsSharingEnabled.value = value
}
internal fun getNotificationForwardingPolicy(appPackageName: String): NotificationForwardingPolicy {
val modeRaw = plainPrefs.getString(notificationsForwardingModeKey, null)
val mode = NotificationPackageFilterMode.fromRawValue(modeRaw)

View File

@@ -12,30 +12,47 @@ import kotlinx.serialization.json.JsonPrimitive
class A2UIHandler(
private val canvas: CanvasController,
private val json: Json,
private val getNodeCanvasHostUrl: () -> String?,
private val getOperatorCanvasHostUrl: () -> String?,
) {
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean = CanvasActionTrust.isTrustedCanvasActionUrl(rawUrl)
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean =
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = rawUrl,
trustedA2uiUrls = listOfNotNull(resolveA2uiHostUrl()),
)
suspend fun ensureA2uiReady(): Boolean {
if (canvas.currentUrl()?.trim() == CanvasActionTrust.localA2uiAssetUrl && isA2uiReady()) {
return true
fun resolveA2uiHostUrl(): String? {
val nodeRaw = getNodeCanvasHostUrl()?.trim().orEmpty()
val operatorRaw = getOperatorCanvasHostUrl()?.trim().orEmpty()
// Prefer node-advertised canvas host; operator URL is a fallback for older hello payloads.
val raw = if (nodeRaw.isNotBlank()) nodeRaw else operatorRaw
if (raw.isBlank()) return null
val base = raw.trimEnd('/')
return "$base/__openclaw__/a2ui/?platform=android"
}
suspend fun ensureA2uiReady(a2uiUrl: String): Boolean {
try {
val already = canvas.eval(a2uiReadyCheckJS)
if (already == "true") return true
} catch (_: Throwable) {
// ignore
}
canvas.showLocalA2ui()
// The bundled A2UI host bootstraps asynchronously after navigation; poll briefly before failing the command.
canvas.navigate(a2uiUrl)
// A2UI host bootstraps asynchronously after navigation; poll briefly before failing the command.
repeat(50) {
if (isA2uiReady()) return true
try {
val ready = canvas.eval(a2uiReadyCheckJS)
if (ready == "true") return true
} catch (_: Throwable) {
// ignore
}
delay(120)
}
return false
}
private suspend fun isA2uiReady(): Boolean =
try {
canvas.eval(a2uiReadyCheckJS) == "true"
} catch (_: Throwable) {
false
}
fun decodeA2uiMessages(
command: String,
paramsJson: String?,

View File

@@ -1,5 +1,7 @@
package ai.openclaw.app.node
import java.net.URI
/**
* Trust helper for WebView-originated canvas/A2UI actions.
*/
@@ -7,15 +9,62 @@ object CanvasActionTrust {
/** Local canvas scaffold is the only trusted file URL. */
const val scaffoldAssetUrl: String = "file:///android_asset/CanvasScaffold/scaffold.html"
/** Local bundled A2UI is the only action-capable A2UI host. */
const val localA2uiAssetUrl: String = "file:///android_asset/CanvasA2UI/index.html"
/** Accepts only app-owned bundled pages. Remote WebView content is render-only. */
fun isTrustedCanvasActionUrl(rawUrl: String?): Boolean {
/** Accepts local scaffold or exact remote A2UI URLs advertised by the gateway. */
fun isTrustedCanvasActionUrl(
rawUrl: String?,
trustedA2uiUrls: List<String>,
): Boolean {
val candidate = rawUrl?.trim().orEmpty()
if (candidate.isEmpty()) return false
if (candidate == scaffoldAssetUrl) return true
if (candidate == localA2uiAssetUrl) return true
return false
val candidateUri = parseUri(candidate) ?: return false
if (candidateUri.scheme.equals("file", ignoreCase = true)) {
return false
}
val normalizedCandidate = normalizeTrustedRemoteA2uiUri(candidateUri) ?: return false
return trustedA2uiUrls.any { trusted ->
matchesTrustedRemoteA2uiUrlExact(normalizedCandidate, trusted)
}
}
private fun matchesTrustedRemoteA2uiUrlExact(
candidateUri: URI,
trustedUrl: String,
): Boolean {
// Gateway-advertised URLs are capabilities. Treat malformed entries as
// absent instead of broadening trust to same-origin or prefix matches.
val trustedUri = parseUri(trustedUrl) ?: return false
val normalizedTrusted = normalizeTrustedRemoteA2uiUri(trustedUri) ?: return false
return candidateUri == normalizedTrusted
}
/** Normalizes only the URL parts allowed to vary across trusted remote A2UI URLs. */
private fun normalizeTrustedRemoteA2uiUri(uri: URI): URI? {
// Keep Android trust normalization aligned with iOS ScreenController:
// exact remote URL match, scheme/host normalized, fragment ignored.
val scheme = uri.scheme?.lowercase() ?: return null
if (scheme != "http" && scheme != "https") return null
val host =
uri.host
?.trim()
?.takeIf { it.isNotEmpty() }
?.lowercase() ?: return null
return try {
URI(scheme, uri.userInfo, host, uri.port, uri.rawPath, uri.rawQuery, null)
} catch (_: Throwable) {
null
}
}
/** Parses untrusted WebView/gateway URL text without throwing into UI event handlers. */
private fun parseUri(raw: String): URI? =
try {
URI(raw)
} catch (_: Throwable) {
null
}
}

View File

@@ -48,8 +48,7 @@ class CanvasController {
private val _currentUrl = MutableStateFlow<String?>(null)
val currentUrl: StateFlow<String?> = _currentUrl.asStateFlow()
private val scaffoldAssetUrl = CanvasActionTrust.scaffoldAssetUrl
private val localA2uiAssetUrl = CanvasActionTrust.localA2uiAssetUrl
private val scaffoldAssetUrl = "file:///android_asset/CanvasScaffold/scaffold.html"
private fun clampJpegQuality(quality: Double?): Int {
val q = (quality ?: 0.82).coerceIn(0.1, 1.0)
@@ -88,13 +87,6 @@ class CanvasController {
reload()
}
/** Shows the app-owned A2UI renderer that is allowed to dispatch native actions. */
fun showLocalA2ui() {
this.url = localA2uiAssetUrl
_currentUrl.value = localA2uiAssetUrl
reload()
}
fun currentUrl(): String? = url
fun isDefaultCanvas(): Boolean = url == null

View File

@@ -28,7 +28,6 @@ class ConnectionManager(
private val callLogAvailable: () -> Boolean,
private val photosAvailable: () -> Boolean,
private val hasRecordAudioPermission: () -> Boolean,
private val installedAppsSharingEnabled: () -> Boolean,
private val manualTls: () -> Boolean,
) {
companion object {
@@ -116,7 +115,6 @@ class ConnectionManager(
voiceWakeEnabled = voiceWakeMode() != VoiceWakeMode.Off && hasRecordAudioPermission(),
motionActivityAvailable = motionActivityAvailable(),
motionPedometerAvailable = motionPedometerAvailable(),
installedAppsSharingEnabled = installedAppsSharingEnabled(),
debugBuild = BuildConfig.DEBUG,
)

View File

@@ -8,7 +8,6 @@ import android.app.ActivityManager
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
@@ -25,121 +24,16 @@ import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.put
import java.util.Locale
private const val DEFAULT_DEVICE_APPS_LIMIT = 100
private const val MAX_DEVICE_APPS_LIMIT = 200
private const val DEVICE_APPS_SYSTEM_FLAGS =
ApplicationInfo.FLAG_SYSTEM or ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
internal fun isSystemDeviceApp(appInfo: ApplicationInfo): Boolean =
(appInfo.flags and DEVICE_APPS_SYSTEM_FLAGS) != 0
internal data class DeviceAppEntry(
val label: String,
val packageName: String,
val system: Boolean,
val enabled: Boolean,
val launchable: Boolean,
)
internal interface DeviceAppSource {
fun listApps(includeNonLaunchable: Boolean): List<DeviceAppEntry>
}
private class AndroidDeviceAppSource(
private val appContext: Context,
) : DeviceAppSource {
override fun listApps(includeNonLaunchable: Boolean): List<DeviceAppEntry> {
val packageManager = appContext.packageManager
val launcherIntent = Intent(Intent.ACTION_MAIN).apply { addCategory(Intent.CATEGORY_LAUNCHER) }
val launchablePackages =
packageManager
.queryIntentActivities(launcherIntent, PackageManager.MATCH_ALL)
.asSequence()
.mapNotNull {
it.activityInfo
?.packageName
?.trim()
?.takeIf(String::isNotEmpty)
}.toSet()
val appInfos =
if (includeNonLaunchable) {
packageManager.getInstalledApplications(PackageManager.MATCH_ALL)
} else {
launchablePackages.mapNotNull { packageName ->
runCatching { packageManager.getApplicationInfo(packageName, 0) }.getOrNull()
}
}
return appInfos
.asSequence()
.mapNotNull { appInfo ->
appInfo.packageName
?.trim()
?.takeIf(String::isNotEmpty)
?.let { packageName ->
val label = packageManager.getApplicationLabel(appInfo).toString().trim()
DeviceAppEntry(
label = label.ifEmpty { packageName },
packageName = packageName,
system = isSystemDeviceApp(appInfo),
enabled = appInfo.enabled,
launchable = packageName in launchablePackages,
)
}
}.distinctBy { it.packageName }
.sortedWith(compareBy<DeviceAppEntry> { it.label.lowercase() }.thenBy { it.packageName })
.toList()
}
}
private data class DeviceAppsRequest(
val includeSystem: Boolean,
val includeDisabled: Boolean,
val includeNonLaunchable: Boolean,
val query: String?,
val limit: Int,
)
/**
* Gateway device command adapter for Android status, info, permission, and health snapshots.
*/
class DeviceHandler private constructor(
class DeviceHandler(
private val appContext: Context,
private val smsEnabled: Boolean = SensitiveFeatureConfig.smsEnabled,
private val callLogEnabled: Boolean = SensitiveFeatureConfig.callLogEnabled,
private val photosEnabled: Boolean = SensitiveFeatureConfig.photosEnabled,
private val appSource: DeviceAppSource = AndroidDeviceAppSource(appContext),
) {
constructor(
appContext: Context,
smsEnabled: Boolean = SensitiveFeatureConfig.smsEnabled,
callLogEnabled: Boolean = SensitiveFeatureConfig.callLogEnabled,
photosEnabled: Boolean = SensitiveFeatureConfig.photosEnabled,
) : this(
appContext = appContext,
smsEnabled = smsEnabled,
callLogEnabled = callLogEnabled,
photosEnabled = photosEnabled,
appSource = AndroidDeviceAppSource(appContext),
)
companion object {
internal fun forTesting(
appContext: Context,
appSource: DeviceAppSource,
smsEnabled: Boolean = SensitiveFeatureConfig.smsEnabled,
callLogEnabled: Boolean = SensitiveFeatureConfig.callLogEnabled,
photosEnabled: Boolean = SensitiveFeatureConfig.photosEnabled,
): DeviceHandler =
DeviceHandler(
appContext = appContext,
smsEnabled = smsEnabled,
callLogEnabled = callLogEnabled,
photosEnabled = photosEnabled,
appSource = appSource,
)
/**
* SMS is available only when the feature flag, telephony hardware, and at least one SMS permission align.
*/
@@ -180,48 +74,6 @@ class DeviceHandler private constructor(
/** Returns coarse device health for memory, power, thermal, battery, and security patch state. */
fun handleDeviceHealth(_paramsJson: String?): GatewaySession.InvokeResult = GatewaySession.InvokeResult.ok(healthPayloadJson())
fun handleDeviceApps(paramsJson: String?): GatewaySession.InvokeResult {
val request = parseDeviceAppsRequest(paramsJson)
val matchingApps =
appSource
.listApps(includeNonLaunchable = request.includeNonLaunchable)
.asSequence()
.filter { request.includeSystem || !it.system }
.filter { request.includeDisabled || it.enabled }
.filter { app ->
val query = request.query ?: return@filter true
app.label.contains(query, ignoreCase = true) || app.packageName.contains(query, ignoreCase = true)
}.toList()
val limitedApps = matchingApps.take(request.limit)
return GatewaySession.InvokeResult.ok(
buildJsonObject {
put("count", JsonPrimitive(limitedApps.size))
put("totalMatched", JsonPrimitive(matchingApps.size))
put("truncated", JsonPrimitive(matchingApps.size > limitedApps.size))
put("visibility", JsonPrimitive(if (request.includeNonLaunchable) "android-visible" else "launcher"))
put("includeSystem", JsonPrimitive(request.includeSystem))
put("includeDisabled", JsonPrimitive(request.includeDisabled))
put(
"apps",
buildJsonArray {
for (app in limitedApps) {
add(
buildJsonObject {
put("label", JsonPrimitive(app.label))
put("packageName", JsonPrimitive(app.packageName))
put("system", JsonPrimitive(app.system))
put("enabled", JsonPrimitive(app.enabled))
put("launchable", JsonPrimitive(app.launchable))
},
)
}
},
)
}.toString(),
)
}
private fun statusPayloadJson(): String {
val battery = readBatterySnapshot()
val powerManager = appContext.getSystemService(PowerManager::class.java)
@@ -513,24 +365,6 @@ class DeviceHandler private constructor(
}.toString()
}
private fun parseDeviceAppsRequest(paramsJson: String?): DeviceAppsRequest {
val params = parseJsonParamsObject(paramsJson)
val includeSystem = parseJsonBooleanFlag(params, "includeSystem") ?: false
val includeDisabled = parseJsonBooleanFlag(params, "includeDisabled") ?: false
val includeNonLaunchable = parseJsonBooleanFlag(params, "includeNonLaunchable") ?: false
val query = parseJsonString(params, "query")?.trim()?.takeIf { it.isNotEmpty() }
val limit =
(parseJsonInt(params, "limit") ?: DEFAULT_DEVICE_APPS_LIMIT)
.coerceIn(1, MAX_DEVICE_APPS_LIMIT)
return DeviceAppsRequest(
includeSystem = includeSystem,
includeDisabled = includeDisabled,
includeNonLaunchable = includeNonLaunchable,
query = query,
limit = limit,
)
}
private fun readBatterySnapshot(): BatterySnapshot {
// ACTION_BATTERY_CHANGED is sticky; registerReceiver(null, ...) reads the last system snapshot.
val intent = appContext.registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))

View File

@@ -28,7 +28,6 @@ data class NodeRuntimeFlags(
val voiceWakeEnabled: Boolean,
val motionActivityAvailable: Boolean,
val motionPedometerAvailable: Boolean,
val installedAppsSharingEnabled: Boolean,
val debugBuild: Boolean,
)
@@ -44,7 +43,6 @@ enum class InvokeCommandAvailability {
PhotosAvailable,
MotionActivityAvailable,
MotionPedometerAvailable,
InstalledAppsSharingEnabled,
DebugBuild,
}
@@ -195,10 +193,6 @@ object InvokeCommandRegistry {
InvokeCommandSpec(
name = OpenClawDeviceCommand.Health.rawValue,
),
InvokeCommandSpec(
name = OpenClawDeviceCommand.Apps.rawValue,
availability = InvokeCommandAvailability.InstalledAppsSharingEnabled,
),
InvokeCommandSpec(
name = OpenClawNotificationsCommand.List.rawValue,
),
@@ -287,7 +281,6 @@ object InvokeCommandRegistry {
InvokeCommandAvailability.PhotosAvailable -> flags.photosAvailable
InvokeCommandAvailability.MotionActivityAvailable -> flags.motionActivityAvailable
InvokeCommandAvailability.MotionPedometerAvailable -> flags.motionPedometerAvailable
InvokeCommandAvailability.InstalledAppsSharingEnabled -> flags.installedAppsSharingEnabled
InvokeCommandAvailability.DebugBuild -> flags.debugBuild
}
}.map { it.name }

View File

@@ -85,10 +85,10 @@ class InvokeDispatcher(
private val smsTelephonyAvailable: () -> Boolean,
private val callLogAvailable: () -> Boolean,
private val photosAvailable: () -> Boolean,
private val installedAppsSharingEnabled: () -> Boolean,
private val debugBuild: () -> Boolean,
private val onCanvasA2uiPush: () -> Unit,
private val onCanvasA2uiReset: () -> Unit,
private val refreshCanvasHostUrl: suspend () -> String?,
private val motionActivityAvailable: () -> Boolean,
private val motionPedometerAvailable: () -> Boolean,
) {
@@ -193,7 +193,6 @@ class InvokeDispatcher(
OpenClawDeviceCommand.Info.rawValue -> deviceHandler.handleDeviceInfo(paramsJson)
OpenClawDeviceCommand.Permissions.rawValue -> deviceHandler.handleDevicePermissions(paramsJson)
OpenClawDeviceCommand.Health.rawValue -> deviceHandler.handleDeviceHealth(paramsJson)
OpenClawDeviceCommand.Apps.rawValue -> deviceHandler.handleDeviceApps(paramsJson)
// Notifications command
OpenClawNotificationsCommand.List.rawValue -> notificationsHandler.handleNotificationsList(paramsJson)
@@ -241,11 +240,24 @@ class InvokeDispatcher(
}
private suspend fun withReadyA2ui(block: suspend () -> GatewaySession.InvokeResult): GatewaySession.InvokeResult {
if (!a2uiHandler.ensureA2uiReady()) {
return GatewaySession.InvokeResult.error(
code = "A2UI_HOST_UNAVAILABLE",
message = "A2UI_HOST_UNAVAILABLE: bundled A2UI host not reachable",
)
var a2uiUrl =
a2uiHandler.resolveA2uiHostUrl()
?: refreshCanvasHostUrl().let { a2uiHandler.resolveA2uiHostUrl() }
?: return GatewaySession.InvokeResult.error(
code = "A2UI_HOST_NOT_CONFIGURED",
message = "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host",
)
val readyOnFirstCheck = a2uiHandler.ensureA2uiReady(a2uiUrl)
if (!readyOnFirstCheck) {
// Gateway canvas host metadata can lag reconnects; refresh once before failing the command.
refreshCanvasHostUrl()
a2uiUrl = a2uiHandler.resolveA2uiHostUrl() ?: a2uiUrl
if (!a2uiHandler.ensureA2uiReady(a2uiUrl)) {
return GatewaySession.InvokeResult.error(
code = "A2UI_HOST_UNAVAILABLE",
message = "A2UI_HOST_UNAVAILABLE: A2UI host not reachable",
)
}
}
return block()
}
@@ -336,15 +348,6 @@ class InvokeDispatcher(
message = "PHOTOS_UNAVAILABLE: photos not available on this build",
)
}
InvokeCommandAvailability.InstalledAppsSharingEnabled ->
if (installedAppsSharingEnabled()) {
null
} else {
GatewaySession.InvokeResult.error(
code = "INSTALLED_APPS_SHARING_DISABLED",
message = "INSTALLED_APPS_SHARING_DISABLED: enable Installed Apps in Settings",
)
}
InvokeCommandAvailability.DebugBuild ->
if (debugBuild()) {
null

View File

@@ -112,7 +112,6 @@ enum class OpenClawDeviceCommand(
Info("device.info"),
Permissions("device.permissions"),
Health("device.health"),
Apps("device.apps"),
;
companion object {

View File

@@ -152,8 +152,9 @@ fun CanvasScreen(
}
}
// The listener accepts any WebView origin at registration time; native
// dispatch still requires the live URL to be an app-owned bundled page.
// The listener accepts any WebView origin at registration time because
// gateway A2UI URLs are dynamic; CanvasActionTrust validates the live URL
// before forwarding each message.
val bridge =
CanvasA2UIActionBridge(
isTrustedPage = { viewModel.isTrustedCanvasActionUrl(currentPageUrlRef.get()) },

View File

@@ -297,15 +297,14 @@ private fun CommandSectionLabel(title: String) {
}
}
internal fun providerCommandSubtitle(
/** Builds provider quick-action metadata from current gateway/catalog state. */
private fun providerCommandSubtitle(
isConnected: Boolean,
providers: List<GatewayModelProviderSummary>,
models: List<GatewayModelSummary>,
): String {
if (!isConnected) return "Connect Gateway to load models"
val expiringProviderCount = expiringModelProviderCount(providers)
if (expiringProviderCount > 0) return "$expiringProviderCount providers expiring"
val readyProviderCount = readyModelProviderCount(providers, models)
val readyProviderCount = providers.count { modelProviderReady(it.status) }
if (readyProviderCount > 0) return "$readyProviderCount providers ready"
if (models.isNotEmpty()) return "${models.size} models available"
return "Configure model access"

View File

@@ -6,7 +6,6 @@ import ai.openclaw.app.SensitiveFeatureConfig
import ai.openclaw.app.gateway.GatewayEndpoint
import ai.openclaw.app.node.DeviceNotificationListenerService
import ai.openclaw.app.ui.design.ClawDesignTheme
import ai.openclaw.app.ui.design.ClawErrorState
import ai.openclaw.app.ui.design.ClawListItem
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawPrimaryButton
@@ -474,14 +473,6 @@ private fun GatewaySetupScreen(
onClick = { advancedOpen = true },
)
}
error?.let { message ->
item {
ClawErrorState(
title = "Setup code issue",
body = message,
)
}
}
item {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Surface(
@@ -514,6 +505,9 @@ private fun GatewaySetupScreen(
}
ClawTextField(value = token, onValueChange = onTokenChange, placeholder = "Token optional")
ClawTextField(value = password, onValueChange = onPasswordChange, placeholder = "Password optional")
error?.let {
Text(text = it, style = ClawTheme.type.caption, color = ClawTheme.colors.warning)
}
}
}
}

View File

@@ -18,15 +18,11 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
@@ -82,16 +78,9 @@ internal fun ProvidersModelsScreen(
}
}
ClawScaffold(
contentPadding = PaddingValues(start = 20.dp, top = 13.dp, end = 20.dp, bottom = 6.dp),
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
) {
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 13.dp, end = 20.dp, bottom = 13.dp)) {
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(7.dp),
contentPadding = PaddingValues(bottom = 4.dp),
) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(7.dp), contentPadding = PaddingValues(bottom = 112.dp)) {
item {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(
@@ -192,9 +181,6 @@ private data class ProviderSetupRow(
val name: String,
val subtitle: String,
val ready: Boolean,
val available: Boolean,
val statusLabel: String,
val warning: Boolean,
)
private data class ProviderRow(
@@ -202,60 +188,37 @@ private data class ProviderRow(
val name: String,
val status: String,
val ready: Boolean,
val available: Boolean,
val setupRequired: Boolean,
val warning: Boolean,
val modelCount: Int,
)
/** Combines auth-provider readiness rows with catalog-only browse providers. */
/** Combines auth-provider readiness rows with catalog-only providers. */
private fun providerRows(
providers: List<GatewayModelProviderSummary>,
models: List<GatewayModelSummary>,
): List<ProviderRow> {
val modelCounts = models.groupingBy { it.provider }.eachCount()
val availableProviderIds =
models
.filter(::modelAvailabilityUsable)
.map { it.provider.normalizedProviderId() }
.toSet()
val authRows =
providers.map { provider ->
val providerId = provider.id.normalizedProviderId()
val authReady = modelProviderReady(provider.status)
val expiring = modelProviderExpiring(provider.status)
val available = providerId in availableProviderIds
val ready = modelProviderReady(provider.status)
ProviderRow(
id = provider.id,
name = provider.displayName,
status =
when {
authReady -> "Ready"
expiring -> "Expiring"
available -> "Available"
else -> "Needs setup"
},
ready = authReady,
available = available || authReady || expiring,
setupRequired = !authReady && !available && !expiring,
warning = expiring,
status = if (ready) "Ready" else "Needs setup",
ready = ready,
modelCount = modelCounts[provider.id] ?: 0,
)
}
// Catalog-only providers can be browsed but are not a readiness signal.
// Static/catalog-only providers may expose models without a matching auth
// provider row; keep them visible as ready providers.
val missingAuthRows =
modelCounts.keys
.filter { provider -> authRows.none { it.id == provider } }
.map { provider ->
val available = provider.normalizedProviderId() in availableProviderIds
ProviderRow(
id = provider,
name = providerDisplayName(provider),
status = if (available) "Available" else "Catalog",
ready = available,
available = available,
setupRequired = false,
warning = false,
status = "Ready",
ready = true,
modelCount = modelCounts[provider] ?: 0,
)
}
@@ -271,9 +234,6 @@ private fun providerSetupRows(providerRows: List<ProviderRow>): List<ProviderSet
name = providerDisplayName(id),
subtitle = providerSetupSubtitle(id, row),
ready = row?.ready == true,
available = row?.available == true,
statusLabel = providerSetupStatusLabel(row),
warning = row?.warning == true || row?.setupRequired == true || row == null,
)
}
}
@@ -283,24 +243,12 @@ private fun providerSetupSubtitle(
row: ProviderRow?,
): String =
when {
row?.warning == true -> "Credential expires soon"
row?.ready == true -> if (row.modelCount > 0) "${row.modelCount} models available" else "Ready"
row?.available == true -> if (row.modelCount > 0) "${row.modelCount} models available" else "Available"
row?.setupRequired == true -> "Finish setup to use ${row.name}"
row != null && row.modelCount > 0 -> "${row.modelCount} catalog models"
row != null -> "Finish setup to use ${row.name}"
id == "ollama" -> "Use models running on your network"
else -> "Add provider credentials on your Gateway"
}
private fun providerSetupStatusLabel(row: ProviderRow?): String =
when {
row?.ready == true -> "Ready"
row?.warning == true -> "Expiring"
row?.available == true -> "Available"
row?.setupRequired == false -> "Catalog"
else -> "Setup"
}
/** Normalizes gateway provider status strings into a ready/not-ready boolean. */
internal fun modelProviderReady(status: String): Boolean {
val normalized = status.trim().lowercase()
@@ -311,30 +259,6 @@ internal fun modelProviderReady(status: String): Boolean {
normalized == "static"
}
private fun modelProviderExpiring(status: String): Boolean = status.trim().lowercase() == "expiring"
internal fun readyModelProviderCount(
providers: List<GatewayModelProviderSummary>,
models: List<GatewayModelSummary>,
): Int {
val authReadyProviders = providers.filter { modelProviderReady(it.status) }.map { it.id.normalizedProviderId() }
val availableModelProviders = models.filter(::modelAvailabilityUsable).map { it.provider.normalizedProviderId() }
return (authReadyProviders + availableModelProviders).distinct().size
}
// Older gateways did not emit `available`; keep those rows on the legacy
// readiness path while still honoring explicit false from upgraded gateways.
internal fun modelAvailabilityUsable(model: GatewayModelSummary): Boolean = model.available != false
internal fun expiringModelProviderCount(providers: List<GatewayModelProviderSummary>): Int =
providers
.filter { modelProviderExpiring(it.status) }
.map { it.id.normalizedProviderId() }
.distinct()
.size
private fun String.normalizedProviderId(): String = trim().lowercase()
/** Groups models by provider using the same display priority as provider rows. */
private fun sortedModelGroups(models: List<GatewayModelSummary>): List<Pair<String, List<GatewayModelSummary>>> =
models
@@ -364,18 +288,7 @@ private fun ProviderList(
ClawPanel(contentPadding = PaddingValues(horizontal = 0.dp, vertical = 0.dp)) {
Column {
if (rows.isEmpty()) {
ProviderListRow(
ProviderRow(
id = "loading",
name = "Provider catalog",
status = if (refreshing) "Loading" else "No providers",
ready = false,
available = false,
setupRequired = false,
warning = false,
modelCount = 0,
),
)
ProviderListRow(ProviderRow(id = "loading", name = "Provider catalog", status = if (refreshing) "Loading" else "No providers", ready = false, modelCount = 0))
} else {
val visibleRows = rows.take(5)
visibleRows.forEachIndexed { index, row ->
@@ -398,12 +311,12 @@ private fun ProviderOverviewPanel(
onRefresh: () -> Unit,
onSetup: () -> Unit,
) {
val readyCount = providerRows.count { it.available }
val needsSetupCount = providerRows.count { it.setupRequired }
val readyCount = providerRows.count { it.ready }
val needsSetupCount = providerRows.count { !it.ready }
ClawPanel(contentPadding = PaddingValues(horizontal = 12.dp, vertical = 12.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
ProviderMetricTile(label = "Available", value = readyCount.toString(), modifier = Modifier.weight(1f))
ProviderMetricTile(label = "Ready", value = readyCount.toString(), modifier = Modifier.weight(1f))
ProviderMetricTile(label = "Models", value = modelCount.toString(), modifier = Modifier.weight(1f))
ProviderMetricTile(label = "Setup", value = needsSetupCount.toString(), modifier = Modifier.weight(1f))
}
@@ -474,14 +387,8 @@ private fun ProviderSetupListRow(
Text(text = row.subtitle, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
val statusColor =
when {
row.warning -> ClawTheme.colors.warning
row.ready || row.available -> ClawTheme.colors.success
else -> ClawTheme.colors.textMuted
}
Box(modifier = Modifier.size(5.dp).clip(CircleShape).background(statusColor))
Text(text = row.statusLabel, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
Box(modifier = Modifier.size(5.dp).clip(CircleShape).background(if (row.ready) ClawTheme.colors.success else ClawTheme.colors.warning))
Text(text = if (row.ready) "Ready" else "Setup", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
Icon(imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, contentDescription = "Open ${row.name}", modifier = Modifier.size(17.dp), tint = ClawTheme.colors.text)
}
}
@@ -497,13 +404,7 @@ private fun ProviderListRow(row: ProviderRow) {
Text(text = if (row.modelCount > 0) "${row.modelCount} models" else "Provider setup", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
}
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
val statusColor =
when {
row.warning || row.setupRequired -> ClawTheme.colors.warning
row.ready || row.available -> ClawTheme.colors.success
else -> ClawTheme.colors.textMuted
}
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(statusColor))
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (row.ready) ClawTheme.colors.success else ClawTheme.colors.warning))
Text(text = row.status, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textMuted, maxLines = 1)
}
}
@@ -579,13 +480,12 @@ private fun ModelGroup(
@Composable
private fun ModelRow(model: GatewayModelSummary) {
val available = modelAvailabilityUsable(model)
Row(modifier = Modifier.fillMaxWidth().heightIn(min = 48.dp).padding(horizontal = 10.dp, vertical = 5.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Text(text = model.name, style = ClawTheme.type.mono, color = ClawTheme.colors.text, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis)
modelCapabilityLabels(model).take(3).forEach { label ->
ProviderMiniTag(text = label)
}
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(if (available) ClawTheme.colors.success else ClawTheme.colors.warning))
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(ClawTheme.colors.success))
}
}

View File

@@ -13,14 +13,11 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -91,15 +88,8 @@ internal fun SessionsScreen(
}
}
ClawScaffold(
contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 6.dp),
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(7.dp),
contentPadding = PaddingValues(bottom = 4.dp),
) {
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 20.dp)) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(7.dp)) {
item {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -143,16 +133,11 @@ internal fun SessionsScreen(
if (visibleSessions.isEmpty()) {
item {
Box(
modifier = Modifier.fillParentMaxHeight(0.56f).fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
ClawEmptyState(
title = emptySessionTitle(filter),
body = emptySessionBody(filter),
action = { ClawPrimaryButton(text = "Start Chat", onClick = onOpenChat) },
)
}
ClawEmptyState(
title = emptySessionTitle(filter),
body = emptySessionBody(filter),
action = { ClawPrimaryButton(text = "Start Chat", onClick = onOpenChat) },
)
}
} else {
items(visibleSessions, key = { it.key }) { session ->
@@ -170,6 +155,10 @@ internal fun SessionsScreen(
)
}
}
item {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}

View File

@@ -44,15 +44,11 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
@@ -718,7 +714,6 @@ private fun PhoneCapabilitiesScreen(
val locationPreciseEnabled by viewModel.locationPreciseEnabled.collectAsState()
val preventSleep by viewModel.preventSleep.collectAsState()
val canvasDebugStatusEnabled by viewModel.canvasDebugStatusEnabled.collectAsState()
val installedAppsSharingEnabled by viewModel.installedAppsSharingEnabled.collectAsState()
val cameraPermissionLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
viewModel.setCameraEnabled(granted)
@@ -773,13 +768,6 @@ private fun PhoneCapabilitiesScreen(
listOf(
SettingsToggleRow("Camera", "Allow camera tools when requested.", Icons.Default.CameraAlt, cameraEnabled, ::setCameraAccess),
SettingsToggleRow("Precise Location", "Share precise location while location is enabled.", Icons.Default.LocationOn, locationPreciseEnabled, ::setPreciseLocation),
SettingsToggleRow(
"Installed Apps",
if (installedAppsSharingEnabled) "OpenClaw can list launcher-visible apps." else "App list stays on this phone.",
Icons.Default.Storage,
installedAppsSharingEnabled,
viewModel::setInstalledAppsSharingEnabled,
),
SettingsToggleRow("Keep Awake", "Keep the node available during active work.", Icons.Default.Bolt, preventSleep, viewModel::setPreventSleep),
SettingsToggleRow("Canvas Status", "Show screen-sharing debug state.", Icons.AutoMirrored.Filled.ScreenShare, canvasDebugStatusEnabled, viewModel::setCanvasDebugStatusEnabled),
),
@@ -1032,11 +1020,8 @@ internal fun SettingsDetailFrame(
onBack: () -> Unit,
content: @Composable () -> Unit,
) {
ClawScaffold(
contentPadding = PaddingValues(start = ClawTheme.spacing.lg, top = 14.dp, end = ClawTheme.spacing.lg, bottom = 6.dp),
contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
) {
LazyColumn(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(10.dp), contentPadding = PaddingValues(bottom = 4.dp)) {
ClawScaffold(contentPadding = PaddingValues(start = ClawTheme.spacing.lg, top = 14.dp, end = ClawTheme.spacing.lg, bottom = 20.dp)) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp)) {
item {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(9.dp)) {
SettingsBackButton(onClick = onBack)
@@ -1052,6 +1037,9 @@ internal fun SettingsDetailFrame(
content()
}
}
item {
Spacer(modifier = Modifier.height(12.dp))
}
}
}
}
@@ -1257,7 +1245,6 @@ private fun cronJobStatus(job: GatewayCronJobSummary): ClawStatus {
}
}
/** Applies query/system visibility rules while always preserving selected packages. */
internal fun filterNotificationAppsForPicker(
apps: List<InstalledApp>,
selectedPackages: Set<String>,
@@ -1276,7 +1263,6 @@ internal fun filterNotificationAppsForPicker(
}
}
/** Summarizes allowlist/blocklist mode with an empty-state warning when needed. */
private fun notificationPackageSelectionSummary(
mode: NotificationPackageFilterMode,
selectedCount: Int,
@@ -1296,7 +1282,6 @@ private fun notificationPackageSelectionSummary(
}
}
/** Builds compact two-letter app badges from package-picker labels. */
private fun notificationAppBadge(label: String): String {
val initials =
label

View File

@@ -9,14 +9,11 @@ import ai.openclaw.app.HomeDestination
import ai.openclaw.app.MainViewModel
import ai.openclaw.app.NodeRuntime
import ai.openclaw.app.ui.chat.ChatScreen
import ai.openclaw.app.ui.design.ClawBottomNav
import ai.openclaw.app.ui.design.ClawDesignTheme
import ai.openclaw.app.ui.design.ClawEmptyState
import ai.openclaw.app.ui.design.ClawNavItem
import ai.openclaw.app.ui.design.ClawPanel
import ai.openclaw.app.ui.design.ClawPrimaryButton
import ai.openclaw.app.ui.design.ClawScaffold
import ai.openclaw.app.ui.design.ClawSecondaryButton
import ai.openclaw.app.ui.design.ClawTheme
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.BorderStroke
@@ -27,26 +24,20 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ExitToApp
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.automirrored.filled.ScreenShare
import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Lock
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.Notifications
@@ -63,7 +54,6 @@ import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
@@ -79,32 +69,23 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
internal enum class Tab(
private enum class Tab(
val key: String,
val label: String,
val icon: ImageVector,
) {
Overview(key = "overview", label = "Home", icon = Icons.Default.Home),
Chat(key = "chat", label = "Chat", icon = Icons.Outlined.ChatBubbleOutline),
Voice(key = "voice", label = "Voice", icon = Icons.Outlined.MicNone),
Sessions(key = "sessions", label = "Sessions", icon = Icons.Outlined.AccessTime),
Settings(key = "settings", label = "Settings", icon = Icons.Outlined.Settings),
ProvidersModels(key = "providers-models", label = "Providers", icon = Icons.Outlined.Inventory2),
Overview(key = "overview", label = "Home"),
Chat(key = "chat", label = "Chat"),
Voice(key = "voice", label = "Voice"),
Sessions(key = "sessions", label = "Sessions"),
Settings(key = "settings", label = "Settings"),
ProvidersModels(key = "providers-models", label = "Providers"),
}
private val shellNavTabs = listOf(Tab.Overview, Tab.Chat, Tab.Voice, Tab.Settings)
private val shellContentInsets: WindowInsets
@Composable get() = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
internal fun shellBottomNavVisible(keyboardVisible: Boolean, commandOpen: Boolean): Boolean = !keyboardVisible && !commandOpen
/** Main post-onboarding shell that owns top-level Android navigation state. */
@Composable
fun ShellScreen(
@@ -150,144 +131,117 @@ fun ShellScreen(
commandOpen = false
}
val density = LocalDensity.current
val keyboardVisible = WindowInsets.ime.getBottom(density) > 0
val showBottomNav = shellBottomNavVisible(keyboardVisible = keyboardVisible, commandOpen = commandOpen)
Scaffold(
modifier = modifier.fillMaxSize(),
containerColor = ClawTheme.colors.canvas,
contentWindowInsets = WindowInsets(0, 0, 0, 0),
bottomBar = {
if (showBottomNav) {
ClawBottomNav(
items = shellNavTabs.map { ClawNavItem(key = it.key, label = it.label, icon = it.icon) },
selectedKey = if (activeTab in shellNavTabs) activeTab.key else Tab.Overview.key,
onSelect = { key ->
val next = shellNavTabs.firstOrNull { it.key == key } ?: Tab.Overview
if (next == Tab.Settings) {
settingsRoute = SettingsRoute.Home
returnToOverviewFromSettings = false
}
activeTab = next
},
)
}
},
) { shellPadding ->
Box(modifier = Modifier.fillMaxSize().padding(shellPadding)) {
when (activeTab) {
Tab.Overview ->
OverviewScreen(
viewModel = viewModel,
onSelectTab = { activeTab = it },
onOpenSettingsRoute = {
settingsRoute = it
returnToOverviewFromSettings = true
activeTab = Tab.Settings
},
onOpenCommand = { commandOpen = true },
)
Tab.Chat ->
ChatShellScreen(
viewModel = viewModel,
onVoice = { activeTab = Tab.Voice },
onOpenSessions = { activeTab = Tab.Sessions },
)
Tab.Voice ->
VoiceShellScreen(
viewModel = viewModel,
onOpenCommand = { commandOpen = true },
onOpenGatewaySettings = {
settingsRoute = SettingsRoute.Gateway
returnToOverviewFromSettings = false
activeTab = Tab.Settings
},
onOpenVoiceSettings = {
settingsRoute = SettingsRoute.Voice
returnToOverviewFromSettings = false
activeTab = Tab.Settings
},
)
Tab.ProvidersModels ->
ProvidersModelsScreen(
viewModel = viewModel,
onBack = { activeTab = Tab.Overview },
onAddProvider = {
settingsRoute = SettingsRoute.Gateway
returnToOverviewFromSettings = false
activeTab = Tab.Settings
},
)
Tab.Sessions ->
SessionsScreen(
viewModel = viewModel,
onOpenCommand = { commandOpen = true },
onOpenChat = { activeTab = Tab.Chat },
)
Tab.Settings ->
SettingsShellScreen(
viewModel = viewModel,
route = settingsRoute,
onRouteChange = {
settingsRoute = it
returnToOverviewFromSettings = false
},
onRouteBack = {
settingsRoute = SettingsRoute.Home
if (returnToOverviewFromSettings) {
returnToOverviewFromSettings = false
activeTab = Tab.Overview
}
},
onBackHome = { activeTab = Tab.Overview },
onOpenCommand = { commandOpen = true },
)
}
if (commandOpen) {
CommandPalette(
Box(modifier = modifier.fillMaxSize()) {
when (activeTab) {
Tab.Overview ->
OverviewScreen(
viewModel = viewModel,
onDismiss = { commandOpen = false },
onOpenChat = {
activeTab = Tab.Chat
commandOpen = false
onSelectTab = { activeTab = it },
onOpenSettingsRoute = {
settingsRoute = it
returnToOverviewFromSettings = true
activeTab = Tab.Settings
},
onOpenVoice = {
activeTab = Tab.Voice
commandOpen = false
},
onOpenSessions = {
activeTab = Tab.Sessions
commandOpen = false
},
onOpenProviders = {
activeTab = Tab.ProvidersModels
commandOpen = false
},
onOpenSettings = {
settingsRoute = SettingsRoute.Home
onOpenCommand = { commandOpen = true },
)
Tab.Chat ->
ChatShellScreen(
viewModel = viewModel,
onBack = { activeTab = Tab.Overview },
onVoice = { activeTab = Tab.Voice },
)
Tab.Voice ->
VoiceShellScreen(
viewModel = viewModel,
onOpenCommand = { commandOpen = true },
onOpenGatewaySettings = {
settingsRoute = SettingsRoute.Gateway
returnToOverviewFromSettings = false
activeTab = Tab.Settings
commandOpen = false
},
onOpenSession = { sessionKey ->
viewModel.switchChatSession(sessionKey)
activeTab = Tab.Chat
commandOpen = false
onOpenVoiceSettings = {
settingsRoute = SettingsRoute.Voice
returnToOverviewFromSettings = false
activeTab = Tab.Settings
},
)
}
Tab.ProvidersModels ->
ProvidersModelsScreen(
viewModel = viewModel,
onBack = { activeTab = Tab.Overview },
onAddProvider = {
settingsRoute = SettingsRoute.Gateway
returnToOverviewFromSettings = false
activeTab = Tab.Settings
},
)
Tab.Sessions ->
SessionsScreen(
viewModel = viewModel,
onOpenCommand = { commandOpen = true },
onOpenChat = { activeTab = Tab.Chat },
)
Tab.Settings ->
SettingsShellScreen(
viewModel = viewModel,
route = settingsRoute,
onRouteChange = {
settingsRoute = it
returnToOverviewFromSettings = false
},
onRouteBack = {
settingsRoute = SettingsRoute.Home
if (returnToOverviewFromSettings) {
returnToOverviewFromSettings = false
activeTab = Tab.Overview
}
},
onOpenCommand = { commandOpen = true },
)
}
pendingTrust?.let { prompt ->
// Gateway certificate trust is modal across the shell so navigation
// cannot hide a changed TLS identity prompt.
GatewayTrustDialog(
prompt = prompt,
onAccept = viewModel::acceptGatewayTrustPrompt,
onDecline = viewModel::declineGatewayTrustPrompt,
)
}
if (commandOpen) {
CommandPalette(
viewModel = viewModel,
onDismiss = { commandOpen = false },
onOpenChat = {
activeTab = Tab.Chat
commandOpen = false
},
onOpenVoice = {
activeTab = Tab.Voice
commandOpen = false
},
onOpenSessions = {
activeTab = Tab.Sessions
commandOpen = false
},
onOpenProviders = {
activeTab = Tab.ProvidersModels
commandOpen = false
},
onOpenSettings = {
settingsRoute = SettingsRoute.Home
returnToOverviewFromSettings = false
activeTab = Tab.Settings
commandOpen = false
},
onOpenSession = { sessionKey ->
viewModel.switchChatSession(sessionKey)
activeTab = Tab.Chat
commandOpen = false
},
)
}
pendingTrust?.let { prompt ->
// Gateway certificate trust is modal across the shell so navigation
// cannot hide a changed TLS identity prompt.
GatewayTrustDialog(
prompt = prompt,
onAccept = viewModel::acceptGatewayTrustPrompt,
onDecline = viewModel::declineGatewayTrustPrompt,
)
}
}
}
@@ -335,41 +289,33 @@ private fun OverviewScreen(
val isConnected by viewModel.isConnected.collectAsState()
val sessions by viewModel.chatSessions.collectAsState()
val pendingRunCount by viewModel.pendingRunCount.collectAsState()
val statusText by viewModel.statusText.collectAsState()
val models by viewModel.modelCatalog.collectAsState()
val providers by viewModel.modelAuthProviders.collectAsState()
val agents by viewModel.gatewayAgents.collectAsState()
val pendingToolCalls by viewModel.chatPendingToolCalls.collectAsState()
val cronStatus by viewModel.cronStatus.collectAsState()
val usageSummary by viewModel.usageSummary.collectAsState()
val skillsSummary by viewModel.skillsSummary.collectAsState()
val nodesDevicesSummary by viewModel.nodesDevicesSummary.collectAsState()
val channelsSummary by viewModel.channelsSummary.collectAsState()
val readyProviderCount = readyModelProviderCount(providers, models)
val expiringProviderCount = expiringModelProviderCount(providers)
val attentionRows =
homeAttentionRows(
isConnected = isConnected,
pendingApprovals = pendingToolCalls.size,
channelsSummary = channelsSummary,
nodesDevicesSummary = nodesDevicesSummary,
readyProviderCount = readyProviderCount,
expiringProviderCount = expiringProviderCount,
)
val readyProviderCount = providers.count { modelProviderReady(it.status) }
LaunchedEffect(isConnected) {
if (isConnected) {
viewModel.refreshChatSessions(limit = 20)
viewModel.refreshModelCatalog()
viewModel.refreshAgents()
viewModel.refreshCronJobs()
viewModel.refreshUsage()
viewModel.refreshSkills()
viewModel.refreshNodesDevices()
viewModel.refreshChannels()
}
}
ClawScaffold(
contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 6.dp),
contentWindowInsets = shellContentInsets,
) {
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 20.dp)) {
Box(modifier = Modifier.fillMaxSize()) {
LazyColumn(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(12.dp), contentPadding = PaddingValues(bottom = 4.dp)) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(10.dp), contentPadding = PaddingValues(bottom = 104.dp)) {
item {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -388,20 +334,41 @@ private fun OverviewScreen(
}
item {
CompanionHeroPanel(
statusText = gatewaySummary(statusText, isConnected),
isConnected = isConnected,
pendingRunCount = pendingRunCount,
onOpenChat = { onSelectTab(Tab.Chat) },
onOpenVoice = { onSelectTab(Tab.Voice) },
onOpenGateway = { onOpenSettingsRoute(SettingsRoute.Gateway) },
)
SectionLabel(title = "MODULES")
}
if (attentionRows.isNotEmpty()) {
item {
HomeAttentionPanel(rows = attentionRows, onSelectTab = onSelectTab, onOpenSettingsRoute = onOpenSettingsRoute)
}
item {
ModuleList(
rows =
listOf(
ModuleRow("Chat", null, null, Icons.Outlined.ChatBubbleOutline, Tab.Chat),
ModuleRow("Sessions", null, if (sessions.isEmpty()) "Empty" else "${sessions.size} recent", Icons.Outlined.AccessTime, Tab.Sessions),
ModuleRow("Voice", null, if (isConnected) "Ready" else "Offline", Icons.Outlined.MicNone, Tab.Voice),
ModuleRow(
title = "Providers & Models",
subtitle = null,
metadata =
when {
!isConnected -> "Offline"
readyProviderCount > 0 -> "$readyProviderCount ready"
models.isNotEmpty() -> "${models.size} models"
else -> "Setup"
},
icon = Icons.Outlined.Inventory2,
tab = Tab.ProvidersModels,
),
ModuleRow("Channels", null, channelsSummaryText(channelsSummary), Icons.Default.Notifications, Tab.Settings, SettingsRoute.Channels),
ModuleRow("Agents", null, if (agents.isEmpty()) "Load" else "${agents.size} ready", Icons.Default.Person, Tab.Settings, SettingsRoute.Agents),
ModuleRow("Approvals", null, approvalsSummary(pendingToolCalls.size), Icons.Default.Lock, Tab.Settings, SettingsRoute.Approvals),
ModuleRow("Cron Jobs", null, cronJobsSummary(cronStatus.jobs), Icons.Outlined.AccessTime, Tab.Settings, SettingsRoute.CronJobs),
ModuleRow("Skills", null, skillsSummaryText(skillsSummary.skills), Icons.Default.Settings, Tab.Settings, SettingsRoute.Skills),
ModuleRow("Nodes & Devices", null, nodesDevicesSummaryText(nodesDevicesSummary), Icons.Default.Cloud, Tab.Settings, SettingsRoute.NodesDevices),
ModuleRow("Usage", null, usageSummaryText(usageSummary.providers.size), Icons.Default.Storage, Tab.Settings, SettingsRoute.Usage),
ModuleRow("Settings", null, null, Icons.Outlined.Settings, Tab.Settings, SettingsRoute.Home),
),
onSelectTab = onSelectTab,
onOpenSettingsRoute = onOpenSettingsRoute,
)
}
item {
@@ -430,7 +397,7 @@ private fun OverviewScreen(
item {
RecentSessionList(
rows =
sessions.take(5).map { session ->
sessions.take(7).map { session ->
RecentSessionListItem(
key = session.key,
title = displaySessionTitle(session.displayName),
@@ -445,40 +412,8 @@ private fun OverviewScreen(
)
}
}
item {
SectionLabel(title = "Control center")
}
item {
ModuleList(
rows =
listOf(
ModuleRow("Sessions", "Conversation history", if (sessions.isEmpty()) "Empty" else "${sessions.size} recent", Icons.Outlined.AccessTime, Tab.Sessions),
ModuleRow(
title = "Providers & Models",
subtitle = "Model setup",
metadata =
when {
!isConnected -> "Offline"
readyProviderCount > 0 -> "$readyProviderCount ready"
expiringProviderCount > 0 -> "$expiringProviderCount expiring"
models.isNotEmpty() -> "${models.size} models"
else -> "Setup"
},
icon = Icons.Outlined.Inventory2,
tab = Tab.ProvidersModels,
),
ModuleRow("Channels", "Connected messengers", channelsSummaryText(channelsSummary), Icons.Default.Notifications, Tab.Settings, SettingsRoute.Channels),
ModuleRow("Nodes & Devices", "Phone and node health", nodesDevicesSummaryText(nodesDevicesSummary), Icons.Default.Cloud, Tab.Settings, SettingsRoute.NodesDevices),
ModuleRow("Approvals", "Tool decisions", approvalsSummary(pendingToolCalls.size), Icons.Default.Lock, Tab.Settings, SettingsRoute.Approvals),
ModuleRow("Settings", "More runtime controls", null, Icons.Outlined.Settings, Tab.Settings, SettingsRoute.Home),
),
onSelectTab = onSelectTab,
onOpenSettingsRoute = onOpenSettingsRoute,
)
}
}
OverviewChatButton(onClick = { onSelectTab(Tab.Chat) }, modifier = Modifier.align(Alignment.BottomEnd).padding(bottom = 20.dp))
}
}
}
@@ -492,115 +427,26 @@ private data class ModuleRow(
val settingsRoute: SettingsRoute? = null,
)
/** Floating overview shortcut that keeps chat one tap away from module lists. */
@Composable
private fun CompanionHeroPanel(
statusText: String,
isConnected: Boolean,
pendingRunCount: Int,
onOpenChat: () -> Unit,
onOpenVoice: () -> Unit,
onOpenGateway: () -> Unit,
private fun OverviewChatButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
ClawPanel(contentPadding = PaddingValues(16.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
Surface(
modifier = Modifier.size(38.dp),
shape = CircleShape,
color = if (isConnected) ClawTheme.colors.successSoft else ClawTheme.colors.surfacePressed,
border = BorderStroke(1.dp, if (isConnected) ClawTheme.colors.success else ClawTheme.colors.border),
) {
Box(contentAlignment = Alignment.Center) {
Icon(imageVector = Icons.Outlined.ChatBubbleOutline, contentDescription = null, modifier = Modifier.size(19.dp), tint = if (isConnected) ClawTheme.colors.success else ClawTheme.colors.text)
}
}
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(3.dp)) {
Text(text = if (pendingRunCount > 0) "OpenClaw is working" else "Ready when you are", style = ClawTheme.type.title.copy(fontSize = 20.sp, lineHeight = 24.sp), color = ClawTheme.colors.text)
Text(text = statusText, style = ClawTheme.type.body, color = ClawTheme.colors.textMuted, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(9.dp)) {
ClawPrimaryButton(text = "Start chat", icon = Icons.Outlined.ChatBubbleOutline, onClick = onOpenChat, modifier = Modifier.weight(1f))
ClawSecondaryButton(text = "Voice", icon = Icons.Outlined.MicNone, onClick = onOpenVoice, modifier = Modifier.weight(1f))
}
if (!isConnected) {
ClawSecondaryButton(text = "Reconnect gateway", icon = Icons.Default.Cloud, onClick = onOpenGateway, modifier = Modifier.fillMaxWidth())
}
}
}
}
internal data class HomeAttentionRow(
val title: String,
val subtitle: String,
val icon: ImageVector,
val tab: Tab,
val settingsRoute: SettingsRoute? = null,
)
internal fun homeAttentionRows(
isConnected: Boolean,
pendingApprovals: Int,
channelsSummary: GatewayChannelsSummary,
nodesDevicesSummary: GatewayNodesDevicesSummary,
readyProviderCount: Int,
expiringProviderCount: Int = 0,
): List<HomeAttentionRow> =
listOfNotNull(
if (!isConnected) {
HomeAttentionRow("Gateway", "Connect before chat, voice, and live status.", Icons.Default.Cloud, Tab.Settings, SettingsRoute.Gateway)
} else {
null
},
if (pendingApprovals > 0) {
HomeAttentionRow("Approvals", approvalsSummary(pendingApprovals), Icons.Default.Lock, Tab.Settings, SettingsRoute.Approvals)
} else {
null
},
if (channelsSummary.channels.any { it.error != null }) {
HomeAttentionRow("Channels", channelsSummaryText(channelsSummary), Icons.Default.Notifications, Tab.Settings, SettingsRoute.Channels)
} else {
null
},
if (nodesDevicesSummary.pendingDevices.isNotEmpty()) {
HomeAttentionRow("Nodes & Devices", nodesDevicesSummaryText(nodesDevicesSummary), Icons.Default.Cloud, Tab.Settings, SettingsRoute.NodesDevices)
} else {
null
},
if (isConnected && expiringProviderCount > 0) {
HomeAttentionRow("Providers", "Provider auth expires soon", Icons.Outlined.Inventory2, Tab.ProvidersModels)
} else {
null
},
if (isConnected && readyProviderCount == 0 && expiringProviderCount == 0) {
HomeAttentionRow("Providers", "No ready providers", Icons.Outlined.Inventory2, Tab.ProvidersModels)
} else {
null
},
)
@Composable
private fun HomeAttentionPanel(
rows: List<HomeAttentionRow>,
onSelectTab: (Tab) -> Unit,
onOpenSettingsRoute: (SettingsRoute) -> Unit,
) {
ClawPanel(contentPadding = PaddingValues(horizontal = 14.dp, vertical = 8.dp)) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = "Needs attention", style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.warning)
rows.forEach { row ->
ModuleListRow(
row = ModuleRow(row.title, row.subtitle, null, row.icon, row.tab, row.settingsRoute),
onClick = {
val route = row.settingsRoute
if (route == null) {
onSelectTab(row.tab)
} else {
onOpenSettingsRoute(route)
}
},
)
}
Surface(
onClick = onClick,
modifier = modifier.height(ClawTheme.spacing.touchTarget),
shape = RoundedCornerShape(ClawTheme.radii.button),
color = ClawTheme.colors.primary,
contentColor = ClawTheme.colors.primaryText,
) {
Row(
modifier = Modifier.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(imageVector = Icons.Outlined.ChatBubbleOutline, contentDescription = null, modifier = Modifier.size(18.dp))
Text(text = "Chat", style = ClawTheme.type.label.copy(fontSize = 16.sp, lineHeight = 20.sp))
}
}
}
@@ -681,18 +527,14 @@ private fun ModuleListRow(
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
Icon(imageVector = row.icon, contentDescription = null, modifier = Modifier.size(20.dp), tint = ClawTheme.colors.text)
Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(
text = row.title,
style = ClawTheme.type.body,
color = ClawTheme.colors.text,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
row.subtitle?.let {
Text(text = it, style = ClawTheme.type.caption.copy(fontSize = 12.5.sp, lineHeight = 16.sp), color = ClawTheme.colors.textSubtle, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
Text(
text = row.title,
style = ClawTheme.type.body,
color = ClawTheme.colors.text,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
row.metadata?.let {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
Box(modifier = Modifier.size(4.5.dp).clip(CircleShape).background(statusDotColor(it)))
@@ -796,18 +638,11 @@ private fun RecentSessionRowContent(
@Composable
private fun ChatShellScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
onVoice: () -> Unit,
onOpenSessions: () -> Unit,
) {
ClawScaffold(
contentPadding = PaddingValues(start = 0.dp, top = 8.dp, end = 0.dp, bottom = 0.dp),
contentWindowInsets = shellContentInsets,
) {
ChatScreen(
viewModel = viewModel,
onVoice = onVoice,
onOpenSessions = onOpenSessions,
)
ClawScaffold(contentPadding = PaddingValues(start = 0.dp, top = 8.dp, end = 0.dp, bottom = 8.dp)) {
ChatScreen(viewModel = viewModel, onBack = onBack, onVoice = onVoice)
}
}
@@ -818,10 +653,7 @@ private fun VoiceShellScreen(
onOpenGatewaySettings: () -> Unit,
onOpenVoiceSettings: () -> Unit,
) {
ClawScaffold(
contentPadding = PaddingValues(start = 0.dp, top = 8.dp, end = 0.dp, bottom = 0.dp),
contentWindowInsets = shellContentInsets,
) {
ClawScaffold(contentPadding = PaddingValues(start = 0.dp, top = 8.dp, end = 0.dp, bottom = 8.dp)) {
VoiceScreen(
viewModel = viewModel,
onOpenCommand = onOpenCommand,
@@ -837,7 +669,6 @@ private fun SettingsShellScreen(
route: SettingsRoute,
onRouteChange: (SettingsRoute) -> Unit,
onRouteBack: () -> Unit,
onBackHome: () -> Unit,
onOpenCommand: () -> Unit,
) {
val displayName by viewModel.displayName.collectAsState()
@@ -876,18 +707,14 @@ private fun SettingsShellScreen(
return
}
ClawScaffold(
contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 6.dp),
contentWindowInsets = shellContentInsets,
) {
LazyColumn(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(13.dp), contentPadding = PaddingValues(bottom = 4.dp)) {
ClawScaffold(contentPadding = PaddingValues(start = 20.dp, top = 14.dp, end = 20.dp, bottom = 20.dp)) {
LazyColumn(verticalArrangement = Arrangement.spacedBy(13.dp)) {
item {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(9.dp),
) {
PlainIconButton(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back to home", onClick = onBackHome)
Text(text = "Settings", style = ClawTheme.type.title.copy(fontSize = 16.sp, lineHeight = 20.sp), color = ClawTheme.colors.text, modifier = Modifier.weight(1f))
SettingsSearchButton(onClick = onOpenCommand)
}

View File

@@ -4,7 +4,6 @@ import ai.openclaw.app.MainViewModel
import ai.openclaw.app.chat.ChatMessage
import ai.openclaw.app.chat.ChatMessageContent
import ai.openclaw.app.chat.ChatPendingToolCall
import ai.openclaw.app.chat.ChatSessionEntry
import ai.openclaw.app.chat.OutgoingAttachment
import ai.openclaw.app.ui.design.ClawListItem
import ai.openclaw.app.ui.design.ClawLoadingState
@@ -38,11 +37,11 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Mic
import androidx.compose.material.icons.filled.MoreHoriz
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
@@ -79,8 +78,8 @@ import java.util.Locale
@Composable
fun ChatScreen(
viewModel: MainViewModel,
onBack: () -> Unit,
onVoice: () -> Unit,
onOpenSessions: () -> Unit,
) {
val messages by viewModel.chatMessages.collectAsState()
val historyLoading by viewModel.chatHistoryLoading.collectAsState()
@@ -159,23 +158,13 @@ fun ChatScreen(
thinkingLevel = thinkingLevel,
healthOk = healthOk,
pendingRunCount = pendingRunCount,
onBack = onBack,
onMore = {
viewModel.refreshChat()
viewModel.refreshChatSessions(limit = 100)
},
)
ChatSessionSwitcher(
sessionKey = sessionKey,
sessions = sessions,
mainSessionKey = mainSessionKey,
onSelectSession = { key ->
viewModel.switchChatSession(key)
viewModel.refreshChatSessions(limit = 100)
},
onOpenSessions = onOpenSessions,
)
errorText?.takeIf { it.isNotBlank() }?.let { error ->
ChatNotice(title = "Chat needs attention", body = userFacingChatError(error))
}
@@ -225,88 +214,13 @@ fun ChatScreen(
}
}
@Composable
private fun ChatSessionSwitcher(
sessionKey: String,
sessions: List<ChatSessionEntry>,
mainSessionKey: String,
onSelectSession: (String) -> Unit,
onOpenSessions: () -> Unit,
) {
val choices =
remember(sessionKey, sessions, mainSessionKey) {
resolveCompactSessionChoices(
currentSessionKey = sessionKey,
sessions = sessions,
mainSessionKey = mainSessionKey,
)
}
if (choices.size <= 1 && sessions.size <= 1) return
Row(
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
choices.forEach { entry ->
ChatSessionChip(
text = chatSessionChipText(entry = entry, mainSessionKey = mainSessionKey),
active = isActiveSessionChoice(entry.key, sessionKey, mainSessionKey),
onClick = { onSelectSession(entry.key) },
)
}
if (sessions.size > choices.size) {
Surface(
onClick = onOpenSessions,
modifier = Modifier.heightIn(min = 36.dp),
shape = RoundedCornerShape(ClawTheme.radii.pill),
color = ClawTheme.colors.canvas,
contentColor = ClawTheme.colors.textMuted,
border = BorderStroke(1.dp, ClawTheme.colors.border),
) {
Row(
modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(5.dp),
) {
Icon(imageVector = Icons.Default.MoreHoriz, contentDescription = null, modifier = Modifier.size(16.dp))
Text(text = "All", style = ClawTheme.type.caption, maxLines = 1)
}
}
}
}
}
@Composable
private fun ChatSessionChip(
text: String,
active: Boolean,
onClick: () -> Unit,
) {
Surface(
onClick = onClick,
modifier = Modifier.heightIn(min = 36.dp),
shape = RoundedCornerShape(ClawTheme.radii.pill),
color = if (active) ClawTheme.colors.primary else ClawTheme.colors.surfaceRaised,
contentColor = if (active) ClawTheme.colors.primaryText else ClawTheme.colors.text,
border = BorderStroke(1.dp, if (active) ClawTheme.colors.primary else ClawTheme.colors.border),
) {
Text(
text = text,
modifier = Modifier.padding(horizontal = 11.dp, vertical = 7.dp),
style = ClawTheme.type.caption,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
@Composable
private fun ChatHeader(
sessionTitle: String,
thinkingLevel: String,
healthOk: Boolean,
pendingRunCount: Int,
onBack: () -> Unit,
onMore: () -> Unit,
) {
Row(
@@ -314,7 +228,7 @@ private fun ChatHeader(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
Box(modifier = Modifier.size(ClawTheme.spacing.touchTarget))
HeaderIcon(icon = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", onClick = onBack)
Column(
modifier = Modifier.weight(1f),
@@ -872,33 +786,13 @@ private fun AttachmentChip(
private fun currentSessionTitle(
sessionKey: String,
sessions: List<ChatSessionEntry>,
sessions: List<ai.openclaw.app.chat.ChatSessionEntry>,
): String {
val entry = sessions.firstOrNull { it.key == sessionKey }
val name = entry?.displayName?.takeIf { it.isNotBlank() } ?: return "New chat"
return friendlySessionName(name)
}
private fun chatSessionChipText(
entry: ChatSessionEntry,
mainSessionKey: String,
): String {
val mainKey = mainSessionKey.trim().ifEmpty { "main" }
if (entry.key == mainKey || (entry.key == "main" && mainKey == "main")) return "Main"
val name = entry.displayName?.takeIf { it.isNotBlank() } ?: entry.key.takeIf { entry.updatedAtMs != null } ?: "Current"
return friendlySessionName(name)
}
private fun isActiveSessionChoice(
choiceKey: String,
sessionKey: String,
mainSessionKey: String,
): Boolean {
val mainKey = mainSessionKey.trim().ifEmpty { "main" }
val current = sessionKey.trim().let { if (it == "main" && mainKey != "main") mainKey else it }
return choiceKey == current
}
@Composable
private fun SendButton(
enabled: Boolean,

View File

@@ -4,9 +4,22 @@ import ai.openclaw.app.chat.ChatSessionEntry
private const val RECENT_WINDOW_MS = 24 * 60 * 60 * 1000L
/**
* Derive a human-friendly label from a raw session key.
* Examples:
* "telegram:g-agent-main-main" -> "Main"
* "agent:main:main" -> "Main"
* "discord:g-server-channel" -> "Server Channel"
* "my-custom-session" -> "My Custom Session"
*/
fun friendlySessionName(key: String): String {
// Strip common prefixes like "telegram:", "agent:", "discord:" etc.
val stripped = key.substringAfterLast(":")
// Remove leading "g-" prefix (gateway artifact)
val cleaned = if (stripped.startsWith("g-")) stripped.removePrefix("g-") else stripped
// Split on hyphens/underscores, title-case each word, collapse "main main" -> "Main"
val words =
cleaned
.split('-', '_')
@@ -65,29 +78,3 @@ fun resolveSessionChoices(
return result
}
fun resolveCompactSessionChoices(
currentSessionKey: String,
sessions: List<ChatSessionEntry>,
mainSessionKey: String,
nowMs: Long = System.currentTimeMillis(),
maxOptions: Int = 5,
): List<ChatSessionEntry> {
val allChoices =
resolveSessionChoices(
currentSessionKey = currentSessionKey,
sessions = sessions,
mainSessionKey = mainSessionKey,
nowMs = nowMs,
)
val mainKey = mainSessionKey.trim().ifEmpty { "main" }
val current = currentSessionKey.trim().let { if (it == "main" && mainKey != "main") mainKey else it }
val pinnedRank = listOf(mainKey, current).filter { it.isNotBlank() }.distinct().withIndex().associate { it.value to it.index }
val unpinnedRank = pinnedRank.size
return allChoices
.withIndex()
.sortedWith(compareBy({ pinnedRank[it.value.key] ?: unpinnedRank }, { it.index }))
.take(maxOptions)
.map { it.value }
}

View File

@@ -61,7 +61,6 @@ internal enum class ClawStatus {
internal fun ClawScaffold(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(horizontal = ClawTheme.spacing.lg, vertical = ClawTheme.spacing.lg),
contentWindowInsets: WindowInsets = WindowInsets.safeDrawing,
content: @Composable () -> Unit,
) {
Box(
@@ -69,7 +68,7 @@ internal fun ClawScaffold(
modifier
.fillMaxSize()
.background(ClawTheme.colors.canvas)
.windowInsetsPadding(contentWindowInsets)
.windowInsetsPadding(WindowInsets.safeDrawing)
.padding(contentPadding),
) {
content()

View File

@@ -1,7 +1,6 @@
package ai.openclaw.app.ui.design
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -92,29 +91,27 @@ internal fun ClawBottomNav(
) {
val safeInsets = WindowInsets.navigationBars.only(androidx.compose.foundation.layout.WindowInsetsSides.Bottom)
Box(modifier = modifier.fillMaxWidth().background(ClawTheme.colors.canvas)) {
Surface(
modifier = Modifier.fillMaxWidth(),
color = ClawTheme.colors.surface.copy(alpha = 0.96f),
border = BorderStroke(1.dp, ClawTheme.colors.border),
shape = RoundedCornerShape(topStart = ClawTheme.radii.sheet, topEnd = ClawTheme.radii.sheet),
Surface(
modifier = modifier.fillMaxWidth(),
color = ClawTheme.colors.surface.copy(alpha = 0.96f),
border = BorderStroke(1.dp, ClawTheme.colors.border),
shape = RoundedCornerShape(topStart = ClawTheme.radii.sheet, topEnd = ClawTheme.radii.sheet),
) {
Row(
modifier =
Modifier
.windowInsetsPadding(safeInsets)
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Row(
modifier =
Modifier
.windowInsetsPadding(safeInsets)
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
items.forEach { item ->
ClawBottomNavItem(
item = item,
selected = item.key == selectedKey,
onClick = { onSelect(item.key) },
modifier = Modifier.weight(1f),
)
}
items.forEach { item ->
ClawBottomNavItem(
item = item,
selected = item.key == selectedKey,
onClick = { onSelect(item.key) },
modifier = Modifier.weight(1f),
)
}
}
}
@@ -132,7 +129,7 @@ private fun ClawBottomNavItem(
modifier = modifier.heightIn(min = 48.dp),
shape = RoundedCornerShape(ClawTheme.radii.control),
color = if (selected) ClawTheme.colors.primary else Color.Transparent,
contentColor = if (selected) ClawTheme.colors.primaryText else ClawTheme.colors.textMuted,
contentColor = if (selected) ClawTheme.colors.primaryText else ClawTheme.colors.textSubtle,
) {
Column(
modifier = Modifier.padding(horizontal = 5.dp, vertical = 6.dp),

View File

@@ -62,21 +62,6 @@ class SecurePrefsTest {
assertFalse(plainPrefs.getBoolean("talk.enabled", false))
}
@Test
fun installedAppsSharing_defaultsOffAndPersistsOptIn() {
val context = RuntimeEnvironment.getApplication()
val plainPrefs = context.getSharedPreferences("openclaw.node", Context.MODE_PRIVATE)
plainPrefs.edit().clear().commit()
val prefs = SecurePrefs(context)
assertFalse(prefs.installedAppsSharingEnabled.value)
prefs.setInstalledAppsSharingEnabled(true)
assertTrue(prefs.installedAppsSharingEnabled.value)
assertTrue(plainPrefs.getBoolean("device.apps.sharing.enabled", false))
}
@Test
fun saveGatewayBootstrapToken_persistsSeparatelyFromSharedToken() {
val context = RuntimeEnvironment.getApplication()

View File

@@ -7,57 +7,66 @@ import org.junit.Test
class CanvasActionTrustTest {
@Test
fun acceptsBundledScaffoldAsset() {
assertTrue(CanvasActionTrust.isTrustedCanvasActionUrl(CanvasActionTrust.scaffoldAssetUrl))
assertTrue(CanvasActionTrust.isTrustedCanvasActionUrl(CanvasActionTrust.scaffoldAssetUrl, emptyList()))
}
@Test
fun acceptsBundledA2uiAsset() {
assertTrue(CanvasActionTrust.isTrustedCanvasActionUrl(CanvasActionTrust.localA2uiAssetUrl))
}
@Test
fun rejectsRemoteHttpA2uiPageEvenWhenGatewayAdvertised() {
assertFalse(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "http://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android",
),
)
}
@Test
fun rejectsRemoteHttpsA2uiPageEvenWhenGatewayAdvertised() {
assertFalse(
fun acceptsTrustedA2uiPageOnAdvertisedCanvasHost() {
assertTrue(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android",
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
),
)
}
@Test
fun rejectsRemoteCanvasPage() {
fun rejectsDifferentOriginEvenIfPathMatches() {
assertFalse(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "https://canvas.example.com:9443/__openclaw__/canvas/",
rawUrl = "https://evil.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android",
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
),
)
}
@Test
fun rejectsDescendantPathUnderBundledA2uiRoot() {
fun rejectsUntrustedCanvasPagePathOnTrustedOrigin() {
assertFalse(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "file:///android_asset/CanvasA2UI/child/index.html",
rawUrl = "https://canvas.example.com:9443/untrusted/index.html",
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
),
)
}
@Test
fun rejectsQueryOrFragmentChangesToBundledA2uiAsset() {
assertFalse(
fun acceptsFragmentOnlyDifferenceForTrustedA2uiPage() {
assertTrue(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "${CanvasActionTrust.localA2uiAssetUrl}?platform=android",
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android#step2",
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
),
)
}
@Test
fun rejectsQueryMismatchOnTrustedOriginAndPath() {
assertFalse(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=ios",
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
),
)
}
@Test
fun rejectsDescendantPathUnderTrustedA2uiRoot() {
assertFalse(
CanvasActionTrust.isTrustedCanvasActionUrl(
rawUrl = "https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/child/index.html?platform=android",
trustedA2uiUrls = listOf("https://canvas.example.com:9443/__openclaw__/cap/token/__openclaw__/a2ui/?platform=android"),
),
)
assertFalse(CanvasActionTrust.isTrustedCanvasActionUrl("${CanvasActionTrust.localA2uiAssetUrl}#step2"))
}
}

View File

@@ -9,7 +9,6 @@ import ai.openclaw.app.gateway.isLoopbackGatewayHost
import ai.openclaw.app.protocol.OpenClawCallLogCommand
import ai.openclaw.app.protocol.OpenClawCameraCommand
import ai.openclaw.app.protocol.OpenClawCapability
import ai.openclaw.app.protocol.OpenClawDeviceCommand
import ai.openclaw.app.protocol.OpenClawLocationCommand
import ai.openclaw.app.protocol.OpenClawMotionCommand
import ai.openclaw.app.protocol.OpenClawPhotosCommand
@@ -476,15 +475,6 @@ class ConnectionManagerTest {
assertTrue(options.caps.contains(OpenClawCapability.VoiceWake.rawValue))
}
@Test
fun buildNodeConnectOptions_advertisesDeviceAppsOnlyWhenUserOptedIn() {
val disabled = newManager(installedAppsSharingEnabled = false).buildNodeConnectOptions()
val enabled = newManager(installedAppsSharingEnabled = true).buildNodeConnectOptions()
assertFalse(disabled.commands.contains(OpenClawDeviceCommand.Apps.rawValue))
assertTrue(enabled.commands.contains(OpenClawDeviceCommand.Apps.rawValue))
}
@Test
fun buildNodeConnectOptions_omitsVoiceWakeWithoutMicrophonePermission() {
val options =
@@ -556,7 +546,6 @@ class ConnectionManagerTest {
callLogAvailable: Boolean = false,
photosAvailable: Boolean = false,
hasRecordAudioPermission: Boolean = false,
installedAppsSharingEnabled: Boolean = false,
): ConnectionManager {
val context = RuntimeEnvironment.getApplication()
val prefs =
@@ -578,7 +567,6 @@ class ConnectionManagerTest {
callLogAvailable = { callLogAvailable },
photosAvailable = { photosAvailable },
hasRecordAudioPermission = { hasRecordAudioPermission },
installedAppsSharingEnabled = { installedAppsSharingEnabled },
manualTls = { false },
)
}

View File

@@ -1,7 +1,6 @@
package ai.openclaw.app.node
import android.content.Context
import android.content.pm.ApplicationInfo
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean
@@ -321,108 +320,6 @@ class DeviceHandlerTest {
system["securityPatchLevel"]?.jsonPrimitive?.content
}
@Test
fun handleDeviceApps_filtersAndLimitsVisibleApps() {
val handler =
DeviceHandler.forTesting(
appContext = appContext(),
appSource =
FakeDeviceAppSource(
listOf(
DeviceAppEntry(
label = "Calendar",
packageName = "com.google.android.calendar",
system = false,
enabled = true,
launchable = true,
),
DeviceAppEntry(
label = "Android System",
packageName = "android",
system = true,
enabled = true,
launchable = false,
),
DeviceAppEntry(
label = "Disabled App",
packageName = "com.example.disabled",
system = false,
enabled = false,
launchable = true,
),
DeviceAppEntry(
label = "Gmail",
packageName = "com.google.android.gm",
system = false,
enabled = true,
launchable = true,
),
),
),
)
val result = handler.handleDeviceApps("""{"query":"google","limit":1}""")
assertTrue(result.ok)
val payload = parsePayload(result.payloadJson)
assertEquals("1", payload.getValue("count").jsonPrimitive.content)
assertEquals("2", payload.getValue("totalMatched").jsonPrimitive.content)
assertTrue(payload.getValue("truncated").jsonPrimitive.boolean)
assertEquals("launcher", payload.getValue("visibility").jsonPrimitive.content)
val apps = payload.getValue("apps").jsonArray
assertEquals(1, apps.size)
val app = apps.first().jsonObject
assertEquals("Calendar", app.getValue("label").jsonPrimitive.content)
assertEquals("com.google.android.calendar", app.getValue("packageName").jsonPrimitive.content)
assertTrue(!app.getValue("system").jsonPrimitive.boolean)
assertTrue(app.getValue("enabled").jsonPrimitive.boolean)
assertTrue(app.getValue("launchable").jsonPrimitive.boolean)
}
@Test
fun handleDeviceApps_canIncludeSystemAndNonLaunchableApps() {
val source =
FakeDeviceAppSource(
listOf(
DeviceAppEntry(
label = "Android System",
packageName = "android",
system = true,
enabled = true,
launchable = false,
),
),
)
val handler = DeviceHandler.forTesting(appContext = appContext(), appSource = source)
val result = handler.handleDeviceApps("""{"includeSystem":true,"includeNonLaunchable":true}""")
assertTrue(result.ok)
val payload = parsePayload(result.payloadJson)
assertEquals("android-visible", payload.getValue("visibility").jsonPrimitive.content)
assertTrue(payload.getValue("includeSystem").jsonPrimitive.boolean)
val app =
payload
.getValue("apps")
.jsonArray
.first()
.jsonObject
assertEquals("android", app.getValue("packageName").jsonPrimitive.content)
assertTrue(app.getValue("system").jsonPrimitive.boolean)
assertTrue(!app.getValue("launchable").jsonPrimitive.boolean)
assertTrue(source.includeNonLaunchableRequests.single())
}
@Test
fun isSystemDeviceApp_treatsUpdatedBuiltInsAsSystemApps() {
val appInfo =
ApplicationInfo().apply {
flags = ApplicationInfo.FLAG_UPDATED_SYSTEM_APP
}
assertTrue(isSystemDeviceApp(appInfo))
}
private fun appContext(): Context = RuntimeEnvironment.getApplication()
private fun parsePayload(payloadJson: String?): JsonObject {
@@ -430,14 +327,3 @@ class DeviceHandlerTest {
return Json.parseToJsonElement(jsonString).jsonObject
}
}
private class FakeDeviceAppSource(
private val apps: List<DeviceAppEntry>,
) : DeviceAppSource {
val includeNonLaunchableRequests = mutableListOf<Boolean>()
override fun listApps(includeNonLaunchable: Boolean): List<DeviceAppEntry> {
includeNonLaunchableRequests += includeNonLaunchable
return apps
}
}

View File

@@ -115,15 +115,6 @@ class InvokeCommandRegistryTest {
assertMissingAll(commands, optionalCommands + debugCommands)
}
@Test
fun advertisedCommands_includesDeviceAppsOnlyWhenUserOptedIn() {
val disabled = InvokeCommandRegistry.advertisedCommands(defaultFlags(installedAppsSharingEnabled = false))
val enabled = InvokeCommandRegistry.advertisedCommands(defaultFlags(installedAppsSharingEnabled = true))
assertFalse(disabled.contains(OpenClawDeviceCommand.Apps.rawValue))
assertTrue(enabled.contains(OpenClawDeviceCommand.Apps.rawValue))
}
@Test
fun advertisedCommands_includesFeatureCommandsWhenEnabled() {
val commands =
@@ -160,7 +151,6 @@ class InvokeCommandRegistryTest {
voiceWakeEnabled = false,
motionActivityAvailable = true,
motionPedometerAvailable = false,
installedAppsSharingEnabled = false,
debugBuild = false,
),
)
@@ -272,7 +262,6 @@ class InvokeCommandRegistryTest {
voiceWakeEnabled: Boolean = false,
motionActivityAvailable: Boolean = false,
motionPedometerAvailable: Boolean = false,
installedAppsSharingEnabled: Boolean = false,
debugBuild: Boolean = false,
): NodeRuntimeFlags =
NodeRuntimeFlags(
@@ -286,7 +275,6 @@ class InvokeCommandRegistryTest {
voiceWakeEnabled = voiceWakeEnabled,
motionActivityAvailable = motionActivityAvailable,
motionPedometerAvailable = motionPedometerAvailable,
installedAppsSharingEnabled = installedAppsSharingEnabled,
debugBuild = debugBuild,
)

View File

@@ -4,7 +4,6 @@ import ai.openclaw.app.gateway.DeviceIdentityStore
import ai.openclaw.app.gateway.GatewaySession
import ai.openclaw.app.protocol.OpenClawCallLogCommand
import ai.openclaw.app.protocol.OpenClawCameraCommand
import ai.openclaw.app.protocol.OpenClawDeviceCommand
import ai.openclaw.app.protocol.OpenClawLocationCommand
import ai.openclaw.app.protocol.OpenClawMotionCommand
import ai.openclaw.app.protocol.OpenClawPhotosCommand
@@ -171,20 +170,6 @@ class InvokeDispatcherTest {
assertEquals("LOCATION_DISABLED: enable Location in Settings", result.error?.message)
}
@Test
fun handleInvoke_blocksDeviceAppsWhenSharingDisabled() =
runTest {
val result =
newDispatcher(installedAppsSharingEnabled = false)
.handleInvoke(OpenClawDeviceCommand.Apps.rawValue, """{"limit":1}""")
assertEquals("INSTALLED_APPS_SHARING_DISABLED", result.error?.code)
assertEquals(
"INSTALLED_APPS_SHARING_DISABLED: enable Installed Apps in Settings",
result.error?.message,
)
}
@Test
fun handleInvoke_blocksMotionActivityWhenUnavailable() =
runTest {
@@ -265,7 +250,6 @@ class InvokeDispatcherTest {
smsTelephonyAvailable: Boolean = true,
callLogAvailable: Boolean = false,
photosAvailable: Boolean = true,
installedAppsSharingEnabled: Boolean = true,
debugBuild: Boolean = false,
motionActivityAvailable: Boolean = false,
motionPedometerAvailable: Boolean = false,
@@ -299,6 +283,8 @@ class InvokeDispatcherTest {
A2UIHandler(
canvas = canvas,
json = Json { ignoreUnknownKeys = true },
getNodeCanvasHostUrl = { null },
getOperatorCanvasHostUrl = { null },
),
debugHandler = DebugHandler(appContext, DeviceIdentityStore(appContext)),
callLogHandler = CallLogHandler.forTesting(appContext, InvokeDispatcherFakeCallLogDataSource()),
@@ -311,10 +297,10 @@ class InvokeDispatcherTest {
smsTelephonyAvailable = { smsTelephonyAvailable },
callLogAvailable = { callLogAvailable },
photosAvailable = { photosAvailable },
installedAppsSharingEnabled = { installedAppsSharingEnabled },
debugBuild = { debugBuild },
onCanvasA2uiPush = {},
onCanvasA2uiReset = {},
refreshCanvasHostUrl = { null },
motionActivityAvailable = { motionActivityAvailable },
motionPedometerAvailable = { motionPedometerAvailable },
)

View File

@@ -57,7 +57,6 @@ class OpenClawProtocolConstantsTest {
assertEquals("device.info", OpenClawDeviceCommand.Info.rawValue)
assertEquals("device.permissions", OpenClawDeviceCommand.Permissions.rawValue)
assertEquals("device.health", OpenClawDeviceCommand.Health.rawValue)
assertEquals("device.apps", OpenClawDeviceCommand.Apps.rawValue)
}
@Test

View File

@@ -1,8 +1,5 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayModelProviderSummary
import ai.openclaw.app.GatewayModelSummary
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
@@ -13,129 +10,8 @@ class ProviderModelStatusTest {
assertTrue(modelProviderReady("static"))
}
@Test
fun expiringProviderStatusIsNotFullyReady() {
assertFalse(modelProviderReady("expiring"))
}
@Test
fun missingProviderStatusIsNotReady() {
assertFalse(modelProviderReady("missing"))
}
@Test
fun readyModelProviderCountUsesAuthBackedProviderStatuses() {
val providers =
listOf(
GatewayModelProviderSummary(id = "openai", displayName = "OpenAI", status = "missing", profileCount = 0),
GatewayModelProviderSummary(id = "anthropic", displayName = "Anthropic", status = "ready", profileCount = 1),
GatewayModelProviderSummary(id = "openai", displayName = "OpenAI", status = "expiring", profileCount = 1),
)
assertEquals(1, readyModelProviderCount(providers, emptyList()))
assertEquals(1, expiringModelProviderCount(providers))
}
@Test
fun readyModelProviderCountUsesAvailableModelsAsServingReadiness() {
val models =
listOf(
model(provider = "anthropic", available = true),
model(provider = "anthropic", available = true),
model(provider = "openrouter", available = false),
)
assertEquals(1, readyModelProviderCount(emptyList(), models))
}
@Test
fun readyModelProviderCountDoesNotTreatCatalogOnlyModelsAsReady() {
val providers =
listOf(
GatewayModelProviderSummary(id = "openrouter", displayName = "OpenRouter", status = "missing", profileCount = 0),
)
val models =
listOf(
model(provider = "openrouter", available = false),
)
assertEquals(0, readyModelProviderCount(providers, models))
}
@Test
fun readyModelProviderCountPreservesLegacyRowsWhenAvailabilityIsMissing() {
val models =
listOf(
model(provider = "openrouter", available = null),
)
assertEquals(1, readyModelProviderCount(emptyList(), models))
}
@Test
fun readyModelProviderCountTreatsExpiringAvailableModelsAsUsableButWarnable() {
val providers =
listOf(
GatewayModelProviderSummary(id = "openai", displayName = "OpenAI", status = "expiring", profileCount = 1),
)
val models =
listOf(
model(provider = "openai", available = true),
)
assertEquals(1, readyModelProviderCount(providers, models))
assertEquals(1, expiringModelProviderCount(providers))
assertFalse(modelProviderReady("expiring"))
}
@Test
fun providerCommandSubtitleSurfacesExpiringBeforeReadyModels() {
val providers =
listOf(
GatewayModelProviderSummary(id = "openai", displayName = "OpenAI", status = "expiring", profileCount = 1),
)
val models =
listOf(
model(provider = "openai", available = true),
)
assertEquals("1 providers expiring", providerCommandSubtitle(isConnected = true, providers = providers, models = models))
}
@Test
fun readyModelProviderCountDoesNotTreatUnavailableModelsAsReadyWhenAuthProviderNeedsSetup() {
val providers =
listOf(
GatewayModelProviderSummary(id = "openai", displayName = "OpenAI", status = "missing", profileCount = 0),
)
val models =
listOf(
model(provider = "openai", available = false),
)
assertEquals(0, readyModelProviderCount(providers, models))
}
@Test
fun modelAvailabilityHonorsExplicitUnavailableRows() {
assertTrue(modelAvailabilityUsable(model(provider = "openai", available = true)))
assertTrue(modelAvailabilityUsable(model(provider = "openai", available = null)))
assertFalse(modelAvailabilityUsable(model(provider = "openai", available = false)))
}
private fun model(
provider: String,
available: Boolean?,
): GatewayModelSummary =
GatewayModelSummary(
id = "$provider/test-model",
name = "test-model",
provider = provider,
available = available,
supportsVision = false,
supportsAudio = false,
supportsDocuments = false,
supportsReasoning = false,
contextTokens = null,
)
}

View File

@@ -1,113 +0,0 @@
package ai.openclaw.app.ui
import ai.openclaw.app.GatewayChannelSummary
import ai.openclaw.app.GatewayChannelsSummary
import ai.openclaw.app.GatewayNodesDevicesSummary
import ai.openclaw.app.GatewayPendingDeviceSummary
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class ShellScreenLogicTest {
@Test
fun bottomNavHidesForKeyboardAndCommandPalette() {
assertTrue(shellBottomNavVisible(keyboardVisible = false, commandOpen = false))
assertFalse(shellBottomNavVisible(keyboardVisible = true, commandOpen = false))
assertFalse(shellBottomNavVisible(keyboardVisible = false, commandOpen = true))
}
@Test
fun homeAttentionRowsSurfaceGatewayWhenDisconnected() {
val rows =
homeAttentionRows(
isConnected = false,
pendingApprovals = 0,
channelsSummary = emptyChannels(),
nodesDevicesSummary = emptyNodesDevices(),
readyProviderCount = 0,
)
assertEquals(listOf("Gateway"), rows.map { it.title })
}
@Test
fun homeAttentionRowsSurfaceOnlyActionableConnectedIssues() {
val rows =
homeAttentionRows(
isConnected = true,
pendingApprovals = 2,
channelsSummary =
GatewayChannelsSummary(
channels =
listOf(
GatewayChannelSummary(
id = "telegram",
label = "Telegram",
accountCount = 1,
enabled = true,
configured = true,
linked = true,
running = false,
connected = false,
error = "offline",
),
),
),
nodesDevicesSummary =
GatewayNodesDevicesSummary(
nodes = emptyList(),
pendingDevices =
listOf(
GatewayPendingDeviceSummary(
requestId = "request-1",
deviceId = "device-1",
displayName = "Phone",
remoteIp = null,
roles = emptyList(),
scopes = emptyList(),
requestedAtMs = null,
repair = false,
),
),
pairedDevices = emptyList(),
),
readyProviderCount = 0,
)
assertEquals(listOf("Approvals", "Channels", "Nodes & Devices", "Providers"), rows.map { it.title })
}
@Test
fun homeAttentionRowsStayQuietWhenConnectedAndHealthy() {
val rows =
homeAttentionRows(
isConnected = true,
pendingApprovals = 0,
channelsSummary = emptyChannels(),
nodesDevicesSummary = emptyNodesDevices(),
readyProviderCount = 1,
)
assertEquals(emptyList<String>(), rows.map { it.title })
}
@Test
fun homeAttentionRowsSurfaceExpiringProviderAuth() {
val rows =
homeAttentionRows(
isConnected = true,
pendingApprovals = 0,
channelsSummary = emptyChannels(),
nodesDevicesSummary = emptyNodesDevices(),
readyProviderCount = 0,
expiringProviderCount = 1,
)
assertEquals(listOf("Provider auth expires soon"), rows.map { it.subtitle })
}
private fun emptyChannels(): GatewayChannelsSummary = GatewayChannelsSummary(channels = emptyList())
private fun emptyNodesDevices(): GatewayNodesDevicesSummary = GatewayNodesDevicesSummary(nodes = emptyList(), pendingDevices = emptyList(), pairedDevices = emptyList())
}

View File

@@ -32,29 +32,4 @@ class SessionFiltersTest {
val result = resolveSessionChoices("custom", sessions, mainSessionKey = "main", nowMs = now).map { it.key }
assertEquals(listOf("main", "custom"), result)
}
@Test
fun compactChoicesKeepMainAndCurrentWhileCappingRecentSessions() {
val now = 1_700_000_000_000L
val sessions =
listOf(
ChatSessionEntry(key = "recent-1", updatedAtMs = now - 1),
ChatSessionEntry(key = "recent-2", updatedAtMs = now - 2),
ChatSessionEntry(key = "recent-3", updatedAtMs = now - 3),
ChatSessionEntry(key = "recent-4", updatedAtMs = now - 4),
ChatSessionEntry(key = "main", updatedAtMs = now - 5),
ChatSessionEntry(key = "active-old", updatedAtMs = now - 30 * 60 * 60 * 1000L),
)
val result =
resolveCompactSessionChoices(
currentSessionKey = "active-old",
sessions = sessions,
mainSessionKey = "main",
nowMs = now,
maxOptions = 4,
).map { it.key }
assertEquals(listOf("main", "active-old", "recent-1", "recent-2"), result)
}
}

View File

@@ -1,8 +1,4 @@
#!/usr/bin/env bun
/**
* Android release helper that bumps version fields, builds release AAB variants,
* verifies signatures, and prints SHA-256 checksums.
*/
import { $ } from "bun";
import { dirname, join } from "node:path";

View File

@@ -1,9 +1,5 @@
# OpenClaw iOS Changelog
## 2026.6.2 - 2026-06-02
Maintenance update for the current OpenClaw release.
## 2026.6.1 - 2026-06-01
Maintenance update for the current OpenClaw release.

View File

@@ -2,8 +2,8 @@
// Source of truth: apps/ios/version.json
// Generated by scripts/ios-sync-versioning.ts.
OPENCLAW_IOS_VERSION = 2026.6.2
OPENCLAW_MARKETING_VERSION = 2026.6.2
OPENCLAW_IOS_VERSION = 2026.6.1
OPENCLAW_MARKETING_VERSION = 2026.6.1
OPENCLAW_BUILD_VERSION = 1
#include? "../build/Version.xcconfig"

View File

@@ -59,11 +59,6 @@ struct SettingsProTab: View {
@State var notificationActionText = "Request Access"
@State var diagnosticsLastRunText = "Not run"
@State var diagnosticsIssueCount: Int?
@State var bottomOverlayInset: CGFloat = 0
var bottomScrollMargin: CGFloat {
max(0, self.bottomOverlayInset - SettingsLayout.rowHeight - SettingsLayout.bottomContentPadding)
}
var body: some View {
NavigationStack {
@@ -76,13 +71,9 @@ struct SettingsProTab: View {
self.gatewaySection
self.settingsListSection
}
.padding(.top, 18)
.padding(.bottom, SettingsLayout.bottomContentPadding)
.padding(.vertical, 18)
}
.contentMargins(.bottom, self.bottomScrollMargin, for: .scrollContent)
SettingsBottomOverlayInsetReader(inset: self.$bottomOverlayInset)
.frame(width: 0, height: 0)
.allowsHitTesting(false)
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
}
.navigationBarHidden(true)
.navigationDestination(for: SettingsRoute.self) { route in

View File

@@ -37,8 +37,6 @@ extension SettingsProTab {
NavigationLink(value: SettingsRoute.gateway) {
self.gatewayConnectionRow
.padding(14)
.frame(maxWidth: .infinity, minHeight: SettingsLayout.rowHeight, alignment: .leading)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
Divider()
@@ -203,13 +201,9 @@ extension SettingsProTab {
self.aboutDestination
}
}
.padding(.top, 18)
.padding(.bottom, SettingsLayout.bottomContentPadding)
.padding(.vertical, 18)
}
.contentMargins(.bottom, self.bottomScrollMargin, for: .scrollContent)
SettingsBottomOverlayInsetReader(inset: self.$bottomOverlayInset)
.frame(width: 0, height: 0)
.allowsHitTesting(false)
.safeAreaPadding(.bottom, OpenClawProMetric.bottomScrollInset)
}
.navigationTitle(self.title(for: route))
.navigationBarTitleDisplayMode(.inline)
@@ -245,11 +239,11 @@ extension SettingsProTab {
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
self.manualGatewayCard
self.deviceIdentityCard
self.agentSelectionCard
self.gatewaySetupCard
self.discoveredGatewaysCard
self.manualGatewayCard
self.gatewayAdvancedCard
}
}
@@ -298,18 +292,6 @@ extension SettingsProTab {
value: self.diagnosticsHealthValue,
color: self.gatewayConnected ? OpenClawBrand.ok : OpenClawBrand.warn)
ProCard(radius: SettingsLayout.cardRadius) {
self.gatewayActionButton(
title: "Run Diagnostics",
icon: "cross.case",
color: Color(red: 0 / 255.0, green: 122 / 255.0, blue: 255 / 255.0),
isBusy: self.isRefreshingGateway)
{
Task { await self.runDiagnostics() }
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
self.diagnosticChecksCard
self.detailListCard {
@@ -322,6 +304,18 @@ extension SettingsProTab {
self.detailRow("Model", value: DeviceInfoHelper.modelIdentifier())
}
ProCard(radius: SettingsLayout.cardRadius) {
self.gatewayActionButton(
title: "Run Diagnostics",
icon: "cross.case",
color: Color(red: 0 / 255.0, green: 122 / 255.0, blue: 255 / 255.0),
isBusy: self.isRefreshingGateway)
{
Task { await self.runDiagnostics() }
}
}
.padding(.horizontal, OpenClawProMetric.pagePadding)
self.diagnosticsAdvancedCard
}
}

View File

@@ -1,6 +1,5 @@
import Darwin
import SwiftUI
import UIKit
enum SettingsRoute: Hashable {
case gateway
@@ -15,110 +14,6 @@ enum SettingsRoute: Hashable {
enum SettingsLayout {
static let cardRadius: CGFloat = 12
static let rowHeight: CGFloat = 58
static let bottomContentPadding: CGFloat = 12
}
struct SettingsBottomOverlayInsetReader: UIViewRepresentable {
@Binding var inset: CGFloat
func makeCoordinator() -> Coordinator {
Coordinator(inset: self.$inset)
}
func makeUIView(context: Context) -> SettingsBottomOverlayInsetProbeView {
let view = SettingsBottomOverlayInsetProbeView()
view.onInsetChange = { value in
context.coordinator.updateInset(value)
}
return view
}
func updateUIView(_ uiView: SettingsBottomOverlayInsetProbeView, context: Context) {
context.coordinator.inset = self.$inset
uiView.onInsetChange = { value in
context.coordinator.updateInset(value)
}
uiView.updateInset()
}
final class Coordinator {
var inset: Binding<CGFloat>
init(inset: Binding<CGFloat>) {
self.inset = inset
}
func updateInset(_ value: CGFloat) {
let rounded = max(0, ceil(value))
guard abs(self.inset.wrappedValue - rounded) > 0.5 else { return }
self.inset.wrappedValue = rounded
}
}
}
final class SettingsBottomOverlayInsetProbeView: UIView {
var onInsetChange: ((CGFloat) -> Void)?
override func didMoveToWindow() {
super.didMoveToWindow()
self.updateInset()
}
override func layoutSubviews() {
super.layoutSubviews()
self.updateInset()
}
func updateInset() {
let value = self.visibleTabBarHeight()
DispatchQueue.main.async { [weak self] in
self?.onInsetChange?(value)
}
}
private func visibleTabBarHeight() -> CGFloat {
let tabBarController = self.nearestViewController()?.tabBarController
?? self.findTabBarController(in: self.window?.rootViewController)
guard let tabBar = tabBarController?.tabBar,
!tabBar.isHidden,
tabBar.alpha > 0.01,
tabBar.window != nil,
self.window != nil
else {
return 0
}
let tabFrame = tabBar.convert(tabBar.bounds, to: nil)
guard tabFrame.height.isFinite else { return 0 }
return max(0, tabFrame.height)
}
private func nearestViewController() -> UIViewController? {
var responder: UIResponder? = self
while let current = responder {
if let viewController = current as? UIViewController {
return viewController
}
responder = current.next
}
return nil
}
private func findTabBarController(in viewController: UIViewController?) -> UITabBarController? {
guard let viewController else { return nil }
if let tabBarController = viewController as? UITabBarController {
return tabBarController
}
if let tabBarController = self.findTabBarController(in: viewController.presentedViewController) {
return tabBarController
}
for child in viewController.children {
if let tabBarController = self.findTabBarController(in: child) {
return tabBarController
}
}
return nil
}
}
enum SettingsDiagnosticIssue: String, Equatable, CaseIterable {

View File

@@ -15,7 +15,6 @@ struct TalkProTab: View {
gatewayConnected: self.gatewayConnected,
isEnabled: self.appModel.talkMode.isEnabled || self.talkEnabled,
statusText: self.appModel.talkMode.statusText,
isConfigLoaded: self.appModel.talkMode.gatewayTalkConfigLoaded,
isListening: self.appModel.talkMode.isListening,
isSpeaking: self.appModel.talkMode.isSpeaking,
isUserSpeechDetected: self.appModel.talkMode.isUserSpeechDetected,
@@ -283,9 +282,6 @@ struct TalkProTab: View {
if self.state
.prefersPermissionCopy { return "Gateway approval is required before this phone can capture voice." }
if !self.gatewayConnected { return "Connect to your gateway to start a voice conversation." }
if !self.appModel.talkMode.gatewayTalkConfigLoaded {
return "Open Voice settings after the gateway loads Talk configuration."
}
let subtitle = (self.appModel.talkMode.gatewayTalkVoiceModeSubtitle ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
if !subtitle.isEmpty { return subtitle }
@@ -369,7 +365,6 @@ struct TalkProState: Equatable {
let gatewayConnected: Bool
let isEnabled: Bool
let statusText: String
let isConfigLoaded: Bool
let isListening: Bool
let isSpeaking: Bool
let isUserSpeechDetected: Bool
@@ -395,7 +390,6 @@ struct TalkProState: Equatable {
default:
break
}
if !self.isConfigLoaded { return "Voice config unavailable" }
if self.isSpeaking { return "Speaking" }
if self.isListening { return "Listening" }
if self.normalizedStatus.contains("connecting") { return "Connecting" }
@@ -418,7 +412,6 @@ struct TalkProState: Equatable {
default:
break
}
if !self.isConfigLoaded { return "Config" }
if self.isSpeaking { return "Speaking" }
if self.isListening { return "Listening" }
if self.isEnabled { return "Ready" }
@@ -439,7 +432,6 @@ struct TalkProState: Equatable {
default:
break
}
if !self.isConfigLoaded { return "exclamationmark.triangle.fill" }
if self.isSpeaking { return "speaker.wave.2.fill" }
if self.isListening { return "mic.fill" }
if self.normalizedStatus.contains("thinking") { return "sparkles" }
@@ -455,7 +447,6 @@ struct TalkProState: Equatable {
case .missingScope, .requestingUpgrade, .upgradeRequested, .apiKeyMissing:
return OpenClawBrand.warn
default:
if !self.isConfigLoaded { return OpenClawBrand.warn }
return self.isEnabled ? OpenClawBrand.ok : OpenClawBrand.accentHot
}
}
@@ -527,7 +518,6 @@ struct TalkProState: Equatable {
default:
break
}
if !self.isConfigLoaded { return .still }
if self.isSpeaking { return .speaking }
if self.isListening, self.isUserSpeechDetected { return .inputSpeech }
if self.isListening { return .level(micLevel) }

View File

@@ -702,9 +702,6 @@ final class GatewayConnectionController {
appModel.gatewayStatusText = "Connecting…"
Task { [weak self, weak appModel] in
guard let self, let appModel else { return }
if forceReconnect {
await appModel.resetGatewaySessionsForForcedReconnect()
}
let nodeOptions = await self.makeConnectOptions(stableID: gatewayStableID)
let cfg = GatewayConnectConfig(
url: url,
@@ -993,10 +990,7 @@ extension GatewayConnectionController {
}
private func currentCaps() -> [String] {
var caps = [
OpenClawCapability.canvas.rawValue,
OpenClawCapability.screen.rawValue,
]
var caps = [OpenClawCapability.canvas.rawValue, OpenClawCapability.screen.rawValue]
// Default-on: if the key doesn't exist yet, treat it as enabled.
let cameraEnabled =

View File

@@ -1,35 +1,106 @@
import Foundation
import Network
import OpenClawKit
enum A2UIReadyState {
case ready
case ready(String)
case hostNotConfigured
case hostUnavailable
}
extension NodeAppModel {
func resolveCanvasHostURL() async -> String? {
guard let raw = await self.gatewaySession.currentCanvasHostUrl() else { return nil }
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil }
if let host = base.host, LoopbackHost.isLoopback(host) {
return nil
}
return base.appendingPathComponent("__openclaw__/canvas/").absoluteString
}
func _test_resolveA2UIHostURL() async -> String? {
await self.resolveA2UIHostURL()
}
func resolveA2UIHostURL() async -> String? {
guard let raw = await self.gatewaySession.currentCanvasHostUrl() else { return nil }
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty, let base = URL(string: trimmed) else { return nil }
if let host = base.host, LoopbackHost.isLoopback(host) {
return nil
}
return base.appendingPathComponent("__openclaw__/a2ui/").absoluteString + "?platform=ios"
}
/// Normalize a URL string for trust comparison: lowercase scheme/host and strip fragment.
/// This matches the normalization applied by ScreenController.isTrustedCanvasUIURL so that
/// SPA hash-routing fragments and scheme/host casing do not silently prevent trust being set.
static func normalizeURLForTrustComparison(_ raw: String) -> String {
guard let url = URL(string: raw),
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
else { return raw }
components.fragment = nil
components.scheme = components.scheme?.lowercased()
components.host = components.host?.lowercased()
return components.url?.absoluteString ?? raw
}
func showA2UIOnConnectIfNeeded() async {
await MainActor.run {
// Keep the bundled home canvas as the default connected view.
// Agents can still explicitly present a remote or local canvas later.
self.lastAutoA2uiURL = nil
self.screen.showDefaultCanvas()
}
}
func ensureA2UIReadyWithCapabilityRefresh(timeoutMs: Int = 5000) async -> A2UIReadyState {
if self.screen.isShowingLocalA2UI(),
await self.screen.waitForA2UIReady(timeoutMs: timeoutMs)
{
return .ready
guard let initialUrl = await self.resolveA2UIHostURLWithCapabilityRefresh() else {
return .hostNotConfigured
}
self.screen.showLocalA2UI()
self.screen.navigate(to: initialUrl, trustA2UIActions: true)
if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) {
return .ready
return .ready(initialUrl)
}
guard let refreshedUrl = await self.resolveA2UIHostURLWithCapabilityRefresh(forceRefresh: true) else {
return .hostUnavailable
}
self.screen.navigate(to: refreshedUrl, trustA2UIActions: true)
if await self.screen.waitForA2UIReady(timeoutMs: timeoutMs) {
return .ready(refreshedUrl)
}
return .hostUnavailable
}
func showLocalCanvasOnDisconnect() {
self.lastAutoA2uiURL = nil
self.screen.showDefaultCanvas()
}
private func resolveA2UIHostURLWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? {
if !forceRefresh, let current = await self.resolveA2UIHostURL() {
return current
}
_ = await self.gatewaySession.refreshCanvasHostUrl()
return await self.resolveA2UIHostURL()
}
private func resolveCanvasHostURLWithCapabilityRefresh(forceRefresh: Bool = false) async -> String? {
if !forceRefresh, let current = await self.resolveCanvasHostURL() {
return current
}
_ = await self.gatewaySession.refreshCanvasHostUrl()
return await self.resolveCanvasHostURL()
}
private static func probeTCP(url: URL, timeoutSeconds: Double) async -> Bool {
guard let host = url.host, !host.isEmpty else { return false }
let portInt = url.port ?? ((url.scheme ?? "").lowercased() == "wss" ? 443 : 80)
return await TCPProbe.probe(
host: host,
port: portInt,
timeoutSeconds: timeoutSeconds,
queueLabel: "a2ui.preflight")
}
}

View File

@@ -173,6 +173,7 @@ final class NodeAppModel {
private let remindersService: any RemindersServicing
private let motionService: any MotionServicing
private let watchMessagingService: any WatchMessagingServicing
var lastAutoA2uiURL: String?
private var pttVoiceWakeSuspended = false
private var talkVoiceWakeSuspended = false
private var backgroundVoiceWakeSuspended = false
@@ -1034,18 +1035,24 @@ final class NodeAppModel {
OpenClawCanvasPresentParams()
let url = params.url?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
if url.isEmpty {
self.screen.presentDefaultCanvas()
self.screen.showDefaultCanvas()
} else {
self.screen.present(urlString: url)
let trustedA2UIURL = await self.resolveA2UIHostURL()
self.screen.navigate(
to: url,
trustA2UIActions: trustedA2UIURL == Self.normalizeURLForTrustComparison(url))
}
return BridgeInvokeResponse(id: req.id, ok: true)
case OpenClawCanvasCommand.hide.rawValue:
self.screen.hideCanvas()
self.screen.showDefaultCanvas()
return BridgeInvokeResponse(id: req.id, ok: true)
case OpenClawCanvasCommand.navigate.rawValue:
let params = try Self.decodeParams(OpenClawCanvasNavigateParams.self, from: req.paramsJSON)
let trimmedURL = params.url.trimmingCharacters(in: .whitespacesAndNewlines)
self.screen.present(urlString: trimmedURL)
let trustedA2UIURL = await self.resolveA2UIHostURL()
self.screen.navigate(
to: trimmedURL,
trustA2UIActions: trustedA2UIURL == Self.normalizeURLForTrustComparison(trimmedURL))
return BridgeInvokeResponse(id: req.id, ok: true)
case OpenClawCanvasCommand.evalJS.rawValue:
let params = try Self.decodeParams(OpenClawCanvasEvalParams.self, from: req.paramsJSON)
@@ -1088,13 +1095,20 @@ final class NodeAppModel {
switch await self.ensureA2UIReadyWithCapabilityRefresh(timeoutMs: 5000) {
case .ready:
break
case .hostNotConfigured:
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: OpenClawNodeError(
code: .unavailable,
message: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host"))
case .hostUnavailable:
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: OpenClawNodeError(
code: .unavailable,
message: "A2UI_HOST_UNAVAILABLE: bundled A2UI host not reachable"))
message: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable"))
}
let json = try await self.screen.eval(javaScript: """
(() => {
@@ -1124,13 +1138,20 @@ final class NodeAppModel {
switch await self.ensureA2UIReadyWithCapabilityRefresh(timeoutMs: 5000) {
case .ready:
break
case .hostNotConfigured:
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: OpenClawNodeError(
code: .unavailable,
message: "A2UI_HOST_NOT_CONFIGURED: gateway did not advertise canvas host"))
case .hostUnavailable:
return BridgeInvokeResponse(
id: req.id,
ok: false,
error: OpenClawNodeError(
code: .unavailable,
message: "A2UI_HOST_UNAVAILABLE: bundled A2UI host not reachable"))
message: "A2UI_HOST_UNAVAILABLE: A2UI host not reachable"))
}
let messagesJSON = try OpenClawCanvasA2UIJSONL.encodeMessagesJSONArray(messages)
@@ -1939,15 +1960,6 @@ extension NodeAppModel {
forceReconnect: forceReconnect)
}
func resetGatewaySessionsForForcedReconnect() async {
self.nodeGatewayTask?.cancel()
self.nodeGatewayTask = nil
self.operatorGatewayTask?.cancel()
self.operatorGatewayTask = nil
await self.operatorGateway.disconnect()
await self.nodeGateway.disconnect()
}
func disconnectGateway() {
self.gatewayAutoReconnectEnabled = false
self.gatewayPairingPaused = false
@@ -4566,10 +4578,6 @@ extension NodeAppModel {
self.clearingBootstrapToken(in: config)
}
func _test_hasGatewayLoopTasks() -> (node: Bool, operator: Bool) {
(self.nodeGatewayTask != nil, self.operatorGatewayTask != nil)
}
func _test_handleSuccessfulBootstrapGatewayOnboarding() async {
await self.handleSuccessfulBootstrapGatewayOnboarding(
url: URL(string: "wss://gateway.example")!,

View File

@@ -200,36 +200,6 @@ struct RootTabs: View {
RootCameraFlashOverlay(nonce: self.appModel.cameraFlashNonce)
}
}
.overlay {
if self.appModel.screen.isCanvasPresented {
self.canvasPresentationOverlay
.transition(.opacity)
.zIndex(20)
}
}
}
private var canvasPresentationOverlay: some View {
ZStack(alignment: .topTrailing) {
Color.black.ignoresSafeArea()
ScreenWebView(controller: self.appModel.screen)
.ignoresSafeArea()
Button {
self.appModel.screen.hideCanvas()
} label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 30, weight: .semibold))
.symbolRenderingMode(.hierarchical)
.foregroundStyle(.white)
.shadow(color: .black.opacity(0.32), radius: 8, y: 2)
.frame(width: 48, height: 48)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
.accessibilityLabel("Close canvas")
.safeAreaPadding(.top, 8)
.padding(.trailing, 12)
}
}
private func rootLifecycle(_ content: some View) -> some View {

View File

@@ -7,10 +7,10 @@ import WebKit
@Observable
final class ScreenController {
private weak var activeWebView: WKWebView?
private var trustedRemoteA2UIURL: URL?
var urlString: String = ""
var errorText: String?
var isCanvasPresented: Bool = false
/// Callback invoked when an openclaw:// deep link is tapped in the canvas
var onDeepLink: ((URL) -> Void)?
@@ -27,10 +27,11 @@ final class ScreenController {
self.reload()
}
func navigate(to urlString: String, trustA2UIActions _: Bool = false) {
func navigate(to urlString: String, trustA2UIActions: Bool = false) {
let trimmed = urlString.trimmingCharacters(in: .whitespacesAndNewlines)
if trimmed.isEmpty {
self.urlString = ""
self.trustedRemoteA2UIURL = nil
self.reload()
return
}
@@ -44,6 +45,7 @@ final class ScreenController {
return
}
self.urlString = (trimmed == "/" ? "" : trimmed)
self.trustedRemoteA2UIURL = trustA2UIActions ? Self.normalizeTrustedRemoteA2UIURL(from: trimmed) : nil
self.reload()
}
@@ -73,42 +75,10 @@ final class ScreenController {
func showDefaultCanvas() {
self.urlString = ""
self.trustedRemoteA2UIURL = nil
self.reload()
}
func presentDefaultCanvas() {
self.isCanvasPresented = true
self.showDefaultCanvas()
}
func present(urlString: String) {
self.isCanvasPresented = true
self.navigate(to: urlString)
}
func hideCanvas() {
self.isCanvasPresented = false
self.showDefaultCanvas()
}
func showLocalA2UI() {
self.isCanvasPresented = true
guard let url = Self.localA2UIURL else {
self.showDefaultCanvas()
return
}
self.urlString = url.absoluteString
self.reload()
}
func isShowingLocalA2UI() -> Bool {
guard let url = URL(string: self.urlString),
url.isFileURL,
let expected = Self.localA2UIURL
else { return false }
return url.standardizedFileURL == expected.standardizedFileURL
}
func setDebugStatusEnabled(_ enabled: Bool) {
self.debugStatusEnabled = enabled
self.applyDebugStatusIfNeeded()
@@ -269,11 +239,6 @@ final class ScreenController {
ext: "html",
subdirectory: "CanvasScaffold")
private static let localA2UIURL: URL? = ScreenController.bundledResourceURL(
name: "index",
ext: "html",
subdirectory: "CanvasA2UI")
func isTrustedCanvasUIURL(_ url: URL) -> Bool {
if url.isFileURL {
let std = url.standardizedFileURL
@@ -282,14 +247,10 @@ final class ScreenController {
{
return true
}
if let expected = Self.localA2UIURL,
std == expected.standardizedFileURL
{
return true
}
return false
}
return false
guard let trusted = self.trustedRemoteA2UIURL else { return false }
return Self.normalizeTrustedRemoteA2UIURL(from: url) == trusted
}
nonisolated static func parseA2UIActionBody(_ body: Any) -> [String: Any]? {
@@ -319,6 +280,26 @@ final class ScreenController {
scrollView.isScrollEnabled = allowScroll
scrollView.bounces = allowScroll
}
private static func normalizeTrustedRemoteA2UIURL(from raw: String) -> URL? {
guard let url = URL(string: raw) else { return nil }
return self.normalizeTrustedRemoteA2UIURL(from: url)
}
private static func normalizeTrustedRemoteA2UIURL(from url: URL) -> URL? {
guard !url.isFileURL else { return nil }
guard let scheme = url.scheme?.lowercased(), scheme == "http" || scheme == "https" else {
return nil
}
guard let host = url.host?.trimmingCharacters(in: .whitespacesAndNewlines), !host.isEmpty else {
return nil
}
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
components?.scheme = scheme
components?.host = host.lowercased()
components?.fragment = nil
return components?.url
}
}
extension Double {

View File

@@ -235,20 +235,6 @@ import UIKit
#expect(appModel.connectedGatewayID == second.stableID)
}
@Test @MainActor func forcedReconnectResetClearsActiveGatewayLoopTasks() async {
let appModel = NodeAppModel()
defer { appModel.disconnectGateway() }
appModel.applyGatewayConnectConfig(Self.makeGatewayConnectConfig())
#expect(appModel._test_hasGatewayLoopTasks().node)
#expect(appModel._test_hasGatewayLoopTasks().operator)
await appModel.resetGatewaySessionsForForcedReconnect()
#expect(!appModel._test_hasGatewayLoopTasks().node)
#expect(!appModel._test_hasGatewayLoopTasks().operator)
}
@Test @MainActor func loadLastConnectionReadsSavedValues() {
let prior = KeychainStore.loadString(service: "ai.openclaw.gateway", account: "lastConnection")
defer {

View File

@@ -623,13 +623,13 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
#expect(appModel.screen.urlString.isEmpty)
}
@Test @MainActor func handleInvokeA2UICommandsFailWhenLocalHostUnavailable() async throws {
@Test @MainActor func handleInvokeA2UICommandsFailWhenHostMissing() async throws {
let appModel = NodeAppModel()
let reset = BridgeInvokeRequest(id: "reset", command: OpenClawCanvasA2UICommand.reset.rawValue)
let resetRes = await appModel._test_handleInvoke(reset)
#expect(resetRes.ok == false)
#expect(resetRes.error?.message.contains("A2UI_HOST_UNAVAILABLE") == true)
#expect(resetRes.error?.message.contains("A2UI_HOST_NOT_CONFIGURED") == true)
let jsonl = "{\"beginRendering\":{}}"
let pushParams = OpenClawCanvasA2UIPushJSONLParams(jsonl: jsonl)
@@ -641,7 +641,7 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc
paramsJSON: pushJSON)
let pushRes = await appModel._test_handleInvoke(push)
#expect(pushRes.ok == false)
#expect(pushRes.error?.message.contains("A2UI_HOST_UNAVAILABLE") == true)
#expect(pushRes.error?.message.contains("A2UI_HOST_NOT_CONFIGURED") == true)
}
@Test @MainActor func handleInvokeUnknownCommandReturnsInvalidRequest() async {

View File

@@ -45,23 +45,6 @@ private func mountScreen(_ screen: ScreenController) throws -> (ScreenWebViewCoo
#expect(screen.urlString.isEmpty)
}
@Test @MainActor func canvasPresentationTracksExplicitPresentAndHide() {
let screen = ScreenController()
#expect(screen.isCanvasPresented == false)
screen.showDefaultCanvas()
#expect(screen.isCanvasPresented == false)
screen.presentDefaultCanvas()
#expect(screen.isCanvasPresented == true)
#expect(screen.urlString.isEmpty)
screen.hideCanvas()
#expect(screen.isCanvasPresented == false)
#expect(screen.urlString.isEmpty)
}
@Test @MainActor func evalExecutesJavaScript() async throws {
let screen = ScreenController()
let (coordinator, _) = try mountScreen(screen)
@@ -83,37 +66,26 @@ private func mountScreen(_ screen: ScreenController) throws -> (ScreenWebViewCoo
}
}
@Test("remote A2UI URL is not trusted for native actions")
@MainActor func remoteA2UIURLIsNotTrustedForNativeActions() throws {
@Test @MainActor func trustedRemoteA2UIURLMustMatchExactly() {
let screen = ScreenController()
let trusted = "https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios"
screen.navigate(to: trusted, trustA2UIActions: true)
#expect(screen.isShowingLocalA2UI() == false)
let urls = try [
trusted,
"https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios#step2",
"http://192.168.0.10:18789/__openclaw__/a2ui/?platform=ios",
"https://node.ts.net:18789/__openclaw__/a2ui/?platform=android",
"https://node.ts.net:18789/__openclaw__/canvas/",
"https://evil.ts.net:18789/__openclaw__/a2ui/?platform=ios",
].map { try #require(URL(string: $0)) }
for url in urls {
#expect(screen.isTrustedCanvasUIURL(url) == false)
}
#expect(screen.isTrustedCanvasUIURL(URL(string: trusted)!) == true)
// Fragment differences must not affect trust (SPA hash routing).
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios#step2")!) == true)
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://node.ts.net:18789/__openclaw__/a2ui/?platform=android")!) == false)
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://node.ts.net:18789/__openclaw__/canvas/")!) == false)
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://evil.ts.net:18789/__openclaw__/a2ui/?platform=ios")!) == false)
#expect(screen.isTrustedCanvasUIURL(URL(string: "http://192.168.0.10:18789/")!) == false)
}
@Test("local A2UI URL is trusted for native actions")
@MainActor func localA2UIURLIsTrustedForNativeActions() throws {
@Test @MainActor func genericNavigationClearsTrustedRemoteA2UIURL() {
let screen = ScreenController()
screen.showLocalA2UI()
screen.navigate(to: "https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios", trustA2UIActions: true)
screen.navigate(to: "https://evil.ts.net:18789/")
let url = try #require(URL(string: screen.urlString))
#expect(url.isFileURL)
#expect(screen.isShowingLocalA2UI() == true)
#expect(screen.isTrustedCanvasUIURL(url) == true)
#expect(screen.isTrustedCanvasUIURL(URL(string: "https://node.ts.net:18789/__openclaw__/a2ui/?platform=ios")!) == false)
}
@Test func parseA2UIActionBodyAcceptsJSONString() throws {

View File

@@ -1,73 +0,0 @@
import Testing
@testable import OpenClaw
@Suite struct TalkProStateTests {
@Test func disabledTalkWithoutLoadedConfigCanStartAndRetryLoad() {
let state = TalkProState(
gatewayConnected: true,
isEnabled: false,
statusText: "Offline",
isConfigLoaded: false,
isListening: false,
isSpeaking: false,
isUserSpeechDetected: false,
permissionState: .unknown)
#expect(state.title == "Voice config unavailable")
#expect(state.chipText == "Config")
#expect(state.primaryAction == .start)
#expect(state.primaryButtonTitle == "Start Talk")
#expect(state.waveformMode(micLevel: 0.8) == .still)
}
@Test func enabledTalkWithoutLoadedConfigCanBeStopped() {
let state = TalkProState(
gatewayConnected: true,
isEnabled: true,
statusText: "Offline",
isConfigLoaded: false,
isListening: false,
isSpeaking: false,
isUserSpeechDetected: false,
permissionState: .unknown)
#expect(state.title == "Voice config unavailable")
#expect(state.chipText == "Config")
#expect(state.primaryAction == .stop)
#expect(state.primaryButtonTitle == "Stop Talk")
#expect(state.waveformMode(micLevel: 0.8) == .still)
}
@Test func enabledTalkWithLoadedConfigCanBeStopped() {
let state = TalkProState(
gatewayConnected: true,
isEnabled: true,
statusText: "Ready",
isConfigLoaded: true,
isListening: false,
isSpeaking: false,
isUserSpeechDetected: false,
permissionState: .ready)
#expect(state.title == "Ready to talk")
#expect(state.chipText == "Ready")
#expect(state.primaryAction == .stop)
}
@Test func missingScopeTakesPriorityOverUnloadedConfig() {
let state = TalkProState(
gatewayConnected: true,
isEnabled: false,
statusText: "Offline",
isConfigLoaded: false,
isListening: false,
isSpeaking: false,
isUserSpeechDetected: false,
permissionState: .missingScope("operator.talk.secrets"))
#expect(state.title == "Gateway permission required")
#expect(state.chipText == "Needs approval")
#expect(state.primaryAction == .enablePermission)
#expect(state.primaryButtonTitle == "Enable Talk")
}
}

View File

@@ -1 +1,5 @@
Maintenance update for the current OpenClaw release.
- Added hosted push relay defaults, realtime Talk playback, and safer WebSocket ping handling for mobile sessions.
- Updated App Store screenshots to cover Gateway pairing, Command, Chat, Talk, Agent, and Settings flows.
- Highlighted realtime Talk relay, Gateway connection status, node capabilities, push wake, and privacy controls.

View File

@@ -1,3 +1,3 @@
{
"version": "2026.6.2"
"version": "2026.6.1"
}

View File

@@ -514,16 +514,12 @@ extension GatewayConnection {
var params: [String: AnyCodable] = [
"message": AnyCodable(trimmed),
"sessionKey": AnyCodable(sessionKey),
"thinking": AnyCodable(invocation.thinking ?? "default"),
"deliver": AnyCodable(invocation.deliver),
"to": AnyCodable(invocation.to ?? ""),
"channel": AnyCodable(invocation.channel.rawValue),
"idempotencyKey": AnyCodable(invocation.idempotencyKey),
]
if let thinking = invocation.thinking?.trimmingCharacters(in: .whitespacesAndNewlines),
!thinking.isEmpty
{
params["thinking"] = AnyCodable(thinking)
}
if let timeout = invocation.timeoutSeconds {
params["timeout"] = AnyCodable(timeout)
}
@@ -668,7 +664,7 @@ extension GatewayConnection {
func chatSend(
sessionKey: String,
message: String,
thinking: String?,
thinking: String,
idempotencyKey: String,
attachments: [OpenClawChatAttachmentPayload],
timeoutMs: Int = 30000) async throws -> OpenClawChatSendResponse
@@ -677,14 +673,10 @@ extension GatewayConnection {
var params: [String: AnyCodable] = [
"sessionKey": AnyCodable(resolvedKey),
"message": AnyCodable(message),
"thinking": AnyCodable(thinking),
"idempotencyKey": AnyCodable(idempotencyKey),
"timeoutMs": AnyCodable(timeoutMs),
]
if let thinking = thinking?.trimmingCharacters(in: .whitespacesAndNewlines),
!thinking.isEmpty
{
params["thinking"] = AnyCodable(thinking)
}
if !attachments.isEmpty {
let encoded = attachments.map { att in

View File

@@ -139,10 +139,7 @@ final class MacNodeModeCoordinator {
locationMode: OpenClawLocationMode,
connectionMode: AppState.ConnectionMode) -> [String]
{
var caps: [String] = [
OpenClawCapability.canvas.rawValue,
OpenClawCapability.screen.rawValue,
]
var caps: [String] = [OpenClawCapability.canvas.rawValue, OpenClawCapability.screen.rawValue]
if browserControlEnabled, connectionMode == .local {
caps.append(OpenClawCapability.browser.rawValue)
}

View File

@@ -15,9 +15,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>2026.6.2</string>
<string>2026.6.1</string>
<key>CFBundleVersion</key>
<string>2026060200</string>
<string>2026053100</string>
<key>CFBundleIconFile</key>
<string>OpenClaw</string>
<key>CFBundleURLTypes</key>

View File

@@ -387,7 +387,7 @@ actor TalkModeRuntime {
let response = try await GatewayConnection.shared.chatSend(
sessionKey: sessionKey,
message: prompt,
thinking: nil,
thinking: "low",
idempotencyKey: runId,
attachments: [])
guard self.isCurrent(gen) else { return }

View File

@@ -34,7 +34,7 @@ enum VoiceWakeForwarder {
struct ForwardOptions {
var sessionKey: String = "main"
var thinking: String?
var thinking: String = "low"
var deliver: Bool = true
var to: String?
var channel: GatewayAgentChannel = .webchat
@@ -97,6 +97,7 @@ enum VoiceWakeForwarder {
return ForwardOptions(
sessionKey: sessionKey,
thinking: "low",
deliver: true,
to: to,
channel: channel,

View File

@@ -173,57 +173,9 @@ private func makeTestGatewayConnection() -> (GatewayConnection, FakeWebSocketSes
let json = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any]
let params = json?["params"] as? [String: Any]
#expect(params?["thinking"] == nil)
#expect(params?["voiceWakeTrigger"] as? String == "")
}
@Test func `chat send omits thinking when inheriting session default`() async throws {
let recorder = WebSocketMessageRecorder()
let session = GatewayTestWebSocketSession(taskFactory: {
GatewayTestWebSocketTask(sendHook: { task, message, sendIndex in
recorder.append(message)
guard sendIndex > 0,
let data = Self.messageData(message),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let id = json["id"] as? String
else { return }
task.emitReceiveSuccess(.data(Self.chatSendOkResponseData(id: id)))
})
})
let connection = GatewayConnection(
configProvider: {
(url: URL(string: "ws://127.0.0.1:1")!, token: nil, password: nil)
},
sessionBox: WebSocketSessionBox(session: session))
_ = try await connection.chatSend(
sessionKey: "main",
message: "hello",
thinking: nil,
idempotencyKey: "chat-1",
attachments: [])
await connection.shutdown()
guard let chatMessage = recorder.snapshot().reversed().first(where: { message in
guard let data = Self.messageData(message),
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any]
else { return false }
return json["method"] as? String == "chat.send"
}) else {
Issue.record("expected chat.send websocket payload")
return
}
guard let payloadData = Self.messageData(chatMessage) else {
Issue.record("unexpected chat.send websocket message type")
return
}
let json = try JSONSerialization.jsonObject(with: payloadData) as? [String: Any]
let params = json?["params"] as? [String: Any]
#expect(params?["thinking"] == nil)
}
private static func messageData(_ message: URLSessionWebSocketTask.Message) -> Data? {
switch message {
case let .string(text):
@@ -234,15 +186,4 @@ private func makeTestGatewayConnection() -> (GatewayConnection, FakeWebSocketSes
nil
}
}
private static func chatSendOkResponseData(id: String) -> Data {
Data("""
{
"type": "res",
"id": "\(id)",
"ok": true,
"payload": { "runId": "chat-1", "status": "ok" }
}
""".utf8)
}
}

View File

@@ -14,7 +14,7 @@ import Testing
@Test func `forward options defaults`() {
let opts = VoiceWakeForwarder.ForwardOptions()
#expect(opts.sessionKey == "main")
#expect(opts.thinking == nil)
#expect(opts.thinking == "low")
#expect(opts.deliver == true)
#expect(opts.to == nil)
#expect(opts.channel == .webchat)
@@ -38,7 +38,6 @@ import Testing
#expect(opts.channel == .telegram)
#expect(opts.to == "telegram:6812765697")
#expect(opts.voiceWakeTrigger == "open claw")
#expect(opts.thinking == nil)
#expect(opts.channel.shouldDeliver(opts.deliver) == true)
}

View File

@@ -201,7 +201,6 @@ struct OpenClawChatComposer: View {
Image(systemName: "paperclip")
}
.help("Add Image")
.accessibilityLabel("Attachments")
.buttonStyle(.plain)
.controlSize(.small)
} else {
@@ -211,7 +210,6 @@ struct OpenClawChatComposer: View {
Image(systemName: "paperclip")
}
.help("Add Image")
.accessibilityLabel("Attachments")
.buttonStyle(.bordered)
.controlSize(.small)
}
@@ -221,7 +219,6 @@ struct OpenClawChatComposer: View {
Image(systemName: "paperclip")
}
.help("Add Image")
.accessibilityLabel("Attachments")
.buttonStyle(.plain)
.controlSize(.small)
.onChange(of: self.pickerItems) { _, newItems in
@@ -232,7 +229,6 @@ struct OpenClawChatComposer: View {
Image(systemName: "paperclip")
}
.help("Add Image")
.accessibilityLabel("Attachments")
.buttonStyle(.bordered)
.controlSize(.small)
.onChange(of: self.pickerItems) { _, newItems in

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 258 KiB

View File

@@ -1,311 +0,0 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>OpenClaw Canvas</title>
<script>
(() => {
const normalizeLower = (value) => {
const trimmed = String(value || "").trim();
return trimmed.toLocaleLowerCase();
};
try {
const params = new URLSearchParams(window.location.search);
const platform = normalizeLower(params.get("platform"));
if (platform) {
document.documentElement.dataset.platform = platform;
return;
}
if (/android/i.test(navigator.userAgent || "")) {
document.documentElement.dataset.platform = "android";
}
} catch (_) {}
})();
</script>
<style>
:root {
color-scheme: dark;
}
@media (prefers-reduced-motion: reduce) {
body::before,
body::after {
animation: none !important;
}
}
html,
body {
height: 100%;
margin: 0;
}
body {
font:
14px system-ui,
-apple-system,
BlinkMacSystemFont,
"Roboto",
sans-serif;
background:
radial-gradient(1200px 900px at 15% 20%, rgba(42, 113, 255, 0.18), rgba(0, 0, 0, 0) 55%),
radial-gradient(900px 700px at 85% 30%, rgba(255, 0, 138, 0.14), rgba(0, 0, 0, 0) 60%),
radial-gradient(1000px 900px at 60% 90%, rgba(0, 209, 255, 0.1), rgba(0, 0, 0, 0) 60%),
#000;
color: #e5e7eb;
overflow: hidden;
}
:root[data-platform="android"] body {
background:
radial-gradient(1200px 900px at 15% 20%, rgba(42, 113, 255, 0.62), rgba(0, 0, 0, 0) 55%),
radial-gradient(900px 700px at 85% 30%, rgba(255, 0, 138, 0.52), rgba(0, 0, 0, 0) 60%),
radial-gradient(1000px 900px at 60% 90%, rgba(0, 209, 255, 0.48), rgba(0, 0, 0, 0) 60%),
#0b1328;
}
body::before {
content: "";
position: fixed;
inset: -20%;
background:
repeating-linear-gradient(
0deg,
rgba(255, 255, 255, 0.03) 0,
rgba(255, 255, 255, 0.03) 1px,
transparent 1px,
transparent 48px
),
repeating-linear-gradient(
90deg,
rgba(255, 255, 255, 0.03) 0,
rgba(255, 255, 255, 0.03) 1px,
transparent 1px,
transparent 48px
);
transform: translate3d(0, 0, 0) rotate(-7deg);
will-change: transform, opacity;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
opacity: 0.45;
pointer-events: none;
animation: openclaw-grid-drift 140s ease-in-out infinite alternate;
}
:root[data-platform="android"] body::before {
opacity: 0.8;
}
body::after {
content: "";
position: fixed;
inset: -35%;
background:
radial-gradient(900px 700px at 30% 30%, rgba(42, 113, 255, 0.16), rgba(0, 0, 0, 0) 60%),
radial-gradient(800px 650px at 70% 35%, rgba(255, 0, 138, 0.12), rgba(0, 0, 0, 0) 62%),
radial-gradient(900px 800px at 55% 75%, rgba(0, 209, 255, 0.1), rgba(0, 0, 0, 0) 62%);
filter: blur(28px);
opacity: 0.52;
will-change: transform, opacity;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
transform: translate3d(0, 0, 0);
pointer-events: none;
animation: openclaw-glow-drift 110s ease-in-out infinite alternate;
}
:root[data-platform="android"] body::after {
opacity: 0.85;
}
@supports (mix-blend-mode: screen) {
body::after {
mix-blend-mode: screen;
}
}
@supports not (mix-blend-mode: screen) {
body::after {
opacity: 0.7;
}
}
@keyframes openclaw-grid-drift {
0% {
transform: translate3d(-12px, 8px, 0) rotate(-7deg);
opacity: 0.4;
}
50% {
transform: translate3d(10px, -7px, 0) rotate(-6.6deg);
opacity: 0.56;
}
100% {
transform: translate3d(-8px, 6px, 0) rotate(-7.2deg);
opacity: 0.42;
}
}
@keyframes openclaw-glow-drift {
0% {
transform: translate3d(-18px, 12px, 0) scale(1.02);
opacity: 0.4;
}
50% {
transform: translate3d(14px, -10px, 0) scale(1.05);
opacity: 0.52;
}
100% {
transform: translate3d(-10px, 8px, 0) scale(1.03);
opacity: 0.43;
}
}
canvas {
position: fixed;
inset: 0;
display: block;
width: 100vw;
height: 100vh;
touch-action: none;
z-index: 1;
}
:root[data-platform="android"] #openclaw-canvas {
background:
radial-gradient(1100px 800px at 20% 15%, rgba(42, 113, 255, 0.78), rgba(0, 0, 0, 0) 58%),
radial-gradient(900px 650px at 82% 28%, rgba(255, 0, 138, 0.66), rgba(0, 0, 0, 0) 62%),
radial-gradient(1000px 900px at 60% 88%, rgba(0, 209, 255, 0.58), rgba(0, 0, 0, 0) 62%),
#141c33;
}
#openclaw-status {
position: fixed;
inset: 0;
display: none;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 24px;
box-sizing: border-box;
pointer-events: none;
z-index: 3;
}
#openclaw-status .card {
width: min(560px, 88vw);
text-align: left;
padding: 14px 16px 12px;
border-radius: 16px;
background: linear-gradient(140deg, rgba(23, 24, 35, 0.78), rgba(18, 19, 28, 0.55));
border: 1px solid rgba(255, 255, 255, 0.12);
box-shadow:
0 16px 46px rgba(0, 0, 0, 0.52),
inset 0 1px 0 rgba(255, 255, 255, 0.06);
-webkit-backdrop-filter: blur(18px) saturate(140%);
backdrop-filter: blur(18px) saturate(140%);
}
#openclaw-status .title {
font:
600 12px/1.2 -apple-system,
BlinkMacSystemFont,
"SF Pro Text",
system-ui,
sans-serif;
letter-spacing: 0.45px;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.7);
}
#openclaw-status .subtitle {
margin-top: 8px;
font:
500 13px/1.45 -apple-system,
BlinkMacSystemFont,
"SF Pro Text",
system-ui,
sans-serif;
color: rgba(255, 255, 255, 0.9);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
openclaw-a2ui-host {
display: block;
height: 100%;
position: fixed;
inset: 0;
z-index: 4;
--openclaw-a2ui-inset-top: 28px;
--openclaw-a2ui-inset-right: 0px;
--openclaw-a2ui-inset-bottom: 0px;
--openclaw-a2ui-inset-left: 0px;
--openclaw-a2ui-scroll-pad-bottom: 0px;
--openclaw-a2ui-status-top: calc(50% - 18px);
--openclaw-a2ui-empty-top: 18px;
}
</style>
</head>
<body>
<canvas id="openclaw-canvas"></canvas>
<div id="openclaw-status" role="status" aria-live="polite">
<section class="card">
<div class="title" id="openclaw-status-title">Ready</div>
<div class="subtitle" id="openclaw-status-subtitle">Waiting for agent</div>
</section>
</div>
<openclaw-a2ui-host></openclaw-a2ui-host>
<script src="a2ui.bundle.js"></script>
<script>
(() => {
const canvas = document.getElementById("openclaw-canvas");
const ctx = canvas.getContext("2d");
const statusEl = document.getElementById("openclaw-status");
const titleEl = document.getElementById("openclaw-status-title");
const subtitleEl = document.getElementById("openclaw-status-subtitle");
const debugStatusEnabledByQuery = (() => {
try {
const params = new URLSearchParams(window.location.search);
const raw = params.get("debugStatus") ?? params.get("debug");
if (!raw) return false;
const normalized = normalizeLower(raw);
return normalized === "1" || normalized === "true" || normalized === "yes";
} catch (_) {
return false;
}
})();
let debugStatusEnabled = debugStatusEnabledByQuery;
function resize() {
const dpr = window.devicePixelRatio || 1;
const w = Math.max(1, Math.floor(window.innerWidth * dpr));
const h = Math.max(1, Math.floor(window.innerHeight * dpr));
canvas.width = w;
canvas.height = h;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener("resize", resize);
resize();
const setDebugStatusEnabled = (enabled) => {
debugStatusEnabled = !!enabled;
if (!statusEl) return;
if (!debugStatusEnabled) {
statusEl.style.display = "none";
}
};
if (statusEl && !debugStatusEnabled) {
statusEl.style.display = "none";
}
window.__openclaw = {
canvas,
ctx,
setDebugStatusEnabled,
setStatus: (title, subtitle) => {
if (!statusEl || !debugStatusEnabled) return;
if (!title && !subtitle) {
statusEl.style.display = "none";
return;
}
statusEl.style.display = "flex";
if (titleEl && typeof title === "string") titleEl.textContent = title;
if (subtitleEl && typeof subtitle === "string") subtitleEl.textContent = subtitle;
if (!debugStatusEnabled) {
clearTimeout(window.__statusTimeout);
window.__statusTimeout = setTimeout(() => {
statusEl.style.display = "none";
}, 3000);
} else {
clearTimeout(window.__statusTimeout);
}
},
};
})();
</script>
</body>
</html>

View File

@@ -4677,7 +4677,6 @@ public struct ModelChoice: Codable, Sendable {
public let name: String
public let provider: String
public let alias: String?
public let available: Bool?
public let contextwindow: Int?
public let reasoning: Bool?
@@ -4686,7 +4685,6 @@ public struct ModelChoice: Codable, Sendable {
name: String,
provider: String,
alias: String?,
available: Bool? = nil,
contextwindow: Int?,
reasoning: Bool?)
{
@@ -4694,7 +4692,6 @@ public struct ModelChoice: Codable, Sendable {
self.name = name
self.provider = provider
self.alias = alias
self.available = available
self.contextwindow = contextwindow
self.reasoning = reasoning
}
@@ -4704,7 +4701,6 @@ public struct ModelChoice: Codable, Sendable {
case name
case provider
case alias
case available
case contextwindow = "contextWindow"
case reasoning
}
@@ -5482,62 +5478,6 @@ public struct SkillsProposalReviseParams: Codable, Sendable {
}
}
public struct SkillsProposalRequestRevisionParams: Codable, Sendable {
public let agentid: String?
public let targetagentid: String?
public let proposalid: String
public let instructions: String
public let sessionkey: String
public let sessionid: String?
public let idempotencykey: String
public init(
agentid: String? = nil,
targetagentid: String?,
proposalid: String,
instructions: String,
sessionkey: String,
sessionid: String?,
idempotencykey: String)
{
self.agentid = agentid
self.targetagentid = targetagentid
self.proposalid = proposalid
self.instructions = instructions
self.sessionkey = sessionkey
self.sessionid = sessionid
self.idempotencykey = idempotencykey
}
private enum CodingKeys: String, CodingKey {
case agentid = "agentId"
case targetagentid = "targetAgentId"
case proposalid = "proposalId"
case instructions
case sessionkey = "sessionKey"
case sessionid = "sessionId"
case idempotencykey = "idempotencyKey"
}
}
public struct SkillsProposalRequestRevisionResult: Codable, Sendable {
public let runid: String
public let status: AnyCodable
public init(
runid: String,
status: AnyCodable)
{
self.runid = runid
self.status = status
}
private enum CodingKeys: String, CodingKey {
case runid = "runId"
case status
}
}
public struct SkillsProposalActionParams: Codable, Sendable {
public let agentid: String?
public let proposalid: String
@@ -6956,20 +6896,6 @@ public struct ChatHistoryParams: Codable, Sendable {
}
}
public struct ChatMetadataParams: Codable, Sendable {
public let agentid: String?
public init(
agentid: String? = nil)
{
self.agentid = agentid
}
private enum CodingKeys: String, CodingKey {
case agentid = "agentId"
}
}
public struct ChatMessageGetParams: Codable, Sendable {
public let sessionkey: String
public let agentid: String?
@@ -7034,7 +6960,6 @@ public struct ChatSendParams: Codable, Sendable {
public let timeoutms: Int?
public let systeminputprovenance: [String: AnyCodable]?
public let systemprovenancereceipt: String?
public let suppresscommandinterpretation: Bool?
public let idempotencykey: String
public init(
@@ -7053,7 +6978,6 @@ public struct ChatSendParams: Codable, Sendable {
timeoutms: Int?,
systeminputprovenance: [String: AnyCodable]?,
systemprovenancereceipt: String?,
suppresscommandinterpretation: Bool?,
idempotencykey: String)
{
self.sessionkey = sessionkey
@@ -7071,7 +6995,6 @@ public struct ChatSendParams: Codable, Sendable {
self.timeoutms = timeoutms
self.systeminputprovenance = systeminputprovenance
self.systemprovenancereceipt = systemprovenancereceipt
self.suppresscommandinterpretation = suppresscommandinterpretation
self.idempotencykey = idempotencykey
}
@@ -7091,7 +7014,6 @@ public struct ChatSendParams: Codable, Sendable {
case timeoutms = "timeoutMs"
case systeminputprovenance = "systemInputProvenance"
case systemprovenancereceipt = "systemProvenanceReceipt"
case suppresscommandinterpretation = "suppressCommandInterpretation"
case idempotencykey = "idempotencyKey"
}
}

View File

@@ -1,6 +1,3 @@
/**
* Knip configuration for OpenClaw root and bundled plugin dependency hygiene.
*/
const BUNDLED_PLUGIN_ROOT_DIR = "extensions";
function bundledPluginFile(pluginId: string, relativePath: string, suffix = ""): string {

View File

@@ -1,4 +1,4 @@
60c0700719fd2fe3f7cec4c35da10227b681d87ed1a3876ef830eb6bd80d43f2 config-baseline.json
2ed21fa4a416ac2cec55eb2b6d1b11859aa04b40bd78c6ed9f3eb45b7240261c config-baseline.core.json
0637c9bdcb9517f56049dd786563366877458d35df575328a6b80a890c8bc915 config-baseline.channel.json
e6a1d6f51f0d9c04bd92d51deebfaca8c7917dd28d7998d225c0074e0a095348 config-baseline.plugin.json
cc0fb4e3f1a7e8f233626adb80d686608ddac8c177fe6a55b33970c2baf4ace4 config-baseline.json
042ca98e6200a365accda00e5a6f3e72bdae5853f39ff0cdc3b2cb9c0d6f8f3e config-baseline.core.json
cbf81829dcc8cfd0a16435912da709f8c1d508707385b6493f94cafe211ec67c config-baseline.channel.json
4012b1f8de6f9527c47320a6c7120f30dc30ac1b5524ed63dadef890aad44b20 config-baseline.plugin.json

View File

@@ -1,2 +1,2 @@
a3ab01b572937539e563aa320ad80135bc701e20fffc43c0351d799590b7a0e0 plugin-sdk-api-baseline.json
9d49587923f8fc4abb16d981bcab54acbf90a3e74ab05933761049e2da0cffe1 plugin-sdk-api-baseline.jsonl
bdcf661ec680f79819096950295bdb04805aac9639477058d8855f294f6d8034 plugin-sdk-api-baseline.json
6b8c92cc5a9277f90973370102fa31efb23ffd93008c3ed961d38e4a8a3073b0 plugin-sdk-api-baseline.jsonl

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 250 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 251 KiB

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 257 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 134 KiB

View File

@@ -162,7 +162,6 @@ Configure your tunnel's ingress rules to only route the webhook path:
4. DM access is pairing by default. Unknown senders receive a pairing code; approve with:
- `openclaw pairing approve googlechat <code>`
5. Group spaces require @-mention by default. Use `botUser` if mention detection needs the app's user name.
6. When an exec or plugin approval request starts from Google Chat and a stable `users/<id>` approver is configured, OpenClaw posts a native Google Chat approval card in the originating space or thread. The card buttons use opaque callback tokens, and the manual `/approve <id> <decision>` prompt is only shown when native approval delivery is unavailable.
## Targets
@@ -215,9 +214,8 @@ Notes:
- Default webhook path is `/googlechat` if `webhookPath` isn't set.
- `dangerouslyAllowNameMatching` re-enables mutable email principal matching for allowlists (break-glass compatibility mode).
- Reactions are available via the `reactions` tool and `channels action` when `actions.reactions` is enabled.
- Native approval cards use Google Chat `cardsV2` button clicks, not reaction events. Approvers come from `dm.allowFrom` or `defaultTo` and must be stable numeric `users/<id>` values.
- Message actions expose `send` for text and `upload-file` for explicit attachment sends. `upload-file` accepts `media` / `filePath` / `path` plus optional `message`, `filename`, and thread targeting.
- `typingIndicator` supports `message` (default), `none`, and `reaction` (reaction requires user OAuth).
- `typingIndicator` supports `none`, `message` (default), and `reaction` (reaction requires user OAuth).
- Attachments are downloaded through the Chat API and stored in the media pipeline (size capped by `mediaMaxMb`).
- Bot-authored Google Chat messages are ignored by default. If you intentionally set `allowBots: true`, accepted bot-authored messages use shared [bot loop protection](/channels/bot-loop-protection). Configure `channels.defaults.botLoopProtection`, then override with `channels.googlechat.botLoopProtection` or `channels.googlechat.groups.<space>.botLoopProtection` when one space needs a different budget.

Some files were not shown because too many files have changed in this diff Show More